奔跑吧小恐龙(Java)
前言
Google浏览器内含了一个小彩蛋当没有网络连接时,浏览器会弹出一个小恐龙,当我们点击它时游戏就会开始进行,大家也可以玩一下试试,网址:恐龙快跑 - 霸王龙游戏. (ur1.fun)
今天我们也可以用Java来简单的实现一下这个小游戏。
一 系统功能结构图
二 系统业务流程图
三 程序目录结构
一 游戏模型设计
游戏模型主要指游戏中出现的刚体。刚体是指不会因为受力而变形的物体。游戏中的刚体包括奔跑的恐龙,石头和仙人掌。背景图片虽然会滚动,但背景图片不参与任何碰撞检测,所以不属于游戏模型。
1.恐龙类
奔跑的小恐龙是游戏的主角,也是玩家控制的角色。项目中的model.Dinosaur就是恐龙类。
1-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; //刷新时间
}
在构造方法中我们要设置恐龙的初始状态,将恐龙横坐标固定在50像素,纵坐标采用落地时的坐标120像素,构造方法的代码如下:
public Dinosaur() {x=50;//横坐标默认是50;y=LOWEST_Y;//纵坐标默认起始值是120image1=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.踏步
游戏中恐龙的横坐标不变但是,背景的运动会使恐龙呈现一中运动的状态,为了使这种假象的运动状态逼真,我们就需要做出恐龙奔跑的动作。step()的方法就是踏步,我们只需要将图片来回切换就可以做到这种效果。
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.跳跃
跳跃是小恐龙躲避障碍的动作,也是我们唯一可以控制恐龙的 行为。当程序调用jump()方法时,该方法会更改恐龙的跳跃属性,也就是让恐龙处于跳跃状态,跳跃的同时也会触发音效。
/*** 跳跃*/public void jump() {if (!jumpState) {// 如果没处于跳跃状态Sound.jump();// 播放跳跃音效}jumpState = true;// 处于跳跃状态}
1-4.移动
move方法是恐龙移动方法,该方法将恐龙的所有动作效果封装起来,然后交由游戏面板调用。每一帧画面都会执行一次恐龙的move方法。move 方法不断地调用step踏步方法,因为stepTimer踏步计时器会有效控制图片的切换频率,所以不用担心频繁调用的问题。
move()方法会判断恐龙是否处于跳跃状态,如果处于跳跃状态,并且恐龙站在地上,就让jumpValue跳跃增变量值变为-4,让恐龙的纵坐标不断与jumpValue 相加,纵坐标值越来越小,这样恐龙的图片位置就会越来越高。当恐龙纵坐标达到跳跃最大高度时,再让jumpValue的值变为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.定义
Obstacle类就是障碍类,该类提供了3个共有属性,分别是横坐标,纵坐标和图片对象,其他属性均为私有属性。因为障碍都会随着背景一起移动,所以障碍的移动速度采用背景图片的速度。
public class Obstacle {public int x, y;// 横纵坐标public BufferedImage image;private BufferedImage stone;// 石头图片private BufferedImage cacti;// 仙人掌图片private int speed;// 移动速度
}
使用构造方法随机生成仙人掌或石头,采用随机数的方法生成0和1,0表示采用仙人掌的图片,1表示采用石头的图片。
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中取一值,若为0image = cacti;// 采用仙人掌图片} else {image = stone;// 采用石头图片}x = 800;// 初始横坐标y = 200 - image.getHeight();// 纵坐标speed = BackgroundImage.SPEED;// 移动速度与背景同步}
2-2.移动
由于我们的画面中恐龙是在原地不同的,而背景画面是向左走的,因此我们的障碍物也要向左移动,像素的位置向左移动也就是行坐标的像素减少。同样我们也设置障碍物的移动方法为move();
/*** 移动*/public void move() {x -= speed;// 横坐标递减}
2-3.消除
当障碍移除游戏画面以后,就不会在的游戏的数据产生影响。为了减除程序计算的压力,我们要将移除游戏画面的障碍消除。isLive()方法用于获取障碍的有效状态,该方法会根据障碍的位置判断返回true和flase,当障碍还在窗体内返回true表示还在窗体内,flase表示没在窗体内,将障碍对象从碰撞集合中删除。
/*** 是否存活* * @return*/public boolean isLive() {// 如果移出了游戏界面if (x <= -image.getWidth()) {return false;// 消亡}return true;// 存活}
2-4.边界对象
为将障碍具体化设置为矩形,方便后面参与碰撞检测,不管是仙人掌还是石头,都要通过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);}
二 音效模块设计
当然一款游戏离不开音乐的支持。因为音频处理功能是JDK早期版本就有,并且一直没有更新,所以目前JDK支持的音乐格式很少。JDK支持的音乐格式可以参看:在线文档-jdk-zh (oschina.net)
我们这里使用JDK支持的WAVE格式
1.音频播放器
MusicPlayer类是音频播放器类,该类实现了Runnable接口,并在线程中定义了一个线程对象,该线程用于启动混音器数据行的业务。
public class MusicPlayer implements Runnable{File soundFile; //音乐文件Thread thread; //父线程boolean circulate; //是否循环播放
}
它的构造方法有两个参数。filepath表示音乐文件的完整文件名,circulate表示是否重复播放,构造方法抛出找不到文件异常,外部类创建MusicPlayer类对象时,必须要捕捉此异常。
/*** 构造方法,默认不循环播放* * @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 + "未找到");}}
既然此类实现了Runnable接口,必须实现run()方法。在run()方法中声明了一个128kb的缓冲字节数组,程序以不断循环的方式将音乐以音频输入流格式读入缓冲区,在把缓冲区的数据写入混音器数据行中,这样就可以不断向外部音频设备发送音频信号,实现播放音乐的效果。
/**重写线程执行方法*/@Overridepublic 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);// 根据循环标志判断是否循环播放}
播放音乐和停止音乐的方法如下:使用start方法启动线程来播放音乐,使用stop方法来强制关闭线程,实现关闭音乐的效果。
/*** 播放*/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();}}
}
三 计分器模块设计
这里计分器使用一个静态的整型数组记录有史以来前三名的成绩,当玩家打破记录时计分器会更新分数,此类为ScoreRecorder类定义如下:
public class ScoreRecorder {private static final String SCOREFILE = "data/soure";// 得分记录文件private static int scores[] = new int[3];// 当前得分最高前三名
}
读取原始分数数据初始化
在使用ScoreRecorder类之前,需要先调用该类的静态方法init。init方法可以让计分器从成绩记录文件中读取到历史前3名数据。成绩记录文件记录了3个历史成绩,这3个成绩升序排列并用“,”分隔。如果成绩记录文件不存在,或者文件中没有记录有效成绩,则会取消读取操作,并让历史前3名成绩均为0。init0方法的具体代码如下:
/*** 分数初始化*/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) {// 如果分割结果小于3Arrays.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();}}}
写入游戏数据并保存
当游戏停止时,要记录最新的前三名的成绩。saveSore()方法可以将当前成绩数组中的值写入成绩记录文件中。
/*** 保存分数*/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();}}}
addNewScore()方法用于向成绩数组中添加新成绩,该方法的score参数就是要添加的新成绩数值。在addNewScoreO方法中,如果添加的新成绩小于历史前3名,则会舍弃;如果新成绩大于历史前3名中的某个成绩,则会重新排列前3名成绩。这个逻辑是通过Arrays 类提供的 sort排序方法和copyOfRange()复制数组元素方法实现的.
/*** 添加分数。如果新添加的分数比排行榜分数高,则会将新分数记入排行榜。* * @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;}
四 视图模块设计
一 主窗体
主窗体是整个游戏最外层的容器。主窗体的本身没有任何内容,仅是一个宽820像素,高260像素的窗体。项目中 view.MainFrame类表示游戏的主窗体类,该类继承于JFrame类。MainFrame类没有成员属性。MainFrame 类的构造方法中定义了窗体的宽、高、标题等特性,同时也具有游戏启动时的初始化功能。例如,第一次载入游戏面板时,初始化计分器,播放背景音乐等。MainFrame类的构造方法的具体代码如下:
public MainFrame() {restart();// 开始setBounds(340, 150, 821, 260);// 设置横纵坐标和宽高setTitle("奔跑吧!小恐龙!");// 标题Sound.backgroud();// 播放背景音乐ScoreRecorder.init();// 读取得分记录addListener();// 添加监听setDefaultCloseOperation(EXIT_ON_CLOSE);// 关闭窗体则停止程序}
构造方法中调用的 restart方法就是让游戏重新开始的方法,也可以用于第一次启动游戏在restart)方法中,首先获取了窗体的主容器对象,然后删除容器中的所有组件,最后创建一个新的游戏面板对象并添加到容器中,同时添加主窗体的键盘事件。方法中最后一行代码尤为关键,如果在删除原组件并添加新的游戏面板之后不做重新验证操作,将会导致新面板无法正确显示。restart()方法的具体代码如下:
/*** 重新开始*/public void restart() {Container c = getContentPane();// 获取主容器对象c.removeAll();// 删除容器中所有组件GamePanel panel = new GamePanel();// 创建新的游戏面板c.add(panel);addKeyListener(panel);// 添加键盘事件c.validate();// 容器重新验证所有组件}
构造方法中调用了addListener()方法用于让窗体添加键盘以外的监听事件,游戏中主要用于在关闭窗口之前保存最新的得分记录。在窗体关闭之前会触发windowClosing()方法,在此方法中调用ScoreRecord计分器的saveScore()方法保存成绩。
/*** 添加监听*/private void addListener() {addWindowListener(new WindowAdapter() {// 添加窗体监听public void windowClosing(WindowEvent e) {// 窗体关闭前ScoreRecorder.saveScore();// 保存得分记录}});}
二 游戏面板
游戏面板是整个程序的核心,几乎所有的算法都是以游戏面板为基础实现的。游戏面板的主要作用是绘制游戏界面,将所有的游戏元素都展现出来。游戏界面会按照(默认)20毫秒一次的刷新频率实现游戏帧数的刷新,这样不仅可以让界面中的元素运动起来,也可以让各个元素在运动的过程中进行逻辑的运算。
项目中的 GamePanel 类表示游戏面板类,该类继承了JPanel面板类,同时实现了KeyListener 键盘事件监听接口。GamePanel类有很多成员属性,其中恐龙对象、背景图片对象、障碍集合和得分都是游戏界面中可以看到的元素。此外,还有很多后台使用的属性,如游戏结束标志、障碍计时器等。
游戏采用双缓冲机制防止界面闪烁,image对象就是缓冲图片对象,也可以成为主图片对象,所有的游戏画面都绘制在image对象中,然后再将image对象绘制到游戏面板中。GamePanel类的定义如下:
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();// 启动线程}
}
在paintlmage)方法中会让每一个游戏元素都执行各自的运动,如背景图片的滚动、恐龙的移动和障碍的移动等。在绘制障碍之前,会先判断障碍集合中的障碍对象是否是有效的,如是无效障碍,则会删除。paintImage0方法的具体代码如下:
/*** 绘制主图片*/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();// 恐龙跳跃}}@Overridepublic void keyReleased(KeyEvent e) {}@Overridepublic void keyTyped(KeyEvent e) {}
三 成绩对话框
成绩对话框会在游戏结束时弹出,对话框中会显示目前为止记录的前3名成绩,单击对话框底部的按钮会重新开始游戏。项目中的 view.ScoreDialog就是成绩对话框类,该类继承JDialog对话框类。
ScoreDialog类中有一个构造方法,构造方法参数为对话框的父窗体。构造方法第一行调用了父类的构造方法,通过父类构造方法阻塞父窗体,这样可以保证弹出成绩对话框之后,主窗体内会停止全部功能且不可选中。这样可以保证玩家单击“重新开始”按钮后,主窗体才会执行restart()方法。
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() {// 按钮添加事件监听@Overridepublic 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);// 显示对话框}
}
五 游戏核心功能设计
一 刷新帧
帧是一个量词,一幅静态画面就是一帧。无数不同的静态画面交替放映,就形成了动画。帧的刷新频率决定着画面中的动作是否流畅,列如,电影在正常情况下是24帧,也就是影片一秒钟会闪过24幅静态画面。想让游戏中的物体运动起来,就需要让游戏画面不断地刷新,像播放电影一样,这就是刷新帧的概念。
项目中的service.FreshThead类就是游戏中的刷新帧线程类,该类继承于Thread线程类,并在线程的主方法中无限地循环,每过20毫秒就执行游戏面板的repaint)方法,每次执行 repaint0方法前都会先执行用户输入的指令,这样每次绘制的画面就会都不一样,极短时间内切换画面就形成了动画效果。游戏面板的isFinish)方法返回 false,就代表游戏结束,当前线程才会停止。
当刷新帧的业务停止后,程序会获取加载游戏面板的主窗体对象,然后弹出成绩对话框,最后让主窗体对象重新开始新游戏。FreshThead类的具体代码如下:
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;// 第一幅图片初始坐标为0x2 = 800;// 第二幅图片初始横坐标为800g.drawImage(image1, x1, 0, null);}
roll方法让图片实现不断的滚动,当有任意一张图片移动出画面时,就立刻回到右侧是初始位置,准备下一轮的滚动。
/*** 滚动*/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);}
三 碰撞检测
java awt.Rectangle类提供了intersects(Rectangle r)方法来判断两个边界是否发生了交汇。当两个边界对象发生交汇时,intersects()方法的返回结果为true;当两个边界对象没有交汇时,intersects()方法的返回结果为false。
因为我们前面为恐龙和石头以及仙人掌做了边界处理,因此我们可以用这个方法来检测是否发生碰撞。
在GamePanel游戏面板类的paintImage()方法中,绘制完每一个障碍后,会判断刚刚绘制的障碍对象是否碰到了恐龙。利用上述的方法进行判断,只要存在true结果,就让游戏结束。
if (o.getBounds().intersects(golden.getFootBounds())|| o.getBounds().intersects(golden.getHeadBounds())) {Sound.hit();// 播放撞击声音gameOver();// 游戏结束}
四 键盘监听
前面GamePanel类实现了KeyListener的接口,该接口实现了三种方法(键盘监听的方法):keyPressed,keyReleased,keyTyped.这里我们就用到了按下的监听事件实现的方法,如果点击空格键就让恐龙实现跳跃的方法。
public void keyPressed(KeyEvent e) {int code = e.getKeyCode();// 获取按下的按键值if (code == KeyEvent.VK_SPACE) {// 如果是空格golden.jump();// 恐龙跳跃}}
【 游戏运行效果】
相关文章:

奔跑吧小恐龙(Java)
前言 Google浏览器内含了一个小彩蛋当没有网络连接时,浏览器会弹出一个小恐龙,当我们点击它时游戏就会开始进行,大家也可以玩一下试试,网址:恐龙快跑 - 霸王龙游戏. (ur1.fun) 今天我们也可以用Java来简单的实现一下这…...
Ubuntu 1804 And Above Coredump Settings
查看 coredump 是否开启 # 查询, 0 未开启, unlimited 开启 xiaoUbuntu:/var/core$ ulimit -c 0# 开启 xiaoUbuntu:/var/core$ ulimit -c unlimited查看 coredump 保存路径 默认情况下,Ubuntu 使用 apport 服务处理 coredump 文件ÿ…...

docker 2:安装
docker 2:安装 ubuntu 安装 docker sudo apt install docker.io 把当前用户放进 docker 用户组,避免每次运行 docker 命都要使用 sudo 或者 root 权限。 sudo usermod -aG docker $USERid $USER 看到用户已加入 docker 组 …...

LeetCode Python - 19.删除链表的倒数第N个结点
目录 题目答案运行结果 题目 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。 示例 1: 输入:head [1,2,3,4,5], n 2 输出:[1,2,3,5] 示例 2: 输入:head [1], n 1 输出&a…...

Spring Boot 笔记 005 环境搭建
1.1 创建数据库和表(略) 2.1 创建Maven工程 2.2 补齐resource文件夹和application.yml文件 2.3 porn.xml中引入web,mybatis,mysql等依赖 2.3.1 引入springboot parent 2.3.2 删除junit 依赖--不能删,删了会报错 2.3.3 引入spring web依赖…...

【解决(几乎)任何机器学习问题】:超参数优化篇(超详细)
这篇文章相当长,您可以添加至收藏夹,以便在后续有空时候悠闲地阅读。 有了优秀的模型,就有了优化超参数以获得最佳得分模型的难题。那么,什么是超参数优化呢?假设您的机器学习项⽬有⼀个简单的流程。有⼀个数据集&…...
面试计算机网络框架八股文十问十答第七期
面试计算机网络框架八股文十问十答第七期 作者:程序员小白条,个人博客 相信看了本文后,对你的面试是有一定帮助的!关注专栏后就能收到持续更新! ⭐点赞⭐收藏⭐不迷路!⭐ 1)UDP协议为什么不可…...

Codeforces Round 926 (Div. 2)
A. Sasha and the Beautiful Array(模拟) 思路 最大值减去最小值 #include<iostream> #include<algorithm> using namespace std; const int N 110; int a[N];int main(){int t, n;cin>>t;while(t--){cin>>n;for(int i 0; i…...

构建智慧交通平台:架构设计与实现
随着城市交通的不断发展和智能化技术的迅速进步,智慧交通平台作为提升城市交通管理效率和水平的重要手段备受关注。本文将探讨如何设计和实现智慧交通平台的系统架构,以应对日益增长的城市交通需求,并提高交通管理的智能化水平。 ### 1. 智慧…...

移动端设置position: fixed;固定定位,底部出现一条缝隙,不知原因,欢迎探讨!!!
1、问题 在父盒子中有一个子盒子,父盒子加了固定定位,需要子盒子上下都有要边距,用margin或者padding挤开时,会出现缝隙是子盒子背景颜色的。 测试过了,有些手机型号有,有些没有,微信小程序同移…...

有关网络安全的课程学习网页
1.思科网络学院 免费学习skillsforall的课程 课程链接:Introduction to Cybersecurity by Cisco: Free Online Course (skillsforall.com) 2.斯坦福大学计算机和网络安全基础 该证书对于初学者来说最有价值,它由最著名的大学之一斯坦福大学提供。您可…...
计算机网络-面试题
一、基础 1、网络编程 网络编程的本质是多台计算机之间的数据交换存在问题 如何准确的定位网络上一台或多台主机如何进行可靠传输2、网络协议 在计算机网络有序的交换数据,就必须遵守一些事先约定好的规则,比如交换数据的格式、是否需要发送一个应答信息。这些规则被称为网络…...
C++虚函数
C虚函数 在C中,虚函数(Virtual Function)是一个使用关键字virtual声明的成员函数,它在基类中被声明,以便在任何派生类中被重写(Override)。使用虚函数的目的是实现多态性——一种允许使用基类指…...

MySQL数据库基础(二):MySQL数据库介绍
文章目录 MySQL数据库介绍 一、MySQL介绍 二、MySQL的特点 三、MySQL版本 四、MySQL数据库下载与安装 1、下载 2、安装 五、添加环境变量(Windows) 六、检测环境变量是否配置成功 MySQL数据库介绍 一、MySQL介绍 MySQL是一个关系型数据库管理…...
常用文件命令
文章目录 文件命令文件内容查看catnlmoreless(more的plus版)headtailod 文件属性操作用户权限常见的权限chownchmodchgrpumask 隐藏属性常见的隐藏属性lsattrchattr 查找文件查看文件类型查找文件位置whichwhereislocatefind 文件操作(复制、…...

在屏蔽任何FRP环境下从零开始搭建安全的FRP内网穿透服务
背景 本人目前在境外某大学读博,校园网屏蔽了所有内网穿透的工具的数据包和IP访问,为了实现在家也能远程访问服务器,就不得不先开个学校VPN,再登陆。我们实验室还需要访问另一个大学的服务器,每次我都要去找另一个大学…...

OpenGL-ES 学习(1)---- AlphaBlend
AlphaBlend OpenGL-ES 混合本质上是将 2 个片元的颜色进行调和(一般是求和操作),产生一个新的颜色 OpenGL ES 混合发生在片元通过各项测试之后,准备进入帧缓冲区的片元和原有的片元按照特定比例加权计算出最终片元的颜色值,不再是新…...
Python 函数的学习笔记
Python 函数的学习笔记 0. Python 函数的概要说明1. 自定义函数示例2. 匿名函数示例3. 内置函数示例3-1. filter() 示例3-2. map() 示例3-3. reduce() 示例 4. 可变长参数*args和**kwargs示例4-1. *args(Positional Variadic Arguments)4-2. **kwargs&am…...

详解 Redis 实现数据去重
✨✨ 欢迎大家来到喔的嘛呀的博客✨✨ 🎈🎈希望这篇博客对大家能有帮助🎈🎈 目录 言 一. Redis去重原理 1. Redis Set 数据结构 2. 基于 Set 实现数据去重 3. 代码示例 4. 总结 …...

FreeRTOS 延迟中断处理
采用二值信号量同步 二值信号量可以在某个特殊的中断发生时,让任务解除阻塞,相当于让任务与中断 同步。这样就可以让中断事件处理量大的工作在同步任务中完成,中断服务例程(ISR) 中只是快速处理少部份工作。如此,中断处理可以说是…...

超短脉冲激光自聚焦效应
前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应,这是一种非线性光学现象,主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场,对材料产生非线性响应,可能…...

关于iview组件中使用 table , 绑定序号分页后序号从1开始的解决方案
问题描述:iview使用table 中type: "index",分页之后 ,索引还是从1开始,试过绑定后台返回数据的id, 这种方法可行,就是后台返回数据的每个页面id都不完全是按照从1开始的升序,因此百度了下,找到了…...

《用户共鸣指数(E)驱动品牌大模型种草:如何抢占大模型搜索结果情感高地》
在注意力分散、内容高度同质化的时代,情感连接已成为品牌破圈的关键通道。我们在服务大量品牌客户的过程中发现,消费者对内容的“有感”程度,正日益成为影响品牌传播效率与转化率的核心变量。在生成式AI驱动的内容生成与推荐环境中࿰…...

现代密码学 | 椭圆曲线密码学—附py代码
Elliptic Curve Cryptography 椭圆曲线密码学(ECC)是一种基于有限域上椭圆曲线数学特性的公钥加密技术。其核心原理涉及椭圆曲线的代数性质、离散对数问题以及有限域上的运算。 椭圆曲线密码学是多种数字签名算法的基础,例如椭圆曲线数字签…...

如何在网页里填写 PDF 表格?
有时候,你可能希望用户能在你的网站上填写 PDF 表单。然而,这件事并不简单,因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件,但原生并不支持编辑或填写它们。更糟的是,如果你想收集表单数据ÿ…...

算法笔记2
1.字符串拼接最好用StringBuilder,不用String 2.创建List<>类型的数组并创建内存 List arr[] new ArrayList[26]; Arrays.setAll(arr, i -> new ArrayList<>()); 3.去掉首尾空格...

保姆级教程:在无网络无显卡的Windows电脑的vscode本地部署deepseek
文章目录 1 前言2 部署流程2.1 准备工作2.2 Ollama2.2.1 使用有网络的电脑下载Ollama2.2.2 安装Ollama(有网络的电脑)2.2.3 安装Ollama(无网络的电脑)2.2.4 安装验证2.2.5 修改大模型安装位置2.2.6 下载Deepseek模型 2.3 将deepse…...

Golang——9、反射和文件操作
反射和文件操作 1、反射1.1、reflect.TypeOf()获取任意值的类型对象1.2、reflect.ValueOf()1.3、结构体反射 2、文件操作2.1、os.Open()打开文件2.2、方式一:使用Read()读取文件2.3、方式二:bufio读取文件2.4、方式三:os.ReadFile读取2.5、写…...

嵌入式学习之系统编程(九)OSI模型、TCP/IP模型、UDP协议网络相关编程(6.3)
目录 一、网络编程--OSI模型 二、网络编程--TCP/IP模型 三、网络接口 四、UDP网络相关编程及主要函数 编辑编辑 UDP的特征 socke函数 bind函数 recvfrom函数(接收函数) sendto函数(发送函数) 五、网络编程之 UDP 用…...

WebRTC调研
WebRTC是什么,为什么,如何使用 WebRTC有什么优势 WebRTC Architecture Amazon KVS WebRTC 其它厂商WebRTC 海康门禁WebRTC 海康门禁其他界面整理 威视通WebRTC 局域网 Google浏览器 Microsoft Edge 公网 RTSP RTMP NVR ONVIF SIP SRT WebRTC协…...