原文: https://zetcode.com/tutorials/javagamestutorial/animation/

在 Java 2D 游戏教程的这一部分中,我们将使用动画。

动画

动画是图像序列的快速显示,会产生运动的错觉。 我们将为董事会上的星星设置动画。 我们将以三种基本方式实现这一运动。 我们将使用 Swing 计时器,标准工具计时器和线程。

动画是游戏编程中的一个复杂主题。 Java 游戏有望在具有不同硬件规格的多种操作系统上运行。 线程提供了最准确的计时解决方案。 但是,对于我们简单的 2D 游戏,其他两个选项也可以是一个选项。

Swing 计时器

在第一个示例中,我们将使用 Swing 计时器来创建动画。 这是在 Java 游戏中为对象设置动画的最简单但最无效的方法。

SwingTimerEx.java

  1. package com.zetcode;
  2. import java.awt.EventQueue;
  3. import javax.swing.JFrame;
  4. public class SwingTimerEx extends JFrame {
  5. public SwingTimerEx() {
  6. initUI();
  7. }
  8. private void initUI() {
  9. add(new Board());
  10. setResizable(false);
  11. pack();
  12. setTitle("Star");
  13. setLocationRelativeTo(null);
  14. setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  15. }
  16. public static void main(String[] args) {
  17. EventQueue.invokeLater(() -> {
  18. SwingTimerEx ex = new SwingTimerEx();
  19. ex.setVisible(true);
  20. });
  21. }
  22. }

这是代码示例的主要类。

  1. setResizable(false);
  2. pack();

setResizable()设置是否可以调整帧大小。 pack()方法使此窗口的大小适合其子级的首选大小和布局。 请注意,这两种方法的调用顺序很重要。 (setResizable()在某些平台上更改了帧的插入;在pack()方法之后调用此方法可能会导致错误的结果-星号不会精确地进入窗口的右下边界。)

Board.java

  1. package com.zetcode;
  2. import java.awt.Color;
  3. import java.awt.Dimension;
  4. import java.awt.Graphics;
  5. import java.awt.Image;
  6. import java.awt.Toolkit;
  7. import java.awt.event.ActionEvent;
  8. import java.awt.event.ActionListener;
  9. import javax.swing.ImageIcon;
  10. import javax.swing.JPanel;
  11. import javax.swing.Timer;
  12. public class Board extends JPanel
  13. implements ActionListener {
  14. private final int B_WIDTH = 350;
  15. private final int B_HEIGHT = 350;
  16. private final int INITIAL_X = -40;
  17. private final int INITIAL_Y = -40;
  18. private final int DELAY = 25;
  19. private Image star;
  20. private Timer timer;
  21. private int x, y;
  22. public Board() {
  23. initBoard();
  24. }
  25. private void loadImage() {
  26. ImageIcon ii = new ImageIcon("src/resources/star.png");
  27. star = ii.getImage();
  28. }
  29. private void initBoard() {
  30. setBackground(Color.BLACK);
  31. setPreferredSize(new Dimension(B_WIDTH, B_HEIGHT));
  32. loadImage();
  33. x = INITIAL_X;
  34. y = INITIAL_Y;
  35. timer = new Timer(DELAY, this);
  36. timer.start();
  37. }
  38. @Override
  39. public void paintComponent(Graphics g) {
  40. super.paintComponent(g);
  41. drawStar(g);
  42. }
  43. private void drawStar(Graphics g) {
  44. g.drawImage(star, x, y, this);
  45. Toolkit.getDefaultToolkit().sync();
  46. }
  47. @Override
  48. public void actionPerformed(ActionEvent e) {
  49. x += 1;
  50. y += 1;
  51. if (y > B_HEIGHT) {
  52. y = INITIAL_Y;
  53. x = INITIAL_X;
  54. }
  55. repaint();
  56. }
  57. }

Board类中,我们将星星从左上角移到右下角。

  1. private final int B_WIDTH = 350;
  2. private final int B_HEIGHT = 350;
  3. private final int INITIAL_X = -40;
  4. private final int INITIAL_Y = -40;
  5. private final int DELAY = 25;

定义了五个常数。 前两个常数是板的宽度和高度。 第三和第四是星星的初始坐标。 最后一个确定动画的速度。

  1. private void loadImage() {
  2. ImageIcon ii = new ImageIcon("src/resources/star.png");
  3. star = ii.getImage();
  4. }

loadImage()方法中,我们创建ImageIcon类的实例。 该图像位于项目目录中。 getImage()方法将从此类返回Image对象。 该对象将绘制在板上。

  1. timer = new Timer(DELAY, this);
  2. timer.start();

