【问题标题】:Smooth Snake movement in SwingSwing 中平滑的 Snake 运动
【发布时间】:2019-02-08 20:49:01
【问题描述】:

我正在使用 Swing 用 Ja​​va 开发一个简单的 Snake 游戏。 到目前为止,我制作了令我困扰的蛇动画:当蛇移动时,它似乎开始无缘无故地滞后,但当我用箭头键移动它时,它变得平滑。

当它在直线上时,我怎样才能使它移动平滑? 代码如下:

游戏框架

public class GameFrame extends JFrame {
    private GamePanel gamePanel;
    private JLabel scoreLabel;

    public GameFrame() throws HeadlessException {
        this.setTitle("Snake");
        this.setBounds(100, 100, 400, 400);
        this.setLocationRelativeTo(null);
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        this.setResizable(false);
        getContentPane().setLayout(null);
        getContentPane().setBackground(Color.BLACK);

        scoreLabel = new JLabel("0000");
        scoreLabel.setBounds(0, 0, 70, 20);
        scoreLabel.setForeground(Color.WHITE);
        getContentPane().add(scoreLabel);

        gamePanel = new GamePanel();
        gamePanel.setBounds(20, 20, 360, 350);
        this.add(gamePanel);

        this.setVisible(true);
    }
}

游戏面板

public class GamePanel extends JPanel implements KeyListener, ActionListener {
    private GameManager gameManager;
    private Timer timer;
    private final int DELAY = 150;

    GamePanel() {
        this.setOpaque(true);
        this.setBackground(new Color(51, 51, 51));
        this.addKeyListener(this);
        this.setFocusable(true);
        this.requestFocus();
        gameManager = new GameManager(this);
        timer = new Timer(DELAY, this);
        timer.start();
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        gameManager.renderSnake(g);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        gameManager.gameLoop();
    }

    @Override
    public void keyPressed(KeyEvent e) {
        switch (e.getKeyCode()) {
            case KeyEvent.VK_UP:
                gameManager.moveSnakeUp();
                break;
            case KeyEvent.VK_DOWN:
                gameManager.moveSnakeDown();
                break;
            case KeyEvent.VK_RIGHT:
                gameManager.moveSnakeRight();
                break;
            case KeyEvent.VK_LEFT:
                gameManager.moveSnakeLeft();
                break;
        }
    }

    @Override
    public void keyTyped(KeyEvent e) { }

    @Override
    public void keyReleased(KeyEvent e) { }
}

游戏管理器

public class GameManager {
    private Color snakeColor;
    private Snake snake;
    private GamePanel gamePanel;
    private boolean running;

    public GameManager(GamePanel gamePanel) {
        this.gamePanel = gamePanel;
        snakeColor = Color.GREEN;
        snake = new Snake();
        running = true;
    }

    public void gameLoop() {
        update();
        draw();
    }

    public void renderSnake(Graphics graphics) {
        Graphics2D graphics2D = (Graphics2D) graphics;
        graphics2D.setColor(snakeColor);
        for (Rectangle tile : snake.getBody())
            graphics2D.fillRect(tile.x, tile.y, tile.width, tile.height);
    }

    public void update() {
        snake.move();
    }

    public boolean isRunning() {
        return running;
    }

    public void draw() {
        gamePanel.repaint();
    }

    public void moveSnakeUp() {
        snake.setUpDirection();
    }

    public void moveSnakeDown() {
        snake.setDownDirection();
    }

    public void moveSnakeRight() {
        snake.setRightDirection();
    }

    public void moveSnakeLeft() {
        snake.setLeftDirection();
    }
}

public class Snake {
    private final int SNAKE_SPEED = 10;
    private final int BODY_WIDTH = 10;

    private LinkedList<Rectangle> body;
    private Directions direction;

    public Snake() {
        direction = Directions.RIGHT;
        body = new LinkedList<>();
        body.add(new Rectangle(50, 20, BODY_WIDTH, BODY_WIDTH));
        body.add(new Rectangle(40, 20, BODY_WIDTH, BODY_WIDTH));
        body.add(new Rectangle(30, 20, BODY_WIDTH, BODY_WIDTH));
        body.add(new Rectangle(20, 20, BODY_WIDTH, BODY_WIDTH));
        body.add(new Rectangle(10, 20, BODY_WIDTH, BODY_WIDTH));
    }

    public LinkedList<Rectangle> getBody() {
        return body;
    }

    public void setUpDirection() {
        if (direction != Directions.UP)
            direction = Directions.UP;
    }

    public void setDownDirection() {
        if (direction != Directions.DOWN)
            direction = Directions.DOWN;
    }

    public void setRightDirection() {
        if (direction != Directions.RIGHT)
            direction = Directions.RIGHT;
    }

    public void setLeftDirection() {
        if (direction != Directions.LEFT)
            direction = Directions.LEFT;
    }

