动画
最后修改于 2023 年 1 月 10 日
在 Java 2D 游戏教程的这一部分,我们将学习动画。
动画
动画 是快速显示一系列图像,从而产生运动的错觉。我们将为我们的面板(Board)上的一个星星制作动画。我们将以三种基本方式实现运动。我们将使用 Swing 计时器、标准实用计时器和线程。
动画是游戏编程中一个复杂的主题。Java 游戏需要在具有不同硬件规格的多个操作系统上运行。线程提供了最精确的计时解决方案。然而,对于我们简单的 2D 游戏,其他两种选项也可以考虑。
Swing 计时器
在第一个示例中,我们将使用 Swing 计时器来创建动画。这是在 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() 方法之后调用此方法可能会导致不正确的结果——星星将无法精确地进入窗口的右下角边框。)
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 JPanel
implements 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();
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
drawStar(g);
}
private void drawStar(Graphics g) {
g.drawImage(star, x, y, this);
Toolkit.getDefaultToolkit().sync();
}
@Override
public 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 接口。
@Override
public 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 上可能不够流畅。
@Override
public 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 游戏,这种方式更准确。
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);
});
}
}
这是主类。
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);
}
@Override
public 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 {
@Override
public 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);
在这里,我们创建了一个计时器并安排了一个具有特定时间间隔的任务。有一个初始延迟。
@Override
public void run() {
...
}
每 10 毫秒,计时器将调用此 run() 方法。
线程
使用线程为对象设置动画是最有效和最准确的动画方式。
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);
});
}
}
这是主类。
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 JPanel
implements 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;
}
@Override
public void addNotify() {
super.addNotify();
animator = new Thread(this);
animator.start();
}
@Override
public 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;
}
}
@Override
public 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() 方法。
@Override
public void addNotify() {
super.addNotify();
animator = new Thread(this);
animator.start();
}
addNotify() 方法在我们将 JPanel 添加到 JFrame 组件之后被调用。此方法通常用于各种初始化任务。
我们希望我们的游戏流畅运行,速度恒定。因此,我们计算系统时间。
timeDiff = System.currentTimeMillis() - beforeTime; sleep = DELAY - timeDiff;
cycle 和 repaint 方法在不同的 while 循环中可能花费的时间不同。我们计算这两个方法运行的时间,并将其从 DELAY 常量中减去。这样,我们希望确保每个 while 循环都以恒定的时间运行。在我们的例子中,每个循环是 DELAY 毫秒。
Java 2D 游戏教程的这一部分介绍了动画。