潜艇游戏需求:
所参与的角色:
战舰、深水炸弹、侦察潜艇、鱼雷潜艇、水雷潜艇、水雷
角色间的关系:
战舰发射深水炸弹
深水炸弹可以打潜艇(侦察潜艇、鱼雷潜艇、水雷潜艇),若打中:
潜艇消失、深水炸弹消失
得东西:
打掉侦察潜艇,玩家得10分
打掉鱼雷潜艇,玩家得40分
打掉水雷潜艇,战舰得1条命
水雷潜艇可以发射水雷
水雷可以击打战舰,若击中:
水雷消失
战舰减1条命(命数为0时游戏结束)
一、day01
创建6个类,创建World类并测试
二、day02
给6个类设计构造方法,并测试
三、day03
设计侦察潜艇数组、鱼雷潜艇数组、水雷潜艇数组、水雷数组、炸弹数组,并测试
设计SeaObject超类,设计6个类继承SeaObject
给SeaObject设计两个构造方法,6个类中分别调用
将侦察潜艇数组、鱼雷潜艇数组、水雷潜艇数组统一组合为SeaObject数组,并测试
四、day04
给6个类重写 move() 使其坐标点移动,并测试
侦察潜艇,鱼雷潜艇,水雷潜艇
由于潜艇是从左往右移动,速度为speed,y 坐标不改变,改变的为 x 坐标
方法实现为 x+=speed
水雷
由于水雷是从下往上移动,速度为speed,x 坐标不改变,改变的为 y 坐标
方法实现为 y-=speed
炸弹
由于炸弹是从上往下移动,速度为speed,x 坐标不改变,改变的为 y 坐标
方法实现为 y+=speed
战舰
由于它的移动方式和其他的不一致,方法只需要重写,具体实现先搁置不写
测试
World
main方法
分别创建 5 个对象
分别输出每个对象的 x,y,speed 数值
分别调用每个对象的 move() 方法
在动用每个对象的 move() 方法后,再次对对象的 x,y,speed 值进行输出
测试可参考以下示例:
ObserveSubmarine o1 = new ObserveSubmarine();
System.out.println("侦察潜艇初始数据-----x:"+o1.x+",y:"+o1.y+",speed:"+o1.speed);
o1.move();
System.out.println("侦察潜艇移动后数据---x:"+o1.x+",y:"+o1.y+",speed:"+o1.speed);在这里插入代码片
给类中成员添加访问控制修饰符
访问控制修饰符是为了控制访问权限的,适当的减小访问权限可以使得代码更加安全
通常我们会将属性(成员变量)私有化private,行为(方法)公开化public
但是由于私有的属性不能被继承,这里我们还需要将 SeaObject 中的属性更改为 protected
将图片拷贝到项目中
在项目下创建 img 文件夹/目录
将8张图片拷贝到 img 文件夹中
最好将图片拷贝之后,进行一个 Build -> Rebuild Project 操作

设计Images图片类
在项目的包下创建类 Images
目的:
这个类是为了封装我们的 图片对象而设计的
思路:
由于我们为了减少内存消耗,想让图片只有一份,这里使用 static 关键字对图片对象进行修饰
由于我们想项目一起动就对图片进行加载,使用 static 关键字修饰静态代码块,为图片对象进行赋值
创建8个图片对象,
public static ImageIcon xxx;
静态代码块对图片对象进行赋值操作
static{xxx = new ImageIcon("img/xxx.png");}
测试
Images 类中创建main方法
在main方法中分别调用 System.out.println(xxx.getImageLoadStatus()); 进行测试
测试结果为 8 ,表示正确
五、day05
今日目标:

