用 Java 语言,写一个植物大战僵尸简易版

内容速读:

作者 | 林Lychee来源 | 程序员小灰前言有谁没玩过植物大战僵尸吗?小灰的一位读者,用Java语言开发了自己的植物大战僵尸游戏。基于此游戏模式,我将该关卡抽取出来,单独做成了一个简易版的植物大战僵尸。那么这里就可以抽出三个父类,分别是植物、僵尸、子弹。

用 Java 语言,写一个植物大战僵尸简易版

作者 | 林Lychee

来源 | 程序员小灰

前言

有谁没玩过植物大战僵尸吗?

小灰的一位读者,用Java语言开发了自己的植物大战僵尸游戏。虽然系统相对简单,但是麻雀虽小五脏俱全,对游戏开发感兴趣的小伙伴可以学习一下哦~~

用 Java 语言,写一个植物大战僵尸简易版

游戏设计

植物大战僵尸中有一个小游戏关卡,屏幕的正上方有一个滚轮机,会随机生成植物,玩家可以选中植物后自由选择草坪来进行安放。基于此游戏模式,我将该关卡抽取出来,单独做成了一个简易版的植物大战僵尸。游戏的画面大概如下:

用 Java 语言,写一个植物大战僵尸简易版

屏幕左侧会自动生成植物的卡牌,单击选中后可以放置在草坪上。右侧会自动生成僵尸,不同的僵尸移动速度不同,血量不同,还有的僵尸有隐藏奖励,比如:全屏僵尸静止、全屏僵尸死亡等。当时竟然没有做游戏的暂停的功能,导致现在截图的时机很难把控,那这里就先说一下游戏暂停的功能应该怎么做吧。

最简单的一种暂停方式是鼠标移出屏幕,游戏暂停。所以这里需要引入一个鼠标监听器事件。

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; } }}

当然,这只是一个简单的通过监听鼠标的位置来改变游戏状态方法。还可以使用键盘监听器,当按下某个键时游戏暂停,这样的用户体验更好。但原理是一样的,这里就不展示代码了。

用 Java 语言,写一个植物大战僵尸简易版

游戏对象

首先分析一下游戏中有哪些对象。各式各样的植物,各式各样的僵尸,各式各样的子弹。那么这里就可以抽出三个父类,分别是植物、僵尸、子弹。在面向对象中,子类将继承父类所有的属性和方法。所以可以将三大类中,共有的属性和方法抽到各自的父类中。比如僵尸父类:

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即可,这里就不做描述了。工作一年回过来看看,这里能优化的地方还有很多,比如对象的血量、攻击力、移动等都可以统统写入到配置文件中,这样在做游戏参数的调整时,不需要去修改代码相关的内容,只需要修改配置文件里面的参数即可。

用 Java 语言,写一个植物大战僵尸简易版

游戏内容

现在我们有了游戏的对象,该开始让对象加入到游戏中来,接着让他们动起来,最后还得让他们打起来。首先,让对象加入到游戏中来我是这么做的,这里还是以僵尸为例:

// 首先要有一个僵尸的集合// 僵尸集合private List zombies = new ArrayList;// 接着定义随机生成僵尸方法public Zombie nextOneZombie { 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++; // 对自增量zombieEnterTime进行取余计算 if(zombieEnterTime%300==0) { // 满足条件就调用随机生成僵尸方法,并将生成的僵尸加入到僵尸的集合中 zombies.add(nextOneZombie); }}

最早时候我用的数据结构是数组,但在后续的编码中发现,对僵尸对象有很多的遍历以及增删操作,数组的增删操作是十分麻烦复杂的,所以我就换成了集合。在工作中也一样,先思考在编码,选择正确的数据结构往往能起到事半功倍的效果。

植物入场的设计,是我当时自认为很精妙的一个点。先说一下当时在编码中发现的问题。首先植物入场时是在滚轮机上的,滚轮机上的移动就会涉及到追击和停止的问题。追击的方式当然是追前一个植物卡牌,但当第一个植物卡牌被选中放置到草地上后,那该如何追击呢?

最开始我的做法是给植物多加几个状态来解决这个问题,但是发现状态过多会导致if判断中的条件将大大增加,并且在尝试后还是没有实现想要的效果,于是我就将植物集合一分为二,在后面的游戏功能设计中,回头过来看才发现将植物集合分为滚轮机上的集合和战场上的集合实在是太精妙了。请听我娓娓道来:

