基于Java基础-面向对象实现植物大战僵尸简易版
基于Java基础-⾯向对象实现植物⼤战僵⼫简易版
最大的变化是什么成语前⾔
从零开始学习的Java的第⼀个⽉,在⾯向对象(OOP)的学习中,跟着讲师完成了飞机⼤战的游戏编码。第⼆个⽉开始接触API,⼼⾎来潮便结合API中的集合、多线程开始植物⼤战僵⼫的编码,从游戏模式的设计到游戏内容的实现⼤约花费2个礼拜的时间。如今是我接触Java第7个⽉,回头来看曾经⾃⼰引以为豪甚⾄在⼀定程度上帮助⾃⼰拿到offer的作品,代码显得稚嫩、⽣涩。本来想好好改造⼀番代码,在新增⼀些未完成的功能,但⼜不忍⼼动⼑,毕竟是⾃⼰的第⼀个完整作品。于是乎想把设计思路和游戏代码都分享出来,和⼤家⼀起互相交流学习。
游戏设计
植物⼤战僵⼫中有⼀个⼩游戏关卡,屏幕的正上⽅有⼀个滚轮机,会随机⽣成植物,玩家可以选中植物后⾃由选择草坪来进⾏安放。基于此游戏模式,我将该关卡抽取出来,单独做成了⼀个简易版的植物⼤战僵⼫。游戏的画⾯⼤概如下
屏幕左侧会⾃动⽣成植物的卡牌,单击选中后可以放置在草坪上。右侧会⾃动⽣成僵⼫,不同的僵⼫移动速度不同,⾎量不同,还有的僵⼫有隐藏奖励,⽐如:全屏僵⼫静⽌、全屏僵⼫死亡等。当时竟然没
有做游戏的暂停的功能,导致现在截图的时机很难把控,那这⾥就先说⼀下游戏暂停的功能应该怎么做吧。
最简单的⼀种暂停⽅式是⿏标移出屏幕,游戏暂停。所以这⾥需要引⼊⼀个⿏标事件。
public void mouseMoved(MouseEvent e) {
// 当游戏处于运⾏状态时
if (status == start) {
// 通过⿏标移动事件的对象获取当前⿏标的位置
int x = e.getX();
int y = e.getY();
// 如果⿏标超出了游戏界⾯
if (x > Game.WIDTH || y > Game.HEIGHT) {
// 将游戏的状态改为暂停状态
status = pause;
}
}
}
当然,这只是⼀个简单的通过监听⿏标的位置来改变游戏状态⽅法。还可以使⽤键盘,当按下某个键时游戏暂停,这样的⽤户体验更好。但原理是⼀样的,这⾥就不展⽰代码了。
游戏对象
⾸先分析⼀下游戏中有哪些对象。各式各样的植物,各式各样的僵⼫,各式各样的⼦弹。那么这⾥就可以抽出三个⽗类,分别是植物、僵⼫、⼦弹。在⾯向对象中,⼦类将继承⽗类所有的属性和⽅法。所以可以将三⼤类中,共有的属性和⽅法抽到各⾃的⽗类中。⽐如僵⼫⽗类
public abstract class Zombie {
// 僵⼫⽗类
/
个人身份证贷款/ 僵⼫共有的属性
protected int width;
protected int height;
protected int live;
protected int x;
protected int y;
......
// 僵⼫的状态
public static final int LIFE = 0;
public static final int ATTACK = 1;
public static final int DEAD = 2;
protected int state = LIFE;
/*
* 这⾥补充⼀下为什么⽗类是抽象类,⽐如每个僵⼫都有移动⽅法,
* 但每个僵⼫的移动⽅式是不同,所以该⽅法的⽅法体可能是不同的,
* 抽象⽅法没有⽅法体,在⼦类中再去进⾏重写就可以了,
* 但有抽象⽅法的类必须是抽象类,因此⽗类⼀般都是抽象类
*/
// 移动⽅式
public abstract void step();
....
}
植物⽗类、⼦弹⽗类就同理可得了。
上⾯说到⼦类共有的⽅法需要抽到⽗类中,那么部分⼦类共有的⽅法该如何处理呢?⽐如,豌⾖射⼿、寒冰射⼿可以发射⼦弹,坚果墙就没有射击的这个⾏为。所以这⾥就需要⽤到接⼝(Interface)。
public interface Shoot {
// 射击接⼝ - 将部分⼦类共有的⾏为抽取到接⼝中
// 接⼝中的⽅法默认是public abstract的,规范的编码应该将该字段舍去
public abstract Bullet[] shoot();
}
到此为⽌,游戏对象的属性、⽅法基本都定义完了,⾄于图⽚的显⽰以及如何将图⽚画出来,只需要使⽤相应的API即可,这⾥就不做描述了。⼯作⼀年回过来看看,这⾥能优化的地⽅还有很多,⽐如对象的⾎量、攻击⼒、移动等都可以统统写⼊到配置⽂件中,这样在做游戏参数的调整时,不需要去修改代码相关的内容,只需要修改配置⽂件⾥⾯的参数即可。
游戏内容
现在我们有了游戏的对象,该开始让对象加⼊到游戏中来,接着让他们动起来,最后还得让他们打起来。⾸先,让对象加⼊到游戏中来我是这么做的,这⾥还是以僵⼫为例
// ⾸先有要⼀个僵⼫的集合
// 僵⼫集合
private List<Zombie> zombies = new ArrayList<Zombie>();
// 接着定义随机⽣成僵⼫⽅法
public Zombie nextOneZombie() {
Random rand = new Random();
// 控制不同种类僵⼫出现的概率
int type = Int(20);
江苏卫视2022跨年晚会节目单
if(type<5) {
return new Zombie0();
}else if(type<10) {
return new Zombie1();
}else if(type<15) {
return new Zombie2();
}else {
return new Zombie3();
}
}
// 僵⼫⼊场
// 设置进场间隔
/*
* 这⾥补充⼀下为什么要设置进场的间隔
* 因为游戏的运⾏是基于定时器的,
* 每隔⼀段时间定时器就会执⾏⼀次你所加⼊定时器的⽅法,
* 所以这⾥需要设置进场间隔来控制游戏的速度。
*/
int zombieEnterTime = 0;
public void zombieEnterAction() {
zombieEnterTime++;
// 对⾃增量zombieEnterTime进⾏取余计算
if(zombieEnterTime%300==0) {
// 满⾜条件就调⽤随机⽣成僵⼫⽅法,并将⽣成的僵⼫加⼊到僵⼫的集合中
zombies.add(nextOneZombie());
}
}
最早时候我⽤的数据结构是数组,但在后续的编码中发现,对僵⼫对象有很多的遍历以及增删操作,数组的增删操作是⼗分⿇烦复杂的,所以我就换成了集合。在⼯作中也⼀样,先思考在编码,选择正确的数据结构往往能起到事半功倍的效果。
植物⼊场的设计,是我当时⾃认为很精妙的⼀个点。先说⼀下当时在编码中发现的问题。⾸先植物⼊场时是在滚轮机上的,滚轮机上的移动就会涉及到追击和停⽌的问题。追击的⽅式当然是追前⼀个植物卡牌,但当第⼀个植物卡牌被选中放置到草地上后,那该如何追击呢?最开始我的做法是给植物多加⼏个状态来解决这个问题,但是发现状态过多会导致if判断中的条件将⼤⼤增加,并且在尝试后还是没有实现想要的效果,于是我就将植物集合⼀分为⼆,在后⾯的游戏功能设计中,回头过来看才发现将植物集合分为滚轮机上的集合和战场上的集合实在是太精妙了。请听我娓娓道来。
// 滚轮机上的植物,状态为stop和wait
private List<Plant> plants = new ArrayList<Plant>();
// 战场上的植物,状态为life和move -move为被⿏标选中移动的状态,这⾥设计不合理,会引发后⾯的⼀个BUG
private List<Plant> plantsLife = new ArrayList<Plant>();
// 植物在滚轮机上的碰撞判定
public void plantBangAction() {
// 遍历滚轮机上植物集合,从第⼆个开始
for(int i=1;i<plants.size();i++) {
// 如果第⼀个植物的y⼤于0,并且是stop状态,则状态改为wait
(0).getY()>0&&(0).isStop()) {
<(0).goWait();
}
// 如果第i个植物y⼩于i-1个植物的y+height,则说明碰到了,改变i的状态为stop
if(((i).isStop()||(i).isWait())&&
((i-1).isStop()||(i-1).isWait())&&
<(i).getY()<=(i-1).getY()+(i-1).getHeight()
) {
传奇归来装备<(i).goStop();
}
/*
* 如果第i个植物y⼤于于i-1个植物的y+height,则说明还没碰到或者第i-1个
* 植物被移⾛了,改变i的状态为wait,可以继续往上⾛
*/
(i).isStop()&&
<(i).getY()&(i-1).getY()+(i-1).getHeight()) {
<(i).goWait();
}
}
}
// 检测滚轮机上的植物状态
public void checkPlantAction1() {
// 迭代器
Iterator<Plant> it = plants.iterator();
while(it.hasNext()) {
Plant p = it.next();
/
*
* 如果滚轮机集合⾥有move或者life状态的植物
* 则添加到战场植物的集合中,并从原数组中删除
*/斗罗大陆胡列娜
/*
* 现在发现把滚轮机上move状态的植物添加到
* 战场上植物集合的最佳操作时间点应该是
* 等植物状态变为life后再添加。
* /
if(p.isMove()||p.isLife()) {
plantsLife.add(p);
}
}
}
当然,滚轮机上的对植物状态判断的代码还是显得⽣涩,也正是⾃⼰想优化这段代码时萌⽣了分享游戏设计过程和游戏代码的念头。那么下⾯就说说,这段代码该如何优化
// 先对状态做下说明
// wait - 植物卡牌在滚轮机上移动状态,因为是等着被⿏标选中,所以取名为wait
// stop - 植物卡牌在滚轮机上停⽌状态,有两种情况,1 - 到顶了 2 - 撞到上⼀个卡牌了
// 开始对以下代码进⾏优化
// 如果第i个植物y⼩于i-1个植物的y+height,则说明碰到了,改变i的状态为stop
//            if(((i).isStop()||(i).isWait())&&
//                    ((i-1).isStop()||(i-1).isWait())&&
//                    (i).getY()<=(i-1).getY()+(i-1).getHeight()
//                    ) {
//                (i).goStop();
//            }
// 优化后的代码是这样的
// 将⼀个复杂的boolean拆成多个if条件
if (!((i).isStop()||(i).isWait()) {
break;
}
if (!((i-1).isStop()||(i-1).isWait())) {
break;
}
if (!((i).getY()<=(i-1).getY()+(i-1).getHeight())) {
break;
}
<(i).goStop();
boolean条件当然也可以进⾏优化,甚⾄还可以简化⼀下植物的状态。这⾥因为游戏的规则,僵⼫只能攻击在草坪上的植物,所以把带放置的植物和草坪上的植物分为两个集合,是⼗分合理精妙的。在判断僵⼫是否攻击植物,只需要去遍历草坪上的植物集合即可。如果不拆分,当要判断僵⼫是否攻击植物的时候,需要遍历的集合将是所有的植物集合,并且需要增加⾄少2个状态来区分植物是在草坪上还是在滚轮机上,这段代码想想就是⼜臭⼜长。
接下来该让对象们都动起来了。之前说到在⽗类中的移动⽅法是抽象⽅法,在各⾃的⼦类中都进⾏重写后,不同的对象移动⽅式就是各式各样的了。
// ⼦弹移动
public void BulletStepAction() {
for(Bullet b:bullets) {
b.step();
}
}
//僵⼫移动
//设置移动间隔
int zombieStepTime = 0;
public void zombieStepAction() {
卡巴斯基key
if(zombieStepTime++%3==0) {
for(Zombie z:zombies) {
//只有活着的僵⼫会移动
if(z.isLife()) {
z.step();
}
}
}
}
看着代码中对集合复杂的遍历,不得不感概lambda表达式真是个好东西

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。