跳转至

动画

原文: 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 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从而制作动画。

Star

图:星星

实用计时器

这与以前的方法非常相似。 我们使用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);        
    }

    @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()方法。

线程

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

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

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

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

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

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

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



回到顶部