原文: https://zetcode.com/tutorials/javagamestutorial/animation/
在 Java 2D 游戏教程的这一部分中,我们将使用动画。
动画
动画是图像序列的快速显示,会产生运动的错觉。 我们将为董事会上的星星设置动画。 我们将以三种基本方式实现这一运动。 我们将使用 Swing 计时器,标准工具计时器和线程。
动画是游戏编程中的一个复杂主题。 Java 游戏有望在具有不同硬件规格的多种操作系统上运行。 线程提供了最准确的计时解决方案。 但是,对于我们简单的 2D 游戏,其他两个选项也可以是一个选项。
Swing 计时器
在第一个示例中,我们将使用 Swing 计时器来创建动画。 这是在 Java 游戏中为对象设置动画的最简单但最无效的方法。
SwingTimerEx.java
package com.zetcode;import java.awt.EventQueue;import javax.swing.JFrame;public class SwingTimerEx extends JFrame {public SwingTimerEx() {initUI();}private void initUI() {add(new Board());setResizable(false);pack();setTitle("Star");setLocationRelativeTo(null);setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);}public static void main(String[] args) {EventQueue.invokeLater(() -> {SwingTimerEx ex = new SwingTimerEx();ex.setVisible(true);});}}
这是代码示例的主要类。
setResizable(false);pack();
setResizable()设置是否可以调整帧大小。 pack()方法使此窗口的大小适合其子级的首选大小和布局。 请注意,这两种方法的调用顺序很重要。 (setResizable()在某些平台上更改了帧的插入;在pack()方法之后调用此方法可能会导致错误的结果-星号不会精确地进入窗口的右下边界。)
Board.java
package com.zetcode;import java.awt.Color;import java.awt.Dimension;import java.awt.Graphics;import java.awt.Image;import java.awt.Toolkit;import java.awt.event.ActionEvent;import java.awt.event.ActionListener;import javax.swing.ImageIcon;import javax.swing.JPanel;import javax.swing.Timer;public class Board extends JPanelimplements ActionListener {private final int B_WIDTH = 350;private final int B_HEIGHT = 350;private final int INITIAL_X = -40;private final int INITIAL_Y = -40;private final int DELAY = 25;private Image star;private Timer timer;private int x, y;public Board() {initBoard();}private void loadImage() {ImageIcon ii = new ImageIcon("src/resources/star.png");star = ii.getImage();}private void initBoard() {setBackground(Color.BLACK);setPreferredSize(new Dimension(B_WIDTH, B_HEIGHT));loadImage();x = INITIAL_X;y = INITIAL_Y;timer = new Timer(DELAY, this);timer.start();}@Overridepublic void paintComponent(Graphics g) {super.paintComponent(g);drawStar(g);}private void drawStar(Graphics g) {g.drawImage(star, x, y, this);Toolkit.getDefaultToolkit().sync();}@Overridepublic void actionPerformed(ActionEvent e) {x += 1;y += 1;if (y > B_HEIGHT) {y = INITIAL_Y;x = INITIAL_X;}repaint();}}
在Board类中,我们将星星从左上角移到右下角。
private final int B_WIDTH = 350;private final int B_HEIGHT = 350;private final int INITIAL_X = -40;private final int INITIAL_Y = -40;private final int DELAY = 25;
定义了五个常数。 前两个常数是板的宽度和高度。 第三和第四是星星的初始坐标。 最后一个确定动画的速度。
private void loadImage() {ImageIcon ii = new ImageIcon("src/resources/star.png");star = ii.getImage();}
在loadImage()方法中,我们创建ImageIcon类的实例。 该图像位于项目目录中。 getImage()方法将从此类返回Image对象。 该对象将绘制在板上。
timer = new Timer(DELAY, this);timer.start();
在这里,我们创建一个 Swing Timer类,并调用其start()方法。 计时器每DELAY毫秒就会调用一次actionPerformed()方法。 为了使用actionPerformed()方法,我们必须实现ActionListener接口。
@Overridepublic void paintComponent(Graphics g) {super.paintComponent(g);drawStar(g);}
自定义绘画是通过paintComponent()方法完成的。 请注意,我们还调用其父级的paintComponent()方法。 实际绘画将委托给drawStar()方法。
private void drawStar(Graphics g) {g.drawImage(star, x, y, this);Toolkit.getDefaultToolkit().sync();}
在drawStar()方法中,我们使用drawImage()方法在窗口上绘制图像。 Toolkit.getDefaultToolkit().sync()在缓冲图形事件的系统上同步绘画。 没有这条线,动画在 Linux 上可能会不流畅。
@Overridepublic void actionPerformed(ActionEvent e) {x += 1;y += 1;if (y > B_HEIGHT) {y = INITIAL_Y;x = INITIAL_X;}repaint();}
计时器反复调用actionPerformed()方法。 在方法内部,我们增加星形对象的 x 和 y 值。 然后我们调用repaint()方法,这将导致paintComponent()被调用。 这样,我们可以定期重绘Board从而制作动画。

