一、 概述
1.1 项目简介
本次Java课程设计是做一个飞机大战的游戏,应用Swing编程,完成一个界面简洁流畅、游戏方式简单,玩起来易于上手的桌面游戏。该飞机大战项目运用的主要技术即是Swing编程中的一些窗口类库、事件监听以及贴图技术。
1.2 实训功能说明
1.2.1 基本功能
(1)通过键盘,方向键和ASWD键可控制战机的位置,空格键和鼠标左键发射子弹。
(2)界面中敌机出现的位置,以及敌机和Boss炸弹的发射均为随机的,敌机与敌机炸弹、Boss炸弹均具有一定的速度,且随着关卡难度的增大,数量和速度均随着关卡数增加而增加。
(3)对于随机产生的敌机和敌机炸弹,若超过矩形区域,则释放该对象。
(4)添加碰撞效果,包括战机子弹打中敌机爆炸、敌机炸弹打中战机爆炸、战机与敌机相撞爆炸、战机子弹与敌机炸弹相撞爆炸、战机子弹打中Boss、战机与Boss碰撞以及战机吃到血包七种碰撞效果。且碰撞发生后子弹、炸弹、血包均消失,战机生命值减一,敌机和Boss生命值减少当前战机炮弹威力的生命值,若敌机或Boss生命值归零,则删除敌机或Boss。
(5)血包:随着关卡游戏进程的进行,会出现一定量的血包供战机补给生命值,血包会在客户区矩形框内运动,10秒后消失;若战机在10秒内吃到血包,则会增加5点生命值知道生命值上限。
(6)每关中战机有三条命,每条命10点生命值,生命使用完后,会进入GameOver界面显示得分数,并提供重新开始游戏和退出功能。
(7)游戏提供10个关卡,每个关卡需要打死相应关卡的敌机数量才能进入Boss模式,打败Boss之后将会进入下一关。10关通关后,显示通关界面,并提供重新开始游戏和退出游戏的功能选项。
(8)暂停功能:游戏进行过程中按下Z键可进入暂停模式,再按Z则返回游戏。
(9)无敌模式:游戏进行过程中按下Y键可进入无敌模式,再按Y则返回正常游戏。该模式下战机生命值不会减少,可供测试使用。
(10)魔法值:游戏进行过程中,战机魔法值会随着时间递增到上限10,魔法值供战机道具功能的使用,过一个关卡魔法值不清零。
(11)战机大招:当战机魔法值为10满状态时,按下X键消耗所有魔法值可发动大招,对屏幕中的敌机进行清屏,Boss扣50点血量。
(12)防护罩:当魔法值不为0时,按下C键可打开防护罩道具,该状态下战机处于无敌状态,不会损失生命值,但魔法值会随着防护罩开启慢慢降低。
(13)战机升级功能:战机子弹单个威力为1,在魔法值不为0时,按下V键开启升级战机模式,战机图标变为动画,子弹威力变成两倍。(若同时开启防护罩和战机升级,则魔法值递减速度翻倍)。
1.2.2 附加功能
(1) 为游戏界面每个关卡添加了滚动背景图片和背景音乐,并在敌机发送炮弹、战机发射子弹、战机击中敌机、敌机击中战机、战机敌机相撞、敌机战机子弹相撞、战机吃到血包、战机大招、战机升级、战机防护罩、游戏结束时均添加了音效。
(2)为美化游戏界面,采用了一部分全民飞机大战图标,并添加了爆炸动画和升级战机动画特效,背景音乐采用微信飞机大战背景音乐和相关特效音效。
(3)为游戏设置了不同的关卡,每个关卡难度不同,敌机与敌机炸弹的速度随着关卡增大而加快,进入第五关以后敌机从上下方均会随机出现,且随机发射炸弹。
(4)前五关卡敌机从上方飞出,速度一定,战机每打掉一架敌机则增加一分,当战机得分超过该关卡所需分数(和关卡数相关)则可进入Boss模式,打败Boss进入下一关;进入第六关以后,敌机分别从上下两方飞出。随着关卡数增加,敌机数量增加,速度增快,敌机炮弹数量和速度也相应增加,进入Boss所需分数增加,Boss生命值和火力也随着关卡数的增加而增加,游戏难度陡然直升。
(5)游戏界面中显示当前状态下的关卡数、当前命数、当前得分、战机血条、战机魔法条、无敌模式提醒和战机道具提醒,Boss模式下还有Boss血条。
(6)增加了鼠标控制战机位置这一效果,战绩的位置随着鼠标的移动而移动,并且点击鼠标左键可使得战机发射子弹。
(7)进入游戏先进入欢迎界面,欢迎界面中显示游戏使用说明,点击鼠标左键和空格键开始游戏。游戏过程中战机命数使用完、通关均有相应界面进行提醒,用户可选择重新开始游戏或退出游戏。
二、 相关技术
2.1 Timer定时器技术
本次项目采用了Java的Timer定时器和TimerTask任务,Timer周期性地在每经过一个指定的时间间隔后就通知TimerTask一次,让TimerTask按照Timer设定的周期循环地执行任务。本程序中使用多个定时器,分别控制不同的功能,分别是屏幕刷新Timer,敌机产生Timer,魔法值变化Timer,血包生命周期Timer。
2.2 透明贴图实现技术
绘制透明位图的关键就是创建一个“掩码”位图(mask bitmap),这个“掩码”位图是一个单色位图,它是位图中图像的一个单色剪影。
整个绘制过程需要使用到ImageUtil类的createImageByMaskColorEx静态方法,把传入的原图BufferedImage中的指定颜色的背景去掉,返回去掉背景的BufferedImage对象,传送到窗口进行展示。
2.3 游戏对象列表
Java类库中提供了丰富的List接口的实现方法,本项目采用ArrayList存放游戏运行过程中的游戏对象。
(1)滚动背景模块
private static BufferedImage titleImage;// 欢迎界面图像列表
public static Scene scene; //游戏背景对象
(2)各游戏对象
public static MyPlane myplane = null;
Enemy enemy = null;
public static Boss boss = null;
Bomb bomb = null;
Ball ball = null;
Explosion explosion = null;
Blood blood = null;
(3)存储游戏对象的对象列表
public static List enemyList = new ArrayList();
public static List meList = new ArrayList();
public static List bombList = new ArrayList();
public static List ballList = new ArrayList();
public static List explosionList = new ArrayList();
public static List bloodList = new ArrayList();
(4)游戏运行相关参数:
int speed;// 战机的速度,方向键控制
public static int myLife;// 为战机设置生命值
public static int lifeNum;// 战机命条数
public static int myScore;// 战机的得分
public static int passScore;// 当前关卡得分数
public static int lifeCount;// 血包产生控制参数
public static boolean bloodExist;// 标记屏幕中是否存在血包
public static int magicCount;// 魔法值,控制能否发大招
public static int bossBlood;// Boss血量
public static int passNum;// 记录当前关卡
(5)游戏运行相关标志位
public static boolean isPass;// 是否通关的标志
public static boolean isPause;// 是否暂停
public static boolean isBoss;// 标记是否进入Boss
public static boolean bossLoaded;// 标记Boss出场完成
public static boolean isProtect;// 标记是否开启防护罩
public static boolean isUpdate;// 标记战机是否升级
public static boolean test;// 无敌模式参数
public static int isStop;// 标记游戏停止
public static boolean isStarted;// 标记欢迎界面是否加载完成
2.4获取矩形区域
使用 Rectangle的intersects方法来判断两个源矩形是否有重合的部分。如果有重合,返回true,没有从何返回false。
2.5 List处理爆炸效果
爆炸效果是连续的显示一系列的图片。如果把每一张图片都在要显示的时候进行加载,占用的时间是非常多的,必然后导致程序的可行性下降。List是一个“图象列表”是相同大小图象的集合,每个图象都可由其基于零的索引来参考。可以用来存放爆炸效果的一组图片,通过Timer消息连续循环绘制出List中的多张图片做成的爆炸效果。
三、 总体设计与详细设计
3.1 系统模块划分
该飞机大战游戏程序分为游戏滚动背景绘制模块、各游戏对象绘制模块、游戏对象之间的碰撞模块、爆炸效果产生模块、游戏界面输出玩家得分关卡信息模块、战机道具技能维护模块、消息处理模块、视图生命周期维护模块。
其中在游戏对象绘制模块中,战机是唯一对象,在游戏开始时产生该对象,赋予其固定的生命值,当其与敌机对象、敌机炸弹碰撞时使其生命值减一,直至生命值为零,便删除战机对象。敌机对象与敌机炸弹对象的绘制中采用定时器技术,定时产生。爆炸对象初始化为空,当游戏过程中即时发生碰撞时,在碰撞位置产生爆炸对象,添加到爆炸链表中,并根据爆炸素材图像分八帧进行输出,达到动画特效。
3.2 主要功能模块
3.2.1 系统对象类图
GameObject是各个游戏对象的抽象父类,继承自Object类,其他的类:战机类、敌机类、爆炸类、子弹类、炸弹类、血包类、文字类都继承了此类,Boss类继承敌机类。
每个游戏对象类中既继承了来自父类GameObject的属性,又有自己的特有属性和方法。
GameObject类介绍:
protected Point point;//该游戏对象在窗口中的坐标位置。
public boolean draw(Graphics g, JPanel panel, boolean pause);//在窗口中绘制该游戏对象图标。
public Rectangle getRect();//获取该游戏对象图标在窗口中的矩形框,进行碰撞检测时使用。
public static boolean loadImage(BufferedImage image, String source);//加载该游戏对象对应的图像到内存,方便之后的显示调用。
继承GameObject的其他游戏对象类也都重载了相关方法,以便于各个游戏对象在程序的调用过程中调用方式的一致性。
3.2.2 项目包和类层次结构图
MyPanel:JPanel的继承类,是飞机大战窗口的总显示面板,其中包含了游戏运行过程中的大量全局参数和全局标记位。
SpaceWar:程序的入口,窗口对象启动的位置,并在此对相关事件进行了监听。
Ball:敌机炮弹类,继承GameObject,有加载图片,获取矩形框,获取对象位置,绘制回想等游戏对象通用操作方法。
Blood:血包类,继承GameObject,有加载图片,获取矩形框,获取对象位置,绘制回想等游戏对象通用操作方法。
Bomb:战机炮弹类,继承GameObject,有加载图片,获取矩形框,获取对象位置,绘制回想等游戏对象通用操作方法。
Boss:Boss类,继承GameObject,有加载图片,获取矩形框,获取对象位置,绘制回想等游戏对象通用操作方法。
Enemy:敌机类,继承GameObject,有加载图片,获取矩形框,获取对象位置,绘制回想等游戏对象通用操作方法。
Explosion:爆炸效果类,继承GameObject,有加载图片,获取矩形框,获取对象位置,绘制图片等游戏对象通用操作方法。
GameObject:游戏对象基类,有加载图片,获取矩形框,获取对象位置,绘制图片的游戏对象统一的方法,统一游戏对象的操作方式。
MyPlane:战机类,继承GameObject,有加载图片,获取矩形框,获取对象位置,绘制图片等游戏对象通用操作方法。
Scene:场景类,实现了背景滚动,并根据关卡显示不同的背景图片。
EnemyTask:实现了TimerTask接口,随着Timer计时器的调用而随时间产生敌机对象和敌机炮弹对象,实现自动产生敌机的效果。
MagicTask:实现了TimerTask接口,随着Timer计时器的调用而随时间改变魔法值。
RefreshTask:实现了TimerTask接口,随着Timer计时器的调用而随时间刷新窗口界面。
AudioUtil:音频操作工具类,提供播放背景音乐和操作音效。
ImageUtil:图片加工工具类,实现了透明贴图方法和背景图片拼接。
3.2.3 系统主程序活动图
3.2.4 系统部分流程图
(1)飞机大战游戏执行流程图
(2)定时器产生敌机和炸弹流程图
(3)血包执行流程图
四、 编码实现
4.1 滚动背景
在滚动背景的初始化方法和释放方法添加背景音乐播放和释放
//场景类
public class Scene {private int beginY;// 背景的Y坐标private List<BufferedImage> images;public Scene() {this.images = new ArrayList<BufferedImage>();}// 初始化场景public boolean initScene() {// 加载开始图片BufferedImage buffer;try {buffer = ImageUtil.copyImage(ImageIO.read(new File("images/start.bmp")));this.images.add(buffer);// 如果加载失败, 返回falsefor (int i = 1; i <= 6; i++) {buffer = ImageUtil.copyImage(ImageIO.read(new File("images/background" + i + ".bmp")));this.images.add(buffer);}} catch (IOException e) {e.printStackTrace();return false;}// 背景起始坐标为0this.beginY = 0;// 播放背景音乐AudioUtil.playBackground();return true;}// 绘制场景public void stickScene(Graphics graphics, int index, ImageObserver observer) {if (index == -1)index = 0;elseindex = index % 6 + 1;BufferedImage image = images.get(index);// 窗口滑在图片中间if (beginY >= 0&& beginY + SpaceWar.WINDOWS_HEIGHT <= image.getHeight()) {BufferedImage buffer = image.getSubimage(0, beginY,image.getWidth(), SpaceWar.WINDOWS_HEIGHT);graphics.drawImage(buffer, 0, 0, SpaceWar.WINDOWS_WIDTH,SpaceWar.WINDOWS_HEIGHT, observer);} else if (beginY < 0) {// 超出图片上界BufferedImage imageUp = image.getSubimage(0, image.getHeight()+ beginY, image.getWidth(), -beginY);graphics.drawImage(imageUp, 0, 0, SpaceWar.WINDOWS_WIDTH, -beginY,observer);graphics.drawImage(image, 0, -beginY, SpaceWar.WINDOWS_WIDTH,SpaceWar.WINDOWS_HEIGHT, observer);if (-beginY > SpaceWar.WINDOWS_HEIGHT) {beginY = image.getHeight() + beginY;}}}// 移动背景public void moveBg() {// 移动背景beginY -= 1;}// 释放内存资源public void releaseScene() {for (int i = 0; i < 7; i++)if (images.get(i) != null)images.get(i).flush();// 关闭背景音乐AudioUtil.stopBackground();}public int getBeginY() {return beginY;}public void setBeginY(int beginY) {this.beginY = beginY;}
}
//在MyPanel中刷新滚动
// 滚动背景
scene.stickScene(g, -1, this);
scene.moveBg();
4.2 显示战机
if (myplane != null) {myplane.draw(g, this, isPause, isProtect);}
4.3 随机产生敌机和敌机炮弹、Boss炮弹
//随机添加敌机,敌机随机发射炸弹,此时敌机速度与数量和关卡有关
// 根据关卡数产生敌机if (MyPanel.passNum <= 5) {// 前五关只有一个方向的敌机Enemy enemy = new Enemy(Enemy.ENEMY_SPEED, 1);// 设置敌机的方向,从上方飞出enemyList.add(enemy);// 随机产生敌机if (new Random().nextInt(2) == 0) {// 控制敌机炮弹发出频率Ball ball = new Ball(enemy.getPoint().x + Enemy.ENEMY_WIDTH / 2,enemy.getPoint().y + Enemy.ENEMY_HEIGHT,enemy.getDirection());ball.setBallSpeed(enemy.getSpeed()+2);MyPanel.ballList.add(ball);// 音效AudioUtil.play(AudioUtil.AUDIO_BALL);}} else if (MyPanel.passNum > 5) {// 第五关之后,两个方向的敌机Enemy enemy1 = new Enemy(Enemy.ENEMY_SPEED, 1);// 设置敌机的方向,从上方飞出enemy1.setSpeed(Enemy.ENEMY_SPEED+ (new Random().nextInt(2) + MyPanel.passNum - 1));enemyList.add(enemy1);Enemy enemy2 = new Enemy(Enemy.ENEMY_SPEED, -1);// 设置敌机的方向,从下方飞出enemy2.setSpeed(Enemy.ENEMY_SPEED+ (new Random().nextInt(2) + MyPanel.passNum - 1));enemyList.add(enemy2);int rand = new Random().nextInt(3);if (rand == 0) {// 控制敌机炮弹发出频率Ball ball = new Ball(enemy1.getPoint().x + Enemy.ENEMY_WIDTH/ 2, enemy1.getPoint().y + Enemy.ENEMY_HEIGHT,enemy1.getDirection());ball.setBallSpeed(enemy1.getSpeed()+2);MyPanel.ballList.add(ball);// 音效AudioUtil.play(AudioUtil.AUDIO_BALL);}if (rand == 1) {// 控制敌机炮弹发出频率Ball ball = new Ball(enemy2.getPoint().x + Enemy.ENEMY_WIDTH/ 2, enemy2.getPoint().y, enemy2.getDirection());ball.setBallSpeed(enemy2.getSpeed()+2);MyPanel.ballList.add(ball);// 音效AudioUtil.play(AudioUtil.AUDIO_BALL);}}if (MyPanel.isBoss) {// Boss发射子弹// 敌机炸弹产生定时器触发// 设置定时器产生敌机炸弹Ball ball1 = new Ball(MyPanel.boss.getPoint().x + Boss.BOSS_WIDTH/ 2, MyPanel.boss.getPoint().y + Boss.BOSS_HEIGHT, 1);ball1.setBallSpeed(Ball.BALL_SPEED + (MyPanel.passNum - 1) * 2);MyPanel.ballList.add(ball1);Ball ball2 = new Ball(MyPanel.boss.getPoint().x + 5,MyPanel.boss.getPoint().y + Boss.BOSS_HEIGHT, 1);ball2.setBallSpeed(Ball.BALL_SPEED + (MyPanel.passNum - 1) * 2);MyPanel.ballList.add(ball2);Ball ball3 = new Ball(MyPanel.boss.getPoint().x + Boss.BOSS_WIDTH- 5, MyPanel.boss.getPoint().y + Boss.BOSS_HEIGHT, 1);ball3.setBallSpeed(Ball.BALL_SPEED + (MyPanel.passNum - 1) * 2);MyPanel.ballList.add(ball3);Ball ball4 = new Ball(MyPanel.boss.getPoint().x + Boss.BOSS_WIDTH/ 2 + 85, MyPanel.boss.getPoint().y + Boss.BOSS_HEIGHT, 1);ball4.setBallSpeed(Ball.BALL_SPEED + (MyPanel.passNum - 1) * 2);MyPanel.ballList.add(ball4);Ball ball5 = new Ball(MyPanel.boss.getPoint().x + Boss.BOSS_WIDTH/ 2 - 85, MyPanel.boss.getPoint().y + Boss.BOSS_HEIGHT, 1);ball5.setBallSpeed(Ball.BALL_SPEED + (MyPanel.passNum - 1) * 2);MyPanel.ballList.add(ball5);// 音效AudioUtil.play(AudioUtil.AUDIO_BALL);}
4.4 显示战机发射子弹
for (int i = 0; i < bombList.size(); i++) {bomb = bombList.get(i);if (bomb == null)continue;bomb.setCurrentIndex(i);bomb.isUpdate = isUpdate;if (!bomb.draw(g, this, isPause))i--;}
4.5 碰撞检测,以战机子弹集中敌机为例
if (MyPanel.myplane != null && !MyPanel.isPause) {// 子弹打中敌机boolean flag = false;for (int i = 0; i < MyPanel.bombList.size(); i++) {Bomb bomb = MyPanel.bombList.get(i);if (bomb == null)continue;Rectangle bombRectangle = bomb.getRect();for (int j = 0; j < MyPanel.enemyList.size(); j++) {Enemy enemy = MyPanel.enemyList.get(j);if (enemy == null)continue;Rectangle enemyRectangle = enemy.getRect();if (enemyRectangle.intersects(bombRectangle)) {Explosion explosion = new Explosion((bomb.getPoint().x + Bomb.BOMB_WIDTH / 2 - Explosion.EXPLOSION_WIDTH / 2),(bomb.getPoint().y + Bomb.BOMB_HEIGHT / 2 - Explosion.EXPLOSION_WIDTH / 2));MyPanel.explosionList.add(explosion);// 音效AudioUtil.play(AudioUtil.AUDIO_EXPLOSION);// 爆炸后删除子弹MyPanel.bombList.remove(i);i--;// 敌机生命值减少enemy.life -= MyPanel.isUpdate ? 2 : 1;if (enemy.life <= 0) {// 增加得分MyPanel.passScore++;// 删除敌机MyPanel.enemyList.remove(j);j--;}// 炮弹已删除,直接跳出本循环flag = true;break;}}if (flag)continue;if (MyPanel.isBoss && bomb != null) {// 获得战机子弹的矩形区域Rectangle bombRect = bomb.getRect();// 获得Boss的矩形区域Rectangle bossRect = MyPanel.boss.getRect();// 判断两个矩形区域是否有交接if (bombRect.intersects(bossRect)) {// 将爆炸对象添加到爆炸链表中Explosion explosion = new Explosion((bomb.getPoint().x + Bomb.BOMB_WIDTH / 2 - Explosion.EXPLOSION_WIDTH / 2),(bomb.getPoint().y + Bomb.BOMB_HEIGHT / 2 - Explosion.EXPLOSION_WIDTH / 2));MyPanel.explosionList.add(explosion);// 音效AudioUtil.play(AudioUtil.AUDIO_EXPLOSION);// 爆炸后删除子弹MyPanel.bombList.remove(i);i--;bomb = null;// 是Boss,不删除敌机,只扣血MyPanel.bossBlood -= MyPanel.isUpdate ? 2 : 1;if (MyPanel.bossBlood <= 0) {Explosion explosion1 = new Explosion(MyPanel.boss.getPoint().x,MyPanel.boss.getPoint().y);MyPanel.explosionList.add(explosion1);Explosion explosion2 = new Explosion((MyPanel.boss.getPoint().x + Boss.BOSS_WIDTH),(MyPanel.boss.getPoint().y + Boss.BOSS_HEIGHT));MyPanel.explosionList.add(explosion2);Explosion explosion3 = new Explosion((MyPanel.boss.getPoint().x + Boss.BOSS_WIDTH),(MyPanel.boss.getPoint().y));MyPanel.explosionList.add(explosion3);Explosion explosion4 = new Explosion((MyPanel.boss.getPoint().x),(MyPanel.boss.getPoint().y + Boss.BOSS_HEIGHT));MyPanel.explosionList.add(explosion4);Explosion explosion5 = new Explosion((MyPanel.boss.getPoint().x+ Boss.BOSS_WIDTH / 2 - Explosion.EXPLOSION_WIDTH / 2),(MyPanel.boss.getPoint().y+ Boss.BOSS_HEIGHT / 2 - Explosion.EXPLOSION_WIDTH / 2));explosion5.setBossDie(true);// 标记最后一个炸弹,炸完之后跳入下一关MyPanel.explosionList.add(explosion5);MyPanel.boss = null;// 过关的标志变量// isPause = TRUE;// CMyPlane* temp = myplane;// myplane = new CMyPlane(FALSE);MyPanel.myplane = null;MyPanel.isPass = true;MyPanel.isBoss = false;}}}}}
4.6 显示爆炸效果
for (int i = 0; i < explosionList.size(); i++) {explosion = explosionList.get(i);if (explosion == null)continue;boolean b = explosion.draw(g, this, isPause);if (!b) {explosionList.remove(i);i--;}}
4.7 血包功能
游戏三分之一和三分之二进程时刻出现血包
//开启血包if (MyPanel.myplane != null && MyPanel.myLife > 0 && !MyPanel.isPause) {// 关卡打了三分之一三分之二处出现血包if (MyPanel.passScore > (MyPanel.PASS_SCORE + MyPanel.passNum * 5)* MyPanel.lifeCount / 3) {// 若屏幕中有未吃掉的血包,这次不产生血包if (!MyPanel.bloodExist) {MyPanel.lifeCount++;// 产生血包Blood blood = new Blood();MyPanel.bloodList.add(blood);MyPanel.bloodExist = true;bloodTimer = new Timer();bloodTimer.schedule(new TimerTask() {@Overridepublic void run() {bloodTimer.cancel();bloodTimer = null;MyPanel.bloodExist = false;// 声明血包位置for (int i = 0; i < MyPanel.bloodList.size(); i++) {MyPanel.bloodList.remove(i);i--;}}}, 10000);} elseMyPanel.lifeCount++;}}
//血包定时器,10秒后血包消失bloodTimer = new Timer();bloodTimer.schedule(new TimerTask() {@Overridepublic void run() {bloodTimer.cancel();bloodTimer = null;MyPanel.bloodExist = false;// 声明血包位置for (int i = 0; i < MyPanel.bloodList.size(); i++) {MyPanel.bloodList.remove(i);i--;}}}, 10000);
//显示血包if (myplane != null && !isPause) {// 检索血包链表,非空时在所在位置显示int i = 0;while (i < bloodList.size()) {blood = bloodList.get(i);if (blood == null)continue;blood.draw(g, this, false);i++;}// while}
//血包碰撞检测if (MyPanel.myplane != null && !MyPanel.isPause) {// 吃到血包// 声明血包位置for (int i = 0; i < MyPanel.bloodList.size(); i++) {Blood blood = MyPanel.bloodList.get(i);// 获得血包矩形Rectangle bloodbRect = blood.getRect();// 获得战机矩形Rectangle planeRect = MyPanel.myplane.getRect();// 判断两个矩形区域是否有交接if (bloodbRect.intersects(planeRect)) {// 音效AudioUtil.play(AudioUtil.AUDIO_BLOOD);// 加血效果MyPanel.myLife += 5;if (MyPanel.myLife > MyPanel.DEFAULT_LIFE)MyPanel.myLife = MyPanel.DEFAULT_LIFE;// TODO 声音// 加血后血包删除MyPanel.bloodList.remove(i);i--;break;}// if}// for}
4.8 通关和死亡消息页面
if (isStop == FLAG_RESTART) {Font textFont = new Font("宋体", Font.BOLD, 20);g.setFont(textFont);// 设置透明背景// cdc.SetBkMode(TRANSPARENT);g.setColor(Color.red);g.drawString("哇,恭喜你已通关!", SpaceWar.WINDOWS_WIDTH / 2 - 100,SpaceWar.WINDOWS_HEIGHT / 2 - 30);g.drawString("您的得分为:" + myScore, SpaceWar.WINDOWS_WIDTH / 2 - 100,SpaceWar.WINDOWS_HEIGHT / 2 - 10);g.drawString("COME ON !重新开始?Y/N", SpaceWar.WINDOWS_WIDTH / 2 - 100,SpaceWar.WINDOWS_HEIGHT / 2 + 10);return;} else if (isStop == FLAG_STOP) {Font textFont = new Font("宋体", Font.BOLD, 20);g.setFont(textFont);// 设置透明背景// cdc.SetBkMode(TRANSPARENT);g.setColor(Color.red);// 显示最后结果g.drawString("GAME OVER!", SpaceWar.WINDOWS_WIDTH / 2 - 100,SpaceWar.WINDOWS_HEIGHT / 2 - 30);g.drawString("您的得分为:" + myScore, SpaceWar.WINDOWS_WIDTH / 2 - 100,SpaceWar.WINDOWS_HEIGHT / 2 - 10);g.drawString("COME ON !重新开始?Y/N", SpaceWar.WINDOWS_WIDTH / 2 - 100,SpaceWar.WINDOWS_HEIGHT / 2 + 10);return;}
4.9 魔法值控制维护
public class MagicTask extends TimerTask {@Overridepublic void run() {if (MyPanel.myplane != null && !MyPanel.isPause && MyPanel.isStarted) {// 防护罩和战机升级没打开,魔法值递增if (!MyPanel.isProtect && !MyPanel.isUpdate) {MyPanel.magicCount++;if (MyPanel.magicCount > 10)MyPanel.magicCount = 10;}// 判断是否打开防护罩if (MyPanel.isProtect) {// 开启防护罩魔法值递减MyPanel.magicCount--;if (MyPanel.magicCount <= 0) {MyPanel.magicCount = 0;MyPanel.isProtect = false;}}// 判断是否升级战机if (MyPanel.isUpdate) {// 战机升级,魔法值递减MyPanel.magicCount--;if (MyPanel.magicCount <= 0) {MyPanel.magicCount = 0;MyPanel.isUpdate = false;MyPanel.myplane.isUpdate = MyPanel.isUpdate;}}}}
}
4.10 得分到达关卡需求,进入Boss
// 进入Bossint pScore = MyPanel.PASS_SCORE + MyPanel.passNum * 5;// TODO调试条件// if (MyPanel.myplane != null && MyPanel.passScore >= 3 &&// !MyPanel.isPause&&!MyPanel.isBoss)if (MyPanel.myplane != null && MyPanel.passScore >= pScore&& !MyPanel.isPause && !MyPanel.isBoss) {// 进入BossMyPanel.isBoss = true;MyPanel.boss = new Boss(1);MyPanel.boss.setSpeed(Boss.BOSS_SPEED + MyPanel.passNum - 1);MyPanel.boss.life = Boss.BOSS_LIFE + MyPanel.passNum * 50;// Boss总血量MyPanel.bossBlood = Boss.BOSS_LIFE + MyPanel.passNum * 50;// 当前Boss血量// Boss出场,暂停游戏MyPanel.bossLoaded = false;// 重新设置Boss的子弹产生频率,增强Boss子弹发射频率MyPanel.enemyTimer.cancel();MyPanel.enemyTimer = null;MyPanel.enemyTimer = new Timer();MyPanel.enemyTimer.schedule(new EnemyTask(MyPanel.enemyList), 0,2000 - MyPanel.passNum * 120);}
//显示Bossif (myplane != null && boss != null && !isPause && isBoss) {boolean status = boss.draw(g, this, passNum, isPause);if (status)bossLoaded = true;}
4.11 检测标记位isPass,判断打赢Boss,进入下一关
if (MyPanel.isPass) {MyPanel.isPass = false;if (MyPanel.passNum == 10)// 10关{// 重新初始化数据MyPanel.killTimer();MyPanel.myplane = new MyPlane(false);MyPanel.isPause = true;MyPanel.isStop = MyPanel.FLAG_RESTART;// 清屏}// ifelse {MyPanel.killTimer();MyPanel.isPause = true;// 保存所需数据int tScore = MyPanel.myScore + MyPanel.passScore;int tPassNum = MyPanel.passNum + 1;boolean tTest = MyPanel.test;int magic = MyPanel.magicCount;// 重新开始游戏MyPanel.Restart();MyPanel.myplane = new MyPlane(false);MyPanel.myScore = tScore;MyPanel.passNum = tPassNum;MyPanel.magicCount = magic;MyPanel.test = tTest;}// else}// if
五、 课程设计中遇到的主要问题及解决方法
5.1滚动背景实现问题
要实现滚动背景,需要在背景图上取出客户区矩形大小的区域,并按照Timer进行递进,并且要在背景图边界处衔接好返回背景图开头位置,实现一张背景图的循环反复,达到滚动背景图的目的,刚开始由于不懂得如何实现开头结尾处的衔接问题,导致滚动背景图实现难度大,后来经由网上查阅的资料得知要把背景图加载两份,第二份背景图开头衔接在第一份结尾处,让客户区矩形在该两份背景图上进行滑动,当滑动到第二份背景图时,再把第一份接到第二份结尾处,从而实现循环反复滚动背景。其中实现的难点在于控制好客户区矩形坐标和背景图上要显示的图片块位于图片上的坐标的对应关系。
5.2 背景音乐循环播放和游戏音效同时输出问题
由于平时接触Swing太少,对Swing操作多媒体文件颇为陌生,对于Java的音频播放也是颇为陌生,通过网上查找了很多相关资料,才把AudioClip对音频的播放功能实现,而且刚开始使用,没注意到音频文件播放完的释放问题,导致程序运行一阶段之后出现了内存溢出而崩溃的情况。
5.3多帧位图素材的动画特效实现问题
飞机大战中的爆炸显示是通过一张带有八个帧的位图进行连续输出实现爆炸特效,刚开始只是简单的在一个while循环中循环八次,结果不尽如人意,后来联想到帧和时间的对应关系,在每一次TimerTask调用时输出一帧,并在爆炸对象中用progress标记位记录播放位置,等到八个帧播放结束时,返回播放结束标记位,在TimerTask中检测并删除播放完成的该爆炸对象。
5.4 游戏结束和游戏重新开始的游戏资源维护问题
该游戏由于实现了多个额外功能,且都是带有全局意义的,因此放置了较多标记位来标记每个状态,通过这些标记位来控制游戏进程中的各个状态,因此在游戏结束和重新开始游戏时,各个标记位的正确重置将会决定重新开始游戏之后的游戏状态。还由于这些操作可能会在TimerTask类调用过程中进行中断,因此程序运行中途的中断时的游戏参数的维护对程序的正确执行至关重要。
六、程序截图
七、 课程设计体会
由于对Swing接触不多,因此在开发过程中走了很多弯路,但是也是这次的实训,让我了解了Swing的事件处理机制,了解了如何在屏幕中绘制需要向用户展示的图形信息,学会了使用基本的操作对屏幕中绘制的图形进行控制,也了解了Java的Timer定时器的机制,学习了Sun封装好的Swing类库,学会遇到问题到JDK文档中查询Api,并对Eclipse的使用和编程也进行了提高。
在开发过程中遇到了大量的异常,通过此次开发,也学会了遇到错误如何慢慢通过IDE的错误信息进行错误查找和代码排错更正,通过添加断点调试程序一步步监视程序运行的具体过程,从而找到程序运行出错的原因。
本次的课程设计的飞机大战代码是基于前一次的MFC飞机大战进行移植的,因此,在模块的分布和设计方面大多维持原来C++的布局,因此,本次项目开发的源码层次接口没有严格的按照Java的规范,这是这个项目的不足之处,如果时间允许,应该着手对其中的代码布局进行相应的修改,以符合Java的风格,增强代码的可读性。
八、交流与联系
q:969060742 报告、完整源码、程序资源