// 滚轮机上的植物,状态为stop和waitprivate List plants = new ArrayList;// 战场上的植物,状态为life和move -move为被鼠标选中移动的状态,这里设计不合理,会引发后面的一个BUGprivate List plantsLife = new ArrayList;// 植物在滚轮机上的碰撞判定public void plantBangAction { // 遍历滚轮机上植物集合,从第二个开始 for(int i=1;i // 如果第一个植物的y大于0,并且是stop状态,则状态改为wait if(plants.get(0).getY>0&&plants.get(0).isStop) { plants.get(0).goWait; } // 如果第i个植物y小于i-1个植物的y+height,则说明碰到了,改变i的状态为stop if((plants.get(i).isStop||plants.get(i).isWait)&& (plants.get(i-1).isStop||plants.get(i-1).isWait)&& plants.get(i).getY<=plants.get(i-1).getY+plants.get(i-1).getHeight ) { plants.get(i).goStop; } /* * 如果第i个植物y大于于i-1个植物的y+height,则说明还没碰到或者第i-1个 * 植物被移走了,改变i的状态为wait,可以继续往上走 */ if(plants.get(i).isStop&& plants.get(i).getY>plants.get(i-1).getY+plants.get(i-1).getHeight) { plants.get(i).goWait; } } } // 检测滚轮机上的植物状态 public void checkPlantAction1 { // 迭代器 Iterator it = plants.iterator; while(it.hasNext) { Plant p = it.next; /* * 如果滚轮机集合里有move或者life状态的植物 * 则添加到战场植物的集合中,并从原数组中删除 */ /* * 现在发现把滚轮机上move状态的植物添加到 * 战场上植物集合的最佳操作时间点应该是 * 等植物状态变为life后再添加。 * / if(p.isMove||p.isLife) { plantsLife.add(p); it.remove; } } }

当然,滚轮机上的对植物状态判断的代码还是显得生涩,也正是自己想优化这段代码时萌生了分享游戏设计过程和游戏代码的念头。那么下面就说说,这段代码该如何优化:

// 先对状态做下说明// wait - 植物卡牌在滚轮机上移动状态,因为是等着被鼠标选中,所以取名为wait// stop - 植物卡牌在滚轮机上停止状态,有两种情况,1 - 到顶了 2 - 撞到上一个卡牌了// 开始对以下代码进行优化// 如果第i个植物y小于i-1个植物的y+height,则说明碰到了,改变i的状态为stop// if((plants.get(i).isStop||plants.get(i).isWait)&&// (plants.get(i-1).isStop||plants.get(i-1).isWait)&&// plants.get(i).getY<=plants.get(i-1).getY+plants.get(i-1).getHeight// ) {// plants.get(i).goStop;// }// 优化后的代码是这样的// 将一个复杂的boolean拆成多个if条件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;

boolean条件当然也可以进行优化,甚至还可以简化一下植物的状态。这里因为游戏的规则,僵尸只能攻击在草坪上的植物,所以把带放置的植物和草坪上的植物分为两个集合,是十分合理精妙的。在判断僵尸是否攻击植物,只需要去遍历草坪上的植物集合即可。如果不拆分,当要判断僵尸是否攻击植物的时候,需要遍历的集合将是所有的植物集合,并且需要增加至少2个状态来区分植物是在草坪上还是在滚轮机上,这段代码想想就是又臭又长。

接下来该让对象们都动起来了。之前说到在父类中的移动方法是抽象方法,在各自的子类中都进行重写后,不同的对象移动方式就是各式各样的了。

// 子弹移动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; } } }}

看着代码中对集合复杂的遍历,不得不感概lambda表达式真是个好东西:

// 子弹移动public void BulletStepAction { bullets.forEach((b)->b.step); ....}

这里好像还是没法展示lambda表达式强大的功能,请看下面的例子:

