Space ship simulator guidance computer targeting with concentric indicator squares
Asked Answered
O

1

11

I'm working on a 3D space trading game with some people, and one of the things I've been assigned to do is to make a guidance computer 'tunnel' that the ship travels through, with the tunnel made of squares that the user flies through to their destination, increasing in number as the user gets closer to the destination.

It's only necessary to render the squares for the points ahead of the ship, since that's all that's visible to the user. On their way to a destination, the ship's computer is supposed to put up squares on the HUD that represent fixed points in space between you and the destination, which are small in the distance and get larger as the points approach the craft.

guidance squares example

I've had a go at implementing this and can't seem to figure it out, mainly using logarithms (Math.log10(x) and such). I tried to get to get the ship position in 'logarithmic space' to help find out what index to start from when drawing the squares, but then the fact that I only have distance to the destination to work with confuses the matter, especially when you consider that the number of squares has to vary dynamically to make sure they stay fixed at the right locations in space (i.e., the squares are positioned at intervals of 200 or so before being transformed logarithmically).

With regard to this, I had a working implementation with the ship between a start of 0.0d and end of 1.0d, although the implementation wasn't so nice. Anyway, the problem essentially boils down to a 1d nature. Any advice would be appreciated with this issue, including possible workarounds to achieve the same effect or solutions.

Frontier: Elite 2