设计窗口的宽和高为常量,在适当的地方做修改
World 类中设计宽WIDTH = 641,高HEIGHT = 479
main方法里内容删除
画窗口
World类 继承 JPanel
main方法里内容复制
public static void main(String[] args) {
JFrame frame = new JFrame(); //3.
World world = new World(); //会创建窗口中的那一堆对象
world.setFocusable(true);
frame.add(world);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(WIDTH+16, HEIGHT+40);
frame.setLocationRelativeTo(null);
frame.setResizable(false);
frame.setVisible(true); //自动调用paint()方法
}
画海洋图、至少准备6个对象并画出来
SeaObject
修改 SeaObject 为抽象类
修改 move 为抽象方法
创建 getImage 抽象方法
返回值类型为 ImageIcon 类型
定义两个状态常量(LIVE,DEAD)和一个变量 表示当前状态(state)
LIVE = 0 表示活着
DEAD = 1 表示死了
state = LIVE 表示默认为活着
创建 isLive 方法
返回值类型:boolean 类型
业务:判断当前状态是否为活着
创建 isDead 方法
返回值类型:boolean 类型
业务:判断当前状态是否为死了
创建 paintImage 方法
返回值类型 void
参数 Graphics g
业务:如果当前状态为活着,画图片
/** 画对象 g:画笔 */
public void paintImage(Graphics g){
if(this.isLive()){ //若活着的
this.getImage().paintIcon(null,g,this.x,this.y); //----不要求掌握
}
}
Battleship
重写 getImage 方法,返回对应的 ImageIcon 对象
ObserveSubmarine
重写 getImage 方法,返回对应的 ImageIcon 对象
TorpedoSubmarine
重写 getImage 方法,返回对应的 ImageIcon 对象
MineSubmarine
重写 getImage 方法,返回对应的 ImageIcon 对象
Bomb
重写 getImage 方法,返回对应的 ImageIcon 对象
Mine
重写 getImage 方法,返回对应的 ImageIcon 对象
World
创建战舰(Battleship对象,SeaObject数组对象,Mine数组对象,Bomb数组对象),并赋值
重画
/** 重写paint()画 g:系统自带的画笔 */
public void paint(Graphics g){
Images.sea.paintIcon(null,g,0,0); //画海洋图
ship.paintImage(g); //画战舰
for(int i=0;i<submarines.length;i++){ //遍历所有潜艇
submarines[i].paintImage(g); //画潜艇
}
for(int i=0;i<mines.length;i++){ //遍历所有水雷
mines[i].paintImage(g); //画水雷
}
for(int i=0;i<bombs.length;i++){ //遍历所有炸弹
bombs[i].paintImage(g); //画炸弹
}
}
测试
修改 SeaObject 类,2个参数的构造方法,将负号去掉,为了能在屏幕上显示已经画好的对象

