61、动画与游戏编程:从弹球到简单游戏实现

动画与游戏编程:从弹球到简单游戏实现

在动画和游戏编程领域,弹球效果是一个基础且有趣的示例,它能帮助我们理解动画的基本原理,如物体的移动、碰撞检测等。下面将详细介绍从简单的弹球动画到实现一个简单游戏的过程。

1. 弹球动画基础

首先,我们从一个简单的弹球动画开始。 PaintSurface 类继承自 JComponent ,用于定义动画的表面。该类的实例变量定义了球的特征,包括球在组件上的 x y 位置以及球的直径。

class PaintSurface extends JComponent
{
    int x_pos = 0;
    int y_pos = 0;
    int d = 20;
    int width = BallRoom.WIDTH;
    int height = BallRoom.HEIGHT;

    public void paint(Graphics g)
    {
        Graphics2D g2 = (Graphics2D)g;
        g2.setRenderingHint(
            RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
        x_pos += 1;
        Shape ball = new Ellipse2D.Float(
            x_pos, y_pos, d, d);
        g2.setColor(Color.RED);
        g2.fill(ball);
    }
}

paint 方法在 PaintSurface 组件需要重绘时被调用,这里通过 AnimationThread 类的 run 方法每 20 毫秒触发一次。该方法首先将图形上下文转换为 Graphics2D 对象并启用抗锯齿,然后通过将 x 位置加 1 来计算球的新位置,最后创建一个 Shape 对象表示球并绘制出来。

2. 双缓冲技术

在动画和游戏编程中,双缓冲技术是实现平滑、无闪烁动画的关键。传统上,使用双缓冲时,不直接将形状绘制到组件上,而是创建一个名为缓冲区的离屏图像对象,将形状绘制到该对象上,绘制完成后将整个缓冲区图像传输到组件上。

幸运的是,在 Swing 组件上进行的任何绘制都会自动进行双缓冲。如果出于某种原因想关闭双缓冲,可以调用绘制组件的 setDoubleBuffered 方法:

this.setDoubleBuffered(false);
3. 实现弹球反弹效果

前面的弹球动画只是让球沿直线飞过屏幕并消失,为了让动画更有趣,我们需要让球在不同方向上移动并从组件边缘反弹。实现这一效果有两种基本方法:
- 精确计算法 :跟踪球的两个变量,即它的运动角度和速度,然后使用高中三角函数为每个动画周期计算球的新 (x, y) 位置。如果球撞到边缘,还需要计算球的新角度,这种方法需要一定的数学知识。
- 简单加法法 :存储两个变量 x_speed y_speed ,分别表示球在每个动画周期内水平和垂直移动的距离。每个周期只需将 x_speed 加到 x 位置,将 y_speed 加到 y 位置。如果球撞到左右边缘,将 x_speed 取反以反转水平方向;如果撞到上下边缘,将 y_speed 取反以反转垂直方向。

以下是实现反弹效果的 PaintSurface 类代码:

class PaintSurface extends JComponent
{
    int x_pos = 0;
    int y_pos = 0;
    int x_speed = 1;
    int y_speed = 2;
    int d = 20;
    int width = BallRoom.WIDTH;
    int height = BallRoom.HEIGHT;

    public void paint(Graphics g)
    {
        Graphics2D g2 = (Graphics2D)g;
        g2.setRenderingHint(
            RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
        if (x_pos < 0 || x_pos > width - d)
            x_speed = -x_speed;
        if (y_pos < 0 || y_pos > height - d)
            y_speed = -y_speed;
        x_pos += x_speed;
        y_pos += y_speed;
        Shape ball = new Ellipse2D.Float(
            x_pos, y_pos, d, d);
        g2.setColor(Color.RED);
        g2.fill(ball);
    }
}

该类的关键元素如下:
- 实例变量跟踪球的 x y 位置、速度、直径以及绘图表面的高度和宽度。
- if 语句检查球是否撞到左右边缘或上下边缘,如果是则反转相应的速度。
- 调整速度后,更新球的位置并绘制。

4. 多个弹球动画

大多数游戏需要同时动画多个精灵,例如屏幕上可能同时有多个球。为了实现这一点,我们可以创建一个表示单个球的类,并将其实例添加到数组列表或其他集合中,然后在 paint 方法中使用循环移动和绘制每个精灵。

4.1 创建 Ball
class Ball extends Ellipse2D.Float
{
    private int x_speed, y_speed;
    private int d;
    private int width = BallRoom.WIDTH;
    private int height = BallRoom.HEIGHT;