    public void move() {
        Rectangle newHead = body.removeLast();
        newHead.x = body.peek().x;
        newHead.y = body.peek().y;

        switch (direction) {
            case UP:
                newHead.y -= SNAKE_SPEED;
                break;
            case DOWN:
                newHead.y += SNAKE_SPEED;
                break;
            case RIGHT:
                newHead.x += SNAKE_SPEED;
                break;
            case LEFT:
                newHead.x -= SNAKE_SPEED;
                break;
        }

        body.addFirst(newHead);
    }
}

我尝试过使用Thread 和不同的延迟时间值,但无济于事。如何改进?

非常感谢。

编辑

我找到了一个使用BufferedImage 的解决方案,它确实有很大的不同,简单但有效:

@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    BufferedImage bufferedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
    Graphics bufferedGraphics = bufferedImage.getGraphics();

    gameManager.renderSnake(bufferedGraphics);
    gameManager.renderFruit(bufferedGraphics);

    g.drawImage(bufferedImage, 0, 0, this);
}

编辑 2

即使没有使用来自java.SwingTimer,我也终于设法制作了一个不错的动画,而是使用了Thread。对我来说效果很好。

对这里感兴趣的人是code

【问题讨论】:

  • 您似乎只在执行操作后才调用gameManager.gameLoop();
  • @kai 这就是代码已经在做的事情。注意使用Timer 来移动蛇。
  • 对不起,我删除了我的 cmets
  • 尝试通过 javax.swing.SwingUtilities.invokeLater(Runnable) 进行线程同步;用于调用游戏循环的计时器。
  • @kai javax.swing.Timer 已经在 AWT 事件调度线程上调用了它的 ActionListener。事实上,这可以说是它的主要目的。

标签: java swing animation


【解决方案1】:

似乎给你带来麻烦的是瓷砖的增量。 我将其更改为 1 像素。这样就更顺畅了。我还添加了一些随机增加大小的东西。您可以跳过平铺方法并使用单个对象作为一个方向的线。您可以使用颜色和混合使其更加平滑。有很大的改进空间——但也许它能让你更进一步。 我还使用 Netbeans 编写 Swing 编辑器,以免浪费太多时间。

import static Snake.GameFrame.SnakeBodyTile.BODYWIDTH;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Random;
import javax.swing.Timer;

/**
 *
 * @author Kai
 */
public class GameFrame extends javax.swing.JFrame implements KeyListener {

    public enum DIRECTION {
        LEFT,
        RIGHT,
        UP,
        DOWN
    }

    public enum STATE {
        SELFBITTEN,
        WALLHIT,
        RUNNING,
        WAITING,
        STOPPED
    }

    public static class SnakeBodyTile {

        public static final int BODYWIDTH = 10;
        public final Rectangle rect;
        public final DIRECTION direction;

        public SnakeBodyTile(Rectangle rect, DIRECTION direction) {
            this.direction = direction;
            this.rect = rect;
        }

        private boolean update() {
            switch (this.direction) {
                case UP:
                    return (1 >= rect.height--);
                case DOWN:
                    rect.y++;
                    return (1 >= rect.height--);
                case LEFT:
                    return (1 >= rect.width--);
                case RIGHT:
                    rect.x++;
                    return (1 >= rect.width--);
            }
            return false;//dummy
        }

    }

    LinkedList<SnakeBodyTile> snake = new LinkedList();
    Color snakeColor;
    Random random = new Random();
    DIRECTION direction = DIRECTION.LEFT;
    STATE state = STATE.STOPPED;
    Timer timer;