在这里,我们创建一个 Swing Timer类,并调用其start()方法。 计时器每DELAY毫秒就会调用一次actionPerformed()方法。 为了使用actionPerformed()方法,我们必须实现ActionListener接口。

  1. @Override
  2. public void paintComponent(Graphics g) {
  3. super.paintComponent(g);
  4. drawStar(g);
  5. }

自定义绘画是通过paintComponent()方法完成的。 请注意,我们还调用其父级的paintComponent()方法。 实际绘画将委托给drawStar()方法。

  1. private void drawStar(Graphics g) {
  2. g.drawImage(star, x, y, this);
  3. Toolkit.getDefaultToolkit().sync();
  4. }

drawStar()方法中,我们使用drawImage()方法在窗口上绘制图像。 Toolkit.getDefaultToolkit().sync()在缓冲图形事件的系统上同步绘画。 没有这条线,动画在 Linux 上可能会不流畅。

  1. @Override
  2. public void actionPerformed(ActionEvent e) {
  3. x += 1;
  4. y += 1;
  5. if (y > B_HEIGHT) {
  6. y = INITIAL_Y;
  7. x = INITIAL_X;
  8. }
  9. repaint();
  10. }

计时器反复调用actionPerformed()方法。 在方法内部,我们增加星形对象的 x 和 y 值。 然后我们调用repaint()方法,这将导致paintComponent()被调用。 这样,我们可以定期重绘Board从而制作动画。

动画 - 图1

图:星星

实用计时器

这与以前的方法非常相似。 我们使用java.util.Timer代替javax.Swing.Timer。 对于 Java Swing 游戏,这种方式更为准确。

UtilityTimerEx.java

  1. package com.zetcode;
  2. import java.awt.EventQueue;
  3. import javax.swing.JFrame;
  4. public class UtilityTimerEx extends JFrame {
  5. public UtilityTimerEx() {
  6. initUI();
  7. }
  8. private void initUI() {
  9. add(new Board());
  10. setResizable(false);
  11. pack();
  12. setTitle("Star");
  13. setLocationRelativeTo(null);
  14. setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  15. }
  16. public static void main(String[] args) {
  17. EventQueue.invokeLater(() -> {
  18. JFrame ex = new UtilityTimerEx();
  19. ex.setVisible(true);
  20. });
  21. }
  22. }

这是主要的类。

Board.java

  1. package com.zetcode;
  2. import java.awt.Color;
  3. import java.awt.Dimension;
  4. import java.awt.Graphics;
  5. import java.awt.Image;
  6. import java.awt.Toolkit;
  7. import java.util.Timer;
  8. import java.util.TimerTask;
  9. import javax.swing.ImageIcon;
  10. import javax.swing.JPanel;
  11. public class Board extends JPanel {
  12. private final int B_WIDTH = 350;
  13. private final int B_HEIGHT = 350;
  14. private final int INITIAL_X = -40;
  15. private final int INITIAL_Y = -40;
  16. private final int INITIAL_DELAY = 100;
  17. private final int PERIOD_INTERVAL = 25;
  18. private Image star;
  19. private Timer timer;
  20. private int x, y;
  21. public Board() {
  22. initBoard();
  23. }
  24. private void loadImage() {
  25. ImageIcon ii = new ImageIcon("src/resources/star.png");
  26. star = ii.getImage();
  27. }
  28. private void initBoard() {
  29. setBackground(Color.BLACK);
  30. setPreferredSize(new Dimension(B_WIDTH, B_HEIGHT));
  31. loadImage();
  32. x = INITIAL_X;
  33. y = INITIAL_Y;
  34. timer = new Timer();
  35. timer.scheduleAtFixedRate(new ScheduleTask(),
  36. INITIAL_DELAY, PERIOD_INTERVAL);
  37. }
  38. @Override
  39. public void paintComponent(Graphics g) {
  40. super.paintComponent(g);
  41. drawStar(g);
  42. }
  43. private void drawStar(Graphics g) {
  44. g.drawImage(star, x, y, this);
  45. Toolkit.getDefaultToolkit().sync();
  46. }
  47. private class ScheduleTask extends TimerTask {
  48. @Override
  49. public void run() {
  50. x += 1;
  51. y += 1;
  52. if (y > B_HEIGHT) {
  53. y = INITIAL_Y;
  54. x = INITIAL_X;
  55. }
  56. repaint();
  57. }
  58. }
  59. }

在此示例中,计时器将定期调用ScheduleTask类的run()方法。

  1. timer = new Timer();
  2. timer.scheduleAtFixedRate(new ScheduleTask(),
  3. INITIAL_DELAY, PERIOD_INTERVAL);

