2

I've been trying to figure out how I can gradually accelerate a sprite on key pressed and then once the key is released, gradually slow down to a stop, much like the ship in Asteroids. I would like to do this without any game engine if possible. I searched this up on SO and found related questions, but they did not answer my question exactly in my opinion.

What I've thought of so far is:

//while the key is being pressed
//move by increasing y value
//but continue to increase the amount y increases by as you hold this down,
//until it reaches certain maxSpeed
//when the key is released, gradually decelerate to zero

I'm just unsure as to how to properly program this, because I can only think of ways to just increase by the same value and not gradually accelerate while holding.

So here is my goal (Gradually speed up and then gradually slow down):

Accelerating sprite

I am a beginner and am open to all ideas, but the fact that I may not be approaching this problem correctly could be because I do not know as much as others

Sam Mitchell
  • 194
  • 2
  • 18
  • 1
    What are you looking for? Code? A general outline? Your idea seems sound you just need to implement it. – Jazzepi Dec 07 '15 at 02:40
  • For [example](http://stackoverflow.com/questions/15897947/problems-with-javas-paint-method-ridiculous-refresh-velocity/15900205#15900205) – MadProgrammer Dec 07 '15 at 02:41
  • I would follow a basic (asteroid) game tutorial. In the simplest case the update calculation will be based on a *delta time* - calculations can use this to compute 'how long the thrusters fired' and apply an appropriate change to the ship state/vectors and keep applying such (ie. [thrust + rotation] -> acceleration -> velocity -> position) until the new world state is achieved. Variations such as lock-stepping the delta to a fixed rate (ie. 60fps) with multiple updates/catchups per render can also be used. – user2864740 Dec 07 '15 at 02:43
  • `I searched this up on SO and found related questions, but they did not answer my question exactly` - Just because something doesn't do "exactly" what you want doesn't mean it is not helpful. Take the ideas from multiple related questions and create your own solution. That is what programming is about. – camickr Dec 07 '15 at 02:44
  • @MadProgrammer ok so because I'm new to this, I've been using JFrame with JPanel and sprites. So it seems the example uses something called action maps. Could I implement the same thing in mine? – Sam Mitchell Dec 07 '15 at 02:44
  • @camickr I spent hours trying to do exactly that, but I think because I'm a newer programmer, it prevents me from being able to implement many of the solutions or suggestions as they are pretty advanced for me right now – Sam Mitchell Dec 07 '15 at 02:46
  • http://stackoverflow.com/questions/7338676/correct-movement-in-asteroids-type-game – user2864740 Dec 07 '15 at 02:49
  • @Wizerman Yes. It uses the key bindings API which is preferred over using `KeyListener` – MadProgrammer Dec 07 '15 at 02:50
  • @MadProgrammer would that be much harder to learn and implement than the method I'm using? – Sam Mitchell Dec 07 '15 at 02:53
  • @Wizerman I have no idea, I don't know what method you're using – MadProgrammer Dec 07 '15 at 02:54
  • @MadProgrammer I meant the keylistener – Sam Mitchell Dec 07 '15 at 02:56
  • @Wizerman No, in fact, it will solve a major issue that `KeyListener` has – MadProgrammer Dec 07 '15 at 02:58
  • @MadProgrammer Major issue? – Sam Mitchell Dec 07 '15 at 03:16
  • @Wizerman `KeyListener` is restricted to responding to key events only when the component is focusable AND has keyboard focus. This can be an issue as the component can loose keyboard focus for a number of reasons. Key Bindings resolve this issue by giving you control over what level of focus you component needs to have before responding to key board events. It also allows you to abstract the input away from the keyboard, so you could have a joy stick or even mouse control, which could be executed through the same `Action` without having to code a lot of separate functionality for each – MadProgrammer Dec 07 '15 at 03:19
  • So did you make an attempt at the solution i posted? ;) – Neuron Dec 09 '15 at 02:41
  • @Neuron I was playing around with the many solutions yes. Right now i am looking into making a stopwatch class and utilizing that. – Sam Mitchell Dec 09 '15 at 03:40

4 Answers4

2

