前言
一 系统功能结构图
二 系统业务流程图
三 程序目录结构
一 游戏模型设计
1.恐龙类
奔跑的小恐龙是游戏的主角,也是玩家控制的角色。项目中的model.Dinosaur就是恐龙类。
1-1 定义
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; //刷新时间
}
在构造方法中我们要设置恐龙的初始状态,将恐龙横坐标固定在50像素,纵坐标采用落地时的坐标120像素,构造方法的代码如下:
public Dinosaur() {
x=50;//横坐标默认是50;
y=LOWEST_Y;//纵坐标默认起始值是120
image1=ImageIO.read(new File("image/恐龙1.png"));
image2=ImageIO.read(new File("image/恐龙2.png"));
image3=ImageIO.read(new File("image/恐龙3.png"));
}
1-2.踏步
public void step() {
// 每过250毫秒,更换一张图片。因为共有3图片,所以除以3取余,轮流展示这三张
int tmp = stepTimer/250%3;
switch(tmp) {
case 1:
image = image1;
break;
case 2:
image = image2;
break;
default:
image = image3;
}
stepTimer += FREASH;//计时器递增
}
1-3.跳跃
/**
* 跳跃
*/
public void jump() {
if (!jumpState) {// 如果没处于跳跃状态
Sound.jump();// 播放跳跃音效
}
jumpState = true;// 处于跳跃状态
}
1-4.移动
/*
* 移动的方法
*/
public void move() {
step();//不断踏步
if(jumpState) {//如果正在跳跃
if(y>=LOWEST_Y) {//如果纵坐标大于等于最低点
jumpValue = -4;//增变量为负值
/*
* 这是因为我们窗体的显示是按照像素的大小和位置决定的
* ,从左上角开始横纵坐标均为0,然后开始增长,向下y增长,向右x增长
*/
}
if(y<=LOWEST_Y-JUMP_HIGHT) {//如果跳过最高点
jumpValue = 4;//增变量为正值
}
y+=jumpValue;//纵坐标发生变化
if(y>=LOWEST_Y) {//如果再次落地
jumpState = false;// 停止跳跃
}
}
}
1-5.边界对象
/**
* 足部边界区域
*
* @return
*/
public Rectangle getFootBounds() {
return new Rectangle(x + 30, y + 59, 29, 18);
}
/**
* 头部边界区域
*
* @return
*/
public Rectangle getHeadBounds() {
return new Rectangle(x + 66, y + 25, 32, 22);
2 .障碍类
2-1.定义
public class Obstacle {
public int x, y;// 横纵坐标
public BufferedImage image;
private BufferedImage stone;// 石头图片
private BufferedImage cacti;// 仙人掌图片
private int speed;// 移动速度
}
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();// 纵坐标
speed = BackgroundImage.SPEED;// 移动速度与背景同步
}
2-2.移动
/**
* 移动
*/
public void move() {
x -= speed;// 横坐标递减
}
2-3.消除
/**
* 是否存活
*
* @return
*/
public boolean isLive() {
// 如果移出了游戏界面
if (x <= -image.getWidth()) {
return false;// 消亡
}
return true;// 存活
}
2-4.边界对象
public Rectangle getBounds() {
if (image == cacti) {// 如果使用仙人掌图片
// 返回仙人掌的边界
return new Rectangle(x + 7, y, 15, image.getHeight());
}
// 返回石头的边界
return new Rectangle(x + 5, y + 4, 23, 21);
}
二 音效模块设计
1.音频播放器
public class MusicPlayer implements Runnable{
File soundFile; //音乐文件
Thread thread; //父线程
boolean circulate; //是否循环播放
}
/**
* 构造方法,默认不循环播放
*
* @param filepath
* 音乐文件完整名称
* @throws FileNotFoundException
*/
public MusicPlayer(String filepath) throws FileNotFoundException {
this(filepath, false);
}
/**
* 构造方法
*
* @param filepath
* 音乐文件完整名称
* @param circulate
* 是否循环播放
* @throws FileNotFoundException
*/
public MusicPlayer(String filepath, boolean circulate) throws FileNotFoundException {
this.circulate = circulate;
soundFile = new File(filepath);
if (!soundFile.exists()) {// 如果文件不存在
throw new FileNotFoundException(filepath + "未找到");
}
}
/*
*重写线程执行方法
*/
@Override
public void run() {
byte[] auBuffer = new byte[1024 * 128];// 创建128k缓冲区
do {
AudioInputStream audioInputStream = null; // 创建音频输入流对象
SourceDataLine auline = null; // 混频器源数据行
try {
// 从音乐文件中获取音频输入流
audioInputStream = AudioSystem.getAudioInputStream(soundFile);
AudioFormat format = 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的数据
byteCount = audioInputStream.read(auBuffer, 0,
auBuffer.length);
if (byteCount >= 0) {// 如果读出有效数据
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);// 根据循环标志判断是否循环播放
}
/**
* 播放
*/
public void play() {
thread = new Thread(this);// 创建线程对象
thread.start();// 开启线程
}
/**
* 停止播放
*/
public void stop() {
thread.stop();// 强制关闭线程
}
/*
2.音效工具类
package service;
import java.io.FileNotFoundException;
/**
* 音效类
* @author JWF
*/
public class Sound {
static final String DIR = "music/";// 音乐文件夹
static final String BACKGROUD = "background.wav";// 背景音乐
static final String JUMP = "jump.wav";// 跳跃音效
static final String HIT = "hit.wav";// 撞击音效
/**
* 播放跳跃音效
*/
static public void jump() {
play(DIR + JUMP, false);// 播放一次跳跃音效
}
/**
* 播放撞击音效
*/
static public void hit() {
play(DIR + HIT, false);// 播放一次撞击音效
}
/**
* 播放背景音乐
*/
static public void backgroud() {
play(DIR + BACKGROUD, true);// 循环播放背景音乐
}
/**
* 播放
*
* @param file
* 音乐文件完整名称
* @param circulate
* 是否循环播放
*/
private static void play(String file, boolean circulate) {
try {
// 创建播放器
MusicPlayer player = new MusicPlayer(file, circulate);
player.play();// 播放器开始播放
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
三 计分器模块设计
public class ScoreRecorder {
private static final String SCOREFILE = "data/soure";// 得分记录文件
private static int scores[] = new int[3];// 当前得分最高前三名
}
读取原始分数数据初始化
/**
* 分数初始化
*/
public static void init() {
File f = new File(SCOREFILE);// 创建记录文件
if (!f.exists()) {// 如果文件不存在
try {
f.createNewFile();// 创建新文件
} catch (IOException e) {
e.printStackTrace();
}
return;// 停止方法
}
FileInputStream fis = null;
InputStreamReader isr = null;
BufferedReader br = null;
try {
fis = new FileInputStream(f);// 文件字节输入流
isr = new InputStreamReader(fis);// 字节流转字符流
br = new BufferedReader(isr);// 缓冲字符流
String value = br.readLine();// 读取一行
if (!(value == null || "".equals(value))) {// 如果不为空值
String vs[] = value.split(",");// 分割字符串
if (vs.length < 3) {// 如果分割结果小于3
Arrays.fill(scores, 0);// 数组填充0
} else {
for (int i = 0; i < 3; i++) {
// 将记录文件中的值赋给当前分数数组
scores[i] = Integer.parseInt(vs[i]);
}
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {// 依次关闭流
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
isr.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
写入游戏数据并保存
/**
* 保存分数
*/
public static void saveScore() {
// 拼接得分数组
String value = scores[0] + "," + scores[1] + "," + scores[2];
FileOutputStream fos = null;
OutputStreamWriter osw = null;
BufferedWriter bw = null;
try {
fos = new FileOutputStream(SCOREFILE);// 文件字节输出流
osw = new OutputStreamWriter(fos);// 字节流转字符流
bw = new BufferedWriter(osw);// 缓冲字符流
bw.write(value);// 写入拼接后的字符串
bw.flush();// 字符流刷新
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {// 依次关闭流
try {
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
osw.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 添加分数。如果新添加的分数比排行榜分数高,则会将新分数记入排行榜。
*
* @param score
* 新分数
*/
static public void addNewScore(int score) {
// 在得分组数基础上创建一个长度为4的临时数组
int tmp[] = Arrays.copyOf(scores, 4);
tmp[3] = score;// 将新分数赋值给第四个元素
Arrays.sort(tmp);// 临时数组降序排列
scores = Arrays.copyOfRange(tmp, 1, 4);// 将后三个元素赋值给得分数组
}
获取分数的方法
/**
* 获取分数
*
* @return
*/
static public int[] getScores() {
return scores;
}
四 视图模块设计
一 主窗体
public MainFrame() {
restart();// 开始
setBounds(340, 150, 821, 260);// 设置横纵坐标和宽高
setTitle("奔跑吧!小恐龙!");// 标题
Sound.backgroud();// 播放背景音乐
ScoreRecorder.init();// 读取得分记录
addListener();// 添加监听
setDefaultCloseOperation(EXIT_ON_CLOSE);// 关闭窗体则停止程序
}
/**
* 重新开始
*/
public void restart() {
Container c = getContentPane();// 获取主容器对象
c.removeAll();// 删除容器中所有组件
GamePanel panel = new GamePanel();// 创建新的游戏面板
c.add(panel);
addKeyListener(panel);// 添加键盘事件
c.validate();// 容器重新验证所有组件
}
/**
* 添加监听
*/
private void addListener() {
addWindowListener(new WindowAdapter() {// 添加窗体监听
public void windowClosing(WindowEvent e) {// 窗体关闭前
ScoreRecorder.saveScore();// 保存得分记录
}
});
}
二 游戏面板
public class GamePanel extends JPanel implements KeyListener {
private BufferedImage image;// 主图片
private BackgroundImage background;// 背景图片
private Dinosaur golden;// 恐龙
private Graphics2D g2;// 主图片绘图对象
private int addObstacleTimer = 0;// 添加障碍计时器
private boolean finish = false;// 游戏结束标志
private List<Obstacle> list = new ArrayList<Obstacle>();// 障碍集合
private final int FREASH = FreshThread.FREASH;// 刷新时间
int score = 0;// 得分
int scoreTimer = 0;// 分数计时器
public GamePanel() {
// 主图片采用宽800高300的彩色图片
image = new BufferedImage(800, 300, BufferedImage.TYPE_INT_BGR);
g2 = image.createGraphics();// 获取主图片绘图对象
background = new BackgroundImage();// 初始化滚动背景
golden = new Dinosaur();// 初始化小恐龙
list.add(new Obstacle());// 添加第一个障碍
FreshThread t = new FreshThread(this);// 刷新帧线程
t.start();// 启动线程
}
}
/**
* 绘制主图片
*/
private void paintImage() {
background.roll();// 背景图片开始滚动
golden.move();// 恐龙开始移动
g2.drawImage(background.image, 0, 0, this);// 绘制滚动背景
if (addObstacleTimer == 1300) {// 每过1300毫秒
if (Math.random() * 100 > 40) {// 60%概率出现障碍
list.add(new Obstacle());
}
addObstacleTimer = 0;// 重新计时
}
for (int i = 0; i < list.size(); i++) {// 遍历障碍集合
Obstacle o = list.get(i);// 获取障碍对象
if (o.isLive()) {// 如果是有效障碍
o.move();// 障碍移动
g2.drawImage(o.image, o.x, o.y, this);// 绘制障碍
// 如果恐龙头脚碰到障碍
if (o.getBounds().intersects(golden.getFootBounds())
|| o.getBounds().intersects(golden.getHeadBounds())) {
Sound.hit();// 播放撞击声音
gameOver();// 游戏结束
}
} else {// 如果不是有效障碍
list.remove(i);// 删除此障碍
i--;// 循环变量前移
}
}
g2.drawImage(golden.image, golden.x, golden.y, this);// 绘制恐龙
if (scoreTimer >= 500) {// 每过500毫秒
score += 10;// 加十分
scoreTimer = 0;// 重新计时
}
g2.setColor(Color.BLACK);// 使用黑色
g2.setFont(new Font("黑体", Font.BOLD, 24));// 设置字体
g2.drawString(String.format("%06d", score), 700, 30);// 绘制分数
addObstacleTimer += FREASH;// 障碍计时器递增
scoreTimer += FREASH;// 分数计时器递增
}
/**
* 重写绘制组件方法
*/
public void paint(Graphics g) {
paintImage();// 绘制主图片内容
g.drawImage(image, 0, 0, this);
}
/**
* 游戏是否结束
*
* @return
*/
public boolean isFinish() {
return finish;
}
/**
* 使游戏结束
*/
public void gameOver() {
ScoreRecorder.addNewScore(score);// 记录当前分数
finish = true;
}
/**
* 实现按下键盘按键方法
*/
public void keyPressed(KeyEvent e) {
int code = e.getKeyCode();// 获取按下的按键值
if (code == KeyEvent.VK_SPACE) {// 如果是空格
golden.jump();// 恐龙跳跃
}
}
@Override
public void keyReleased(KeyEvent e) {
}
@Override
public void keyTyped(KeyEvent e) {
}
三 成绩对话框
public class ScoreDialog extends JDialog {
/**
* 构造方法
*
* @param frame
* 父窗体
*/
public ScoreDialog(JFrame frame) {
super(frame, true);// 调用父类构造方法,阻塞父窗体
int scores[] = ScoreRecorder.getScores();// 获取当前前三名成绩
JPanel scoreP = new JPanel(new GridLayout(4, 1));// 成绩面板,4行1列
scoreP.setBackground(Color.WHITE);// 白色背景
JLabel title = new JLabel("得分排行榜", JLabel.CENTER);// 标题标签,居中
title.setFont(new Font("黑体", Font.BOLD, 20));// 设置字体
title.setForeground(Color.RED);// 红色体字
JLabel first = new JLabel("第一名:" + scores[2], JLabel.CENTER);// 第一名标签
JLabel second = new JLabel("第二名:" + scores[1], JLabel.CENTER);// 第二名标签
JLabel third = new JLabel("第三名:" + scores[0], JLabel.CENTER);// 第三名标签
JButton restart = new JButton("重新开始");// 重新开始按钮
restart.addActionListener(new ActionListener() {// 按钮添加事件监听
@Override
public void actionPerformed(ActionEvent e) {// 当点击时
dispose();// 销毁对话框
}
});
scoreP.add(title);// 成绩面板添加标签
scoreP.add(first);
scoreP.add(second);
scoreP.add(third);
Container c = getContentPane();// 获取主容器
c.setLayout(new BorderLayout());// 使用边界布局
c.add(scoreP, BorderLayout.CENTER);// 成绩面板放中间
c.add(restart, BorderLayout.SOUTH);// 按钮放底部
setTitle("游戏结束");// 对话框标题
int width, height;// 对话框宽高
width = height = 200;// 对话框宽高均为200
// 获得主窗体中居中位置的横坐标
int x = frame.getX() + (frame.getWidth() - width) / 2;
// 获得主窗体中居中位置的纵坐标
int y = frame.getY() + (frame.getHeight() - height) / 2;
setBounds(x, y, width, height);// 设置坐标和宽高
setVisible(true);// 显示对话框
}
}
五 游戏核心功能设计
一 刷新帧
public class FreshThread extends Thread {
public static final int FREASH = 20;// 刷新时间
GamePanel p;// 游戏面板
public FreshThread(GamePanel p) {
this.p = p;
}
public void run() {
while (!p.isFinish()) {// 如果游戏未结束
p.repaint();// 重绘游戏面板
try {
Thread.sleep(FREASH);// 按照刷新时间休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Container c = p.getParent();// 获取面板父容器
while (!(c instanceof MainFrame)) {// 如果父容器不是主窗体类
c = c.getParent();// 继续获取父容器的父容器
}
MainFrame frame = (MainFrame) c;// 将容器强制转换为主窗体类
new ScoreDialog(frame);// 弹出得分记录对话框
frame.restart();// 主窗体重载开始游戏
}
}
二 滚动背景
public class BackgroundImage {
public BufferedImage image;// 背景图片
private BufferedImage image1, image2;// 滚动的两个图片
private Graphics2D g;// 背景图片的绘图对象
public int x1, x2;// 两个滚动图片的坐标
public static final int SPEED = 4;// 滚动速度
}
构造方法
public BackgroundImage() {
try {
image1 = ImageIO.read(new File("image/背景.png"));
image2 = ImageIO.read(new File("image/背景.png"));
} catch (IOException e) {
e.printStackTrace();
}
// 主图片采用宽800高300的彩色图片
image = new BufferedImage(800, 300, BufferedImage.TYPE_INT_RGB);
g = image.createGraphics();// 获取主图片绘图对象
x1 = 0;// 第一幅图片初始坐标为0
x2 = 800;// 第二幅图片初始横坐标为800
g.drawImage(image1, x1, 0, null);
}
/**
* 滚动
*/
public void roll() {
x1 -= SPEED;// 第一幅图片左移
x2 -= SPEED;// 第二幅图片左移
if (x1 <= -800) {// 如果第一幅图片移出屏幕
x1 = 800;// 回到屏幕右侧
}
if (x2 <= -800) {// 如果第二幅图片移出屏幕
x2 = 800;// 回到屏幕右侧
}
g.drawImage(image1, x1, 0, null); // 在主图片中绘制两幅图片
g.drawImage(image2, x2, 0, null);
}
三 碰撞检测
if (o.getBounds().intersects(golden.getFootBounds())
|| o.getBounds().intersects(golden.getHeadBounds())) {
Sound.hit();// 播放撞击声音
gameOver();// 游戏结束
}
四 键盘监听
public void keyPressed(KeyEvent e) {
int code = e.getKeyCode();// 获取按下的按键值
if (code == KeyEvent.VK_SPACE) {// 如果是空格
golden.jump();// 恐龙跳跃
}
}
【 游戏运行效果】