用 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;
}
}
}
public abstract class Zombie {
// 僵尸父类
// 僵尸共有的属性
protected int width;
protected int height;
protected int live;
protected int x;
protected int y;
......
// 僵尸的状态
public static final int LIFE = ;
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();
}
游戏内容
// 首先要有一个僵尸的集合
// 僵尸集合
private List<Zombie> zombies = new ArrayList<Zombie>();
// 接着定义随机生成僵尸方法
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 = ;
public void zombieEnterAction() {
zombieEnterTime++;
// 对自增量zombieEnterTime进行取余计算
if(zombieEnterTime%300==) {
// 满足条件就调用随机生成僵尸方法,并将生成的僵尸加入到僵尸的集合中
zombies.add(nextOneZombie());
}
}
关注公众号「程序员的成长之路」后回复「2048」关键字,免费获取5T技术学习资源!包括但不限于:C/C++,Linux,Python,Java,PHP,人工智能,单片机,树莓派,等等。
// 滚轮机上的植物,状态为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
if(plants.get().getY()>&&plants.get().isStop()) {
plants.get().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<Plant> 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();
// 子弹移动
public void BulletStepAction() {
for(Bullet b:bullets) {
b.step();
}
}
//僵尸移动
//设置移动间隔
int zombieStepTime = ;
public void zombieStepAction() {
if(zombieStepTime++%3==) {
for(Zombie z:zombies) {
//只有活着的僵尸会移动
if(z.isLife()) {
z.step();
}
}
}
}
// 子弹移动
public void BulletStepAction() {
bullets.forEach((b)->b.step());
....
}
// 为了应对产品不断变更的需求,前辈们总结经验得出的设计模式已经能在一定程度上应对此问题
// 设计模式,声明策略接口,在实现类中完成过滤逻辑
public List<Student> filterStudentByStrategy(List<Student> students, SimpleStrategy<Student> strategy){
List<Student> 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);
}
// 无需接口便可实现需求的快速变更
List<Student> lambdaStudents =
students.stream().filter(student -> student.getGender()==1).collect(Collectors.toList());
// 僵尸的超类中定义了僵尸的攻击方法,
// 由于僵尸们的攻击行为是相同,所以这里是普通方法
// 僵尸攻击植物
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;
}
// 僵尸攻击
// 设置攻击间隔
int zombieHitTime = ;
public void zombieHitAction() {
if(zombieHitTime++%100==) {
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();
}
}
}
}
}
如果出现了一些效果的偏移,造成的原因是图片大小不一造成的坐标偏移,因为图片都是网上找的,所以效果不是太理想。
关注公众号「程序员的成长之路」后回复「2048」关键字,免费获取5T技术学习资源!包括但不限于:C/C++,Linux,Python,Java,PHP,人工智能,单片机,树莓派,等等。
游戏优化
1.放置植物的优化
2.移除植物的优化
// 铲子集合
private List<Shovel> shovels = new ArrayList<Shovel>();
// 铲子入场
public void shovelEnterAction() {
// 铲子只有一把
if(shovels.size()==) {
shovels.add(new Shovel());
}
}
// 使用铲子
Iterator<Shovel> it = shovels.iterator();
Iterator<Plant> 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&&Mx<x2&&My>y1&&My<y2&&shovelCheck) {
// 移除植物
it2.remove();
// 移除铲子
it.remove();
shovelCheck = false;
}
}
}
}
看着这极其复杂好像很厉害的代码,我又萌生了痛下狠手的想法,但为了保持原生,我忍住。
public interface Award {
// 奖励接口
/*
* 这里还是存在代码不规范的问题
* 接口的方式默认是public abstract
* 接口中的变量默认是public static final
* 这些默认的字段应该舍去
*/
// 全屏静止
public static final int CLEAR = ;
// 全屏清除
public static final int STOP = 1;
public abstract int getAwardType();
}
// 检测僵尸状态
public void checkZombieAction() {
// 迭代器
Iterator<Zombie> it = zombies.iterator();
while(it.hasNext()) {
Zombie z = it.next();
// 僵尸血量小于0则死亡,死亡的僵尸从集合中删除
if(z.getLive()<=) {
// 判断僵尸是否有奖励的接口
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.添加游戏背景音乐
// 启动线程加载音乐
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;
}
......
后续优化
源码分享
朕已阅
相关文章