Essentially acceleration and deceleration is the change of speed over time.

This assumes a few things.

  1. You are updating the state at a regular interval, so the acceleration/deceleration can be applied to the objects current speed
  2. You can monitor the state of a given input, so you know when and what should be applied

This example uses a Swing Timer as the main "game loop". This is used to continuously update the state of the player object.

It uses the key bindings API to change a GameState object which carries information about the current inputs into the game, this is used by the Player to make decisions about what deltas should be applied.

The example has quite a large maximum speed/rotation, so you can play with these, but if you release all the inputs, the game object will decelerate back to a "neutral" position (of 0 delta inputs).

If you apply input in one direction for short period of time, then apply an input into the opposite direction, the player will need to decelerate down through 0 and then accelerate in the opposite direction (to a maximum speed), so it has breaking

Ponioids

import java.awt.Container;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Ponies {

    public static void main(String[] args) {
        new Ponies();
    }

    public Ponies() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new GameView());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class GameView extends JPanel {

        private GameState gameState;
        private Player player;

        public GameView() {
            gameState = new GameState();
            addKeyBindingForInput(GameInput.DOWN, KeyEvent.VK_S);
            addKeyBindingForInput(GameInput.UP, KeyEvent.VK_W);
            addKeyBindingForInput(GameInput.LEFT, KeyEvent.VK_A);
            addKeyBindingForInput(GameInput.RIGHT, KeyEvent.VK_D);
            addKeyBindingForInput(GameInput.ROTATE_LEFT, KeyEvent.VK_LEFT);
            addKeyBindingForInput(GameInput.ROTATE_RIGHT, KeyEvent.VK_RIGHT);

            try {
                player = new Player(400, 400);

                Timer timer = new Timer(40, new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        player.update(GameView.this, gameState);
                        repaint();
                    }
                });
                timer.start();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }

        protected void addKeyBindingForInput(GameInput input, int virtualKey) {
            InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
            ActionMap actionMap = getActionMap();

            inputMap.put(KeyStroke.getKeyStroke(virtualKey, 0, false), input + ".pressed");
            actionMap.put(input + ".pressed", new GameInputAction(gameState, input, true));

            inputMap.put(KeyStroke.getKeyStroke(virtualKey, 0, true), input + ".released");
            actionMap.put(input + ".released", new GameInputAction(gameState, input, false));
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(400, 400);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            player.paint(g2d, this);

            List<GameInput> inputs = gameState.getInputs();
            FontMetrics fm = g2d.getFontMetrics();
            int y = getHeight() - (fm.getHeight() * inputs.size());
            for (GameInput input : inputs) {
                String text = input.name();
                g2d.drawString(text, getWidth() - fm.stringWidth(text), y + fm.getAscent());
                y += fm.getHeight();
            }
            g2d.dispose();
        }

    }

    public class GameInputAction extends AbstractAction {

        private final GameState gameState;
        private final GameInput input;
        private final boolean pressed;

        public GameInputAction(GameState gameState, GameInput input, boolean pressed) {
            this.gameState = gameState;
            this.input = input;
            this.pressed = pressed;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            if (pressed) {
                gameState.addInput(input);
            } else {
                gameState.removeInput(input);
            }
        }

    }

    public enum GameInput {
        LEFT, RIGHT,
        UP, DOWN,
        ROTATE_LEFT, ROTATE_RIGHT
    }

    public class GameState {

        private Set<GameInput> inputs;

        public GameState() {
            inputs = new HashSet<>(25);
        }

        public boolean hasInput(GameInput input) {
            return inputs.contains(input);
        }

        public void addInput(GameInput input) {
            inputs.add(input);
        }

        public void removeInput(GameInput input) {
            inputs.remove(input);
        }

        public List<GameInput> getInputs() {
            return new ArrayList<GameInput>(inputs);
        }

    }

    public static class Player {

        protected static final int MAX_DELTA = 20;
        protected static final int MAX_ROTATION_DELTA = 20;

        protected static final int MOVE_DELTA = 4;
        protected static final int ROTATION_DELTA = 4;

        private int x, y;
        private int xDelta, yDelta;
        private BufferedImage sprite;

        private double angle;
        private double rotationDelta;

        public Player(int width, int height) throws IOException {
            sprite = ImageIO.read(getClass().getResource("/PlayerSprite.png"));
            x = (width - sprite.getWidth()) / 2;
            y = (height - sprite.getHeight()) / 2;
        }

        public void update(Container container, GameState state) {
            if (state.hasInput(GameInput.LEFT)) {
                xDelta -= MOVE_DELTA;
            } else if (state.hasInput(GameInput.RIGHT)) {
                xDelta += MOVE_DELTA;
            } else if (xDelta < 0) {
                xDelta++;
            } else if (xDelta > 0) {
                xDelta--;
            }
            if (state.hasInput(GameInput.UP)) {
                yDelta -= MOVE_DELTA;
            } else if (state.hasInput(GameInput.DOWN)) {
                yDelta += MOVE_DELTA;
            } else if (yDelta < 0) {
                yDelta++;
            } else if (yDelta > 0) {
                yDelta--;
            }
            if (state.hasInput(GameInput.ROTATE_LEFT)) {
                rotationDelta -= MOVE_DELTA;
            } else if (state.hasInput(GameInput.ROTATE_RIGHT)) {
                rotationDelta += MOVE_DELTA;
            } else if (rotationDelta < 0) {
                rotationDelta++;
            } else if (rotationDelta > 0) {
                rotationDelta--;
            }

            xDelta = Math.max(-MAX_DELTA, Math.min(xDelta, MAX_DELTA));
            yDelta = Math.max(-MAX_DELTA, Math.min(yDelta, MAX_DELTA));
            rotationDelta = Math.max(-MAX_ROTATION_DELTA, Math.min(rotationDelta, MAX_ROTATION_DELTA));

            x += xDelta;
            y += yDelta;
            angle += rotationDelta;

            if (x < -sprite.getWidth()) {
                x = container.getWidth();
            } else if (x + sprite.getWidth() > container.getWidth() + sprite.getWidth()) {
                x = 0;
            }

            if (y < -sprite.getHeight()) {
                y = container.getHeight();
            } else if (y + sprite.getHeight() > container.getHeight() + sprite.getHeight()) {
                y = 0;
            }
        }

        public void paint(Graphics2D g2d, ImageObserver io) {
            Graphics2D copy = (Graphics2D) g2d.create();
            copy.translate(x, y);
            copy.rotate(Math.toRadians(angle), sprite.getWidth() / 2.0d, sprite.getHeight() / 2.0d);
            copy.drawImage(sprite, 0, 0, io);
            copy.dispose();
        }

    }

}

