动画与游戏编程:从弹球到简单游戏实现
在动画和游戏编程领域,弹球效果是一个基础且有趣的示例,它能帮助我们理解动画的基本原理,如物体的移动、碰撞检测等。下面将详细介绍从简单的弹球动画到实现一个简单游戏的过程。
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 游戏,从简单的弹球动画逐步引入用户交互,完成了从动画到游戏的转变。在实际应用中,可以根据需求进一步扩展和优化游戏,例如增加关卡、改变球的速度和颜色等,以提高游戏的趣味性和挑战性。
超级会员免费看
2432

被折叠的 条评论
为什么被折叠?



