资源下载地址:https://download.csdn.net/download/sheziqiong/86812910
资源下载地址:https://download.csdn.net/download/sheziqiong/86812910
玩家通过种植太阳花收取阳光来购买植物;通过种植不同的植物来抵御僵尸的攻击。当一个关卡里的僵尸全部被消灭时,玩家胜;当僵尸越过地图的右边界时,僵尸胜利。
购买植物需要花费一定数量的阳光,每购买一次过后需要冷却一段时间才能再次购买。每个关卡允许使用的植物各有不同,不同植物的特性也不同
植物主要分为:攻击类,防御类和生产类。也可以分为夜间植物和白天植物(夜间植物只能在夜间出现)
当僵尸进入射手的射程内时,射手会发射豌豆攻击。
豌豆射手
生产阳光,供拾取购买植物
在游戏中,不同种类的僵尸会一波波的攻击。不同僵尸的攻击值,耐久,特性有所不同。
僵尸分为:
逐个关卡击破,取得最终胜利。
关卡很有趣,请各位自行体验,这里不再赘述。
整体:
核心部分:
大图见docs文件夹
I. 更新植物
II. 开始新游戏
III. 向僵尸总HP最少的行添加一个指定的僵尸
IV. 阳光下落
V. 向音频池添加游戏音效
这是一份项目开始时准备的的TODO-List
游戏核心
- [x] 单次派发系统
- [x] 基于FSM的状态机制
- [x] 基于多线程的音频池(使用.wav)
- [x] 附带(类似)回调函数的杂项GIF播放系统
- [x] 交互界面
- [x] 开始界面
- [x] 游戏界面
- [x] 后院
- [x] 卡牌槽
- [x] 阳光槽
- [x] 卡牌槽
- [x] 卡牌
- [x] 关卡进度
- [x] 界面交互
- [x] 放置植物
- [x] 收集阳光
- [x] 暂停界面
- [x] 通关/失败提示
- [x] 关卡设计
- 考虑一关总僵尸量一定
- 每间隔一段时间放置
- 原则:
- 在僵尸总HP最少的行上放置
植物
- [x] 普通植物
- [x] 向日葵
- [x] 双子向日葵
- [x] 豌豆射手
- [x] 双发射手
- [x] 三线射手
- [x] 机枪射手
- [x] 寒冰射手
- [x] 坚果墙
- [x] 高坚果
- [x] 食人花
- [x] 樱桃炸弹
- [x] 火爆辣椒
- [x] 土豆雷
- [x] 火炬树桩
- [ ] 夜间植物
- [x] 小喷菇
- [x] 胆小菇
- [ ] 大喷菇
- [x] 阳光菇
僵尸
- [x] 普通僵尸
- [x] 旗帜僵尸
- [x] 路障僵尸
- [x] 铁桶僵尸
- [x] 橄榄球僵尸
- [ ] 小丑僵尸
- [x] 读报僵尸
- [ ] 撑杆跳僵尸
子弹类
- [x] 豌豆
- [x] 冰豌豆
- [x] 燃烧豌豆
- [x] 孢子
- [ ] 大孢子(大喷菇)
一、名词解释
.negate()
即可实现) -> 0.negate()
) -> 1.negate()
) -> 3Predicate
:
true/false
BiPredicate
,接受两个参数,一是游戏棋盘的总体,二是实体本身Consumer
BiConsumer
,接受两个参数,同上二、代码组织形式
model
模型
base
基本类型bullets
子弹plants
植物sound
音频zombies
僵尸level
关卡view
视图渲染部分controller
用户交互界面GameBoard
成分
zombieMap
plantMap
bulletMap
extraMap
sumMap
,原本设计在杂项里,但考虑到检测的效率,故独立之注意,本部分代码因项目后期没有维护,可能已过时,详情请参考具体代码
abstract void setStateTable(); //设置状态表
Root
的子类都必须实现的方法Pershooter
类为例:protected void setStateTable()
{
// 0 : 正常
// 1 : 攻击
// 2 : HP耗尽
BiConsumer attack = ((gameBoard, root) ->
// 攻击时应该做什么
{
if ((intervalCount++) % attackPerTicks == 0)
{
Plant pt = (Plant) root;
for (Object ob : gameBoard.zombieMap.getRow(pt.getY()))
{
Zombie zb = (Zombie) ob;
if (zb.getX() - pt.getX() <= MAX_PROBE_RANGE)
{
gameBoard.bulletMap.getRow(pt.getY()).add(
new BeanBullet(pt.getX() + pt.getWidth() / 2, pt.getY()));
SoundPool.addSound(GameRule.choice(GameRule.pea_shoot));
return;
}
}
}
});
addState(0, "peashooter.gif", null);
addState(1, "peashooter.gif", attack);
addState(2, "peashooter.gif", (gameBoard, root) -> finish = true);
}
addState
void addState(int state, String fName, BiConsumer action)
state
为当前状态码fName
为状态要播放的gif,可为null
action
为BiConsumer
,是该状态要执行的操作,可为null
abstract void setStateTransfer(); //设置转移条件
Root
的子类都必须实现的方法Pershooter
类为例:protected void setStateTransfer()
{
// BOTH -> HP耗尽死亡
addTransfer(new int[] {0, 1}, 2, ((gameBoard, root) -> hp <= 0));
// 待机 -> 开始攻击
addTransfer(0, 1, getAttackTransfer(MAX_PROBE_RANGE));
// 攻击 -> 待机
addTransfer(1, 0, getAttackTransfer(MAX_PROBE_RANGE).negate());
}
addTransfer
void addTransfer(int from, int to, BiPredicate cond)
from
为来自的状态to
为满足cond
后转移到的状态cond
为BiPredicate
,是状态转移条件void addTransfer(int[] froms, int to, BiPredicate cond)
froms
数组中各元素都会被展开成原方法形式音频池SoundPool
系统
public static void addSound(String fName)
public static void setBGmusic(String fName)
SoundPool.addSound(xxxxx);
SoundPool.setBGmusic(xxxxx);
[完成]初步优化:使用字典管理正在播放的音频,限制同一音频最多同时播放3个,稍有改善
[待进行]进一步构思:
关卡Level & LevelManager & LevelFactory
系统
Level
: 保存一个关卡的信息LevelManager
: 组合一个Level
并被多个类所传递,用于获取关卡信息与处理游戏胜负LevelFactory
: 关卡工厂,想添加新的关卡就写在这里Level level = new Level();
HashMap zombieCount = new HashMap<>();
ArrayList cards = new ArrayList<>();
ArrayList pres = new ArrayList<>();
zombieCount.put("normal_zombie", 10); // 10个普通僵尸
zombieCount.put("football_zombie", 10); // 10个橄榄球僵尸
zombieCount.put("buckethead_zombie", 5); // 5个铁桶僵尸
zombieCount.put("conehead_zombie", 5); // 5个路障僵尸
zombieCount.put("newspaper_zombie", 5); // 5个读报僵尸
cards.add(new PlantInfo("Chomper", 50, 2));
// 可使用食人花, 花费50阳光,冷却2s
cards.add(new PlantInfo("Jalapeno", 50, 2)); // 火爆辣椒
cards.add(new PlantInfo("Peashooter", 100, 2)); // 豌豆射手
cards.add(new PlantInfo("Threepeater", 100, 2)); // 三发射手
cards.add(new PlantInfo("CherryBomb", 100, 2)); // 樱桃炸弹
cards.add(new PlantInfo("ScaredyShroom", 100, 2)); // 胆小菇
// 预设植物, new (name, col, row)
pres.add(new PreSetPlant("PotatoMine", 5, 0));
pres.add(new PreSetPlant("PotatoMine", 5, 1));
pres.add(new PreSetPlant("PotatoMine", 5, 2));
pres.add(new PreSetPlant("PotatoMine", 5, 3));
pres.add(new PreSetPlant("PotatoMine", 5, 4));
level.setLevelNumber(1) // 关卡编号1
.setZombies(zombieCount) // 设置所有僵尸(见上方)
.setWaves(10) // 10波
.setLevelTime(50) // 50秒(最后一波僵尸出现的时间)
.setLevelNext(2) // 下一关进入编号2
.setPlantInfos(cards) // 设置可使用的植物
.setlevelImg(GameRule.backgroundDay) // 关卡背景
.setLevelBgmusic(GameRule.dayBG) // 背景音乐
.setDropSun(true) // 白天,从天上掉阳光
.setDropSunPerSeconds(5) // 第一关,阳光掉落频繁
.setPrePlants(pres); // 预设植物,可为空
僵尸、植物、子弹或杂项何时被系统删除
Root
类中有一名为finish
的布尔型变量,一旦为true
,则会在最近一次的更新中被移除sleep
对应时间后在设置为finish=true
finish=true
,在杂项map
内添加这样的gif// 僵尸被炸死
addState(5, null, (gameBoard, root) ->
{
finish = true;
gameBoard.extraMap.add(new Extra(getPath() + "boom_die.gif", 3500, getX(), getY()));
});
// 僵尸HP耗尽
addState(6, null, (gameBoard, root) ->
{
finish = true;
gameBoard.extraMap.add(new Extra(getPath() + "nohead_die.gif", 1500, getX(), getY()));
});
Extra
类的构造函数签名如下
public Extra(String fName, int ms, int x, int y, boolean bullet)
public Extra(String fName, int ms, int x, int y)
fName
为需要播放的gif文件ms
为gif所需执行时间,用于Thread.sleep()
x
, y
,物体坐标bullet
,是否为子弹,true
代表是,不填为否Extra
类被构造后会在最近一次的更新中被添加finish=true
raMap.add(new Extra(getPath() + “nohead_die.gif”, 1500, getX(), getY()));
});
```
- Extra
类的构造函数签名如下
- public Extra(String fName, int ms, int x, int y, boolean bullet)
- 与public Extra(String fName, int ms, int x, int y)
- 其中
- fName
为需要播放的gif文件
- ms
为gif所需执行时间,用于Thread.sleep()
- x
, y
,物体坐标
- bullet
,是否为子弹,true
代表是,不填为否
- Extra
类被构造后会在最近一次的更新中被添加
- 在延迟时间到后自动被回收,实现方法同为finish=true
END
资源下载地址:https://download.csdn.net/download/sheziqiong/86812910
资源下载地址:https://download.csdn.net/download/sheziqiong/86812910