在按下按键时逐渐加速精灵;释放按键时逐渐减速

Gradually accelerate sprite on key pressed; gradually decelerate on key released

我一直在想办法逐渐在按下按键时加速精灵,然后一旦松开按键,逐渐减速直至停止,就像小行星中的飞船。如果可能的话,我想在没有任何游戏引擎的情况下做到这一点。我在 SO 上搜索了这个并找到了相关问题,但我认为他们没有完全回答我的问题。

目前我想到的是:

//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

我只是不确定如何正确编程,因为我只能想办法增加相同的值,而不是在保持时逐渐加速。

所以这是我的目标(逐渐加速然后逐渐减速):

我是初学者,对所有想法都持开放态度,但我可能没有正确解决这个问题,这可能是因为我不像其他人那样了解

物理学中有两个 "concepts" 您需要实现: 速度和摩擦力。

这是一个简单的二维示例。您还可以使用包装器 类 将 x/y 变量组合到一个对象中,并提供方便的方法来更改它们的内容。

每个对象都需要有一个位置和一个速度变量。我们还需要摩擦力,它对于每个 material 都是一个常数(因为您的物体可能总是在相同的 material 中行进,我们将模型摩擦力设为常数)。在这个简单的模拟中,随着值接近 1,摩擦变弱。这意味着在 friction=1 时你没有摩擦,在 friction=0 时你的物体将立即停止:

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;
    }
}

注意你的对象有一个更新方法。需要定期对场景中的所有对象调用此方法,以应用移动。在这种方法中,您还可以处理碰撞检测,敌人可以执行他们的 AI 逻辑..

保留两个变量。

  1. 你的速度。这是每次游戏时你移动船的距离"ticks".

  2. 你的加速度。这是每次游戏滴答时你增加多少速度。

要模拟飞船,请执行以下操作。

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.
}

这只适用于沿直线单向行驶的船。您应该能够轻松地将其升级到二维或三维 space 通过跟踪船指向的位置,并将速度分配给 x 和 y and/or z.

您还需要注意限制值。您可能需要 maxAccelerationmaxVelocity。如果船不能倒退,那么你要确保 currentVelocity 永远不会小于 0。

这也只代表恒定加速度。在你放气之前的第一秒,你得到的速度变化量与你在放气之前的最后一秒得到的速度变化量相同。我不是汽车专家,但我认为大多数车辆在启动大约片刻后会加速得更快。您可以通过使用分段函数来计算加速度,其中您将 howManyTicksTheForwardKeyHasBeenPressedFor 放在 x 中的函数 f(x) 中,并得出加速度以应用于您的速度。也许前 3 个刻度加 1,其他刻度加 2 到你的速度。

玩得开心!

前面的问题已经基本涵盖了这个主题,但是如果你想要完美,你还需要注意一件事:非均匀时间片,即不同持续时间的滴答。

通常,您的 tick() 方法应该接受当前时间和最后一个报价持续时间作为参数。 (如果没有,那么您需要通过查询当前时间并记住最后一次报价发生的时间来计算最后一次报价的持续时间,以便您可以从另一个中减去。)

因此,在每次报价时,您不应该简单地将当前速度添加到您的位置;您应该在每次跳动时做的是将当前速度乘以最后一次跳动的持续时间添加到您的位置。

这样,如果一个滴答发生得很快,而另一个滴答需要很长时间才能完成,你的飞船的运动仍然是均匀的。

加减速本质上就是速度随时间的变化

这假设了一些事情。

  1. 您定期更新状态,因此 acceleration/deceleration 可以应用于对象当前速度
  2. 您可以监控给定输入的状态,这样您就知道应该应用什么时间和应用什么

此示例使用 Swing Timer 作为主体 "game loop"。这用于持续更新播放器对象的状态。

它使用键绑定 API 来更改 GameState 对象,该对象携带有关当前游戏输入的信息,Player 使用它来决定什么应该应用增量。

该示例有相当大的最大值 speed/rotation,因此您可以玩这些,但如果您释放所有输入,游戏对象将减速回到 "neutral" 位置(0增量输入)。

如果你短时间向一个方向施加输入,然后向相反方向施加输入,玩家将需要通过 0 减速然后向相反方向加速(到最大速度),所以它有 breaking

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();
        }

    }

}

查看 How to Use Key Bindings and How to use Swing Timers 了解更多详情