(Also, there's a Youtube video showing this effect: http://www.youtube.com/watch?v=79F9Nj7GgfM&t=3m5s)

Cheers,
Chris

Edit: rephrased the entire question.

Edit: new testbed code:

package st;

import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferStrategy;
import java.text.DecimalFormat;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class StUI2 extends JFrame {
    public static final double DEG_TO_RAD = Math.PI / 180.0d;
    public static final DecimalFormat decimalFormat = new DecimalFormat("0.0000");

    public static final Font MONO = new Font("Monospaced", Font.PLAIN, 10);

    public class StPanel extends Canvas {
        protected final Object imgLock  = new Object();
        protected int lastWidth = 1, lastHeight = 1;
        protected boolean first = true;
        protected Color bgColour = Color.DARK_GRAY, gridColour = Color.GRAY;

        double shipWrap = 700;
        double shipFrame = 100;
        double shipPos = 0;
        long lastUpdateTimeMS = -1;
        long currUpdateTimeMS = -1;

        public StPanel() {      
            setFocusable(true);
            setMinimumSize(new Dimension(1, 1));
            setAlwaysOnTop(true);
        }

        public void internalPaint(Graphics2D g) {
            synchronized (imgLock) {
                if (lastUpdateTimeMS < 0) {
                    lastUpdateTimeMS = System.currentTimeMillis();
                }
                currUpdateTimeMS = System.currentTimeMillis();
                long diffMS = currUpdateTimeMS - lastUpdateTimeMS;

                g.setFont(MONO);

                shipPos += (60d * ((double)diffMS / 1000));
                if (shipPos > shipWrap) {
                    shipPos = 0d;
                }

                double shipPosPerc = shipPos / shipWrap;
                double distToDest = shipWrap - shipPos;
                double compression = 1000d / distToDest;

                g.setColor(bgColour);
                Dimension d = getSize();
                g.fillRect(0, 0, (int)d.getWidth(), (int)d.getHeight());

                //int amnt2 = (int)unlog10((1000d / distToDest));

                g.setColor(Color.WHITE);
                g.drawString("shipPos:    " + decimalFormat.format(shipPos),     10, 10);
                g.drawString("distToDest: " + decimalFormat.format(distToDest),  10, 20);

                g.drawString("shipWrap:   " + decimalFormat.format(shipWrap),    150, 10);

                int offset = 40;

                g.setFont(MONO);

                double scalingFactor = 10d;

                double dist = 0;
                int curri = 0;
                int i = 0;
                do {
                    curri = i;
                    g.setColor(Color.GREEN);

                    dist = distToDest - getSquareDistance(distToDest, scalingFactor, i);
                    double sqh = getSquareHeight(dist, 100d * DEG_TO_RAD);
                    g.drawLine(30 + (int)dist, (offset + 50) - (int)(sqh / 2d), 30 + (int)dist, (offset + 50) + (int)(sqh / 2d));
                    g.setColor(Color.LIGHT_GRAY);
                    g.drawString("i: " +  i + ", dist: " + decimalFormat.format(dist), 10, 120 + (i * 10));
                    i++;
                } while (dist < distToDest);

                g.drawLine(10, 122, 200, 122);
                g.drawString("last / i: " +  curri + ", dist: " + decimalFormat.format(dist), 10, 122 + (i * 10));

                g.setColor(Color.MAGENTA);
                g.fillOval(30 + (int)shipPos, offset + 50, 4, 4);

                lastUpdateTimeMS = currUpdateTimeMS;
            }
        }

        public double getSquareDistance(double initialDist, double scalingFactor, int num) {
            return Math.pow(scalingFactor, num) * num * initialDist;
        }

        public double getSquareHeight(double distance, double angle) {
            return distance / Math.tan(angle);
        }

        /* (non-Javadoc)
         * @see java.awt.Canvas#paint(java.awt.Graphics)
         */
        @Override
        public void paint(Graphics g) {
            internalPaint((Graphics2D)g);
        }

        public void redraw() {
            synchronized (imgLock) {
                Dimension d = getSize();
                if (d.width == 0)  d.width = 1;
                if (d.height == 0) d.height = 1;

                if (first || d.getWidth() != lastWidth || d.getHeight() != lastHeight) {
                    first = false;

                    // remake buf
                    GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
                    //create an object that represents the device that outputs to screen (video card).
                    GraphicsDevice gd = ge.getDefaultScreenDevice();
                    gd.getDefaultConfiguration();

                    createBufferStrategy(2);

                    lastWidth  = (int)d.getWidth();
                    lastHeight = (int)d.getHeight();
                }

                BufferStrategy strategy = getBufferStrategy();
                Graphics2D g = (Graphics2D)strategy.getDrawGraphics();
                internalPaint(g);
                g.dispose();
                if (!strategy.contentsLost()) strategy.show();
            }
        }
    }

    protected final StPanel canvas;

    protected Timer viewTimer = new Timer(1000 / 60, new ActionListener() {     
        @Override
        public void actionPerformed(ActionEvent e) {
            canvas.redraw();
        }
    });
    {
        viewTimer.setRepeats(true);
        viewTimer.setCoalesce(true);
    }

    /**
     * Create the applet.
     */
    public StUI2() {
        JPanel panel = new JPanel(new BorderLayout());
        setContentPane(panel);
        panel.add(canvas = new StPanel(), BorderLayout.CENTER);
        setVisible(true);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setSize(800, 300);
        setTitle("Targetting indicator test #2");
        viewTimer.start();
    }

    public static double unlog10(double x) {  
        return Math.pow(10d, x);
    }   

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                StUI2 ui = new StUI2();
            }
        });
    }
}
Orleanist answered 21/3, 2012 at 0:31 Comment(13)
1) Don't mix Swing (JApplet) & AWT (Canvas) components. 2) An applet is slightly harder for others to test. Please consider developing using a hybrid application/applet. 3) Do you have a question? What is it?Calibre
OK. I've now run the code. Besides the question, I am still not really clear on what should be happening. Should the red and yellow dots be aligned? Also the magenta craft is moving left to right. Is the yellow pattern supposedly what the pilot sees out the front window (would be projected on the right hand wall from our perspective)?Calibre
Yeah, it'd be ideal if the red and yellow dots were aligned. I need to produce a small subsection of the red dots, which should be the yellow dots. They should remain at fixed points in space on the target path relating to distance to the target.Orleanist
To clarify, this game has a 3D interface? Is it first-person from within the cockpit of your craft?Romaine
Yep, it has a 3D interface, but that's not really part of the question, since I can easily get the size required for the squares that appear based on their position to the destination. In essence, the problem is simply one-dimensional. I'm guessing the crux of this is some sort of y = log(x) problem, with some value modification (I'm working on the code now).Orleanist
I am interested in the source code too... So that I can test my equations ;)Taranto
The source code is at pioneerspacesim.net, but it's hardly a SCCEE ;) I may hack up a 3D test bed for messing with algos.Orleanist
What is SCCEE? I don't think you meant South Carolina Council of Economic Education, which is the only resolution of the acronym I could find :)Spadework
Got it slightly wrong: Short, Self Contained, Correct (Compilable), Example (sscce.org) ;)Orleanist
Would you accept my question? If there is something you think it is missing before you can accept it, please let me know.Spadework
Do you also consider it bounty worthy?Spadework
Of course :) It's great in the game. Just needs a few more tweaks, but I can do these myself.Orleanist
Thank you! I'm glad to have been able to help :)Spadework
S
10

Assuming you want the squares to be equal height (when you reach them), you can calculate a scaling factor based on the distance to the destination (d) and the required height of the squares upon reaching them (h).

From these two pieces of information you can calculate the inverse tangent (atan) of the angle (alpha) between the line connecting the ship to the destination (horizontal line in your image) and the line connecting the top of the squares with the destination (angled line in your image).