图:星星
实用计时器
这与以前的方法非常相似。 我们使用java.util.Timer代替javax.Swing.Timer。 对于 Java Swing 游戏,这种方式更为准确。
UtilityTimerEx.java
package com.zetcode;import java.awt.EventQueue;import javax.swing.JFrame;public class UtilityTimerEx extends JFrame {public UtilityTimerEx() {initUI();}private void initUI() {add(new Board());setResizable(false);pack();setTitle("Star");setLocationRelativeTo(null);setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);}public static void main(String[] args) {EventQueue.invokeLater(() -> {JFrame ex = new UtilityTimerEx();ex.setVisible(true);});}}
这是主要的类。
Board.java
package com.zetcode;import java.awt.Color;import java.awt.Dimension;import java.awt.Graphics;import java.awt.Image;import java.awt.Toolkit;import java.util.Timer;import java.util.TimerTask;import javax.swing.ImageIcon;import javax.swing.JPanel;public class Board extends JPanel {private final int B_WIDTH = 350;private final int B_HEIGHT = 350;private final int INITIAL_X = -40;private final int INITIAL_Y = -40;private final int INITIAL_DELAY = 100;private final int PERIOD_INTERVAL = 25;private Image star;private Timer timer;private int x, y;public Board() {initBoard();}private void loadImage() {ImageIcon ii = new ImageIcon("src/resources/star.png");star = ii.getImage();}private void initBoard() {setBackground(Color.BLACK);setPreferredSize(new Dimension(B_WIDTH, B_HEIGHT));loadImage();x = INITIAL_X;y = INITIAL_Y;timer = new Timer();timer.scheduleAtFixedRate(new ScheduleTask(),INITIAL_DELAY, PERIOD_INTERVAL);}@Overridepublic void paintComponent(Graphics g) {super.paintComponent(g);drawStar(g);}private void drawStar(Graphics g) {g.drawImage(star, x, y, this);Toolkit.getDefaultToolkit().sync();}private class ScheduleTask extends TimerTask {@Overridepublic void run() {x += 1;y += 1;if (y > B_HEIGHT) {y = INITIAL_Y;x = INITIAL_X;}repaint();}}}
在此示例中,计时器将定期调用ScheduleTask类的run()方法。
timer = new Timer();timer.scheduleAtFixedRate(new ScheduleTask(),INITIAL_DELAY, PERIOD_INTERVAL);
在这里,我们创建一个计时器并按特定的时间间隔安排任务。 有一个初始延迟。
@Overridepublic void run() {...}
计时器每 10 毫秒将调用此run()方法。
线程
使用线程对对象进行动画处理是最有效,最准确的动画处理方式。
ThreadAnimationEx.java
package com.zetcode;import java.awt.EventQueue;import javax.swing.JFrame;public class ThreadAnimationEx extends JFrame {public ThreadAnimationEx() {initUI();}private void initUI() {add(new Board());setResizable(false);pack();setTitle("Star");setLocationRelativeTo(null);setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);}public static void main(String[] args) {EventQueue.invokeLater(() -> {JFrame ex = new ThreadAnimationEx();ex.setVisible(true);});}}
This is the main class.
Board.java
package com.zetcode;import java.awt.Color;import java.awt.Dimension;import java.awt.Graphics;import java.awt.Image;import java.awt.Toolkit;import javax.swing.ImageIcon;import javax.swing.JOptionPane;import javax.swing.JPanel;public class Board extends JPanelimplements Runnable {private final int B_WIDTH = 350;private final int B_HEIGHT = 350;private final int INITIAL_X = -40;private final int INITIAL_Y = -40;private final int DELAY = 25;private Image star;private Thread animator;private int x, y;public Board() {initBoard();}private void loadImage() {ImageIcon ii = new ImageIcon("src/resources/star.png");star = ii.getImage();}private void initBoard() {setBackground(Color.BLACK);setPreferredSize(new Dimension(B_WIDTH, B_HEIGHT));loadImage();x = INITIAL_X;y = INITIAL_Y;}@Overridepublic void addNotify() {super.addNotify();animator = new Thread(this);animator.start();}@Overridepublic void paintComponent(Graphics g) {super.paintComponent(g);drawStar(g);}private void drawStar(Graphics g) {g.drawImage(star, x, y, this);Toolkit.getDefaultToolkit().sync();}private void cycle() {x += 1;y += 1;if (y > B_HEIGHT) {y = INITIAL_Y;x = INITIAL_X;}}@Overridepublic void run() {long beforeTime, timeDiff, sleep;beforeTime = System.currentTimeMillis();while (true) {cycle();repaint();timeDiff = System.currentTimeMillis() - beforeTime;sleep = DELAY - timeDiff;if (sleep < 0) {sleep = 2;}try {Thread.sleep(sleep);} catch (InterruptedException e) {String msg = String.format("Thread interrupted: %s", e.getMessage());JOptionPane.showMessageDialog(this, msg, "Error",JOptionPane.ERROR_MESSAGE);}beforeTime = System.currentTimeMillis();}}}
在前面的示例中,我们以特定的间隔执行任务。 在此示例中,动画将在线程内进行。 run()方法仅被调用一次。 这就是为什么我们在方法中有一个while循环的原因。 从该方法中,我们称为cycle()和repaint()方法。
@Overridepublic void addNotify() {super.addNotify();animator = new Thread(this);animator.start();}
在将我们的JPanel添加到JFrame组件后,将调用addNotify()方法。 此方法通常用于各种初始化任务。
我们希望我们的游戏以恒定的速度平稳运行。 因此,我们计算系统时间。
timeDiff = System.currentTimeMillis() - beforeTime;sleep = DELAY - timeDiff;
cycle()和repaint()方法可能在不同的while周期中花费不同的时间。 我们计算两种方法的运行时间,并将其从DELAY常数中减去。 这样,我们要确保每个while周期都在恒定时间运行。 在我们的情况下,每个周期为DELAY ms。
Java 2D 游戏教程的这一部分涵盖了动画。