Take a look at How to Use Key Bindings and How to use Swing Timers for more details

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
1

There are two "concepts" from physics which you need to implement: Speed and friction.

Here is a simple example in 2D. You may also use wrapper classes to combine the x/y variables into a single object and provide handy methods to alter their content.

Each object needs to have a position and a speed variable. We also need friction, which is a constant for each material (as your object probably always travels in the same material, we will just model friction to be constant). In this simple simulation, friction gets weaker, as the value approaches 1. That means at friction=1 you have no friction and at friction=0 your objects will stop immediately:

public class PhysicsObject{
    public static final double FRICTION = 0.99;
    private double posX;
    private double posY;
    private double speedX = 0;
    private double speedY = 0;
    public PhysicsObject(double posX, double posY){
        this.posX = posX;
        this.posY = posY;
    }
    public void accelerate(double accelerationX, double accelerationY){
        speedX += accelerationX;
        speedY += accelerationY;
    }
    public void update(){
        posX += speedX;
        posY += speedY;
        speedX *= FRICTION;
        speedY *= FRICTION;
    }
    public double getPosX(){
        return posX;
    }
    public double getPosY(){
        return posY;
    }
}

Notice that your object has an update method. this method needs to be called on all objects in your scene regularly, to apply the movement. In this method you could also process collision detection and enemies could do their AI logic..

