奔跑吧小恐龙是一款简单的跑酷游戏(代码简单,适合初学者学习)。玩家控制小恐龙向前狂奔,躲避沿途出现的石头和仙人掌,跑的越远,分数越高。游戏内还增加了背景音乐、跳跃音乐和碰撞音乐。
本文的代码虽然长,但不难理解,希望大家能够耐心看完。
文中代码均可直接运行,完整代码请见github
文章目录
- 系统结构设计
- 游戏功能架构图
- 项目目录结构预览
- 游戏模型设计
- 恐龙类
- 障碍类
- 音效模块设计
- 音频播放器
系统结构设计
游戏功能架构图
项目目录结构预览
游戏模型设计
恐龙类
1.定义
Dinosaur类的成员属性绝大多数都是私有属性,只有少数公有属性用于游戏面板绘图使用。Dinosaur类的私有属性包含3张来回切换的跑步照片:
Dinosaur类的定义如下:
public class Dinosaur {
public BufferedImage image;// 主图片
private BufferedImage image1, image2, image3;// 跑步图片
public int x, y;// 坐标
private int jumpValue = 0;// 跳跃的增变量
private boolean jumpState = false;// 跳跃状态
private int stepTimer = 0;// 踏步计时器
private final int JUMP_HIGHT = 100;// 跳起最大高度
private final int LOWEST_Y = 120;// 落地最低坐标
private final int FREASH = FreshThread.FREASH;// 刷新时间--刷新帧线程
}
在Dinosaur类的构造方法中将恐龙的横坐标固定在50像素的位置,纵坐标采用落地时的坐标,具体代码如下:
public Dinosaur() {
x = 50; // 恐龙的横坐标
y = LOWEST_Y; // 恐龙的纵坐标
try {
image1 = ImageIO.read(new File("image/恐龙1.png")); // 读取恐龙的图片
image2 = ImageIO.read(new File("image/恐龙2.png"));
image3 = ImageIO.read(new File("image/恐龙3.png"));
} catch (IOException e) {
e.printStackTrace();
}
}
2.踏步
游戏中恐龙的横坐标虽然一直没变,但背景的运动会造成恐龙向前移动的假象。Step()方法就是踏步动作的方法,具体代码如下:
/**
* 踏步
*/
private void step() {
// 每过250毫秒,更换一张图片。因为共有3图片,所以除以3取余,轮流展示这三张
int tmp = stepTimer / 250 % 3; // 0、1、2
switch (tmp) {
case 1 :
image = image1;
break;
case 2 :
image = image2;
break;
default :
image = image3;
}
stepTimer += FREASH;// 计时器递增
}
3.跳跃
跳跃是小恐龙躲避障碍的动作,也是玩家可以控制恐龙做出的唯一动作。
具体代码如下:
/**
* 跳跃
*/
public void jump() {
if (!jumpState) {// 如果没处于跳跃状态
Sound.jump();// 播放跳跃音效
}
jumpState = true;// 处于跳跃状态
}
4.移动
move()是恐龙移动方法,move()方法不断地调用step()踏步方法
stepTimer踏步计时器会有效控制图片地切换频率
move()方法地具体代码如下:
/**
* 移动
*/
public void move() {
step();// 不断踏步
if (jumpState) {// 如果正在跳跃
if (y == LOWEST_Y) {// 如果纵坐标大于等于最低点---(越往上坐标越小)
jumpValue = -4;// 增变量为负值--向上跳
}
if (y <= LOWEST_Y - JUMP_HIGHT) {// 如果跳过最高点
jumpValue = 4;// 增变量为正值--向下跳
}
y += jumpValue;// 纵坐标发生变化
if (y == LOWEST_Y) {// 如果再次落地
jumpState = false;// 停止跳跃
}
}
}
5.边界对象
java.awt.Rectangle类是举行边界类。为恐龙的头部和足部创建矩形边界对象,用于做碰撞测试
getFootBounds()方法用于获取恐龙的足部边界对象,具体代码如下:
/**
* 足部边界区域
*
* @return
*/
public Rectangle getFootBounds() { // 获取恐龙的脚部边界对象
return new Rectangle(x + 30, y + 59, 29, 18); // 用于后续做碰撞检测
}
getHeadBounds()方法用于获取恐龙的头部边界对象,具体代码如下:
/**
* 头部边界区域
*
* @return
*/
public Rectangle getHeadBounds() { // 获取恐龙的头部边界对象
return new Rectangle(x + 66, y + 25, 32, 22); // new Rectangle(x, y, width, height)
}
障碍类
游戏中的障碍有两种,一种是很矮的石头,另一种是很高的仙人掌,如下图所示。恐龙碰到任何一种障碍,都会导致游戏的结束:
1.定义
项目中的com.mr.modle.Obstacle类是障碍类,
因为所有的障碍都会随着背景一起移动,所以障碍的移动速度采用背景图片的速度。Obstacle类的定义如下:
public class Obstacle {
public int x, y;// 横纵坐标
public BufferedImage image;
private BufferedImage stone;// 石头图片---(32,26)
private BufferedImage cacti;// 仙人掌图片---(32,59)
private int speed;// 移动速度--图片跟着背景走
}
在Obstacle类的构造方法中,Obstacle对象会随机成为石头或者仙人掌。具体代码如下:
public Obstacle() {
try {
stone = ImageIO.read(new File("image/石头.png")); // 石头图片
cacti = ImageIO.read(new File("image/仙人掌.png")); // 仙人掌图片
} catch (IOException e) {
e.printStackTrace();
}
Random r = new Random();// 创建随机对象
if (r.nextInt(2) == 0) {// 从0和1中取一值,若为0
image = cacti;// 采用仙人掌图片
} else {
image = stone;// 采用石头图片
}
x = 800;// 初始横坐标
y = 200 - image.getHeight();// 纵坐标--使图片处在地平线上
// System.out.println(image.getWidth());
speed = BackgroundImage.SPEED;// 移动速度与背景同步--BackgroundImage=背景
}
2.移动
Obstacle类中的move()方法可以让障碍在游戏画面中移动,且移动速度与背景图片的移动速度相同。move()方法的具体代码如下:
/**
* 移动
*/
public void move() {
x -= speed;// 横坐标递减--障碍物的速度与背景的速度一致
}
3.消除
当障碍移动出游戏画面之后,就不会再对游戏产生任何影响。
要及时将移出画面之外的障碍删除掉,isLive()方法用于获取障碍的有效状态。isLive()的具体代码如下:
/**
* 是否存活
*
* @return
*/
public boolean isLive() {
// 如果移出了游戏界面
if (x <= -image.getWidth()) {
return false;// 消亡
}
return true;// 存活
}
4.边界对象
障碍的边界对象用于做碰撞检测计算。
用getBounds()方法返回边界对象,该方法的具体代码如下:
/**
* 获取边界
*
* @return
*/
// 通过getBounds()方法返回边界对象
public Rectangle getBounds() {
if (image == cacti) {// 如果使用仙人掌图片
// 返回仙人掌的边界
return new Rectangle(x + 7, y, 15, image.getHeight());
}
// 返回石头的边界
return new Rectangle(x + 5, y + 4, 23, 21);
}
音效模块设计
本节介绍如何利用java代码播放音乐文件。
JDK支持播放的音乐格式非常少,本章使用的所有音乐文件均为WAVE格式。JDK支持的具体音乐格式可以参考javax.sound.sampled.AudioFileFormat.Type类
音频播放器
项目中的com.mr.service.MusicPlayer类是音频播放器类,该类使用javax.sound.sampled包提供的混音器工具实现音频播放功能。
MusicPlayer类实现了Runnerable接口,并在成员属性中定义了一个线程对象,该线程对象用于启动混音器数据行的读写业务。MusicPlayer类的定义如下:
```java
/**
* 音乐播放器
*/
public class MusicPlayer implements Runnable {
File soundFile; // 音乐文件
Thread thread;// 父线程 -- 执行run方法
boolean circulate;// 是否循环播放
MusicPlayer类的构造方法有两个参数,filepath表示音乐文件的完整文件名,circulate表示是否重复播放。MusicPlayer类构造方法的具体代码如下:
/**
* 构造方法
*
* @param filepath 音乐文件完整名称
* @param circulate 是否循环播放
* @throws FileNotFoundException
*/
// 构造方法--2
public MusicPlayer(String filepath, boolean circulate) throws FileNotFoundException {
this.circulate = circulate;
soundFile = new File(filepath); // File soundFile
if (!soundFile.exists()) {// 如果文件不存在
throw new FileNotFoundException(filepath + "未找到"); // 抛出错误,文件未找到
}
}
MusicPlayer类要实现Runnable接口,必须先实现run()方法。
在run()方法中声明了一个128K的缓冲区字节数组,程序以不断循环的方式将音乐文件以音频输入流格式读入缓冲区,再把缓冲区的数据写入混音器源数据行中,实现播放音乐的效果。run()方法的具体代码如下:
```java
/**
* 音乐播放器
*/
public class MusicPlayer implements Runnable {
File soundFile; // 音乐文件
Thread thread;// 父线程 -- 执行run方法
boolean circulate;// 是否循环播放
MusicPlayer类的构造方法有两个参数,filepath表示音乐文件的完整文件名,circulate表示是否重复播放。MusicPlayer类构造方法的具体代码如下:
/**
* 构造方法
*
* @param filepath 音乐文件完整名称
* @param circulate 是否循环播放
* @throws FileNotFoundException
*/
// 构造方法--2
public MusicPlayer(String filepath, boolean circulate) throws FileNotFoundException {
this.circulate = circulate;
soundFile = new File(filepath); // File soundFile
if (!soundFile.exists()) {// 如果文件不存在
throw new FileNotFoundException(filepath + "未找到"); // 抛出错误,文件未找到
}
}
MusicPlayer类要实现Runnable接口,必须先实现run()方法。
在run()方法中声明了一个128K的缓冲区字节数组,程序以不断循环的方式将音乐文件以音频输入流格式读入缓冲区,再把缓冲区的数据写入混音器源数据行中,实现播放音乐的效果。run()方法的具体代码如下:
/*
* 在run()方法中声明一个128K的缓冲区字节数组
* 程序以不断循环的方式将音乐文件以音频输入流格式读入缓冲区
* 把缓冲区的数据写入混音器源数据行中
* */
public void run() {
byte[] auBuffer = new byte[1024 * 128];// 创建128k缓冲区
do {
AudioInputStream audioInputStream = null; // 创建音频输入流对象
SourceDataLine auline = null; // 混频器源数据行
try {
// 从音乐文件中获取音频输入流
audioInputStream = AudioSystem.getAudioInputStream(soundFile);// soundFile--音乐文件
AudioFormat format = audioInputStream.getFormat(); // 获取音频格式
// System.out.println(audioInputStream.getFormat());
// 按照源数据行类型和指定音频格式创建数据行对象
DataLine.Info info = new DataLine.Info(SourceDataLine.class, format); // 获取音乐格式
// 利用音频系统类获得与指定 Line.Info 对象中的描述匹配的行,并转换为源数据行对象
auline = (SourceDataLine) AudioSystem.getLine(info);
auline.open(format);// 按照指定格式打开源数据行
auline.start();// 源数据行开启读写活动
int byteCount = 0;// 记录音频输入流读出的字节数
while (byteCount != -1) {// 如果音频输入流中读取的字节数不为-1
// 从音频数据流中读出128K的数据--auBuffer=128k缓冲区
byteCount = audioInputStream.read(auBuffer, 0, auBuffer.length);
if (byteCount >= 0) {// 如果读出有效数据--auline=混频器源数据行
auline.write(auBuffer, 0, byteCount);// 将有效数据写入数据行中
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (UnsupportedAudioFileException e) {
e.printStackTrace();
} catch (LineUnavailableException e) {
e.printStackTrace();
} finally {
auline.drain();// 清空数据行
auline.close();// 关闭数据行
}
} while (circulate);// 根据循环标志判断是否循环播放
}
现在只要将MusicPlayer类放入线程中,就可以播放音乐。创建play()方法,传入参数,然后开启线程。play()方法的具体代码如下:
/**
* 播放
*/
public void play() {
thread = new Thread(this);// 创建线程对象
thread.start();// 开启线程
}
创建stop()方法,在该方法中强制停止线程。
stop()方法的具体代码如下:
/**
* 停止播放
*/
public void stop() {
thread.stop();// 强制关闭线程
}