作者:林Lychee
https://blog.csdn.net/weixin_43776741/article/details/88924146
有谁没玩过植物大战僵尸吗?一位读者用Java语言开发了自己的植物大战僵尸游戏。虽然系统相对简单,但是麻雀虽小五脏俱全,对游戏开发感兴趣的小伙伴可以学习一下。
游戏设计
植物大战僵尸中有一个小游戏关卡,屏幕的正上方有一个滚轮机,会随机生成植物,玩家可以选中植物后自由选择草坪来进行安放。基于此游戏模式,我将该关卡抽取出来,单独做成了一个简易版的植物大战僵尸。游戏的画面大概如下:
屏幕左侧会自动生成植物的卡牌,单击选中后可以放置在草坪上。右侧会自动生成僵尸,不同的僵尸移动速度不同,血量不同,还有的僵尸有隐藏奖励,比如:全屏僵尸静止、全屏僵尸死亡等。当时竟然没有做游戏的暂停的功能,导致现在截图的时机很难把控,那这里就先说一下游戏暂停的功能应该怎么做吧。
最简单的一种暂停方式是鼠标移出屏幕,游戏暂停。所以这里需要引入一个鼠标监听器事件。
<code>import
psutil pl = psutil.pids()/<code>
当然,这只是一个简单的通过监听鼠标的位置来改变游戏状态方法。还可以使用键盘监听器,当按下某个键时游戏暂停,这样的用户体验更好。但原理是一样的,这里就不展示代码了。
游戏对象
首先分析一下游戏中有哪些对象。各式各样的植物,各式各样的僵尸,各式各样的子弹。那么这里就可以抽出三个父类,分别是植物、僵尸、子弹。在面向对象中,子类将继承父类所有的属性和方法。所以可以将三大类中,共有的属性和方法抽到各自的父类中。比如僵尸父类:
<code>for
pid in pl: if psutil.Process(pid).name()==ProcessName: get_desk() Warnning()/<code>
植物父类、子弹父类就同理可得了。
上面说到子类共有的方法需要抽到父类中,那么部分子类共有的方法该如何处理呢?比如,豌豆射手、寒冰射手可以发射子弹,坚果墙就没有射击的这个行为。所以这里就需要用到接口(Interface)。
<code>public
interface
Shoot
{public
abstract
Bullet[]shoot
(); }/<code>
到此为止,游戏对象的属性、方法基本都定义完了,至于图片的显示以及如何将图片画出来,只需要使用相应的API即可,这里就不做描述了。工作一年回过来看看,这里能优化的地方还有很多,比如对象的血量、攻击力、移动等都可以统统写入到配置文件中,这样在做游戏参数的调整时,不需要去修改代码相关的内容,只需要修改配置文件里面的参数即可。
游戏内容
现在我们有了游戏的对象,该开始让对象加入到游戏中来,接着让他们动起来,最后还得让他们打起来。首先,让对象加入到游戏中来我是这么做的,这里还是以僵尸为例:
<code>private
List zombies =new
ArrayList();public
ZombienextOneZombie
() { Random rand =new
Random();int
type = rand.nextInt(20
);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++;if
(zombieEnterTime%300
==0
) { zombies.add
(nextOneZombie()); } }/<code>
最早时候我用的数据结构是数组,但在后续的编码中发现,对僵尸对象有很多的遍历以及增删操作,数组的增删操作是十分麻烦复杂的,所以我就换成了集合。在工作中也一样,先思考在编码,选择正确的数据结构往往能起到事半功倍的效果。
植物入场的设计,是我当时自认为很精妙的一个点。先说一下当时在编码中发现的问题。首先植物入场时是在滚轮机上的,滚轮机上的移动就会涉及到追击和停止的问题。追击的方式当然是追前一个植物卡牌,但当第一个植物卡牌被选中放置到草地上后,那该如何追击呢?
最开始我的做法是给植物多加几个状态来解决这个问题,但是发现状态过多会导致if判断中的条件将大大增加,并且在尝试后还是没有实现想要的效果,于是我就将植物集合一分为二,在后面的游戏功能设计中,回头过来看才发现将植物集合分为滚轮机上的集合和战场上的集合实在是太精妙了。请听我娓娓道来:
<code>private
List plants =new
ArrayList();private
List plantsLife =new
ArrayList();public
void
plantBangAction
() {for
(int
i=1
;i/<code>
当然,滚轮机上的对植物状态判断的代码还是显得生涩,也正是自己想优化这段代码时萌生了分享游戏设计过程和游戏代码的念头。那么下面就说说,这段代码该如何优化:
<code>if
(!(plants.get
(i).isStop()||plants.get
(i).isWait()) {break
; }if
(!(plants.get
(i-1
).isStop()||plants.get
(i-1
).isWait())) {break
; }if
(!(plants.get
(i).getY()<=plants.get
(i-1
).getY()+plants.get
(i-1
).getHeight())) {break
; } plants.get
(i).goStop();/<code>
boolean条件当然也可以进行优化,甚至还可以简化一下植物的状态。这里因为游戏的规则,僵尸只能攻击在草坪上的植物,所以把带放置的植物和草坪上的植物分为两个集合,是十分合理精妙的。在判断僵尸是否攻击植物,只需要去遍历草坪上的植物集合即可。如果不拆分,当要判断僵尸是否攻击植物的时候,需要遍历的集合将是所有的植物集合,并且需要增加至少2个状态来区分植物是在草坪上还是在滚轮机上,这段代码想想就是又臭又长。
接下来该让对象们都动起来了。之前说到在父类中的移动方法是抽象方法,在各自的子类中都进行重写后,不同的对象移动方式就是各式各样的了。
<code>public
void
BulletStepAction
()
{for
(Bullet b:bullets) { b.step(); } }int
zombieStepTime =0
;public
void
zombieStepAction
()
{if
(zombieStepTime++%3
==0
) {for
(Zombie z:zombies) {if
(z.isLife()) { z.step(); } } } }/<code>
看着代码中对集合复杂的遍历,不得不感概lambda表达式真是个好东西:
<code>def
Warnning
()
: win32api.MessageBox(0
,"别打游戏,我看着你呢"
,"提醒"
, win32con.MB_ICONWARNING)/<code>
这里好像还是没法展示lambda表达式强大的功能,请看下面的例子:
<code>public
ListfilterStudentByStrategy
(List students, SimpleStrategy strategy)
{ List filterStudents =new
ArrayList<>();for
(Student student : filterStudents) {if
(strategy.operate(student)){ filterStudents.add(student); } }return
filterStudents; }public
interface
SimpleStrategy
<T
> {public
boolean
operate
(T t)
; }/<code>
但好像还是有点麻烦,又要写接口,又要写实现类,后续的维护也是个头疼问题,这个时候救世主lambda表达式就出现了:
<code>List
lambdaStudents = students.stream().filter(student -> student.getGender()==1
).collect(Collectors.toList());/<code>
让我们看看上面到底发生了啥。首先将数据的集合流化,接着调用过滤方法,强大lambda表达式让代码变得简洁,并且判断条件的修改可在代码中直接维护无需在策略接口的实现类维护。最后在转成集合,返回一个满足产品需求的集合。
回到正题,如何让对象们打起来呢?下面以僵尸攻击植物为例:
<code>public
boolean
zombieHit
(Plant p)
{int
x1 =this
.x-p.getWidth();int
x2 =this
.x+this
.width;int
y1 =this
.y-p.getHeight();int
y2 =this
.y+this
.width;int
x = p.getX();int
y = p.getY();return
x>=x1 && x<=x2 && y>=y1 && y<=y2; }/<code>
结合图片来看,上述代码应该就更好理解。黑框P代表植物,黑框Z代表植物,虚线是指两者接触的极限距离,当僵尸进入虚线内,就保证可以攻击到植物。
<code>int
zombieHitTime =0
;public
void
zombieHitAction
()
{if
(zombieHitTime++%100
==0
) {for
(Zombie z:zombies) {if
(!z.isDead()) { z.goLife(); }for
(Plant p:plantsLife) {if
(z.isLife()&&!p.isDead()&&z.zombieHit(p)&&!(pinstanceof
Spikerock)) { z.goAttack(); p.loseLive(); } } } } }/<code>
如果出现了一些效果的偏移,造成的原因是图片大小不一造成的坐标偏移,因为图片都是网上找的,所以效果不是太理想。
至此,游戏的基本功能基本实现了。Java是一门面向对象的语言,万物皆对象,特征皆属性,行为皆方法。肉眼能看到的僵尸、植物、草坪都是对象,对象的特性比如血量、移动速度都是属性,对象的行为比如移动、攻击、死亡都是方法。
下面说说对游戏功能的优化。
游戏优化
1.放置植物的优化
已经放置过植物的草地不能再放置植物了。之前是将草地设计成empty和hold两种状态,现在来看其实只需要返回一个true和false就行了,将整个植物集合定义成一个虚拟的boolean集合即可。
2.移除植物的优化
设计思路是新增一个铲子对象:
<code>private
List shovels =new
ArrayList();public
void
shovelEnterAction
() {if
(shovels.size()==0
) { shovels.add
(new
Shovel()); } } Iterator it = shovels.iterator(); Iterator it2 = plantsLife.iterator();while
(it.hasNext()) { Shovel s = it.next();if
(s.isMove()) {while
(it2.hasNext()) { Plant p = it2.next();int
x1 = p.getX();int
x2 = p.getX()+p.getWidth();int
y1 = p.getY();int
y2 = p.getY()+p.getHeight();if
((p.isLife()||((Blover) p).isClick())&&Mx>x1&&Mxy1&&My/<code>
看着这极其复杂好像很厉害的代码,我又萌生了痛下狠手的想法,但为了保持原生,我忍住。于是乎还发现了一个BUG。如果选中铲子后,战场上唯一的植物被僵尸吃掉了,那么这个铲子将一直跟随着鼠标无法达到使用后消除的效果了。解决方案当然也很简单,当战场上植物集合的size为0时,清空铲子集合即可。
3.游戏可玩性的优化
上文在游戏设计中提到的击杀僵尸后可能随机获得奖励类型是这样实现的。还是从设计分析开始,并非击杀任何类型的僵尸都可以获得奖励,所以奖励应该放在接口中:
<code>public
interface
Award
{public
static
final
int
CLEAR =0
;public
static
final
int
STOP =1
;public
abstract
int
getAwardType
()
; }/<code>
当僵尸死亡时,需要去判断该僵尸是否有奖励接口,如果有则执行相应奖励的方法:
<code>public
void
checkZombieAction
() { Iterator it = zombies.iterator();while
(it.hasNext()) { Zombie z = it.next();if
(z.getLive()0
) {if
(z instanceof Award) { Award a = (Award)z;int
type = a.getAwardType();switch
(type) {case
Award.CLEAR:for
(Zombie zo:zombies) { zo.goDead(); }break
;case
Award.STOP:for
(Zombie zom:zombies) { zom.goStop(); timeStop =1
; }break
; } } z.goDead(); it.remove
(); }if
(z.OutOfBound()) { gameLife--; it.remove
(); } } }/<code>
4.添加游戏背景音乐
bgm是一个游戏的灵魂之一。这里给游戏添加背景音乐,我的选择是新建一条线程专门用来执行音乐的解析和播放:
<code> Runnable r =new
zombieAubio("bgm.wav"
); Thread t =new
Thread(r); t.start();public
class
zombieAubio
implements
Runnable
{private
String filename;public
zombieAubio
(String wavfile)
{ filename=wavfile; } ....../<code>
这里需要注意的是,Java中解析音乐的API只支持WAV格式的文件,文件格式的转换大多数音乐播放器都可以做到。
后续优化
1.植物种类的扩充及对应功能的实现
比如杀伤力最大的玉米加农炮。需要4个小玉米进行合成,那么在判断是否能够合成玉米加农炮时,需要对植物集合进行遍历来做坐标的判断,所以这边建议最好把可合成的植物单独放在一个集合中,这样在做合成判断的时候会简单很多,当集合的size小于4时,就可以提示合成失败了。冰冻西瓜的设计思路也是如此。
2.动作类僵尸的加入,如撑杆跳僵尸、跳舞僵尸等
说一下撑杆跳僵尸的设计思路,此类僵尸和其他僵尸相比,多了一种跳的行为,所以会有一个单独的方法和单独的状态。并且,跳只能触发一次,所以撑杆跳僵尸的状态变化应该是行走->遇到植物跳过去->再遇到植物就开始攻击,在执行状态变化的时候,应该要去考虑当前的状态是否还可跳跃。
3.当植物攻击范围内不存在僵尸时,植物停止攻击
这个就简单拉,在植物执行攻击方法时,校验一下是否有Y坐标相同的僵尸即可。
GitHub项目源码地址:
https://github.com/llx330441824/plant_vs_zombie_simple.git