作者:林荔枝
序
谁没玩过植物大战僵尸?
一位读者用Java开发了自己的游戏《植物大战僵尸》。虽然系统比较简单,虽然麻雀虽小五脏俱全,但是对游戏开发感兴趣的朋友可以了解一下~ ~
游戏《植物大战僵尸》里有一个小游戏关卡。屏幕上方有一个滚筒机,会随机生成植物。玩家选择植物后可以自由选择草坪放置。基于这个游戏模式,我提取了这个关卡,做了一个简单版的《植物大战僵尸》。游戏画面大致如下:
屏幕左侧会自动生成植物的卡片,点击选择后可以放在草坪上。僵尸会在右侧自动生成。不同的僵尸移动速度不同,血量也不同,有的僵尸还有隐藏奖励,比如:全屏僵尸静止,全屏僵尸死亡等。当时没有暂停游戏的功能,导致现在截图的时机很难控制。所以下面是如何做暂停游戏的功能。
最简单的暂停方法是将鼠标移出屏幕,暂停游戏。所以这里我们需要引入一个鼠标监听器事件。
游戏运行时公共鼠标移动(mouse event e){//If(status==start){//通过移动事件的对象获取当前鼠标位置int x=e . getx();int y=e . gety();//如果鼠标超出游戏界面If(xgame . width | | ygame . height){//将游戏状态改为pause status=pause}}}当然,这只是通过监测鼠标的位置来改变游戏状态的简单方法。还可以使用键盘显示器,按下一个键游戏暂停,用户体验更好。但原理是一样的,这里就不展示代码了。
游戏首先分析游戏中有哪些对象。各种植物,各种丧尸,各种子弹。然后你可以在这里画出三个父类,分别是植物,僵尸,子弹。在面向对象中,子类继承父类的所有属性和方法。因此,可以将三个类别中的公共属性和方法绘制到各自的父类中。比如僵尸父类:
公共抽象类Zombie {//zombie父类//zombie共享的属性protected int width受保护的int高度;受保护的int live受保护的int x;受保护的int y;//僵尸的state public static final int LIFE=0;public static final int ATTACK=1;public static final int DEAD=2;protected int state=LIFE/* *这里补充一下为什么父类是抽象类。比如每个僵尸都有一个移动方法,*但是每个僵尸移动的方式不一样,所以这个方法的方法体可能不一样。*抽象方法中没有方法体,所以可以在子类中重写。*但有抽象方法的类一定是抽象类,所以父类一般是抽象类*//移动方式是公共抽象void step();这是一个很好的例子.工厂父类和项目符号父类可以用同样的方法获得。
如上所述,子类共享的方法需要被拉入父类,那么如何处理部分子类共享的方法呢?比如豌豆射手和雪豌豆可以发射子弹,坚果墙不行。所以你需要使用这里的接口。
interface shot {//Shooting interface——将一些子类共有的行为提取到接口中的方法//接口默认为公共抽象,标准代码要将这个字段丢弃到公共抽象Bullet[]shoot();}到目前为止,游戏对象的属性和方法基本都定义好了。至于图片的显示以及如何绘制,只需要使用相应的API就可以了,这里就不描述了。一年工作下来回头看,这里还有很多可以优化的地方。比如血量,攻击力,物体的移动都可以写入配置文件。这样在调整游戏参数时,不需要修改代码相关的内容,只需要修改配置文件中的参数即可。
游戏内容现在我们有了游戏的对象,是时候开始让他们加入游戏,然后让他们移动,最后让他们战斗了。首先,让物体加入游戏。我是这么做的。这里有一个僵尸的例子:
//首先要有一套僵尸。//僵尸的集合privatelistzombies=new ArrayList();//然后定义随机生成的僵尸方法public zombie next zombie(){ random rand=new random();//控制不同种类僵尸出现的概率int type=rand . nextint(20);if(type 5){ return new zombie 0();} else if(type 10){ return new zombie 1();} else if(type 15){ return new zombie 2();}else {返回新僵尸3();} }//僵尸入场//设置入场间隔/* *这里补充一下为什么要设置入场间隔*因为游戏的运行是基于一个计时器的,*每隔一段时间,计时器就会执行你添加到计时器中的方法,*所以这里需要设置入场间隔来控制游戏的速度。*/int zombientertime=0;public void zombienteraction(){ zombientertime;//为自增僵尸计算余数if(zombientertime==0){//在满足条件时调用随机生成僵尸的方法,将生成的僵尸添加到zombie . add(nextone zombie())的集合中;}}一开始我是用数组作为数据结构的,但是在后续的编码中,我发现对僵尸对象的遍历和增删操作很多,数组的增删操作非常麻烦复杂,所以就用集合代替了。在工作中,首先考虑编码并选择正确的数据结构通常会事半功倍。
厂房入口的设计是我当时觉得很精致的一个点。先说说当时编码中发现的问题。首先,植物进场是在滚筒上,滚筒上的运动会涉及到追停的问题。当然追击的方式是追前面的植物卡,但是第一张植物卡被选中放在草地上,怎么追击呢?
一开始我的做法是给植物增加更多的状态来解决这个问题,但是我发现太多的状态会大大增加if判断中的条件,尝试了一下还是没有达到想要的效果,于是我把植物集合分成了两部分。在后期的游戏功能设计中,回过头来看,我发现植物集可以分为滚筒集和战场集,真的很精致。请听我说:
//滚筒上的植物处于停止等待私有列表Plants=NEW ArrayList()状态;//战场上的植物处于生命状态,move -move是被鼠标选中并移动的状态。这里的设计不合理,会导致下面一个bug私有列表Plants Life=New ArrayList();//确定植物在滚筒上的碰撞。public void plant ban(){//遍历roller上的plant集合,从第二个for(int I=1;i0plants.get(0)。isStop()) { plants.get(0)。goWait();}//如果第I株植物的y小于第i-1株植物的y高度,则表示已经被触碰,I的改变状态为STOP IF ((plants。获取(I)。是STOP () ||植物。获取(I)。是WAIT())(植物。获取(i-1)。是停()|植物。获取(i-1)。是WAIT())植物。=plants.get(i-1)。getY() plants.get(i-1)。getHeight() ) { plants.get(i)。goStop();}/* *如果第I棵植物的y大于第i-1棵植物的y高,说明没有被碰过或者第i-1 *棵植物被移走了。把I的状态改成等待,可以继续上*/if (plants.get (i)。isstop () plants.get (i)。gety () plants.get (i-1)。gety () plants.get (i-1)。get height()){ plants . get(I)} } } }//检测滚筒机器上植物的状态public void checkplantaction 1(){//Iterator Iterator it=plants . Iterator();while(it . has next()){ Plant p=it . next();/* *如果滚轮组*中有处于移动或生命状态的植物,将其添加到战场植物组,并从原数组中删除*/* *现在发现,将滚轮组上处于移动状态的植物添加到战场植物组的最佳时间应该是*等到植物变为生命后再添加。*/if(p . is move()| | p . is life()){ plants life . add(p);it . remove();}}}当然,判断滚筒上植物状态的代码看起来还是比较粗糙的,也是在我想优化这段代码的时候,才有了分享游戏设计过程和游戏代码的想法。所以我们来谈谈如何优化这段代码:
//先说明状态。//wait-植物卡在滚筒上移动的状态。因为等待被鼠标选中,所以命名为wait//stop——植物卡停在滚筒上的状态。有两种情况,1-登顶2-打到最后一张牌。//开始优化下面的代码。//如果第I棵植物y小于第i-1棵植物的y高度,则表示满足,I的变化状态为STOP//IF ((plants.get (i))。iStop () || plants.get (i)。iSWAIT ())//(plants.get (i-1)。iStop () || plants.get (i-1)。iSWAIT。=plants.get(i-1)。getY() plants.get(i-1)。getHeight()//) {//plants.get(i)。goStop();//}//优化后的代码看起来是这样的//把一个复杂的布尔分解成多个if条件if(!(plants.get(i)。isStop()||plants.get(i)。is wait()){ break;}如果(!(plants.get(i-1)。isStop()||plants.get(i-1)。is wait())){ break;}如果(!(plants.get(i)。getY()=plants.get(i-1)。getY() plants.get(i-1)。getHeight())){ break;} plants.get(i)。goStop();布尔条件当然可以优化,甚至可以简化植物的状态。因为这里的游戏规则,僵尸只能攻击草坪上的植物,所以把用带子放置的植物和草坪上的植物分成两组是非常合理和微妙的。判断丧尸是否攻击植物,我们只需要遍历草坪上的植物集合。如果不拆分,当需要判断僵尸是否攻击植物时,需要遍历的集合将是所有植物集合,至少需要增加2个状态来区分植物是在草坪上还是在滚筒上。仔细想想这段代码又臭又长。
接下来,是时候让物体移动了。当涉及到移动父类中的方法时,它们是抽象方法。在各自的子类中重写后,不同的移动对象的方式是各种各样的。
//Bullet movement public void Bullet stepping(){ for(Bullet b : bullets){ b . step();} }//僵尸移动//设置移动间隔int zombieStepTime=0;公共僵尸职业(){if(僵尸职业% 3==0) {for(僵尸z 3360僵尸){//只有活着的僵尸才会动if(z . is life()){ z . step();}}}}看代码中集合的复杂遍历,不得不觉得lambda表达式真的是个好东西:
//Bullet movement public void Bullet stepping(){ bullets . foreach((b)-b . step()));}这里似乎还是无法展现lambda表达式的强大功能。请看下面的例子:
//为了满足产品不断变化的需求,前人经验得出的设计模式已经能够在一定程度上满足这个问题。//设计模式声明策略接口,完成过滤逻辑public list filterstudentbystrategy(list students,simple strategy strategy){ list filter students=new ArrayList();for(学生Student : filter students){ if(strategy . operate(Student)){ filter students . add(Student);} }返回filterStudents}//当需求发生变化时,只需要改变策略接口实现类中的判断逻辑,使公共接口简单策略{公共布尔运算(t t);}但是好像还是有点麻烦。需要写接口和实现类,后续的维护也很头疼。这时,救世主拉姆达的表情出现了:
//不需要接口就可以实现需求的快速变化。list lambda students=students . stream()。filter(student-student . getgender()==1)。collect(collectors . to list());让我们看看上面发生了什么。首先,对数据集进行流式处理,然后调用过滤方法。强大的lambda表达式使得代码简洁,判断条件的修改可以直接在代码中维护,无需在策略接口的实现类中维护。最后转换成集合,返回一个符合产品要求的集合。
回到正题,如何让对象打起来?以丧尸攻击植物为例:
//僵尸的攻击方式是在僵尸的超类中定义的。//因为丧尸的攻击行为是一样的,这里是常用的方法。//僵尸攻击植物公共布尔僵尸hit(植物p){ intx 1=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}结合图片,上面的代码应该比较好理解。黑盒P代表植物,黑盒Z代表植物,虚线表示两者之间的极限距离。僵尸进入虚线,保证攻击植物。
//僵尸攻击//设置攻击间隔int zombieHitTime=0;public Zombie health(){ If(Zombie health 0==0){ for(Zombie z 3360 zombies){//如果战场上没有植物,将所有僵尸的状态改为生命/* *这里补充一下为什么要先将所有僵尸的状态改为生命,也就是移动状态*因为下面判断僵尸是否攻击植物是从遍历战场上的植物集合开始的*如果一个僵尸吃了植物,吃了战场上唯一的一个,*那么僵尸的状态会从攻击变为移动?*所以这里用的是逆向思路。第一,所有僵尸改为移动状态*如果满足攻击条件,则改为攻击状态。*即使战场上没有植物,那么丧尸还是处于移动状态*/if(!z . is dead()){ z . golife();}//这里应该有一个穿越战场上植物集合的判断为(Plant p:plantsLife) {//如果僵尸是活的并且植物是活的,并且僵尸在攻击植物的范围内/* *这里有一个BUG,僵尸会攻击没有被鼠标选择放下的植物,*所以下面的判断条件应该也需要移除鼠标选择的植物*/If(z . is . p . is dead()z . zombiehit(p)!(p instanceof Spikerock)) {//僵尸状态改为攻击状态z . goat tack();//植物失血p . lose live();}}}}}如果有一些效果偏移,原因是图片大小不一导致的坐标偏移。因为图片都是网上找的,效果不太理想。
至此,游戏的基本功能基本实现。Java是一种面向对象的语言。一切都是对象,特征是属性,行为是方法。肉眼能看到的僵尸、植物、草坪都是对象。血量、移动速度等物体特征都是属性,移动、攻击、死亡等物体行为都是方法。
先说游戏功能的优化。
游戏优化1。工厂布局的优化
在已经种过植物的草地上,不能种植物。以前,草原被设计成空旷的,可以容纳。现在实际上只需要返回一个true和false,整个植物集合就可以定义为一个虚拟的布尔集合。
2.搬迁植物的优化
设计思想是添加一个铲子对象:
//铲集privatelistshoes=newarraylist();//铲子入场公共void铲子action(){//只有一个铲子if(铲子.大小()=0){铲子. add(新铲子());} }//使用铲子迭代器it=铲子.迭代器();迭代器it2=plants life . iterator();while(it.hasNext()) {铲子s=it . next();//如果铲子在移动,遍历植物集If(s . is move()){ while(it2 . has next()){ 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 ()) Mmx1mx1MY看着这段看似很强大的极其复杂的代码,我又有痛宰手的想法,但为了保持原生,我忍住了。所以我发现了一个漏洞。如果选择了铁锹,战场上唯一的植物被丧尸吃掉了,那么铁锹就会一直跟着鼠标,使用后无法消灭。当然解决方法也很简单。当战场上的植物集合大小为0时,清空铁铲集合即可。
3.游戏可玩性的优化
上面游戏设计中提到的杀死僵尸后可能随机获得的奖励类型就是这样实现的。还是从设计分析出发,不是杀死任何一种僵尸都可以获得奖励,所以奖励要放在界面里:
接口奖励{//奖励接口/* *这里还是有代码不规范的问题*接口的默认模式是public abstract *接口中的默认变量是public static final *这些默认字段应该丢弃*//全屏静态public static final int CLEAR=0;//全屏清除public static final int STOP=1;公共抽象int getAwardType();}僵尸死亡时,需要判断僵尸是否有奖励界面,如果有,执行相应的奖励方式:
//检测僵尸状态public void check zombie(){//Iterator Iterator it=zombies . Iterator();while(it.hasNext()) {僵尸z=it . next();//一个僵尸在血量小于0时死亡,死亡的僵尸从集合中删除if (z . get live()=0){//接口if(z实例of award) {award a=(award) z判断一个僵尸是否有奖励;int type=a . getawardtype();开关(类型){案例奖。CLEAR: for(僵尸zo :僵尸){ zo . godead();}破;案例奖。STOP: for(僵尸STOP:僵尸){ zom . gostop();timeStop=1;//zombieGoLife();}破;} } z . godead();it . remove();}//僵尸跑进屋,游戏生命减一,删除僵尸if(z . out of bound()){ game life-;it . remove();} }}4.添加游戏背景音乐
Bgm是游戏的灵魂之一。在这里为游戏添加背景音乐,我的选择是创建一个新的线程专门用于执行音乐解析和播放:
//启动线程加载音乐Runnable r=New zombie bio(‘ bgm . wav ‘);螺纹t=新螺纹(r);t . start();类僵尸bio实现runnable {//read audio WAV格式私有字符串文件名;public zombie aubio(String wav file){ filename=wav file;} .这里需要注意的是,Java解析音乐的API只支持WAV格式的文件,文件格式的转换大部分音乐播放器都可以完成。
后续1。植物种类的扩展和相应功能的实现。
比如杀伤力最大的玉米炮。合成需要四个小玉米,所以在判断玉米炮能否合成时,需要遍历植物集来判断坐标。所以这里建议,最好把可以合成的植物单独放在一个集合里,这样会让合成判断容易很多。当集合的大小小于4时,可以指示合成失败。冻西瓜的设计思路也是一样的。
2.加入动作僵尸,比如撑杆跳僵尸,跳舞僵尸。
先说撑杆跳僵尸的设计思路。这种僵尸相比其他僵尸多了一个跳跃行为,所以会有单独的方法和单独的状态。而且跳跃只能触发一次,所以撑杆跳僵尸的状态变化应该是行走-遇到植物时跳跃-再次遇到植物时攻击。在进行状态改变时,要考虑当前状态是否还能跳转。
3.当植物的攻击范围内没有僵尸时,植物停止攻击。
很简单的事情。当植物执行攻击方法时,检查是否有相同Y坐标的僵尸。
Github源地址:https://github.com/llx330441824/plant_vs_zombie_simple.git