奔跑吧小恐龙(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) 中只是快速处理少部份工作。如此,中断处理可以说是…...
计网体系结构
计算机网络的概述 概念 网络:网状类的东西或系统。 计算机网络:是一个将分散的、具有独立性功能的计算机系统,通过通信设备与线路连接起来,由功能完善的软件实现资源共享和信息传递的系统。即计算机网络是互连(通过通信链路互连…...
linux系统zabbix工具监控web页面
web页面监控 内建key介绍浏览器配置浏览器页面查看方式 监控指定的站点的资源下载速度,及页面响应时间,还有响应代码; web Scenario: web场景(站点)web page :web页面,一个场景有多…...
VMware虚拟机网络配置
VMware虚拟机网络配置 桥接模式NAT网络 桥接模式 桥接模式其实就是借助你宿主机上的网卡进行联网和通信,所以相当于虚拟机和宿主机平级,处于同一个网段中。 配置要点: 注意选择正确的宿主机网卡 查看宿主机的网络信息,这些信息指…...
代码随想录算法训练营DAY18 | 二叉树 (5)
一、LeetCode 513 找树左下角的值 题目链接:513.找树左下角的值https://leetcode.cn/problems/find-bottom-left-tree-value/ 思路一:递归回溯全局变量比深度。 class Solution {int Max_depth 0;int result 0;public int findBottomLeftValue(TreeNo…...
企业微信自动推送机器人的应用与价值
随着科技的快速发展,企业微信自动推送机器人已经成为了企业数字化转型的重要工具。这种机器人可以自动推送消息、执行任务、提供服务,为企业带来了许多便利。本文将探讨企业微信自动推送机器人的应用和价值。 一、企业微信自动推送机器人的应用 企业微信…...
Matplotlib plt.plot:从入门到精通,只需一篇文章!
Matplotlib plt.plot:从入门到精通,只需一篇文章! 利用Matplotlib进行数据可视化示例 🌵文章目录🌵 📊 1. 引言:为什么Matplotlib在数据可视化中如此重要?📊✨ 2. plt.pl…...
Linux中sigaction函数和SIGCHLD信号的使用
sigaction函数: 函数说明:注册一个信号处理函数 函数原型:int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); 函数参数: signum:捕捉的信号act:传入参数,…...
【MySQL】操作库 —— 表的操作 -- 详解
一、增加表 1、创建表 mysql> create database [if not exists] table_name ( -> field1 datatype, -> field2 datatype, -> field3 datatype -> ) character set 字符集 collate 校验规则 engine 存储引擎; 注意 :最后一行也可以写成&#x…...
ZigBee学习——在官方例程实现组网
✨Z-Stack版本:3.0.2 ✨IAR版本:10.10.1 ✨这篇博客是在善学坊BDB组网实验的基础上进行完善,并指出实现的过程中会出现的各种各样的问题! 善学坊教程地址: ZigBee3.0 BDB组网实验 文章目录 一、基础工程选择二、可能遇…...
ES实战--wildcard正则匹配exists过滤字段是否存在
wildcard 通配符中的 * 表示任意数量的字符 ?表示任意单个字符 #正则匹配 GET /wildcard-test/_search {"query": {"wildcard": {"title": {"wildcard": "ba*n"}}} } #响应:"hits": {"total": {"…...
C++学习:二分查找
二分查找的前提 库函数只能对数组进行二分查找。 对一个数组进行二分查找的前提是这个数组中的元素是单调的。 一般为单调不减,当然如果是单调不增也可以(需要修改比较函数) 例如: [1,5,5,9,18]是单调的 [1 , 9, 9,…...
语言与科技创新(大语言模型对科技创新的影响)
1.语言因素对科技创新的影响 科技创新中的语言因素至关重要,具体体现在以下几个方面: 科技文献交流: 英语作为全球科学研究的通用语言,极大地推动了科技成果的国际传播与合作。在国际上,科学家们在发表论文、报告研究…...
【C语言】简单贪吃蛇实现保姆级教学!!!
关注小庄 顿顿解馋૮(˶ᵔ ᵕ ᵔ˶)ა 新年快乐呀小伙伴 引言: 小伙伴们应该都有一个做游戏的梦吧?今天让小庄来用C语言简单实现一下我们的童年邪典贪吃蛇,顺便巩固我们的C语言知识,请安心食用~ 文章目录 贪吃蛇效果一.游戏前工作…...
rtt设备io框架面向对象学习-uart设备
目录 1.uart设备基类2.uart设备基类的子类3.初始化/构造流程3.1设备驱动层3.2 设备驱动框架层3.3 设备io管理层 4.总结5.使用 1.uart设备基类 此层处于设备驱动框架层。也是抽象类。 在/ components / drivers / include / drivers 下的serial.h定义了如下uart设备基类 struc…...
Innodb下修改事务工作流程(buffer pool、redo log、undolog)
1、在Buffer Pool中读取数据:当InnoDB需要更新一条记录时,首先会在Buffer Pool中查找该记录是否在内存中。如果没有在内存中,则从磁盘读取该页到Buffer Pool中。 2、记录UndoLog:在修改操作前,InnoDB会在Undo Log中记…...
redis为什么使用跳跃表而不是树
Redis中支持五种数据类型中有序集合Sorted Set的底层数据结构使用的跳跃表,为何不使用其他的如平衡二叉树、b树等数据结构呢? 1,redis的设计目标、性能需求: redis是高性能的非关系型(NoSQL)内存键值数据…...
【matalab】基于Octave的信号处理与滤波分析案例
一、基于Octave的信号处理与滤波分析案例 GNU Octave是一款开源软件,类似于MATLAB,广泛用于数值计算和信号处理。 一个简单的信号处理与滤波分析案例,说明如何在Octave中生成一个有噪声的信号,并设计一个滤波器来去除噪声。 首…...
Elasticsearch:特定领域的生成式 AI - 预训练、微调和 RAG
作者:来自 Elastic Steve Dodson 有多种策略可以将特定领域的知识添加到大型语言模型 (LLM) 中,并且作为积极研究领域的一部分,正在研究更多方法。 对特定领域数据集进行预训练和微调等方法使 LLMs 能够推理并生成特定领域语言。 然而&#…...
HarmonyOS—UI 开发性能提升的推荐方法
开发者若使用低性能的代码实现功能场景可能不会影响应用的正常运行,但却会对应用的性能造成负面影响。本章节列举出了一些可提升性能的场景供开发者参考,以避免应用实现上带来的性能劣化。 使用数据懒加载 开发者在使用长列表时,如果直接采用…...
84 CTF夺旗-PHP弱类型异或取反序列化RCE
目录 案例1:PHP-相关总结知识点-后期复现案例2:PHP-弱类型对比绕过测试-常考点案例3:PHP-正则preg_match绕过-常考点案例4:PHP-命令执行RCE变异绕过-常考点案例5:PHP-反序列化考题分析构造复现-常考点涉及资源…...
Duilib List 控件学习
这是自带的一个示例; 一开始运行的时候List中是空的,点击Search按钮以后就填充列表框; 先看一下列表框列头是在xml文件中形成的; <List name="domainlist" bkcolor="#FFFFFFFF" ... menu="true"> <ListHeader height="24…...
详细了解Node.js的配置与使用!
详细了解Node.js的配置与使用! Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。它允许开发者在服务器端运行 JavaScript,从而实现全栈 JavaScript 开发。本文将介绍 Node.js 的配置和 npm 的应用。 一、Node.js 配置 下载与安装 首先&…...
OpenCV 移动最小二乘图像变形
文章目录 一、简介二、实现代码三、实现效果参考文献一、简介 在现实生活中,我们常常应用一些刚性的变换来实现物体的旋转平移,对于非刚性的变换我们都没有在意,其实这种变换也是无处不在的,如我们经常看的动画就可以通过一些非刚性的变换达到一些非常夸张的效果。这里,我…...
【深度学习】S2 数学基础 P4 概率论
目录 基本概率论概率论公理随机变量 多个随机变量联合概率条件概率贝叶斯定理求和法则独立性 期望与方差小结 基本概率论 机器学习本质上,就是做出预测。而概率论提供了一种量化和表达不确定性水平的方法,可以帮助我们量化对某个结果的确定性程度。 在…...
跟我学c++中级篇——静态多态
一、多态 Polymorphism,多态。学习过c的人如果不知道多态,基本上就是打入c内部的C程序员了。在前边曾经对多态进行过分析,对其中的虚函数(虚表等)也进行过较为详细的说明。 多态其实非常好理解,不要硬扣书…...
设计模式--桥接模式(Bridge Pattern)
桥接模式(Bridge Pattern)是一种结构型设计模式,它主要是用于将抽象部分与实现部分分离,使它们可以独立地变化。 桥接模式主要包含以下几个角色: Abstraction(抽象类):定义抽象类的…...
统计图饼图绘制方法(C语言)
统计图饼图绘制方法(C语言) 常用的统计图有条形图、柱形图、折线图、曲线图、饼图、环形图、扇形图。 前几类图比较容易绘制,饼图绘制较难。今值此介绍饼图的绘制方法。 本方法采用C语言的最基本功能: ( 1.)…...
洛谷C++简单题小练习day12—寻找最小值小程序
day12--寻找最小值--2.16 习题概述 题目描述 给出 n 和 n 个整数 ai,求这 n 个整数中最小值是什么。 输入格式 第一行输入一个正整数 n,表示数字个数。 第二行输入 n 个非负整数,表示 1,2…a1,a2…an,以空格隔开。 …...
相机图像质量研究(13)常见问题总结:光学结构对成像的影响--鬼影
系列文章目录 相机图像质量研究(1)Camera成像流程介绍 相机图像质量研究(2)ISP专用平台调优介绍 相机图像质量研究(3)图像质量测试介绍 相机图像质量研究(4)常见问题总结:光学结构对成像的影响--焦距 相机图像质量研究(5)常见问题总结:光学结构对成…...
车载诊断协议DoIP系列 —— 车辆以太网节点需求汇总
车载诊断协议DoIP系列 —— 车辆以太网节点需求汇总 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师(Wechat:gongkenan2013)。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 本就是小人物,输了就是输了,不要在意别人怎么看自己。江湖一碗茶,…...