# DinosaurRun **Repository Path**: kevin996/DinosaurRun_Test ## Basic Information - **Project Name**: DinosaurRun - **Description**: 使用Java Swing编写、仿制的Chrome浏览器的恐龙跳跃游戏小彩蛋。 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 6 - **Forks**: 1 - **Created**: 2019-10-19 - **Last Updated**: 2024-06-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Dinosaur Run 游戏分析与实现 一个水平拙劣的初学者项目,欢迎大佬来交流。 ## 一、游戏概述 ### 概述 本游戏灵感源于谷歌浏览器内置的小彩蛋——恐龙小游戏(Chrome Dino),本游戏为该彩蛋的仿制版。在此基础上,我们增加了困难模式以及记录分数的排行榜,并对界面元素进行了重新着色。这是一款无限跑酷游戏,游戏主角是一只小恐龙,通过躲避与仙人掌、鸟的相撞而生存下来赢得分数。 ### 游戏结构分析 游戏主要由游戏主体、音乐、排行榜三部分构成。 ### 开发平台与工具 - **开发语言** Java - **开发平台与工具** 开发时所使用的操作系统是Windows 10,IDE为Jetbrains IDEA 2019.1,代码托管平台为码云(Gitee)。所开发的游戏包(.jar文件)理论上可以在装有Java虚拟机(JRE)的Windows、Mac OS和Linux上运行。 ## 二、游戏需求分析 ### 游戏的难度 游戏有“简单模式”和“困难模式”两种难度,其中“困难模式”的速度是“简单”模式的2倍。 ### 游戏主体的操作 游戏通过上箭头“↑”来跳跃来跳过仙人掌,通过下箭头“↓”来 俯身奔跑以躲避与鸟相撞。当有紧急情况时,可以通过按“Enter”键来暂停游戏/继续游戏。 ### 分数计算方式 游戏分数采用带可变参数的线性增加方式计算。分数计算满足公式:y = y0 + v × 10,其中: - y是当前分数; - y0是上一次计算时的分数; - v为当前游戏进度下小恐龙的移动速度(正整数),该速度会随游戏的进行而逐渐加快,在游戏开始的7s之后第一次增加,之后每13s增速一次,增速幅度为1。 该分数每100毫秒更新一次,因此该分数显示在界面上体现为一直变换,具有游戏性。 ### 游戏记录存储 游戏分数的记录采用写入外部存储的"score.txt"文本来保存分数。其运作机制为:当程序开始运行时,从文本中读取历史分数到线性表中,当一轮游戏结束,程序将新的分数插入到线性表,最后将线性表里的数据写入文本进行保存。值得一提的是,线性表中的数据均为String类型,其包括分数以及创下该分数的时间,利用分割字符串方法substring()对其进行处理。 ## 三、系统设计 ### 游戏流程 游戏的流程图如下: ![图片](https://images.gitee.com/uploads/images/2020/0219/215655_ba355514_5136971.png) ### 实现思路 游戏采用面向对象的编程思想,结合数据结构知识和多线程的使用编写实现。 在面向对象部分,根据需要定义了若干类(详见附录UML类图)来描述游戏中各元素的运动效果和属性。在数据结构方面,使用了顺序表和队列暂存游戏所要加载的图片资源,使用文本文件存储分数和游戏历史记录,分数排行榜的排序则使用Timsort排序算法对分数进行从高到低的排序。 本游戏的主要亮点为使用了Java内置的线程类和接口(Thread和Runnable)实现程序的多线程运行。对于游戏主角的变换、背景变化、障碍物生成、碰撞检测、音频播放五个方面分别使用了七个线程同时运行,做到互不干扰又可以互相通信的情况下实现游戏流畅运行。 游戏中各部分的运动和变化,则是变换图片的坐标,然后在短时间内重复绘制,从而实现运动的效果。 ### 数据结构设计 - 本游戏所用到的数据结构大致有两种:顺序表(数组)和队列,所使用的排序算法为二分排序与归并排序相结合的Timsort排序算法(针对排行榜的分数排序)。 - 对于小恐龙和小鸟的形态变换,采用了数组方式进行存储,通过间隔一定的时间循环遍历数组,调用其中的图片并展示在界面上,实现形态变换。以鸟儿为例: ``` //Bird.java @Override public void roll() {         if (getX() < -1 * getTempImage().getWidth()) {             threadSleep(10000);             setX(GamePane.FULL_WINDOW_WIDTH);         } else {             time = time >= (100 * 2 * 2) ? 0 : time;//防止溢出             int temp = time / (100 * 2) % 2;//time每计算20次换一张             switch (temp) {                 case 0:                     setTempImage(bird1); //第1张                     threadSleep(DinosaurThread.millis);                     break;                 case 1:                     setTempImage(bird2); //第2张                     threadSleep(DinosaurThread.millis);                     break;             }             time += fresh;             setX(getX() - DinosaurMain.speed * 2);//鸟儿以2倍速speed向左移动         }         pane.repaint();         try {             sleep(10); //线程休眠刷新时间间隔,默认10ms         } catch (InterruptedException e) {             e.printStackTrace();         } } ``` - 对于随机出现的障碍物,其本质上是一个随机队列:利用生成的伪随机数选中障碍物数组中的某个对象,进入队列中,之后在合适的时间里出队(即展示在界面上,从屏幕的右侧向左运动),直到完全移动到界面外后销毁。该队列每出队一个障碍物对象即立刻更新,选择一个新的随机障碍物对象入队,循环往复直至游戏结束。出队的时间取决于游戏的进度和小恐龙的速度。 ``` @Override public void roll() {         long temp;         if (getX() < -1 * getTempImage().getWidth()) {             //当前障碍物消失,或时长到达一定阈值,生成新的障碍物             if (delay) {                 threadSleep(r.nextInt(1000) + 800);             }             //间隔出现障碍物等待的时间(毫秒) long             temp = (long) r.nextDouble();             threadSleep(100 * temp);             //选取新的障碍物下标             if (DinosaurMain.speed < 7) //如果速度不是很快,就用前7个                 i = r.nextInt(7);             else                 i = r.nextInt(5) + 3; //速度够快了就全部任意选用             setCactusY(i);             setTempImage(cactusGroup[i]);             setX(GamePane.FULL_WINDOW_WIDTH);             System.out.println("Cactus[" + i + "] is coming.");         } else {             setX(getX() - DinosaurMain.speed);//障碍物以speed向左移动         }         pane.repaint();         threadSleep(10); } ``` - 关于所使用的排序算法,通过调用Collections.sort()方法来间接调用Timsort方法,通过实现Comparator接口,重写compare()方法完成对分数记录的降序排序。 @Kevin² ``` // 重写compare比较方法 @Override public int compare(String o1, String o2) { double d1 = Double.parseDouble(subToString(o1)); double d2 = Double.parseDouble(subToString(o2)); if (d1 < d2) return 1; else if (d1 > d2) return -1; else return 0; } // 分割字符串 public String subToString(String str) { String substr = str.substring(str.indexOf("Score:") + 6, str.indexOf(" | ")); return substr; } // 记录分数方法 public void recordRank() throws IOException { date = new Date(); SimpleDateFormat sf = new SimpleDateFormat("MM.dd HH:mm:ss"); tempRank.add("Score:" + String.format("%.0f", score) + " | " + "Time:" + sf.format(date)); Collections.sort(tempRank, this::compare); try { new FileWriter(file, false); } catch (IOException e) { e.printStackTrace(); } for (String si : tempRank) { scoreBoard.writeFile(file, si); } System.out.println("write File successfully!"); } ``` ### 核心算法简介 @Kevin² - 碰撞检测算法 使用Shape.intersects(Rectangle2D r)方法,原理是Shape的内部区域与矩形的内部区域相 交,或者相交的可能性很大且执行计算的代价太高,则返回true。 而在本程序中,Shape为需要判断的对象的图片,r为恐龙的图片矩形。 在具体实现中,同时运用了切割矩形原理。即:首先获取障碍物的图片矩形与小恐龙的图片矩形,当两个矩形出现相交的情况时,再将障碍物、恐龙的图片矩形进行切割,分成多块后再进行判断;一旦满足切割的小矩形之间有相交,即可较为准确地判定碰撞发生。其中部分代码如下: ``` // 关于对恐龙的矩形切割 public Rectangle boundDino(){ // 整个恐龙的矩形 return new Rectangle(getX(),getY(),getDinoWidth(),getDinoHeight()); } public Rectangle boundDinoHead(){ // 切割成一个31×44的头部矩形 return new Rectangle(getX()+getDinoWidth()/2, getY()+5,getDinoWidth()/2-6,getDinoHeight()/3); } public Rectangle boundDinoBody(){ // 切割成一个31×58的身体矩形 return new Rectangle(getX()+8,getY()+getDinoHeight()/3+2, getDinoWidth()-28,getDinoHeight()/3); } public Rectangle boundDinoFeet(){ // 切割成一个28×28的脚矩形 return new Rectangle(getX()+24,getY()+63,28,28); } // 对鸟与恐龙的碰撞检测 if (!dino.downJudge && (bird.boundBird().intersects(dino.boundDino())) ){ if (bird.boundBirdHead().intersects(dino.boundDinoHead()) || bird.boundBirdHead().intersects(dino.boundDinoBody()) || bird.boundBirdHead().intersects(dino.boundDinoFeet()) || bird.boundBirdBody().intersects(dino.boundDinoFeet()) || bird.boundBirdBody().intersects(dino.boundDinoHead())){ Game_over(); } } ``` - 分数排行榜排序算法 游戏结束后需要将分数加入线性表中并对线性表进行排序,此处用到的排序算法为结合归并排序(merge sort)与二分排序(binary sort)的Timsort排序算法。其实现原理主要为当数组大小小于32时,使用二分排序算法;当数组大小大于32时,先算出一个合适的大小,将输入按其升序和降序特点进行了分区。排序的输入的单位不是一个个单独的数字,而是一个个的块分区。其中每一个分区叫一个run。针对这些 run 序列,每次拿一个run出来按规则进行合并。每次合并会将两个run合并成一个 run。合并的结果保存到栈中。合并直到消耗掉所有的run,这时将栈上剩余的 run合并到只剩一个 run 为止。这时这个仅剩的 run 便是排好序的结果。 ### 界面设计 界面设计上,采用了Java内置的AWT和Swing框架进行编写,绘制了窗口、按钮和图片并绑定相应的事件。 ## 四、运行情况 ### 主菜单 游戏启动时所显示的主菜单界面如图所示。 ![图片](https://images.gitee.com/uploads/images/2020/0219/215851_de6627f1_5136971.png) 图:主菜单界面 该菜单主要展示了四个按钮:简单模式、困难模式、查看排行榜和离开游戏。点击“简单模式”或“困难模式”即可开始游戏,点击“查看排行榜”显示当前文件中分数排名前十的游戏记录,点击“离开游戏”则关闭整个游戏程序。 ### 游戏主界面 在Chrome中,Dino小游戏是作为彩蛋隐藏在浏览器当中的,因此其界面元素相对简约,只有黑白两色。在复刻时,为了增加游戏的趣味性和创新性,我们对各界面元素进行了重新上色,使其看起来更生动灵活。 ![图片](https://images.gitee.com/uploads/images/2020/0219/215917_08797e8b_5136971.png) 图:使用Photoshop进行元素上色和切图 ![图片](https://images-cdn.shimo.im/2pQDPxgQj40s4l1V/原版与重制版对比.jpg) 图:游戏运行界面(上)和Chrome原版(下)对比图 - **恐龙** 小恐龙作为游戏的主角,在游戏中的主要行为表现为跳跃和蹲下,以及相对地面进行运动时的行走形态变换。 ![图片](https://images.gitee.com/uploads/images/2020/0219/215946_66bbc561_5136971.png) 图:小恐龙多帧形态 - **障碍物** 障碍物包括了长在地上的仙人掌和低空飞行的小鸟。它们的出现和上场时间都具有随机性,而移动速度则根据游戏进度进行相应的变化。在游戏开始的7s之后,速度进行第一次增加,之后每13s增速一次,增速幅度为1。 ![图片](https://images.gitee.com/uploads/images/2020/0219/215955_51125c39_5136971.png) 图:鸟儿和仙人掌的多帧形态 - **背景** 背景主要由蓝天、云朵、太阳和地面组成。蓝天直接在面板中填充蓝色(RGB: 193, 233, 255)实现。云朵则在空中(界面上半部分)以障碍物移动速度的1/4缓慢飘过。太阳因为相对运动幅度较小,可以视为是静止的,因此使其固定在了界面上。对于地面的绘制,则是利用两张地面图片前后拼接、平移、复位、循环,来实现无限滚动的效果。 - **分数** 分数在游戏界面上方进行显示,且实时刷新,富有趣味性和游戏性。同时显示当前分数和历史最高分。若当前分数高于历史最高分时,历史最高分也会与当前分数一同实时刷新。 - **操作提示** 在界面上方添加了一个简单的文本标签来进行游戏操作的提示。 ![图片](https://images.gitee.com/uploads/images/2020/0219/220004_13e6e4f0_5136971.png) 图:分数与操作提示 - **音频播放** 在游戏运行中时,当小恐龙跳跃或触碰到障碍物时,会播放对应的音频,给予玩家恰当的反馈。该音频取自Chromium开源项目中Dino小游戏的音频文件。 ### 排行榜 @Kevin² @grey030 ![图片](https://images.gitee.com/uploads/images/2020/0219/220014_e436f82d_5136971.png) 图:排行榜界面显示 - **文件****操作**** **@Kevin² 首先是创建File类下"score.txt"的文件对象,接着将对象作为形参传入readFile方法中将文本中的分数读入线性表中;游戏结束后要将分数add到线性表末端,然后对其进行排序后写入文本。部分代码如下: ``` // 按行读文本 public List readFile(File file) { if (!file.exists() || !file.isFile()) { return null; } List content = new ArrayList(); try { FileInputStream fileInputStream = new FileInputStream(file); InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "GBK"); BufferedReader reader = new BufferedReader(inputStreamReader); String lineContent = ""; while ((lineContent = reader.readLine()) != null) { content.add(lineContent); } fileInputStream.close(); inputStreamReader.close(); reader.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return content; } ··· // 按行写文本 public void writeFile(File file, String content) throws IOException { synchronized (file) { FileWriter fileWriter = new FileWriter(file, true); BufferedWriter bufferedWriter = new BufferedWriter(fileWriter); bufferedWriter.write(content + "\n"); bufferedWriter.flush(); bufferedWriter.close(); } } ``` - **分数展示** 读取该有序表的前十个元素,按行构成字符串之后展示在面板上,即组成了“排行榜”。每次需要打开排行榜界面时,都需要重新读取一次以获取最新数据。部分代码如下: ``` //src/dinosaur/ScoreThread.java/updateScore() ... for (String s :tempRank) {     int score = Integer.parseInt(s.substring(s.indexOf("Score:") + 6, s.indexOf(" ")));     String date = s.substring(s.indexOf("Time:") + 5);     scoreArea.append(String.format("   [%02d]\t\t", num));     scoreArea.append(String.format("Score: %6d | Date: %s   \n\n",score,date));     if (++num > 10)         break; } ... //src/dinosaur/ScoreThread.java/showScoreBoard() public void showScoreBoard() {     updateScore();     scoreBoard.setVisible(true); } ``` ## 五、总结 ### 存在的问题与改进 1. 游戏使用了过多的线程,占用的系统资源稍多(如在某些情况下会导致分数跳动变慢,但若干秒后可恢复),并且造成后期的升级维护性欠佳。在现代计算机处理器中,一瞬间可以执行上千条语句,但额外开辟线程所需的处理器资源却要多得多。 **改进:**在绘制多个同类的物体(如背景、云朵、太阳等与主体不相关的图像)时,可以放在一个线程内,减少额外开辟线程的开销。 2. 对面向对象的编程思想理解不深刻,项目中的类和包的管理较混乱,增加了后期升级维护的难度。 **改进:**这其中的原因,可能是本项目中存在许多未接触的知识点,需要现学现做,会出现需要什么加什么而没有提前规划的坏习惯。因此在开始项目前要更清晰详尽地规划好流程图、类图和约定一些全局调用的变量;同时应该降低各个模块之间的耦合度,这样再新增一个功能时才不会“动一发而牵全身”。 3. 在实现的过程中经常出现NullPointerException异常。 **改进:**要随时养成初始化的习惯,当某些声明的成员变量或者局部变量为空时就对其进行操作时(如读取其第i个字符),就会出现空指针错误,在逻辑处理层面应多加注意。 ### 收获与感受 本次课程设计的最大收获之一是第一次进行了一次完整的团队协作编程,这极大地丰富了我们的项目经验,我们也从中学会了许多工具的使用(如Git、在线文档等协作工具),极大地提高了交流和工作效率。 在实现的过程中,我们难免要遇到不会的知识(如多线程的使用、绘制动态的图像、响应键盘事件、碰撞检测算法、音频控制等)、未曾遇见过的错误(线程使用不当导致的崩溃)和难以预料的问题,这也考验着我们的自学和自行查阅资料解决问题的能力。所幸的是我们都能通过各种途径解决了项目中遇到的问题,也有积极地交流与沟通各自的想法,在交流中相互学习,共同提高编程能力。在本次课程设计中,我们收获颇丰。 ## 六、参考文献 - [1] 陈国君. Java程序设计基础(第五版)[M]. 北京:清华大学出版社,2015年. (199-217, 248-266, 346-347) - [2] 罗杰·杜德勒. Git简易使用指南. Bootstrap中文网,[https://www.bootcss.com/p/git-guide/](https://www.bootcss.com/p/git-guide/) - [3] javatuanzhang. Java打飞机小游戏. cnblogs,[https://www.cnblogs.com/java1024/p/7985173.html](https://www.cnblogs.com/java1024/p/7985173.html),2017-12-05 - [4] 子耶. Java多线程实现简单动画(小球运动)效果. CSDN,[https://blog.csdn.net/qq_36962569/article/details/80629927](https://blog.csdn.net/qq_36962569/article/details/80629927),2018-06-09 - [5] 逐影.Chrome自带恐龙小游戏的源码研究(七). 博客园, [https://www.cnblogs.com/undefined000/p/trex_7.html,](https://www.cnblogs.com/undefined000/p/trex_7.html,)2016-12-12 - [6] 写出高级BUG.JDK(二)JDK1.8源码分析【排序】timsort. 博客园, https://www.cnblogs.com/warehouse/p/9342279.html,2018-07-20. - [7]youxin.java 播放声音. 博客园,ht[tps://www.cnblogs.com/youxin/archive/2012/04/21/2462485.html,2012-04-21.](http://tps://www.cnblogs.com/youxin/archive/2012/04/21/2462485.html,2012-04-21.)