    public Ball(int diameter)
    {
        super((int)(Math.random() * (BallRoom.WIDTH - 20) + 1),
              (int)(Math.random() * (BallRoom.HEIGHT - 20) + 1),
              diameter, diameter);
        this.d = diameter;
        this.x_speed = (int)(Math.random() * 5 + 1);
        this.y_speed = (int)(Math.random() * 5 + 1);
    }

    public void move()
    {
        if (super.x < 0 || super.x > width - d)
            x_speed = -x_speed;
        if (super.y < 0 || super.y > height - d)
            y_speed = -y_speed;
        super.x += x_speed;
        super.y += y_speed;
    }
}

这个类的特点如下:
- 继承自 Ellipse2D.Float ,这样可以直接将 Ball 对象传递给 draw fill 方法来绘制球。
- 定义了五个私有实例变量,分别表示球的 x y 速度、直径以及组件的宽度和高度。由于 Ellipse2D.Float 类已经跟踪其 x y 位置,所以不需要额外的实例变量。
- 构造函数接受球的直径作为参数,并随机计算其他值,因此每次创建的球都有不同的起始位置和轨迹。
- move 方法用于移动球,首先检查球是否撞到边缘并调整轨迹,然后更新球的位置。

4.2 动画多个随机球

以下是一个 PaintSurface 类,用于创建一个包含 10 个随机放置球的数组列表,并在 paint 方法中绘制每个球:

class PaintSurface extends JComponent
{
    public ArrayList<Ball> balls = new ArrayList<Ball>();

    public PaintSurface()
    {
        for (int i = 0; i < 10; i++)
            balls.add(new Ball(20));
    }

    public void paint(Graphics g)
    {
        Graphics2D g2 = (Graphics2D)g;
        g2.setRenderingHint(
            RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setColor(Color.RED);
        for (Ball ball : balls)
        {
            ball.move();
            g2.fill(ball);
        }
    }
}

这个类的工作流程如下:
1. 声明一个名为 balls 的实例变量,用于存储要动画的球。
2. 在构造函数中,使用 for 循环创建 10 个球并添加到集合中。
3. 在 paint 方法中,每 20 毫秒调用一次,使用 for 循环调用每个球的 move 方法,然后将球传递给 fill 方法在组件上绘制。

5. 实现可碰撞的球

前面创建的球有一个不太真实的行为,即它们相互透明,如果两个球同时到达同一位置,它们会直接穿过彼此。为了让球既能从墙壁反弹又能相互反弹,需要修改 Ball 类的 move 方法。

class Ball extends Ellipse2D.Float
{
    public int x_speed, y_speed;
    private int d;
    private int width = BallRoom.WIDTH;
    private int height = BallRoom.HEIGHT;
    private ArrayList<Ball> balls;

    public Ball(int diameter, ArrayList<Ball> balls)
    {
        super((int)(Math.random() * (BallRoom.WIDTH - 20) + 1),
              (int)(Math.random() * (BallRoom.HEIGHT - 20) + 1),
              diameter, diameter);
        this.d = diameter;
        this.x_speed = (int)(Math.random() * 5 + 1);
        this.y_speed = (int)(Math.random() * 5 + 1);
        this.balls = balls;
    }