在这里,我们创建一个计时器并按特定的时间间隔安排任务。 有一个初始延迟。

  1. @Override
  2. public void run() {
  3. ...
  4. }

计时器每 10 毫秒将调用此run()方法。

线程

使用线程对对象进行动画处理是最有效,最准确的动画处理方式。

ThreadAnimationEx.java

  1. package com.zetcode;
  2. import java.awt.EventQueue;
  3. import javax.swing.JFrame;
  4. public class ThreadAnimationEx extends JFrame {
  5. public ThreadAnimationEx() {
  6. initUI();
  7. }
  8. private void initUI() {
  9. add(new Board());
  10. setResizable(false);
  11. pack();
  12. setTitle("Star");
  13. setLocationRelativeTo(null);
  14. setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  15. }
  16. public static void main(String[] args) {
  17. EventQueue.invokeLater(() -> {
  18. JFrame ex = new ThreadAnimationEx();
  19. ex.setVisible(true);
  20. });
  21. }
  22. }

This is the main class.

Board.java

  1. package com.zetcode;
  2. import java.awt.Color;
  3. import java.awt.Dimension;
  4. import java.awt.Graphics;
  5. import java.awt.Image;
  6. import java.awt.Toolkit;
  7. import javax.swing.ImageIcon;
  8. import javax.swing.JOptionPane;
  9. import javax.swing.JPanel;
  10. public class Board extends JPanel
  11. implements Runnable {
  12. private final int B_WIDTH = 350;
  13. private final int B_HEIGHT = 350;
  14. private final int INITIAL_X = -40;
  15. private final int INITIAL_Y = -40;
  16. private final int DELAY = 25;
  17. private Image star;
  18. private Thread animator;
  19. private int x, y;
  20. public Board() {
  21. initBoard();
  22. }
  23. private void loadImage() {
  24. ImageIcon ii = new ImageIcon("src/resources/star.png");
  25. star = ii.getImage();
  26. }
  27. private void initBoard() {
  28. setBackground(Color.BLACK);
  29. setPreferredSize(new Dimension(B_WIDTH, B_HEIGHT));
  30. loadImage();
  31. x = INITIAL_X;
  32. y = INITIAL_Y;
  33. }
  34. @Override
  35. public void addNotify() {
  36. super.addNotify();
  37. animator = new Thread(this);
  38. animator.start();
  39. }
  40. @Override
  41. public void paintComponent(Graphics g) {
  42. super.paintComponent(g);
  43. drawStar(g);
  44. }
  45. private void drawStar(Graphics g) {
  46. g.drawImage(star, x, y, this);
  47. Toolkit.getDefaultToolkit().sync();
  48. }
  49. private void cycle() {
  50. x += 1;
  51. y += 1;
  52. if (y > B_HEIGHT) {
  53. y = INITIAL_Y;
  54. x = INITIAL_X;
  55. }
  56. }
  57. @Override
  58. public void run() {
  59. long beforeTime, timeDiff, sleep;
  60. beforeTime = System.currentTimeMillis();
  61. while (true) {
  62. cycle();
  63. repaint();
  64. timeDiff = System.currentTimeMillis() - beforeTime;
  65. sleep = DELAY - timeDiff;
  66. if (sleep < 0) {
  67. sleep = 2;
  68. }
  69. try {
  70. Thread.sleep(sleep);
  71. } catch (InterruptedException e) {
  72. String msg = String.format("Thread interrupted: %s", e.getMessage());
  73. JOptionPane.showMessageDialog(this, msg, "Error",
  74. JOptionPane.ERROR_MESSAGE);
  75. }
  76. beforeTime = System.currentTimeMillis();
  77. }
  78. }
  79. }

在前面的示例中,我们以特定的间隔执行任务。 在此示例中,动画将在线程内进行。 run()方法仅被调用一次。 这就是为什么我们在方法中有一个while循环的原因。 从该方法中,我们称为cycle()repaint()方法。

  1. @Override
  2. public void addNotify() {
  3. super.addNotify();
  4. animator = new Thread(this);
  5. animator.start();
  6. }

在将我们的JPanel添加到JFrame组件后,将调用addNotify()方法。 此方法通常用于各种初始化任务。

我们希望我们的游戏以恒定的速度平稳运行。 因此,我们计算系统时间。

  1. timeDiff = System.currentTimeMillis() - beforeTime;
  2. sleep = DELAY - timeDiff;

cycle()repaint()方法可能在不同的while周期中花费不同的时间。 我们计算两种方法的运行时间,并将其从DELAY常数中减去。 这样,我们要确保每个while周期都在恒定时间运行。 在我们的情况下,每个周期为DELAY ms。

Java 2D 游戏教程的这一部分涵盖了动画。