// 为了应对产品不断变更的需求,前辈们总结经验得出的设计模式已经能在一定程度上应对此问题// 设计模式,声明策略接口,在实现类中完成过滤逻辑public List filterStudentByStrategy(List students, SimpleStrategy strategy){ List filterStudents = new ArrayList<>; for (Student student : filterStudents) { if(strategy.operate(student)){ filterStudents.add(student); } } return filterStudents;}// 当需求变更时,只需要在策略接口的实现类中,变更判断逻辑即可public interface SimpleStrategy { public boolean operate(T t);}

但好像还是有点麻烦,又要写接口,又要写实现类,后续的维护也是个头疼问题,这个时候救世主lambda表达式就出现了:

// 无需接口便可实现需求的快速变更List lambdaStudents = students.stream.filter(student -> student.getGender==1).collect(Collectors.toList);

让我们看看上面到底发生了啥。首先将数据的集合流化,接着调用过滤方法,强大lambda表达式让代码变得简洁,并且判断条件的修改可在代码中直接维护无需在策略接口的实现类维护。最后在转成集合,返回一个满足产品需求的集合。

回到正题,如何让对象们打起来呢?下面以僵尸攻击植物为例:

// 僵尸的超类中定义了僵尸的攻击方法,// 由于僵尸们的攻击行为是相同,所以这里是普通方法// 僵尸攻击植物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;}

用 Java 语言,写一个植物大战僵尸简易版

结合图片来看,上述代码应该就更好理解。黑框P代表植物,黑框Z代表植物,虚线是指两者接触的极限距离,当僵尸进入虚线内,就保证可以攻击到植物。

// 僵尸攻击// 设置攻击间隔int zombieHitTime = 0;public void zombieHitAction { if(zombieHitTime++%100==0) { for(Zombie z:zombies) { // 如果战场上没有植物,则把所有僵尸的状态改为life /* * 这里补充一下为什么要先将所有的僵尸的状态先改成life状态,也就是移动状态 * 因为下面对僵尸是否攻击的植物的判断,是从遍历战场上的植物集合开始的 * 假如有只僵尸在吃植物,把战场上唯一的一个植物吃掉了, * 那么僵尸的状态将从攻击改成移动呢? * 所以这里运用了逆向的思想,先将所有的僵尸改为移动状态 * 如果符合攻击的条件,那么再改为攻击状态, * 即便是战场上没有植物,那么僵尸还依然是移动的状态 */ if(!z.isDead) { z.goLife; } // 这里应该有个对战场上植物集合的判断在进行遍历 for(Plant p:plantsLife) { // 如果僵尸是活的,并且植物是活的,并且僵尸进入攻击植物的范围 /* * 这里有个BUG,僵尸竟然会攻击鼠标选中还未放下的植物, * 所以下面的判断条件中应该还需要移除被鼠标选中状态下植物 */ if(z.isLife&&!p.isDead&&z.zombieHit(p)&&!(p instanceof Spikerock)) { // 僵尸状态改为攻击状态 z.goAttack; // 植物掉血 p.loseLive; } } } }}

用 Java 语言,写一个植物大战僵尸简易版

如果出现了一些效果的偏移,造成的原因是图片大小不一造成的坐标偏移,因为图片都是网上找的,所以效果不是太理想。

至此,游戏的基本功能基本实现了。Java是一门面向对象的语言,万物皆对象,特征皆属性,行为皆方法。肉眼能看到的僵尸、植物、草坪都是对象,对象的特性比如血量、移动速度都是属性,对象的行为比如移动、攻击、死亡都是方法。

下面说说对游戏功能的优化。

用 Java 语言,写一个植物大战僵尸简易版

游戏优化

已经放置过植物的草地不能再放置植物了。之前是将草地设计成empty和hold两种状态,现在来看其实只需要返回一个true和false就行了,将整个植物集合定义成一个虚拟的boolean集合即可。

2.移除植物的优化

设计思路是新增一个铲子对象:

// 铲子集合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 // 移除植物 it2.remove; // 移除铲子 it.remove; shovelCheck = false; } } }}
用 Java 语言,写一个植物大战僵尸简易版用 Java 语言,写一个植物大战僵尸简易版

看着这极其复杂好像很厉害的代码,我又萌生了痛下狠手的想法,但为了保持原生,我忍住。于是乎还发现了一个BUG。如果选中铲子后,战场上唯一的植物被僵尸吃掉了,那么这个铲子将一直跟随着鼠标无法达到使用后消除的效果了。解决方案当然也很简单,当战场上植物集合的size为0时,清空铲子集合即可。