EDIT: corrected
formula Using the angle, you can calculate the height of the square (h') at any given distance from the destination: you know the distance to the destination (d') and the angle (alpha); The height of the square at distance d' is h'=r'*sin(alpha) -- sin(alpha)=cos(alpha)*tan(alpha) and r'=d'/cos(alpha) (the distance between the destination and the top of the square -- the "radius"). Or more easily: h'=d'*tan(alpha).

Note: adopting the algorithm to varying height (when you reach them) squares is relatively simple: when calculating the angle, just assume a (phantom) square of fixed height and scale the squares relatively to that.

If the height of the square at distance d' is calculated for you by your graphic library, all the better, you only need to figure out the distances to place the squares.

What distances to place the squares from the destination?

1) If you want a varying number of squares shown (in front of the ship), but potentially infinite number of squares to consider (based on d), you can chose the distance of the closest square to the destination (d1) and calculate the distances of other squares by the formula s^k*k*d1, where s (scaling factor) is a number > 1 for the k'th square (counting from the destination). You can stop the algorithm when the result is larger than d.

Note that if d is sufficiently large, the squares closest to the distance will block the destination (there are many of them and their heights are small due to the low angle). In this case you can introduce a minimal distance (possibly based on d), below which you do not display the squares -- you will have to experiment with the exact values to see what looks right/acceptable.

2) If you want a fixed amount of squares (sn) showing always, regardless of d, you can calculate the distances of the squares from the destination by the formula d*s^k, where s is a number < 1, k is the index of the square (counting from the ship). The consideration about small squares probably don't apply here unless sn is high.

To fix the updated code, change the relavant part to:

double dist = 0;
double d1 = 10;
int curri = 0; 
int i = 1; 
int maxSquareHeight = 40;
double angle = Math.atan(maxSquareHeight/distToDest);
while (true)
{ 
  curri = i; 
  g.setColor(Color.GREEN); 

  dist = getSquareDistance(d1, scalingFactor, i); 
  if (dist > distToDest) {
    break;
  }
  double sqh = getSquareHeight(dist, angle); 
  g.drawLine(30 + (int)(shipWrap - dist), offset+50-(int)(sqh / 2d), 30 + (int)(shipWrap - dist), offset+50+(int)(sqh / 2d)); 
  g.setColor(Color.LIGHT_GRAY); 
  i++; 
}

public double getSquareHeight(double distance, double angle) { 
  return distance * Math.tan(angle); 
} 

You should also reduce scalingFactor to the magnitude of ~1.5.

EDIT: If you replace the formula s^k*k*d1 with s^(k-1)*k*d1, then the first square will be exactly at distance d1.

EDIT: fixed square height calculating formula

EDIT: updated code

Spadework answered 26/3, 2012 at 14:53 Comment(12)
Cheers - will try this out and get back to you :)Orleanist
Updated formulas in 3rd paragraphSpadework
Regarding order of operations on s^k*k*d1, is it (s^(k*k))*d1 ?Orleanist
No, it is (s^k)*kd1. The idea was to calculate an equi-distance (kd1) then "scale it up" by an ever-growing factor. You can potentially leave the second k out, if it is scaling too fast, but then the second square (from the destination) will have less distance from dest, then twice the first -- you decide if it bothers you.Spadework
OTOH, calculating s^(k*k)*d1 might work as well, but the scaling can insrease too fast as k gets larger -- In any rate, experiment and see what looks bestSpadework
I'm not getting this working at all at the moment -- I'll post up the 2D java testbed code so you can have a look :) Again, thanks for your help. I did get something that kind-of worked, but with the variables in different places in the equation.Orleanist
Edited original question to put Java SCCEE in BTW :)Orleanist
Thanks! Only just noticed the modified code. This seems to work great. I'll try integrating it tomorrow in the game itself and get back to you. I still might have issues with HUD translation as it has to be compacted down to 2D with squares on the flat surface of the cockpit screen :)Orleanist
How can shipWrap be refactored out of it? I notice you're using it to make the entire thing flip about, but the real thing will have no such variable when implemented (just distance to destination).Orleanist
I don't think it can be. shipwrap essentially denotes the destiantion (at least that's how I used it), which you need, to be able to count back from. You need it to calculate the ship's distance to it also. -- Let me know if I misunderstood your intention of the variable and its purpose (besides the obvious, that it is the point where the ship's position is reset). If you _do_have distance to destination, you can add that to the ship's position and get the destination (== shipwrap)Spadework
I guess if it's being flattened on the HUD, then the whole line position drawing thing will disappear anyway (the only place where shipWrap is used) and be replaced by flat square drawing of reducing sizes (which seems to be working okay :)).Orleanist
let us continue this discussion in chatSpadework

© 2022 - 2024 — McMap. All rights reserved.