运行测试(海洋,战舰,潜艇[侦查潜艇,鱼雷潜艇,水雷潜艇],水雷,炸弹等对象是否显示在窗口里)
六、day06
清空成员变量数组中的元素
潜艇数组 submarines
水雷数组 mines
炸弹数组 bombs
定时做某件事:
定义 action 方法,作为游戏的启动执行方法
由于游戏是要求定时做某件事,所以创建定时器对象 Timer,util 包下的 Timer
调用 Timer对象中 schedule 方法 - 3个参数的方法
在方法中将定时器中调用需要定时执行的方法,定时间隔,每10毫秒走一次
潜艇入场
水雷入场
海洋对象移动
重画
在 main 方法中调用 action 方法
潜艇入场:
生成潜艇对象方法(侦察潜艇、鱼雷潜艇、水雷潜艇)
方法名 nextSubmarine
返回值类型 SeaObject
要求随机返回某一潜艇对象,为了统一返回的类型,所以使用父类 SeaObject 作为返回值类型
方法具体实现
业务:按照一定比例随机生成潜艇对象
侦查潜艇:50%
鱼雷潜艇:30%
水雷潜艇:20%
思路:
通过随机数的形式,加上if…else,返回对应的潜艇对象
潜艇入场方法
方法名 submarineEnterAction
返回值类型 void
方法具体实现
业务:每400毫秒调用nextSubmarine方法生成一个潜艇对象
每10豪秒走一次方法submarineEnterAction,现在想每400毫秒走一次/调用一个 nextSubmarine 生成潜艇对象
之前我们已经清空了潜艇数组(为的就是想让潜艇对象从无到有)
将生成的潜艇对象添加到World类中,成员变量(submarines)的潜艇数组中
思路:添加一个计数的变量subEnterIndex,通过整除的方式(计数变量自增,取余),if判断等方式达到目的
水雷入场(上):
水雷入场方法
方法名:mineEnterAction
返回值类型:void
方法具体实现
业务:
每1000毫秒方法生成一个水雷对象
每10豪秒走一次方法mineEnterAction,现在想每1000毫秒走一次/调用一个 xxx方法(当日未实现) 生成潜艇对象
思路:添加一个计数的变量mineEnterIndex,通过整除的方式(计数变量自增,取余),if判断等方式达到目的
海洋对象移动:
方法名:moveAction
返回值类型:void
方法具体实现:
业务:
每10豪秒走一次方法moveAction
分别循环遍历海洋对象(潜艇对象,水雷对象,炸弹)
循环中调用每个子类中的重写的move方法
七、day07
深水炸弹入场:
业务:
深水炸弹是由战舰发出的
我们深水炸弹是通过键盘进行触发的
将深水炸弹存放进炸弹数组
思路:
通过监听键盘,去调用创建深水炸弹的方法
将深水炸弹存放进炸弹数组
由于深水炸弹是由战舰发出的,坐标点为战舰的坐标点,所以这个生成深水炸弹对象的方法需要写在战舰类上(Battleship)
// 可以参考,不需要必须会写
KeyAdapter k = new KeyAdapter(){
public void keyPressed(KeyEvent e) {
if(e.getKeyCode() == KeyEvent.VK_SPACE){
// 深水炸弹入场
}
}
};
this.addKeyListener(k);
战舰移动:
业务:
通过键盘进行触发战舰的左右移动
思路
和上述空格键触发深水炸弹一样,写在键盘出发的方法里
通过不同的按键触发,调用不同的方法
通过按 ← 箭头,触发 Battleship 中的 moveLeft 战舰左移方法
通过按 → 箭头,触发 Battleship 中的 moveRight 战舰右移方法
if(e.getKeyCode()==KeyEvent.VK_LEFT){ //不要求掌握--若抬起的是左箭头
// 战舰左移
}
if(e.getKeyCode()==KeyEvent.VK_RIGHT){ //不要求掌握--若抬起的是右箭头
//战舰右移
}
moveLeft 和 moveRight 实现,移动 Battleship 的 x 坐标
删除越界的海洋对象:
方法名:outOfBoundsAction
返回值类型:void
业务:
由于超越边界的潜艇,深水炸弹,鱼雷就没有作用了,并且占用内存资源,所以我们需要删除它们
潜艇是超出右边的边界删除
深水炸弹是超出底下的边界删除
鱼雷是超过海平面伤处
思路
分别循环所有的海洋对象,判断是否越界,如果越界,从数组中删除
判断是否越界
SeaObject(潜艇越界)
定义 isOutOfBounds 方法,返回值类型为boolean类型
潜艇的x坐标 >= 世界的WIDTH
Bomb(深水炸弹越界)
重写 isOutOfBounds 方法
深水炸弹的y坐标 >= 世界的WEIGHT
Mine(鱼雷越界)
重写 isOutOfBounds 方法
鱼雷的y坐标 <= 150(海平面的y坐标)- 鱼雷的高height
数组删除
将数组的最后一个元素赋值给想要删除的元素
数组缩容
设计EnemyScore得分接口、EnemyLife得命接口,侦察潜艇与鱼雷潜艇实现EnemyScore接口,水雷潜艇实现EnemyLife接口
EnemyScore 得分接口
声明得分的抽象方法
方法名:getScore
返回值类型:int
侦察潜艇和鱼类潜艇实现得分接口,重写 getScore 方法
侦察潜艇得10分
鱼类潜艇得40分
EnemyLife 得命接口
声明得名的抽象方法
方法名:getLife
返回值类型:int
水雷潜艇实现得命接口,重写 getLife 方法
水雷潜艇得1条命
八、day08
水雷入场(下):
水雷入场之前我们已经将方法创建完成,mineEnterAction,但是具体的生成水雷对象及将水雷对象添加到水雷数组中未做,我们需要的就是根据之前的水雷入场继续做即可
业务:
生成水雷对象,并将水雷对象添加到水雷数组中
由于是定时出现水雷,之后我们需要将 mineEnterAction 方法在定时器中调用
思路:
由于水雷对象是由水雷潜艇产生,水雷对象的初始坐标点与水雷潜艇的坐标点有关,所以我们应该在水雷潜艇类中创建生成水雷对象的方法 shootMine
我们需要先遍历潜艇数组中所有的潜艇对象,再判断这些潜艇对象是否是水雷潜艇(instanceof),如果是水雷潜艇(MineSubmarine),调用创建水雷对象方法(shootMine),并且对水雷数组(mines)进行扩容,之后将水雷对象(Mine)添加到水雷数组中
炸弹与潜艇的碰撞:
在 SeaObject 中设计 isHit() 检测碰撞方法
由于碰撞的两者都是 SeaObject 类型,所以将方法写在 SeaObject 类中,
由于碰撞是两个对象的事,所以我们在设计这个方法的时候,需要提供一个参数 (SeaObject other)
由于这个方法是判断是否碰撞,所以返回值类型为 boolean 类型
碰撞逻辑如下图所示:
参考代码:
/**
* 检测碰撞
* @param other 另一个对象 this表示一个对象
* @return 若撞上了则返回true,否则返回false
*/
public boolean isHit(SeaObject other){
//假设:this为潜艇,other为炸弹
int x1 = this.x-other.width; //x1:潜艇的x-炸弹的宽
int x2 = this.x+this.width; //x2:潜艇的x+潜艇的宽
int y1 = this.y-other.height; //y1:潜艇的y-炸弹的高
int y2 = this.y+this.height; //y2:潜艇的y+潜艇的高
int x = other.x; //x:炸弹的x
int y = other.y; //y:炸弹的y //练习-----------2:34继续
return x>=x1 && x<=x2
&&
y>=y1 && y<=y2; //x在x1与x2之间,并且,y在y1与y2之间,即为撞上了
}
在 SeaObject 中设计 goDead() 去死方法 - 变更状态
业务:
变更状态,将海洋对象的状态 state 从 LIVE 变为 DEAD
方法名: goDead
返回值: void
在 Battleship 中设计 addLife() 增命方法
业务:设计增命方法
思路:给 Battleship 的 life 属性累加
方法名:addLife
参数:int num
在 World 中设计 bombBangAction() 炸弹与潜艇的碰撞方法
业务:
判断炸弹与潜艇是否碰撞,如果碰撞,让炸弹和潜艇一起去死
如果摧毁的是水雷潜艇得命
如果摧毁的是侦察潜艇得10分
如果摧毁的是鱼雷潜艇得40分
思路:
判断所有炸弹与潜艇是否碰撞
首先需要嵌套循环两个数组(bombs 炸弹,submarines 潜艇)
判断炸弹是否活着(isLive())
判断潜艇是否活着(isLive())
判断炸弹与潜艇是否碰撞(isHit())
如果都满足则代表碰撞上了
让炸弹去死(goDead),让潜艇去死(goDead)
判断摧毁的是得分的潜艇(侦察/鱼雷)
向下转型(引用类型强转)
判断这个对象是否实现了得分接口 (instanceof)
调用 getScore 方法
判断摧毁的是得命的潜艇
向下转型(引用类型强转)
判断这个对象是否实现了得命接口 (instanceof)
调用 getLife 获取命数的方法 和 addLife 增加命数的方法
在定时器中调用 bombBangAction() 方法
海洋对象死亡删除
在 outOfBoundsAction 删除越界的海洋对象的方法中添加是否死了的判断
将方法改为 越界和死亡 都删除删除对象
参考代码:
画分和画命:
在 Battleship 中设计 getLife() 获取命数
方法名:getLife
返回值类型:int - 获取 Battleship 中的 life 属性
在World类的 paint() 中:画分和画命------------不要求掌握
g.drawString("SCORE: "+score,200,50); //画分----不要求掌握
g.drawString("LIFE: "+ship.getLife(),400,50);
这是一个关于使用Java开发潜艇大战小游戏的系列教程,涵盖了从创建类到实现游戏逻辑的全过程。包括潜艇、深水炸弹、水雷等角色的设计,角色间的交互,以及游戏对象的移动、碰撞检测和状态管理。教程分为8天,逐步构建游戏世界、实现对象移动、绘制游戏画面、添加键盘控制、处理对象入场和碰撞效果,以及分数和生命值的管理。
1632

被折叠的 条评论
为什么被折叠?