    public void move()
    {
        // detect collision with other balls
        Rectangle2D r = new Rectangle2D.Float(
            super.x, super.y, d, d);
        for (Ball b : balls)
        {
            if (b != this &&
                b.intersects(r))
            {
                // on collision, the balls swap speeds
                int tempx = x_speed;
                int tempy = y_speed;
                x_speed = b.x_speed;
                y_speed = b.y_speed;
                b.x_speed = tempx;
                b.y_speed = tempy;
                break;
            }
        }
        if (super.x < 0)
        {
            super.x = 0;
            x_speed = Math.abs(x_speed);
        }
        else if (super.x > width - d)
        {
            super.x = width - d;
            x_speed = -Math.abs(x_speed);
        }
        if (super.y < 0)
        {
            super.y = 0;
            y_speed = Math.abs(y_speed);
        }
        else if (super.y > height - d)
        {
            super.y = height - d;
            y_speed = -Math.abs(y_speed);
        }
        super.x += x_speed;
        super.y += y_speed;
    }
}

这个版本的 Ball 类的关键步骤如下:
- 构造函数接受一个引用,指向存储所有球的数组列表,以便每个球可以确定是否与其他球发生碰撞。
- move 方法首先创建一个矩形表示当前球,然后使用 for 循环检查是否与其他球发生碰撞。如果发生碰撞,交换两个球的 x y 速度。
- 检查球是否撞到边缘,如果是则调整球的位置和速度,确保球在组件内移动。

弹球动画与简单游戏实现的流程总结

graph TD;
    A[开始] --> B[创建PaintSurface类绘制单个球];
    B --> C[使用双缓冲技术优化动画];
    C --> D[实现球的反弹效果];
    D --> E[创建Ball类实现多个球动画];
    E --> F[实现球的相互碰撞效果];
    F --> G[添加用户交互实现简单游戏];
    G --> H[结束];

代码关键元素总结

功能 关键代码元素
单个球动画 PaintSurface 类的 paint 方法
双缓冲 setDoubleBuffered 方法
球反弹 x_speed y_speed 的调整
多个球动画 Ball 类和 ArrayList
球碰撞 Ball 类的 move 方法中的碰撞检测
用户交互 鼠标事件监听器

通过以上步骤,我们从简单的弹球动画逐步实现了多个球的动画、球的相互碰撞以及用户交互的简单游戏,掌握了动画和游戏编程的一些基本技巧。接下来,将进一步介绍如何将这些技术应用到一个简单的 Pong 类游戏中。

动画与游戏编程:从弹球到简单游戏实现

6. 实现简单游戏 - 类 Pong 游戏

将动画程序转变为游戏程序的关键在于添加用户交互,可通过鼠标或键盘实现。具体做法是添加事件监听器来处理键盘或鼠标事件,然后在事件监听器中根据用户的操作对游戏精灵进行相应的更改。

以下是一个简单的类 Pong 游戏的完整代码:

import java.applet.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.awt.geom.*;

public class NotPong extends JApplet
{
    public static final int WIDTH = 400;
    public static final int HEIGHT = 400;
    private PaintSurface canvas;

    public void init()
    {
        this.setSize(WIDTH, HEIGHT);
        canvas = new PaintSurface();
        this.add(canvas, BorderLayout.CENTER);
        Thread t = new AnimationThread(this);
        t.start();
    }
}

class AnimationThread extends Thread
{
    JApplet c;

    public AnimationThread(JApplet c)
    {
        this.c = c;
    }

    public void run()
    {
        while (true)
        {
            c.repaint();
            try
            {
                Thread.sleep(20);
            }
            catch (InterruptedException ex)
            {
                // swallow the exception
            }
        }
    }
}

class PaintSurface extends JComponent
{
    int paddle_x = 0;
    int paddle_y = 360;
    int score = 0;
    float english = 1.0f;
    Ball ball;
    Color[] color = {Color.RED, Color.ORANGE, 
                     Color.MAGENTA, Color.ORANGE, 
                     Color.CYAN, Color.BLUE};
    int colorIndex;

    public PaintSurface()
    {
        addMouseMotionListener(new MouseMotionAdapter()
        {
            public void mouseMoved(MouseEvent e)
            {
                if (e.getX() - 30 - paddle_x > 5)
                    english = 1.5f;
                else if (e.getX() - 30 - paddle_x < -5)
                    english = -1.5f;
                else
                    english = 1.0f;
                paddle_x = e.getX() - 30;
            }
        } );
        ball = new Ball(20);
    }