    /**
     * Creates new form GameFrame
     */
    @SuppressWarnings("LeakingThisInConstructor")
    public GameFrame() {
        initComponents();
        snakeColor = Color.GREEN;
        direction = DIRECTION.LEFT;
        state = STATE.STOPPED;

        snake.add(new SnakeBodyTile(new Rectangle(100, 100, 10, 10), direction));
        timer = new Timer(15, new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                timerAction(e);
            }
        });
        super.addKeyListener(this);
        state = STATE.WAITING;
        gamePanel.repaint();
        timer.start();
    }

    public void timerAction(ActionEvent e) {
        moveSnake();
        gamePanel.repaint();
    }

    @Override
    public void keyTyped(KeyEvent e) {
    }

    @Override
    public void keyPressed(KeyEvent e) {
        switch (e.getKeyCode()) {
            case KeyEvent.VK_UP:
                direction = DIRECTION.UP;
                break;
            case KeyEvent.VK_DOWN:
                direction = DIRECTION.DOWN;
                break;
            case KeyEvent.VK_RIGHT:
                direction = DIRECTION.RIGHT;
                break;
            case KeyEvent.VK_LEFT:
                direction = DIRECTION.LEFT;
                break;
        }
        if (state == STATE.WAITING) {
            snake.clear();
            snake.add(new SnakeBodyTile(new Rectangle(100, 100, 10, 10), direction));
            state = STATE.RUNNING;
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {
    }

    public void moveSnake() {
        if (state != STATE.RUNNING) {
            return;
        }
        final SnakeBodyTile first = snake.getFirst();
        final SnakeBodyTile last = snake.getLast();
        switch (direction) {
            case DOWN:
                if (first.rect.height >= BODYWIDTH) {
                    Rectangle rect = new Rectangle(first.rect.x + (first.direction == DIRECTION.LEFT ? 0 : first.rect.width - BODYWIDTH), first.rect.y + BODYWIDTH, BODYWIDTH, 1);
                    snake.addFirst(new SnakeBodyTile(rect, direction));
                } else {
                    first.rect.height++;
                }
                break;
            case UP:
                if (first.rect.height >= BODYWIDTH) {
                    Rectangle rect = new Rectangle(first.rect.x + (first.direction == DIRECTION.LEFT ? 0 : first.rect.width - BODYWIDTH), first.rect.y, BODYWIDTH, 1);
                    snake.addFirst(new SnakeBodyTile(rect, direction));
                } else {
                    first.rect.y--;
                    first.rect.height++;
                }
                break;
            case LEFT:
                if (first.rect.width >= BODYWIDTH) {
                    Rectangle rect = new Rectangle(first.rect.x, first.rect.y + (first.direction == DIRECTION.UP ? 0 : first.rect.height - BODYWIDTH), 1, BODYWIDTH);
                    snake.addFirst(new SnakeBodyTile(rect, direction));
                } else {
                    first.rect.x--;
                    first.rect.width++;
                }
                break;
            case RIGHT:
                if (first.rect.width >= BODYWIDTH) {
                    Rectangle rect = new Rectangle(first.rect.x + first.rect.width, first.rect.y + (first.direction == DIRECTION.UP ? 0 : first.rect.height - BODYWIDTH), 1, BODYWIDTH);
                    snake.addFirst(new SnakeBodyTile(rect, direction));
                } else {
                    first.rect.width++;
                }
                break;
        }
        if (last.update() && random.nextInt(4) != 1) {
            snake.removeLast();
        }
    }

    void updateGamePanel(Graphics graphics) {
        final Graphics2D graphics2D = (Graphics2D) graphics;
        graphics2D.setColor(snakeColor);
        for (Iterator<SnakeBodyTile> it = snake.iterator(); it.hasNext();) {
            final Rectangle tile = it.next().rect;
            graphics2D.fillRect(tile.x, tile.y, tile.width, tile.height);
        }
    }

    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">                          
    private void initComponents() {

        ScoreLable = new javax.swing.JLabel();
        gamePanel = new javax.swing.JPanel(){
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);
                updateGamePanel(g);
            };
        };
        jLabel2 = new javax.swing.JLabel();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setTitle("Snake");
        setBackground(java.awt.Color.black);
        setBounds(new java.awt.Rectangle(100, 100, 400, 400));
        setResizable(false);
        getContentPane().setLayout(new org.netbeans.lib.awtextra.AbsoluteLayout());

        ScoreLable.setText("0000");
        getContentPane().add(ScoreLable, new org.netbeans.lib.awtextra.AbsoluteConstraints(50, 0, 50, -1));

        gamePanel.setLayout(null);
        getContentPane().add(gamePanel, new org.netbeans.lib.awtextra.AbsoluteConstraints(0, 30, 400, 240));

        jLabel2.setText("Score:");
        getContentPane().add(jLabel2, new org.netbeans.lib.awtextra.AbsoluteConstraints(0, 0, -1, -1));

        pack();
    }// </editor-fold>                        

    /**
     * @param args the command line arguments
     */
    public static void main(String args[]) {
        /* Set the Nimbus look and feel */
        //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
        /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
         * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html 
         */
        try {
            for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
                if ("Nimbus".equals(info.getName())) {
                    javax.swing.UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        } catch (ClassNotFoundException ex) {
            java.util.logging.Logger.getLogger(GameFrame.class
                    .getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (InstantiationException ex) {
            java.util.logging.Logger.getLogger(GameFrame.class
                    .getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            java.util.logging.Logger.getLogger(GameFrame.class
                    .getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (javax.swing.UnsupportedLookAndFeelException ex) {
            java.util.logging.Logger.getLogger(GameFrame.class
                    .getName()).log(java.util.logging.Level.SEVERE, null, ex);
        }
        //</editor-fold>

        /* Create and display the form */
        java.awt.EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                new GameFrame().setVisible(true);
            }
        });
    }

    // Variables declaration - do not modify                     
    private javax.swing.JLabel ScoreLable;
    private javax.swing.JPanel gamePanel;
    private javax.swing.JLabel jLabel2;
    // End of variables declaration                   
}

【讨论】:

  • 感谢您的建议,这似乎很酷,但是我找到了一个名为 Double Buffering 的解决方案,它很有帮助。看看我所做的编辑和this video
  • 双缓冲对您有帮助吗?似乎我的计算机速度更快或处理方式不同:除了 timertickdelta 之外,我没有看到任何延迟。但在这种情况下,您也可以尝试删除不必要的背景。这有助于减少闪烁。我会看你的视频。祝你好运!
猜你喜欢
  • 1970-01-01
  • 2020-06-29
  • 1970-01-01
  • 2016-09-25
  • 1970-01-01
  • 2018-09-30
  • 1970-01-01
  • 1970-01-01
  • 2023-02-06
相关资源
最近更新 更多