Neuron
  • 5,141
  • 5
  • 38
  • 59
  • So, my object can move diagonally. Does this mean `pos` needs to be equal to a coordinate pair? – Sam Mitchell Dec 07 '15 at 03:02
  • you would then have posX, posY, speedX and speedY. I will update my answer to reflect that. (You can also use a class which holds two double values to get the job done..) – Neuron Dec 07 '15 at 03:04
  • Thanks for the help, I'm going to tackle your suggestion tomorrow when I'm less tired and will be sure to mark your's as the answer if it works! – Sam Mitchell Dec 07 '15 at 03:15
  • Yea, that would be great! Hope this approach will help. It is btw a simple version of the very standard way of doing it, so getting used to working like that is surely good practise :) – Neuron Dec 07 '15 at 03:18
1

Keep two variables.

  1. Your velocity. This is how far you move the ship each time the game "ticks".

  2. Your acceleration. This is how much you increase the velocity each time the game ticks.

To emulate the ship do something like this.

tick() {
    if(forward key is pressed) {
        howManyTicksTheForwardKeyHasBeenPressedFor++;
        currentAcceleration += howManyTicksTheForwardKeyHasBeenPressedFor++; //This is the slow ramp of speed. Notice that as you hold the key longer, your acceleration grows larger.
        currentVelocity += currentAcceleration; //This is how the ship actually moves faster.
    } else {
        howManyTicksTheForwardKeyHasBeenPressedFor--; 
        currentAcceleration -= howManyTicksTheForwardKeyHasBeenPressedFor; //This is the slow loss of speed. You'll need to make a decision about how this works when a user goes from holding down the forward key to letting go. Here I'm assuming that the speed bleeds off at a constant rate the same way it gets added.
        currentVelocity += currentAcceleration; //This is how the ship actually slows down.
    }        
    ship.position += currentVelocity; //This is how you actually move the ship.
}

This only works for a ship going in a single direction along a straight line. You should be able to easily upgrade this to a two dimensional or three dimension space by keeping track of where the ship is pointed, and dividing the velocity amongst x and y and/or z.

You also need to be mindful of clamping values. You'll probably want a maxAcceleration, and maxVelocity. If the ship cannot go in reverse, then you want to make sure that currentVelocity never goes less than 0.

This also only represents constant acceleration. You get the same amount of change in velocity the first second you put on the gas as you do the last second before you let it go. I'm not a car guy, but I think most vehicles accelerate faster after they've had a moment or so to get started. You could emulate that by using a piecewise function to calculate the acceleration where the function f(x) where you put howManyTicksTheForwardKeyHasBeenPressedFor in for x and get out the acceleration to apply to your velocity. Maybe the first 3 ticks add 1, and the other ticks add 2 to your velocity.

Have fun!

Jazzepi
  • 5,259
  • 11
  • 55
  • 81
  • Thanks for the help, I'm going to tackle your suggestion tomorrow when I'm less tired and will be sure to mark your's as the answer if it works – Sam Mitchell Dec 07 '15 at 03:16
  • @Wizerman Do checkout Mike Nakis's answer. He's completely correct. Any kind of game framework needs to keep track of the time since the last tick was called (the delta). That'll let you know how much "game time" has passed. You then need apply acceleration, velocity changes, and move your ship based on the magnitude of your delta. If you don't do this you get weirdness where the framerate effects the physics of the game. Fallout 4 did that and it's awful. http://www.technobuffalo.com/2015/11/10/fallout-4-framerate-capped-and-tied-to-game-speed/ – Jazzepi Dec 07 '15 at 13:49
1

The previous questions have pretty much covered the subject, but if you want perfection, there is one more thing you need to be aware of: non-uniform time slices, that is, ticks of varying durations.

Normally, your tick() method should accept the current time and the duration of the last tick as parameters. (If it does not, then you need to calculate the duration of the last tick by querying the current time and remembering the time at which the last tick occurred, so that you can subtract one from the other.)

So, on each tick, you should not simply add the current speed to your position; what you should do instead on each tick, is add the current speed multiplied by the duration of the last tick to your position.

This way, if one tick happens very fast, and another tick takes a long time to complete, the movement of your spaceship will still be uniform.

Mike Nakis
  • 56,297
  • 11
  • 110
  • 142