    public void paint(Graphics g)
    {
        Graphics2D g2 = (Graphics2D)g;
        g2.setRenderingHint(
            RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
        Shape paddle = new Rectangle2D.Float(
            paddle_x, paddle_y, 60, 8);
        g2.setColor(color[colorIndex % 6]);
        if (ball.intersects(paddle_x, paddle_y, 60, 8)
            && ball.y_speed > 0)
        {
            ball.y_speed = -ball.y_speed;
            ball.x_speed = (int)(ball.x_speed * english);
            if (english != 1.0f)
                colorIndex++;
            score += Math.abs(ball.x_speed * 10);
        }
        if (ball.getY() + ball.getHeight()
            >= NotPong.HEIGHT)
        {
            ball = new Ball(20);
            score -= 1000;
            colorIndex = 0;
        }
        ball.move();
        g2.fill(ball);
        g2.setColor(Color.BLACK);
        g2.fill(paddle);
        g2.drawString("Score: " + score, 250, 20);
    }
}

class Ball extends Ellipse2D.Float
{
    public int x_speed, y_speed;
    private int d;
    private int width = NotPong.WIDTH;
    private int height = NotPong.HEIGHT;

    public Ball(int diameter)
    {
        super((int)(Math.random() * (NotPong.WIDTH - 20) + 1),
              0, diameter, diameter);
        this.d = diameter;
        this.x_speed = (int)(Math.random() * 5 + 5);
        this.y_speed = (int)(Math.random() * 5 + 5);
    }

    public void move()
    {
        if (super.x < 0 || super.x > width - d)
            x_speed = -x_speed;
        if (super.y < 0 || super.y > height - d)
            y_speed = -y_speed;
        super.x += x_speed;
        super.y += y_speed;
    }
}

这个程序的详细解释如下:
- NotPong
- 继承自 JApplet ,可以通过少量修改使其作为独立的 Swing 应用程序运行。
- init 方法在小程序启动时被调用,它设置小程序的大小,创建一个新的 PaintSurface 对象并添加到小程序中,然后创建并启动控制动画的线程。
- AnimationThread
- 与前面的程序中的 AnimationThread 类相同。在 run 方法中,使用 while 循环调用 repaint 方法强制动画更新,然后休眠 20 毫秒。
- PaintSurface
- 继承自 JComponent ,提供绘制动画的表面。
- 实例变量定义了球拍的初始位置、分数、应用于球的旋转(English)、球对象以及球的颜色数组和索引。
- 构造函数添加了一个鼠标运动监听器,用于更新球拍的 x 位置,并创建一个新的球对象。
- paint 方法在组件重绘时被调用,大约每 20 毫秒一次。该方法首先将图形上下文转换为 Graphics2D 对象并启用抗锯齿,然后创建球拍的 Shape 对象,检查球是否与球拍碰撞,如果碰撞则更新球的速度和颜色索引,并增加分数;如果球撞到南墙,则创建一个新球,重置颜色索引并扣除 1000 分;最后移动球、绘制球和球拍,并显示分数。
- Ball
- 与前面创建的 Ball 类类似,用于定义球的属性和移动方法。

7. 简单游戏实现步骤总结
graph TD;
    A[创建NotPong类] --> B[设置小程序大小并添加PaintSurface];
    B --> C[创建并启动动画线程];
    C --> D[创建PaintSurface类处理绘制和交互];
    D --> E[添加鼠标运动监听器更新球拍位置];
    E --> F[在paint方法中检查球与球拍碰撞];
    F --> G[根据碰撞结果更新球和分数];
    G --> H[创建Ball类定义球的属性和移动];
    H --> I[结束];
8. 游戏关键元素总结
功能 关键代码元素
小程序启动 NotPong 类的 init 方法
动画更新 AnimationThread 类的 run 方法
球拍移动 PaintSurface 类的鼠标事件监听器
球与球拍碰撞 PaintSurface 类的 paint 方法中的碰撞检测
分数计算 PaintSurface 类的 paint 方法中的分数更新
球的属性和移动 Ball 类的构造函数和 move 方法

通过以上步骤,我们成功实现了一个简单的类 Pong 游戏,从简单的弹球动画逐步引入用户交互,完成了从动画到游戏的转变。在实际应用中,可以根据需求进一步扩展和优化游戏,例如增加关卡、改变球的速度和颜色等,以提高游戏的趣味性和挑战性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值