3.游戏可玩性的优化

上文在游戏设计中提到的击杀僵尸后可能随机获得奖励类型是这样实现的。还是从设计分析开始,并非击杀任何类型的僵尸都可以获得奖励,所以奖励应该放在接口中:

public interface Award { // 奖励接口 /* * 这里还是存在代码不规范的问题 * 接口的方式默认是public abstract * 接口中的变量默认是public static final * 这些默认的字段应该舍去 */ // 全屏静止 public static final int CLEAR = 0; // 全屏清除 public static final int STOP = 1; public abstract int getAwardType;}

当僵尸死亡时,需要去判断该僵尸是否有奖励接口,如果有则执行相应奖励的方法:

// 检测僵尸状态public void checkZombieAction { // 迭代器 Iterator it = zombies.iterator; while(it.hasNext) { Zombie z = it.next; // 僵尸血量小于0则死亡,死亡的僵尸从集合中删除 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; //zombieGoLife; } break; } } z.goDead; it.remove; } // 僵尸跑进房子,而游戏生命减一,并删除僵尸 if(z.OutOfBound) { gameLife--; it.remove; } }}

4.添加游戏背景音乐

bgm是一个游戏的灵魂之一。这里给游戏添加背景音乐,我的选择是新建一条线程专门用来执行音乐的解析和播放:

// 启动线程加载音乐Runnable r = new zombieAubio("bgm.wav");Thread t = new Thread(r);t.start;
public class zombieAubio implements Runnable{ // 读音频WAV格式专用线程 private String filename; public zombieAubio(String wavfile){ filename=wavfile; } ......

这里需要注意的是,Java中解析音乐的API只支持WAV格式的文件,文件格式的转换大多数音乐播放器都可以做到。

用 Java 语言,写一个植物大战僵尸简易版

后续优化

1.植物种类的扩充及对应功能的实现

比如杀伤力最大的玉米加农炮。需要4个小玉米进行合成,那么在判断是否能够合成玉米加农炮时,需要对植物集合进行遍历来做坐标的判断,所以这边建议最好把可合成的植物单独放在一个集合中,这样在做合成判断的时候会简单很多,当集合的size小于4时,就可以提示合成失败了。冰冻西瓜的设计思路也是如此。

2.动作类僵尸的加入,如撑杆跳僵尸、跳舞僵尸等

说一下撑杆跳僵尸的设计思路,此类僵尸和其他僵尸相比,多了一种跳的行为,所以会有一个单独的方法和单独的状态。并且,跳只能触发一次,所以撑杆跳僵尸的状态变化应该是行走->遇到植物跳过去->再遇到植物就开始攻击,在执行状态变化的时候,应该要去考虑当前的状态是否还可跳跃。

3.当植物攻击范围内不存在僵尸时,植物停止攻击

这个就简单拉,在植物执行攻击方法时,校验一下是否有Y坐标相同的僵尸即可。

GitHub 源码:

https://github.com/llx330441824/plant_vs_zombie_simple.git

相关推荐

影之诗:机械自然JAVA龙——我龙族誓不为奴

本文作者:NGA-瑠璃色の輝き卡组最左为3张飞翔的龙人。卡组思路:Plan A:通过岚铁的龙人/钢铁与大地之神加手的0费卡,在8费回合配合JAVA打出一波大场面,运气好可以直接推死,就算推不死,也能靠着巨大的场面压死对手;Plan B:没有JAVA的时候就靠着单卡质量一张张把对面耗死。组件上的选择也...

老季侃热血传奇之七月独家龙渊版本

影之诗:机械自然JAVA龙——我龙族誓不为奴影之诗:机械自然JAVA龙——我龙族誓不为奴第三,独家的光柱图标,让你刷怪的时候,时不时震慑一下你的小心心!

“锚没有九条命!”我的世界20w20a新增10大奇葩成就,mc谐音梗?

mc又新增10大成就挑战?万万没想到《我的世界》1.16下界更新,临了还给了玩家来了一个小惊喜。进度系统,简单理解就是JAVA版所独有的一套“成就”,它与基岩版成就有着些许差异,但对于我们mc玩家而言,你没必要去做区分。因此,当你使用四颗荧石充满锚的能量之后,便可完成此项成就。所以,锚只有4条命!

用223秒征服原著迷!漫改游戏盯上一拳超人,为何此次都在叫好?

用223秒征服原著迷!漫改游戏盯上一拳超人,为何此次都在叫好?

用2个月做一把武器,引来央视报道的B站神级UP主,被难住了?

很多人对B站UP主“才疏学浅的才浅”第一印象,绝对是“卧X,这人也太牛批了”。才浅最早在B站发视频是2017年的7月份,到现在已经过去了4年多的时间,却只有48个视频,平均下来每个月就发1个,这绝对算不上什么高产。幻塔做的是轻科幻废土风的开放世界,而高度发达的科技也催生了武器的拟态系统,正常只是道具...

用直播带货开启16周年庆,这个游戏IP的征途之路,越走越宽

2021年,征途全系IP问世以来的第十六个年,也是自史玉柱2018年出席周年活动、确立IP赛道重塑方向的第四年。10月21日晚,征途与快手平台合作举办了“征途全系16周年庆典线上直播”。在征途IP直播间,多款征途产品的开发负责人作为嘉宾,一边与玩家聊游戏周年庆版本新内容,一边以0元抽、1元秒的形式不...

用136秒征服原著迷!国产漫改游戏仅用1个PV,就获得了1900W阅读

国产漫改游戏仅用1个PV,就获得了1900W阅读。《一拳超人》漫迷们最近算是迎来了值得庆贺的日子。漫改游戏《一拳超人:世界》PV曝光,从晚上的0点过到现在,《一拳超人:世界》相关的话题评论在各个游戏论坛,一拳超人漫画社区刷屏,其中在微博相关的话题阅读量更是达到了1900W,可见玩家们的热情和《一拳超...

用8天时间走红的火线妹,终于放下了大主播的“面子”?

用8天时间走红的火线妹,终于放下了大主播的“面子”?

王者荣耀:6元新皮即将上线,制作用时超1年,光这出场动画就值68

王者荣耀:6元新皮即将上线,制作用时超1年,光这出场动画就值68

王者荣耀:6元新皮即将上线,制作用时超1年,出场动画就值688

相信各位热爱王者荣耀的小伙伴们都知道,王者荣耀前阵子便邀请各位玩家们对云中君的源梦皮肤纤云弄巧进行了各种投票,以此决定皮肤最后的效果,而皮肤价格也十分优惠,相信各位小伙伴们也是十分的期待此皮肤的上线,最近,这款皮肤终于要上线了。在王者荣耀最近的一次更新中,增加了很多新的活动,以及很多诱人的奖励。源梦...

用闪暖3D的方式打开奇暖?星之海封神,这暗黑哥特很还原

奇迹暖暖是叠纸最先出的2D换装游戏,之后又出了姐妹3D游戏闪耀暖暖,两个游戏因为都出自同一家公司,所以在题材上就有很多类似的套装,有些玩家不经怀疑叠纸有吃老本嫌疑。除了大家熟知的神作星之海之外,还有很多比较经典的相似套装,也是常常被玩家们调侃“叠纸自己抄自己”。说到戏曲行头就不得不说这两套了,这两套...

用炉石打开超炮T:大霸星祭(上)【鼠标垫的炉石剧场】

用炉石打开超炮T:大霸星祭(上)【鼠标垫的炉石剧场】

用安卓平板试玩steam好评如潮的解谜游戏小小梦魇的续作

用安卓平板试玩steam好评如潮的解谜游戏小小梦魇的续作

用种植向日葵的方式对抗僵王,难度飙升1000颗星,值得一战

用种植向日葵的方式对抗僵王,难度飙升1000颗星,值得一战

用游戏假想入侵德国第三次世界大战第四集

哎呀不好啦,呆呆小世界被鬼畜入侵了!做日常任务就有熟练和各种福利赠送,还有大礼包赠送,steam搜索 舰队使命 即可,服务器支持国际线路,有外国玩家同服

盒子游戏,游戏玩家专属个性阅读社区


©CopyRight 2010- 2020 BOXUU.COM Inc All Rights Reserved

鄂公网安备 35020302000061号- 鄂ICP备2020015574号-1