植物大战僵尸这款经典游戏相信大家都玩过,最近我用原生JS+ES6语法,并通过canvas绘制的方式实现了这个游戏的一些基本功能,在这里我会介绍一下实现这个游戏的心路历程。
可能又人会问,实现这样的一个小游戏难吗?其实单看每一个实现的游戏模块来说,这个游戏的实现难度并不大,难点主要在于将所有游戏中的模块如何合理的融合在一起,在实现这个小游戏的过程中,我也踩过不少坑,也重写过一部分游戏逻辑。写这篇文章的目的也是为了记录和总结自己最近在开发中遇到的一些问题和解决问题的思路。
很多程序猿可能会说,学习编程不过是为了赚钱而已,也有人是为了提高自身的技术?这个有道理的,为什么么要提高自身的技术呢,其实,最终目的是为了解决问题,解决用户的问题。
既然我们是使用编程解决问题的人,那么,学习它的目的就是为了解决问题,也就是说只要能达到解决问题的深度就可以了。当然问题的大小不同,对语言掌握的程度就有不同的要求。因此,在学习编程的过程中切不可脱离了实际,单纯的为了学习编程语言而学习。
当然,这里可能扯的有点远了,但是主要想说明一下自己写这个小游戏的初衷,也是为了提醒自己,在这个浮躁的时代,不忘初心,方得始终。
先上张截图:
试玩链接:植物大战僵尸
在开发这个游戏的时候,我选择基于ES6的class的方式抽象了游戏相关函数,关于这个小游戏的核心引擎,主要的相关属性如下:
- class Game {
- constructor (_main) {
- ...
- state: 0, // 游戏状态值,初始默认为 0
- state_LOADING: 0, // 准备阶段
- state_START: 1, // 游戏开始
- state_RUNNING: 2, // 游戏运行
- state_STOP: 3, // 游戏暂停
- state_PLANTWON: 4, // 游戏结束,玩家胜利
- state_ZOMBIEWON: 5, // 游戏结束,僵尸胜利
- canvas: document.getElementById("canvas"), // canvas元素
- context: document.getElementById("canvas").getContext("2d"), // canvas画布
- timer: null, // 轮询定时器
- fps: _main.fps, // 动画帧数
- }
- init () { // 初始化函数
- let g = this
- ...
- // 设置轮询定时器
- g.timer = setInterval(function () {
- // 根据游戏状态,在canvas中绘制不同游戏场景
- }, 1000/g.fps)
- ...
- }
- }
其实核心逻辑很简单,就是定义一个游戏引擎主函数,生成一个定时器以每秒60帧的频率不停在canvas画布上绘制游戏场景相关元素,然后在定时器函数中根据当前游戏状态(
、
- 游戏准备
、
- 游戏开始
、
- 游戏运行
、
- 游戏暂停
)来绘制对应游戏场景。
- 游戏结束
游戏状态:游戏引擎绘制了页面载入图片,并添加了一个开始游戏按钮
- loading
游戏状态:游戏开始读条倒计时,提醒用户游戏即将开始
- start
游戏状态:绘制游戏运行时所需所有游戏场景素材
- running
游戏状态:游戏进入暂停阶段,游戏中如生成阳关、僵尸的定时器都将清除,角色动画处于静止状态
- stop
游戏状态:分为玩家获得胜利以及僵尸获得胜利两种情况,并分别绘制不同游戏结算画面
- gameover
在这里我将游戏中所有可控制的元素都归于游戏场景中,并且将这些元素都抽象为类,方便管理,这里包括:
、
- 植物类
、
- 僵尸类
、
- 阳光计分板类
、
- 植物卡片类
,
- 动画类
。
- 子弹类
游戏场景中最核心的两个类为
、
- 植物类
,不过在这两个核心类中都会用到
- 僵尸类
,这里我先介绍一下。
- 动画类
动画类的作用是将每一个角色的不同动画序列保存起来,每隔一定时间切换当前显示的图片对象,达到播放动画的效果。
- class Animation{
- constructor (role, action, fps) {
- let a = {
- type: role.type, // 动画类型(植物、僵尸等等)
- section: role.section, // 植物或者僵尸类别
- action: action, // 根据传入动作生成不同动画对象数组
- images: [], // 当前引入动画图片对象数组
- img: null, // 当前显示动画图片
- imgIdx: 0, // 当前角色图片序列号
- count: 0, // 计数器,控制动画运行
- fps: fps, // 角色动画运行速度系数,值越小,速度越快
- }
- Object.assign(this, a)
- }
- }
这里用到的
,就是通过
- images
的方式生成并添加到
- new Image()
中组成的动画序列:
- images
其中
和
- type
用于判断当前需要加载植物或僵尸、哪一个动作所对应动画序列,
- section
和
- count
用于控制当前动画的播放速度,而
- fps
用于表示当前所展示的图片对象,即
- img
,其关系类似于以下代码:
- images[imgIdx]
- // 在全局定时器中每1/60秒计算一次
- // 获取动画序列长度
- let animateLen = images.length
- // 计数器自增
- count++
- // 设置当前显示动画序列号
- imgIdx = Math.floor(count / fps)
- // 当一整套动画完成后重置动画计数器
- imgIdx === animateLen - 1 ? count = 0 : count = count
- // 设置当前显示动画图片
- img = images[imgIdx]
- class Plant{
- constructor (_main, obj) {
- let p = {
- _main: _main, // 游戏主函数对象
- type: obj.type, // 角色类型(植物)
- section: obj.section, // 角色类别
- x: obj.x, // x轴坐标
- y: obj.y, // y轴坐标
- w: 0, // 角色图片宽度
- h: 0, // 角色图片高度
- row: obj.row, // 角色初始化行坐标
- col: obj.col, // 角色初始化列坐标
- life: 3, // 角色血量
- idle: null, // 站立动画对象
- attack: null, // 攻击动画对象
- bullets: [], // 子弹对象数组
- isAnimeLenMax: false, // 是否处于动画最后一帧,用于判断动画是否执行完一轮
- isDel: false, // 判断是否死亡并移除当前角色
- isHurt: false, // 判断是否受伤
- state: 1, // 保存当前状态值,默认为0
- state_IDLE: 1, // 站立不动状态
- state_ATTACK: 2, // 攻击状态
- }
- Object.assign(this, p)
- }
- }
这里就不过多说明这些基本属性
、
- type
、
- section
、
- x
、
- y
、
- w
、
- h
、
- row
、
- col
,其中
- life
和
- row
属性用于控制植物在草坪上绘制的横纵坐标(即
- col
轴和
- x
轴方向位于第几个方格),
- y
属性用于区分当前植物到底是哪一种,如豌豆射手、双发射手、加特林射手。
- section
其中较为比较重要的属性为
、
- idel
、
- attack
、
- bullets
,其中
- state
和
- idel
为动画对象,相信看过上面关于
- attack
介绍的小伙伴应该能理解其作用,
- 动画类
即用于保存当前植物的所有子弹对象(同
- bullets
,
- 动画类
也有属性、方法的配置,这里就不详细叙述了)。
- 子弹类
关于植物的状态控制属性,如
属性会在植物受伤时,切换为
- isHurt
,并由此给动画添加一个透明度,模拟受伤效果;
- true
属性会在植物血量降为0时,将植物从植物对象数组中移除,即不再绘制当前植物;
- isDel
属性用于植物在两种形态中进行切换,即普通形态、攻击形态,当前状态值为哪种形态,即播放对应形态动画,对应关系如下:
- state
- state === state_IDLE => // 播放植物普通形态动画 idle
- state === state_ATTACK => // 播放植物攻击形态动画 attack
攻击形态的切换,这里就涉及需要循环当前植物对象与所有的僵尸对象所组成的数组,判断是否有僵尸处于当前植物对象的射程内(即处于同一行草坪,且进行屏幕显示范围)。
- // 僵尸类
- class Zombie{
- constructor (_main, obj) {
- let z = {
- _main: _main, // 游戏主函数对象
- type: obj.type, // 角色类型
- section: obj.section, // 角色类别
- x: obj.x, // x轴坐标
- y: obj.y, // y轴坐标
- w: 0, // 角色图片宽度
- h: 0, // 角色图片高度
- row: obj.row, // 角色初始化行坐标
- col: obj.col, // 角色初始化列坐标
- life: 10, // 角色血量
- idle: null, // 站立动画对象
- run: null, // 奔跑动画对象
- attack: null, // 攻击动画对象
- dying: null, // 濒临死亡动画对象
- die: null, // 死亡动画对象
- isAnimeLenMax: false, // 是否处于动画最后一帧,用于判断动画是否执行完一轮
- isDel: false, // 判断是否死亡并移除当前角色
- isHurt: false, // 判断是否受伤
- state: 1, // 保存当前状态值,默认为1
- state_IDLE: 1, // 站立不动状态
- state_RUN: 2, // 奔跑状态
- state_ATTACK: 3, // 攻击状态
- state_DYING: 4, // 濒临死亡状态
- state_DIE: 5, // 死亡状态
- speed: 3, // 移动速度
- }
- Object.assign(this, z)
- }
- }
这里可以看到
的很多属性与
- 僵尸类
类似,就不过多叙述了,由于目前只开发了一种僵尸,所以
- 植物类
属性是固定值。
- section
关于僵尸的动画对象可能会比植物复杂一点,包含
、
- idle
、
- run
、
- attack
、
- dying
五种形态的动画序列,其中
- die
和
- dying
对应僵尸较低血量和血量为0时所播放的动画。
- die
在僵尸的控制属性上,与植物同理,这里僵尸的五种动画对象也对应五种状态值,并随状态值的切换而切换。
在游戏主函数中,将会把之前所有用到的游戏相关类,进行实例化,并保存在
对象中,在这里调用
- _main
游戏启动函数,将会开启游戏引擎,开始绘制游戏场景,所以游戏启动函数会在页面加载完成后立即调用。
- start
- let _main = {
- allSunVal: 200, // 阳光总数量
- loading: null, // loading 动画对象
- sunnum: null, // 阳光实例对象
- cards: [], // 实例化植物卡片对象数组
- cards_info: { // 初始化参数
- x: 0,
- y: 0,
- position: [
- {name: 'peashooter', row: 1, sun_val: 100},
- {name: 'repeater', row: 2, sun_val: 150},
- {name: 'gatlingpea', row: 3, sun_val: 200},
- ]
- },
- plants: [], // 实例化植物对象数组
- zombies: [], // 实例化僵尸对象数组
- plants_info: { // 初始化参数
- type: 'plant', // 角色类型
- x: 250, // 初始 x 轴坐标,递增量 80
- y: 92, // 初始 y 轴坐标,递增量 100
- len: 0,
- position: [] // section:植物类别,row:横行坐标(最小值为 5),col:竖列坐标(最大值为 9)
- },
- zombies_info: { // 初始化参数
- type: 'zombie', // 角色类型
- x: 250, // x轴坐标
- y: 15, // y轴坐标
- position: [] // section:僵尸类别,row:横行坐标(最小值为 9),col:竖列坐标(最大值为 13)
- },
- zombies_idx: 0, // 随机生成僵尸 idx
- zombies_row: 0, // 随机生成僵尸的行坐标
- zombies_iMax: 30, // 随机生成僵尸数量上限
- sunTimer: null, // 全局定时器,用于控制全局定时生成阳光
- zombieTimer: null, // 全局定时器,用于控制全局定时生成僵尸
- game: null, // 游戏引擎对象
- fps: 60, // 动画帧数
- ...
- start () { // 游戏启动函数
- // 实例化游戏场景篇中的所有类
- }
- }
这里就简单介绍下
、
- plants
对象数组;当游戏运行时,所以种植的植物以及生成的僵尸都会配合其相关初始化参数
- zombies
、
- plants_info
进行实例化再分别保存在
- zombies_info
、
- plants
对象数组中。
- zombies
较以往的经验来看,关于游戏中相关的方法逻辑作者就不详细介绍了,这个分享主要是为了提供给对小游戏感兴趣,但是却不知如何下手的小伙伴一个思路和经验,如果只是单纯的贴代码,就没有任何意义了。
如果有小伙伴对游戏相关代码有任何疑问,或想了解相关小游戏的实现逻辑,都可以通过以下方式联系作者,当然如果有妹纸对作者感兴趣的话,随时可以联系作者(逃~)。
博客:www.yangyunhe.me
QQ:314786482
邮箱:yangyunhe369@qq.com
来源: https://juejin.im/post/5a16431f6fb9a0451f309975