实验三 敏捷开发与XP实践
一、实验内容
1. XP基础
2. XP核心实践
3. 相关工具
二、实验步骤
(一)敏捷开发与XP
软件工程是把系统的、有序的、可量化的方法应用到软件的开发、运营和维护上的过程。软件工程包括下列领域:软件需求分析、软件设计、软件构建、软件测试和软件维护。 人们在开发、运营、维护软件的过程中有很多技术、做法、习惯和思想体系。软件工程把这些相关的技术和过程统一到一个体系中,叫“软件开发流程”。软件开发流程的目的是为了提高软件开发、运营、维护的效率,并提高软件的质量、用户满意度、可靠性和软件的可维护性。 光有各种流程的思想是不够的,我们还要有一系列的工具来保证这些思想能够在实践中有效率地运作。软件开发很重要的一点不是看你能对多少理论讲的头头是道,还要看你对相关工具应用的如何,比如Java中单元测试要和JUnit的应用结合起来,建模要和Umbrello或StarUML的应用结合起来。编程学习是一个习而学
的过程。 常见的开发流程有:
- RUP(Rational Unified Process)
- PSP(Personal Software Process )
- TSP(Team Software Process )
- Agile Process
- ……
敏捷开发包括很多模式:
其中,极限编程(eXtreme Programming,XP)是是一种全新而快捷的软件开发方法。XP团队使用现场客户、特殊计划方法和持续测试来提供快速的反馈和全面的交流:
- XP是以开发符合客户需要的软件为目标而产生的一种方法论
- XP是一种以实践为基础的软件工程过程和思想
- XP认为代码质量的重要程度超出人们一般所认为的程度
- XP特别适合于小型的有责任心的、自觉自励的团队开发需求不确定或者迅速变化的软件
XP软件开发是什么样的
通过 XP准则来表达:
- 沟通 :XP认为项目成员之间的沟通是项目成功的关键,并把沟通看作项目中间协调与合作的主要推动因素。
- 简单 :XP假定未来不能可靠地预测,在现在考虑它从经济上是不明智的,所以不应该过多考虑未来的问题而是应该集中力量解决燃眉之急。
- 反馈 :XP认为系统本身及其代码是报告系统开发进度和状态的可靠依据。系统开发状态的反馈可以作为一种确定系统开发进度和决定系统下一步开发方向的手段。
- 勇气:代表了XP认为人是软件开发中最重要的一个方面的观点。在一个软件产品的开发中人的参与贯穿其整个生命周期,是人的勇气来排除困境,让团队把局部的最优抛之脑后,达到更重大的目标。表明了XP对“人让项目取得成功”的基本信任态度。
一项实践在XP环境中成功使用的依据通过XP的法则
呈现,包括:快速反馈、假设简单性、递增更改、提倡更改、优质工作。
XP软件开发的基石是XP的活动
,包括:编码、测试、倾听、设计。
以测试为核心的开发流程:
(二)编码标准
编写代码一个重要的认识是“程序大多时候是给人看的”,编程标准使代码更容易阅读和理解,甚至可以保证其中的错误更少。编程标准包含:具有说明性的名字、清晰的表达式、直截了当的控制流、可读的代码和注释,以及在追求这些内容时一致地使用某些规则和惯用法的重要性。
我们常见的是这样的代码:
程序没有最基本的缩进,让人读起来很费劲,这个问题在Eclipse中比较容易解决,我们单击Eclipse菜单中的source
->Format
或用快捷键Ctrl+Shift+F
就可以按Eclipse规定的规范缩进,效果如下:
根据代码逻辑加入一些空行,如下图:
代码标准中很重要的一项是如何给包、类、变量、方法等标识符命名,能很好的命名可以让自己的代码立马上升一个档次。Java中的一般的命名规则有:
- 要体现各自的含义
- 包、类、变量用名词
- 方法名用动宾
- 包名全部小写,如:io,awt
- 类名第一个字母要大写,如:HelloWorldApp
- 变量名第一个字母要小写,如:userName
- 方法名第一个字母要小写:setName
- ...
有一些公司比如Google公开了自己的编码标准,可以作为学习不错的参考,大家参考一下范飞龙老师写的代码规范&代码风格,有兴趣的可以尝试如何在Eclipse中实践Google Java Style(中文版),也就是说如何做到“按一下快捷键Ctrl+Shift+F
就可以让自己的代码符合Google Java Style(中文版)的要求”,完成后单独写一篇Blog,有加分的。
(三)结对编程
结对编程是XP中的重要实践。在结对编程模式下,一对程序员肩并肩、平等地、互补地进行开发工作。他们并排坐在一台电脑前,面对同一个显示器,使用同一个键盘、同一个鼠标一起工作。他们一起分析,一起设计,一起写测试用例,一起编码,一起做单元测试,一起做集成测试,一起写文档等。 结对编程中有两个角色:
- 驾驶员(Driver)是控制键盘输入的人。
- 领航员(Navigator)起到领航、提醒的作用。
结对编程和两人合作的重点是:
- 驾驶员:写设计文档,进行编码和单元测试等XP开发流程。
- 领航员:审阅驾驶员的文档、驾驶员对编码等开发流程的执行;考虑单元测试的覆盖率;思考是否需要和如何重构;帮助驾驶员解决具体的技术问题。
- 驾驶员和领航员不断轮换角色,不要连续工作超过一小时,每工作一小时休息15分钟。领航员要控制时间。
- 主动参与。任何一个任务都首先是两个人的责任,也是所有人的责任。没有“我的代码”、“你的代码”或“他/她的代码”,只有“我们的代码”。
- 只有水平上的差距,没有级别上的差异。两人结对,尽管可能大家的级别资历不同,但不管在分析、设计或编码上,双方都拥有平等的决策权利。
(四)版本控制
XP的集体所有制意味着每个人都对所有的代码负责;这一点,反过来又意味着每个人都可以更改代码的任意部分。结对编程
对这一实践贡献良多:借由在不同的结对中工作,所有的程序员都能看到完全的代码。集体所有制的一个主要优势是提升了开发程序的速度,因为一旦代码中出现错误,任何程序员都能修正它。 这意味着代码要放到一个大家都能方便获取的地方,我们叫代码仓库。这引出另外一个话题叫版本控制(Version Control)。
不论是对于团队还是个体,版本控制都提供了很多好处。
- 版本控制提供项目级的 undo(撤销) 功能: 没有什么事情是终结版本, 任何错误必须很容易回滚。 假设你在使用世界上最复杂的文字处理系统。 它具备了所有的能想到的功能,就是没有支持 DELETE(删除) 键。想象你打字的时候得多么的谨慎和缓慢吧, 特别是一篇超大的文档的快临近末尾的时候, 一个不小心就要重头再来(试想你选中所有的文字, 不小心按了 DELETE 键, 因为没有撤销功能,只好重新录入)。编辑文字和版本控制相同,任何时候都需要回滚,无论是一个小时, 一天, 还是一周, 这让你的团队工作自由快速的工作, 而且对于修正错误也非常自信。
- 版本控制允许多人在同一代码上工作, 只要遵守一定的控制原则就行。 再也不会发生诸如一个人覆盖了另一个人编辑的代码,导致那个人的修改无效这样的情况。
- 版本控制系统保存了过去所作的修改的历史记录。如果你遭遇到一些惊讶的代码,通过版本控制系统可以很容易找出是谁干的, 修改了什么, 修改的时间, 如果幸运的话,还能找出原因。
- 版本控制系统还支持在主线上开发的同时发布多个软件版本。在软件发布的时候也不需要整个团队的停止工作,不需要冻结代码。
- 版本控制也是项目级的时间机器,你可以选择任何一个时间, 精确地查看项目在当时的情况。 这对研究非常有用, 也是重现以前某个有问题的发布版本的基础。
流行的版本控制工具有CVS,SVN,Git等。Git是Linus除了Linux操作系统外的另外一个重要发明。
实验楼上线我的代码库功能,为大家提供实验环境内置的公开的git服务。学习的课程会自动创建一个公开的代码仓库,命名为shiyanlou_cs[课程ID]
,比如本课程的代码库命名shiyanlou_cs212
,为启动实验时会在环境中自动执行git pull
,获取课程仓库最新代码,存放在/home/shiyanlou/Code
目录。git push
操作需要手动完成,请务必在停止实验前push全部修改,否则代码就丢了。
使用方法如下:
- 如果对Git不熟悉,推荐先学习Git课程
- 开始实验时,如果您已经有了该课程的代码仓库则会自动同步(git pull)到实验环境中/home/shiyanlou/Code目录,如果还没有创建过则会自动创建并同步
- 进入到实验环境中修改代码,完成后需要依次执行下述命令即可提交:
- $ cd /home/shiyanlou/Code/shiyanlou_cs212
- # 修改代码文件
- # 添加修改文件
- $ git add 所有修改的文件 # 提交到环境中本地代码仓库 $ git commit -m '本次修改的描述' # push到git.shiyanlou.com,无需输入密码 $ git push
克隆其他用户代码仓库只需要知道对方的仓库链接,我们鼓励在别人代码基础上修改:
$ git clone http://git.shiyanlou.com/[对方的专属用户名]/[课程代码仓库名]
我们给一个HelloWorld
的例子: 首先进入Code
目录,你会发现有了shiyanlou_cs212
目录,进入shiyanlou_cs212
,如下图所示:
创建HelloWorld
目录,如下图所示:
创建并编辑HelloWorld.java
文件,如下图所示:
注意一点,往代码库提交的代码一定编译、运行、测试都没有问题的代码,我们上面测试代码没有问题了,就可以提交了:
如图:我们可以先用git status
查看一下代码状态,显示有未跟踪的代码,并建议用git add <file>...
添加,我们使用git add HelloWorld.*
把要提交的文件的信息添加到索引库中。当我们使用git commit
时,git将依据索引库中的内容来进行文件的提交。这只是在本地操作,关闭实验环境,会删除代码的,如果想把代码保存到远程托管服务器中,需要使用git push
,实验完成前,一定不要忘了使用git push
,否则就是相当于你在Word中编辑了半天文件最后却没有保存。 我们可以修改HelloWorld.java
,如下图所示:
编译、运行、测试没有问题后进行提交,这儿使用的是git commit -a
:
我们可以通过git log
查看代码提交记录:
有个问题是HelloWorld.class
是不应该保存在代码库中的,我们只要有HelloWorld.java
就行了,这怎么办?通过搜索引擎解决一下。
当然,Git不是只有程序员才用到,所有需要"Undo"的场合,版本控制都能帮助你。参考一下Git for Non-Programmers(中文版)。
更进一步的学习,参考Git使用、工具、原理、进阶的几个链接
(五)重构
我们先看看重构的概念:
重构(Refactor),就是在不改变软件外部行为的基础上,改变软件内部的结构,使其更加易于阅读、易于维护和易于变更 。
重构中一个非常关键的前提就是“不改变软件外部行为”,它保证了我们在重构原有系统的同时,不会为原系统带来新的BUG,以确保重构的安全。如何保证不改变软件外部行为?重构后的代码要能通过单元测试。如何使其更加易于阅读、易于维护和易于变更 ?设计模式给出了重构的目标。
重构重要吗?你看看Eclipse菜单中有个refactor
菜单就知道了,重构几乎是现代IDE的标配了.我们在编码标准
中说“给标识符命名”是程序员一项重要技能,以前没有这个意识,现在知道了怎么办?没问题,上图中重构的第一项功能就是Rename
,可以给类、包、方法、变量改名字。 例如这有个ABC
类:
这个类,类名,方法名和方法的参数名都有问题,没有注释的话是无法理解代码的。我们可以使用Eclipse中的重构功能来改名。修改方法是,用鼠标单击要改的名字,选择Eclipse中菜单中的Refactor
->Rename...
:
重构完的效果如下:
功能不变,代码水平立马上了一个档次,体会到命名的威力了吧?
学过C语言的学生学Java时常犯的毛病是不会封装,该用类的地方都用了结构体。比如要定义一个类Student
,会出现这样的代码:
Eclipse中菜单中的Refactor
->Encapsulate Field...
,如下图:
同样可以封装id
和age
两个成员变量,结果如下:
上面第33行还是有问题的,每次打印学生信息都这么写代码违反了DRY原则,造成代码重复,正常的重构可以使用Eclipse中的Extract Method...
,
由于Java中所有的类都有个专门的toString方法,我们使用Eclipse中Source
->Generate toString()...
给Student
类产生一个toString
方法
修改main的代码,结果如下:
大家想一想,这样重构后有什么好处?重构有什么问题吗?
我们要修改软件,万变不离其宗,无非就是四种动机:
- 增加新功能;
- 原有功能有BUG;
- 改善原有程序的结构;
- 优化原有系统的性能 。
第一种和第二种动机,都是源于客户的功能需求,而第四种是源于客户的非功能需求。软件的外部质量,其衡量的标准就是客户对软件功能需求与非功能需求的满意度。它涉及到一个企业、一个软件的信誉度与生命力,因此为所有软件企业所高度重视。要提高软件内部质量,毫无疑问就是软件修改的第三个动机:改善原有程序的结构。它的价值是隐性的,并不体现在某一次或两次开发中,而是逐渐体现在日后长期维护的软件过程中。 高质量的软件,可以保证开发人员(即使是新手)能够轻易看懂软件代码,能够保证日后的每一次软件维护都可以轻易地完成(不论软件经历了多少次变更,维护了多少年),能够保证日后的每一次需求变更都能够轻易地进行(而不是伤筋动骨地大动)。要做到这几点其实并不容易,它需要我们持续不断地对系统内部质量进行优化与改进。这,就是系统重构的价值。 下面一个重要问题是哪些地方需要重构?有臭味道(Bad Smell)的代码。 什么是臭味道?想象一下你打开冰箱门,出来一股臭味道你就知道冰箱里有东西腐坏了,要清除了。代码一样有臭味道:
臭味行列中首当其冲的就是Duplicated Code(重复的代码)。如果你在一个以上的地点看到相同的程序结构,那么当可肯定:设法将它们合而为一,程序会变得更好。
- 最单纯的
Duplicated Code
就是[同一个class内的两个方法含有相同表达式(expression)]。这时候你需要做的就是采用Extract Method
提炼出重复的代码,然后让这两个地点都调用被提炼出来的那一段代码。 - 另一种常见情况就是[两个互为兄弟(sibling)的subclasses内含有相同表达式]。要避免这种情况,只需要对两个classes都使用
Extract Method
,然后再对被提炼出的代码使用Pull Up Method
,将它推入superclass内。 - 如果代码之间只是类似,并非完全相同,那么就得运用
Extract Method
将相似部分和差异部分割开,构成单独一个方法。然后你可能发现或许可以运用Form Template Method
获得一个Template Method
设计模式。 - 如果有些方法以不同的算法做相同的事,你可以择定其中较清晰的一个,并使用
Substitute Algorithm
将其它方法的算法替换掉。 - 如果两个毫不相关的classes内出现
Duplicaded Code
,你应该考虑对其中一个使用Extract Class
,将重复代码提炼到一个独立class中,然后在另一个class内使用这个新class。但是,重复代码所在的方法也可能的确只应该属于某个class,另一个class只能调用它,抑或这个方法可能属于第三个class,而另两个classes应该引用这第三个class。你必须决定这个方法放在哪儿最合适,并确保它被安置后就不会再在其它任何地方出现。
其他Bad Smell
与相应的重构手法如下表所示:
Eclipse中Refactor
菜单中的重构手法的应用时机如下图所示:
更完整的手法可以参考《重构》作者Martin Fowler的博客。Eclipse中基本手法的使用大家可以参考任何人都可以重构来进行学习实践。
一个完整的重构流程包括:
- 从版本控制系统代码库中Check out code
- 读懂代码(包括测试代码)
- 发现bad smell
- Refactoring
- 运行所有的Unit Tests
- 往代码库中Check in code
我们结合Git给出一个比较完整的例子。
(六)实践项目
1. 以结对编程的方式编写一个软件,Blog中要给出结对同学的Blog网址,可以拍照展现结对编程情况,可以参考一下其他学校的作业
2.代码
package com.iamlishuai.flappybird;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import javax.swing.ImageIcon;
/*
* 小鸟类
* 拥有自由落体、飞翔能力
*/
public class Bird extends Thread {
// 鸟的位置
private int x;
private int y;
// 保持初始位置
private int oldx;
private int oldy;
private int ooldx;
private int ooldy;
// 鸟的图片
private Image[] bird = {
new ImageIcon(getClass().getResource("/Birds_01.png")).getImage(),
new ImageIcon(getClass().getResource("/Birds_02.png")).getImage(),
new ImageIcon(getClass().getResource("/Birds_03.png")).getImage() };
private int imageIndex = 0;
// 鸟下落的速度 、上升的速度
private int downv;
private int upv;
private int upvn = 0;
// 鸟上升的大小
private int up;
// 下降或上升
private static int DOWN = 0;
private static int FLY = 1;
// 下降或上升的标志
private int flag = Bird.FLY;
// 自由落体的重力加速度
private double g = 0.0003;
// 构造方法
public Bird(int x, int y, int downv, int upv, int up) {
this.x = x;
this.y = y;
this.oldx = x;
this.oldy = y;
this.downv = downv;
this.upv = upv;
this.up = up;
ooldx = x;
ooldy = y;
this.upvn = upv;
}
// 设置图片索引
public void setImageIndex() {
if (imageIndex == 2) {
imageIndex = 0;
} else {
imageIndex++;
}
}
// 绘制小鸟图片
public void drawSelf(Graphics g) {
//让画笔倾斜一定角度,绘制鸟的倾斜效果
Graphics2D g2 = (Graphics2D) g;
double a = Math.atan((y - ooldy + 0.000001) / 50);
if(a > Math.atan(2)){
a = Math.atan(2);
}else if(a < Math.atan(-2)){
a = Math.atan(-2);
}
if (GameUI.flag == 1) {
g2.rotate(a, x + 17, y + 17);
}
g.drawImage(bird[imageIndex], x, y, bird[imageIndex].getWidth(null),
bird[imageIndex].getHeight(null), null);
//将画笔反向倾斜来回正
if (GameUI.flag == 1) {
g2.rotate(-a, x + 17, y + 17);
}
}
// 切换上升和下降状态
public void setStatus() {
if (flag == Bird.DOWN) {
flag = Bird.FLY;
ooldx = x;
ooldy = y;
GameUI.start = System.currentTimeMillis();
} else {
flag = Bird.DOWN;
ooldx = x;
ooldy = y;
GameUI.start = System.currentTimeMillis();
upv = upvn;
}
}
public void setFlyStatus() {
flag = Bird.FLY;
ooldx = x;
ooldy = y;
GameUI.start = System.currentTimeMillis();
upv = upvn;
}
// 获取小鸟的位置
public int getX() {
return x;
}
public int getY() {
return y;
}
// 重新开始
public void reStart() {
x = oldx;
y = oldy;
ooldx = x;
ooldy = y;
}
// 运动方法
public void move() {
if (GameUI.flag == 0) {
setImageIndex();
return;
} else if (GameUI.flag == 2) {
return;
}
if (flag == Bird.DOWN) {
long end = System.currentTimeMillis();
long t = (end - GameUI.start);
int oy = (int) (ooldy + 0.5 * g * t * t);
y = oy;
} else {
y--;
long end = System.currentTimeMillis();
long t = (end - GameUI.start);
upv += 20 * g * t;
if((upvn - 60 * g * t) <= 0){
setStatus();
}
if(y <= 0){
y = 0;
}
}
// 改变小鸟的图片
setImageIndex();
}
// 线程,让鸟一直运动
public void run() {
while (true) {
move();
try {
if (flag == Bird.FLY) {
sleep(upv);
} else {
sleep(downv);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.iamlishuai.flappybird;
import java.awt.Graphics;
import java.awt.Image;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.PrintStream;
import javax.swing.ImageIcon;
public class GameStatus {
//游戏开始画面
private Image ready;
private Image begin;
//游戏分数
private Image[] score;
//游戏结束画面
private Image gameOver;
private Image scoreBoard;
private Image[] endScore;
private Image restart;
private Image list;
//构造方法
public GameStatus(){
ready = new ImageIcon(getClass().getResource("/ready.png")).getImage();
begin = new ImageIcon(getClass().getResource("/begin.png")).getImage();
score = new Image[4];
score[0] = new ImageIcon(getClass().getResource("/0.png")).getImage();
endScore = new Image[4];
gameOver = new ImageIcon(getClass().getResource("/gameover.png")).getImage();
scoreBoard = new ImageIcon(getClass().getResource("/ScoreBoard.png")).getImage();
restart = new ImageIcon(getClass().getResource("/Buttons.png")).getImage();
list = new ImageIcon(getClass().getResource("/Buttons_02.png")).getImage();
}
//设置重新开始图片
public void setRestart(){
restart = new ImageIcon(getClass().getResource("/Buttons_01.png")).getImage();
}
//绘制游戏未开始、进行中、结束的画面
public void drawSelf(Graphics g){
if(GameUI.flag == 0){
//游戏未开始
g.drawImage(ready,246,80,ready.getWidth(null),ready.getHeight(null),null);
g.drawImage(begin,246,200,begin.getWidth(null),begin.getHeight(null),null);
}else if(GameUI.flag == 1){
//游戏进行中
showStartScore(g);
}else{
//游戏结束
g.drawImage(gameOver,396 - gameOver.getWidth(null) / 2 ,80,gameOver.getWidth(null),
gameOver.getHeight(null),null);
g.drawImage(scoreBoard, 396 - scoreBoard.getWidth(null) / 2,200,scoreBoard.getWidth(null),
scoreBoard.getHeight(null),null);
g.drawImage(restart,225,400,restart.getWidth(null),restart.getHeight(null),null);
g.drawImage(list, 405, 400, list.getWidth(null), list.getHeight(null), null);
showEndScore(g);
}
}
//显示游戏进行中的得分
public void showStartScore(Graphics g){
//将得分数转换为字符数组
String s = String.valueOf(GameUI.getScore());
char[] ss = s.toCharArray();
int start = 396 - ss.length * 20;
//对每个字符进行判断,显示相应的图片
for(int i = 0; i < ss.length; i++){
score[i] = new ImageIcon(getClass().getResource("/" + ss[i] + ".png")).getImage();
g.drawImage(score[i], start + i * 40, 30, 40, 60, null);
}
}
//显示游戏结束后的得分和最高分
public void showEndScore(Graphics g){
//将成绩与文件中保存的成绩比较,若大于文件中的成绩,才将成绩写入文件
FileOutputStream fos;
FileInputStream fis;
int max = GameUI.getScore();
int is = 0;
try {
fis = new FileInputStream("MaxScore.txt");
byte[] b = new byte[100];
int has = fis.read(b);
String stringScore = new String(b,0,has);
is = Integer.valueOf(stringScore);
} catch (Exception e) {
e.printStackTrace();
}
if(max <= is){
scoreBoard = new ImageIcon(getClass().getResource("/ScoreBoard.png")).getImage();
max = is;
}else{
scoreBoard = new ImageIcon(getClass().getResource("/NewScoreBoard.png")).getImage();
try {
fos = new FileOutputStream("MaxScore.txt");
PrintStream ps = new PrintStream(fos);
ps.print(max);
} catch (Exception e) {
e.printStackTrace();
}
}
//将得分数转换为字符数组
String s = String.valueOf(GameUI.getScore());
char[] ss = s.toCharArray();
int start = 396 - ss.length * 12;
//对每个字符进行判断,显示相应的图片
for (int i = 0; i < ss.length; i++) {
endScore[i] = new ImageIcon(getClass().getResource("/mini" + ss[i] + ".png")).getImage();
g.drawImage(endScore[i], start + i * 25, 253, 25, 32, null);
}
//显示最高分(先将最高分转换成字符数组)
String s1 = String.valueOf(max);
char[] ss1 = s1.toCharArray();
int start1 = 396 - ss1.length * 12;
for (int i = 0; i < ss1.length; i++) {
endScore[i] = new ImageIcon(getClass().getResource("/mini" + ss1[i] + ".png")).getImage();
g.drawImage(endScore[i], start1 + i * 25, 320, 25, 32, null);
}
}
}
package com.iamlishuai.flappybird;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.Random;
import javax.swing.JFrame;
/**
* 主UI类
* @author Dangerous
*
*/
public class GameUI extends JFrame implements MouseListener, MouseMotionListener {
/*
* 游戏界面组成
* gamePanel : 游戏画面的容器,也是画板,主要绘制游戏界面
* bird : 游戏中的主角,即小鸟
* pipe : 游戏中的障碍物,即管道
* score : 游戏得分
*/
private MyPanel gamePanel;
private Bird bird;
private Pipe[] pipe;
private static int score;
//管道夹缝宽度
public static final int w = 100;
//两个管道之间的宽度
public static final int d = 300;
/*
* 游戏状态标志
* 0 : 游戏未开始
* 1 : 游戏进行中
* 2: 游戏结束
*/
public static int flag = 0;
//游戏开始时间
public static long start = 0;
//声音播放器(一个专门播放声音的线程)
private PlaySounds player;
//游戏状态
private GameStatus gs;
//管道位置
private int x = 800;
private int[] ypoints = { new Random().nextInt(322 - GameUI.w) + GameUI.w + 10,
new Random().nextInt(322 - GameUI.w) + GameUI.w + 10,
new Random().nextInt(322 - GameUI.w) + GameUI.w + 10,
new Random().nextInt(322 - GameUI.w) + GameUI.w + 10 };
public GameUI(){
//初始化游戏得分
GameUI.score = 0;
//初始化游戏状态
gs = new GameStatus();
//初始化游戏开始时间
GameUI.start = System.currentTimeMillis();
//初始化声音播放器
player = new PlaySounds();
//初始化小鸟
bird = new Bird(300,200,10,5,60);
// 初始化管道
pipe = new Pipe[4];
for (int i = 0; i < ypoints.length; i++) {
pipe[i] = new Pipe(x + i * GameUI.d, ypoints[i], 7);
}
// 游戏画面
gamePanel = new MyPanel(bird, pipe, gs, player);
// 添加鼠标监听
gamePanel.addMouseListener(this);
gamePanel.addMouseMotionListener(this);
//启动线程
bird.start();
player.start();
for(int i = 0; i < pipe.length; i++){
pipe[i].start();
}
Thread gp = new Thread(gamePanel);
gp.start();
//设置窗体相关属性
add(gamePanel);
setResizable(false);
setTitle("flappy bird");
setVisible(true);
setBounds(10,10,800,600);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
//获取游戏得分
public static int getScore(){
return GameUI.score;
}
//加分
public static void addScore(){
if(GameUI.flag == 1){
GameUI.score++;
}
}
//改变游戏状态
public void setStatus(int status){
GameUI.flag = status;
}
@Override
public void mouseClicked(MouseEvent e) {
int x = e.getX();
int y = e.getY();
//将小鸟的状态设置为飞行状态
bird.setFlyStatus();
/*
* 当游戏结束后,根据点击鼠标的位置
* 来判断是不是点击了重新开始按钮
*/
if(GameUI.flag == 2){
if(x > 225 && x < 381 && y > 400 && y < 493 ){
GameUI.flag = 0;
}
GameUI.score = 0;
GameUI.start = System.currentTimeMillis();
return;
}
//将游戏状态设置为游戏进行中
GameUI.flag = 1;
//将声音播放器设置为播放游戏进行中相关的音乐
player.setIsPlay(1);
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
// TODO Auto-generated method stub
}
@Override
public void mousePressed(MouseEvent e) {
// TODO Auto-generated method stub
}
@Override
public void mouseReleased(MouseEvent arg0) {
// TODO Auto-generated method stub
}
@Override
public void mouseDragged(MouseEvent arg0) {
// TODO Auto-generated method stub
}
@Override
public void mouseMoved(MouseEvent e) {
// int x = e.getX();
// int y = e.getY();
// if(GameUI.flag == 2 && x > 225 && x < 381 && y > 400 && y < 493 ){
// gs.setRestart();
// //gamePanel.repaint();
// }
}
}
package com.iamlishuai.flappybird;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JPanel;
public class MyPanel extends JPanel implements Runnable {
//小鸟对象
private Bird bird;
//管道对象
private Pipe[] pipe;
//声音播放器
private PlaySounds player;
//背景图片
private Image background;
private Image ground;
//游戏状态
private GameStatus gs;
private int rp = 1;
//构造方法
public MyPanel(Bird bird,Pipe[] pipe,GameStatus gs,PlaySounds player){
//初始化背景
background = new ImageIcon(getClass().getResource("/Background.png")).getImage();
ground = new ImageIcon(getClass().getResource("/Ground.png")).getImage();
//初始化小鸟
this.bird = bird;
//初始化声音播放器
this.player = player;
//初始化管道
this.pipe = new Pipe[4];
for(int i = 0; i < this.pipe.length; i++){
this.pipe[i] = pipe[i];
}
this.gs = gs;
}
public void paint(Graphics g){
super.paint(g);
if(GameUI.flag == 0){
bird.reStart();
}
for(int i = 0; i < pipe.length; i++){
if(GameUI.flag == 0){
pipe[i].reStart();
}
}
//绘制背景
g.drawImage(background,0,0,800,600,null);
//绘制管道
for(int i = 0; i < pipe.length; i++){
pipe[i].drawSelf(g);
if(pipe[i].isBirdDied(bird)){
player.setIsPlay(3);
GameUI.flag = 2;
};
if(pipe[i].isBirdPass(bird)){
player.setIsPlay(2);
GameUI.addScore();
}
}
//绘制背景
g.drawImage(ground, 0, 480, 800, 112, null);
//绘制小鸟
bird.drawSelf(g);
gs.drawSelf(g);
}
@Override
public void run() {
while(true){
long start = System.currentTimeMillis();
//当游戏状态变为游戏结束时,只重绘一次(为了显示游戏得分板)
if(GameUI.flag == 2){
if(rp == 1){
repaint();
rp = 0;
}
continue;
}
repaint();
if(rp == 0) rp = 1;
long end = System.currentTimeMillis();
//确定游戏帧率为60
if(end - start < (1000 / 60)){
try {
Thread.sleep(1000 / 60 - end + start);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
package com.iamlishuai.flappybird;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Polygon;
import java.awt.geom.Area;
import java.util.Random;
import javax.swing.ImageIcon;
public class Pipe extends Thread {
//位置
private int x;
private int y;
//管道重置标志
private int isReset = 0;
//移动速度
private int v;
//管道图片
private Image top;
private Image bottom;
//保存初始化时的位置
private int oldx;
private int oldy;
//构造方法
public Pipe(int x, int y,int v){
this.x = x;
this.y = y;
this.v = v;
oldx = x;
oldy = y;
top = new ImageIcon(getClass().getResource("/Pipe_Top.png")).getImage();
bottom = new ImageIcon(getClass().getResource("/Pipe_Bottom.png")).getImage();
}
//绘制管道
public void drawSelf(Graphics g){
g.drawImage(top, x, y - top.getHeight(null), top.getWidth(null), top.getHeight(null), null);
g.drawImage(bottom, x, y + GameUI.w , bottom.getWidth(null), bottom.getHeight(null) , null);
}
//移动
public void move(){
if(GameUI.flag == 0){
return;
}
if(x > -52){
x--;
}
}
//重置管道位置
public void resetPipe(){
if(x <= -52){
x = 4 * GameUI.d - 52;
y = new Random().nextInt(322 - GameUI.w) + GameUI.w + 10;
isReset = 0;
}
}
//是否撞到小鸟
public boolean isBirdDied(Bird bird){
//上面管道的位置
int[] xpoints1 = {x, x, x + top.getWidth(null), x + top.getWidth(null)};
int[] ypoints1 = {y - top.getHeight(null), y, y, y - top.getHeight(null)};
//下面管道的位置
int[] xpoints2 = {x, x, x + bottom.getWidth(null), x + bottom.getWidth(null)};
int[] ypoints2 = {y + GameUI.w, y + GameUI.w + bottom.getHeight(null),
y + GameUI.w + bottom.getHeight(null), y + GameUI.w };
Polygon p1 = new Polygon(xpoints1, ypoints1, 4);
Polygon p2 = new Polygon(xpoints2, ypoints2, 4);
Area a1 = new Area(p1);
Area a2 = new Area(p2);
if(a1.intersects(bird.getX(), bird.getY(), 35, 30) || a2.intersects(bird.getX(), bird.getY(), 35, 35)
|| bird.getY() > 450){
return true;
}else{
return false;
}
}
//小鸟是否通过
public boolean isBirdPass(Bird bird){
if(bird.getX() > x && isReset == 0){
isReset = 1;
return true;
}else{
return false;
}
}
//重新开始
public void reStart(){
x = oldx;
y = oldy;
isReset = 0;
}
//线程
public void run(){
while(true){
move();
resetPipe();
try {
sleep(v);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.iamlishuai.flappybird;
import java.applet.Applet;
import java.applet.AudioClip;
import java.net.MalformedURLException;
import java.net.URL;
/*
* 为了防止阻塞UI线程,创建下面这个专门播放声音的线程
*/
public class PlaySounds extends Thread {
//播放器
private AudioClip[] player;
//是否播放声音
private int isPlay = 0;
public PlaySounds(){
player = new AudioClip[3];
try {
player[0] = Applet.newAudioClip(new URL("file:res/Wing.wav"));
player[1] = Applet.newAudioClip(new URL("file:res/Point.wav"));
player[2] = Applet.newAudioClip(new URL("file:res/Hit.wav"));
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
//重置播放声音标志
public void setIsPlay(int i){
isPlay = i;
}
//播放声音
public void run(){
while(true){
if(isPlay == 1 && GameUI.flag == 1){
player[0].play();
isPlay = 0;
} else if(isPlay == 2 && GameUI.flag == 1){
player[1].play();
isPlay = 0;
} else if(isPlay == 3 && GameUI.flag == 2){
player[2].play();
isPlay = 0;
}
}
}
}
package com.iamlishuai.flappybird;
public class StartGame {
public static void main(String[] args) {
new GameUI();
}
}