前端小游戏——植物大战僵尸【附源码】
嘘-臣妾做不到啊
2022年12月27日 18:08
收录于文集
共12篇

今天一时兴起,想玩会儿游戏。突然想到了很久没有玩的植物大战僵尸,之前有一阵子我是非常迷恋这个游戏的。

相信大家玩过它的人不在少数,但是如何用自己学习的技术来实现游戏的功能呢?今天就来分享一下用原生JS+ES6语法,并通过canvas绘制的方式实现这个游戏的一些基本功能。

游戏页面展示

我觉得这关好难

游戏引擎篇

在开发这个游戏的时候,我选择基于ES6的class的方式抽象了游戏相关函数,关于这个小游戏的核心引擎,主要的相关属性如下:

代码块
JavaScript
自动换行
复制代码

class Game {
  constructor () {
      ...
      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: window._main.fps,                                        // 动画帧数
  }
  init () { // 初始化函数
    let g = this
    ...
    // 设置轮询定时器
    g.timer = setInterval(function () {
      // 根据游戏状态,在canvas中绘制不同游戏场景
    }, 1000/g.fps)
    ...
  }
}
复制成功

其实核心逻辑很简单,就是定义一个游戏引擎主函数,生成一个定时器以每秒60帧的频率不停在canvas画布上绘制游戏场景相关元素,然后在定时器函数中根据当前游戏状态()来绘制对应游戏场景。

游戏状态:游戏引擎绘制了页面载入图片,并添加了一个开始游戏按钮  游戏状态:游戏开始读条倒计时,提醒用户游戏即将开始 游戏状态:绘制游戏运行时所需所有游戏场景素材 游戏状态:游戏进入暂停阶段,游戏中如生成阳关、僵尸的定时器都将清除,角色动画处于静止状态 游戏状态:分为玩家获得胜利以及僵尸获得胜利两种情况,并分别绘制不同游戏结算画面

游戏场景篇

在这里我将游戏中所有可控制的元素都归于游戏场景中,并且将这些元素都抽象为类,方便管理,这里包括:,

游戏场景中最核心的两个类为,不过在这两个核心类中都会用到,这里我先介绍一下。

动画类(Animation)

动画类的作用是将每一个角色的不同动画序列保存起来,每隔一定时间切换当前显示的图片对象,达到播放动画的效果。

代码块
JavaScript
自动换行
复制代码

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)
  }
}
复制成功

这里用到的,就是通过的方式生成并添加到中组成的动画序列:

其中用于判断当前需要加载植物或僵尸、哪一个动作所对应动画序列,用于控制当前动画的播放速度,而用于表示当前所展示的图片对象,即,其关系类似于以下代码:

代码块
JavaScript
自动换行
复制代码

// 在全局定时器中每1/60秒计算一次
// 获取动画序列长度
let animateLen = images.length
// 计数器自增
count++
// 设置当前显示动画序列号
imgIdx = Math.floor(count / fps)
// 当一整套动画完成后重置动画计数器
imgIdx === animateLen - 1 ? count = 0 : count = count
// 设置当前显示动画图片
img = images[imgIdx]
复制代码
复制成功

角色类(Role)

代码块
JavaScript
自动换行
复制代码

class Role{
  constructor (obj) {
    let r = {
      id: Math.random().toFixed(6) * Math.pow(10, 6),      // 随机生成 id 值,用于设置当前角色 ID,区分不同角色
      type: obj.type,                                      // 角色类型(植物或僵尸)
      section: obj.section,                                // 角色类别(豌豆射手、双发射手...)
      x: obj.x,                                            // x轴坐标
      y: obj.y,                                            // y轴坐标
      w: 0,                                                // 角色图片宽度
      h: 0,                                                // 角色图片高度
      row: obj.row,                                        // 角色初始化行坐标
      col: obj.col,                                        // 角色初始化列坐标
      isAnimeLenMax: false,                                // 是否处于动画最后一帧,用于判断动画是否执行完一轮
      isDel: false,                                        // 判断是否死亡并移除当前角色
      isHurt: false,                                       // 判断是否受伤
    }
    Object.assign(this, r)
  }
}
复制代码
复制成功

这里的角色类主要用于抽象植物类和僵尸类的公共属性,基本属性包括,其中属性用于控制角色在草坪上绘制的横纵坐标(即轴和轴方向位于第几个方格),属性用于区分当前角色到底是哪一种,如豌豆射手、双发射手、加特林射手、普通僵尸。

植物类(Plant)

代码块
JavaScript
自动换行
复制代码

class Plant{
  constructor (obj) {
    let p = {
      life: 3,                                             // 角色血量
      idle: null,                                          // 站立动画对象
      attack: null,                                        // 攻击动画对象
      bullets: [],                                         // 子弹对象数组
      state: 1,                                            // 保存当前状态值,默认为1      
      state_IDLE: 1,                                       // 站立不动状态
      state_ATTACK: 2,                                     // 攻击状态
    }
    Object.assign(this, p)
  }
  // 绘制方法
  draw(cxt) {
      // 根据当前植物的状态,分别绘制正常状态动画,以及受伤时的半透明状态动画
      let self = this
      cxt.drawImage(self[stateName].img, self.x, self.y)
  }
  // 更新当前植物状态
  update() {
      // 通过动画计数器计算出当前植物显示动画序列的图片
  }
  // 判断当前植物是否可进入攻击状态
  canAttack() {
      // 通过轮询僵尸对象数组,判断处于当前植物同行的僵尸,且进入草坪内时,即开始攻击僵尸
      // 目前仅有三种射手类植物可使用子弹攻击,樱桃炸弹属于范围伤害类植物(判断范围为其周围八个格子内)
      // 攻击成功时,减少对应僵尸血量,并在僵尸血量到达特殊值时,切换其动画(如濒死状态,死亡状态),在血量为 0 时,从僵尸对象数组中移除当前僵尸
  }
  // 射击方法
  shoot() {
      // 当前植物攻击时, bullets 数组添加子弹对象
      let self = this
      self.bullets[self.bullets.length] = Bullet.new(self)
  }
}
复制代码
复制成功

植物类的私有属性包括,其中为动画对象,相信看过上面关于介绍的小伙伴应该能理解其作用,即用于保存当前植物的所有子弹对象(同也有属性、方法的配置,这里就不详细叙述了)。

关于植物的状态控制属性,如属性会在植物受伤时,切换为,并由此给动画添加一个透明度,模拟受伤效果;属性会在植物血量降为0时,将植物从植物对象数组中移除,即不再绘制当前植物;属性用于植物在两种形态中进行切换,即普通形态、攻击形态,当前状态值为哪种形态,即播放对应形态动画,对应关系如下:

代码块
JavaScript
自动换行
复制代码

 state === state_IDLE => // 播放植物普通形态动画 idle
 state === state_ATTACK => // 播放植物攻击形态动画 attack
复制代码
复制成功

攻击形态的切换,这里就涉及需要循环当前植物对象与所有的僵尸对象所组成的数组,判断是否有僵尸处于当前植物对象的射程内(即处于同一行草坪,且进行屏幕显示范围)。

这里主要介绍了植物类的相关属性,其方法包括...

僵尸类(Zombie)

代码块
JavaScript
自动换行
复制代码

// 僵尸类
class Zombie{
  constructor (obj) {
    let z = {
      life: 10,                                            // 角色血量
      idle: null,                                          // 站立动画对象
      run: null,                                           // 奔跑动画对象
      attack: null,                                        // 攻击动画对象
      dying: null,                                         // 濒临死亡动画对象
      die: null,                                           // 死亡动画对象
      state: 1,                                            // 保存当前状态值,默认为1
      state_IDLE: 1,                                       // 站立不动状态
      state_RUN: 2,                                        // 奔跑状态
      state_ATTACK: 3,                                     // 攻击状态
      state_DYING: 4,                                      // 濒临死亡状态
      state_DIE: 5,                                        // 死亡状态
      canMove: true,                                       // 判断当前角色是否可移动
      attackPlantID: 0,                                    // 当前攻击植物对象 ID
      speed: 3,                                            // 移动速度
    }
    Object.assign(this, z)
  }
  // 绘制方法
  draw() {
      // 根据当前僵尸的状态,分别绘制正常状态动画,以及受伤时的半透明状态动画
      let self = this
      cxt.drawImage(self[stateName].img, self.x, self.y)
  }
  // 更新当前僵尸状态
  update() {
      // 动画计数器计算出当前植物显示动画序列的图片
  }
  // 判断当前僵尸是否可进入攻击状态
  canAttack() {
      // 通过轮询植物对象数组,判断处于当前僵尸同行的植物,且进入其攻击范围内时,即开始攻击植物
      // 攻击成功时,当前僵尸 canMove 属性将为 false ,记录其 attackPlantID ,即所攻击植物 id 值,并减少对应植物血量;
      // 在植物血量为 0 时,切换其动画(进入死亡状态),并从植物对象数组中移除该植物,同时
      // 将所有攻击该植物的僵尸的状态切换为移动状态, canMove 属性值改为 true
  }
}
复制代码
复制成功

这里就简单介绍下对象数组;当游戏运行时,所以种植的植物以及生成的僵尸都会配合其相关初始化参数进行实例化再分别保存在对象数组中。

后记

较以往的经验来看,关于游戏中相关的方法逻辑就不详细介绍了,这个分享主要是为了提供给对小游戏感兴趣,但是却不知如何下手的小伙伴一个思路和经验。

代码块
JavaScript
自动换行
复制代码
完整源码获取,关注公粽号“it就自学”私信:植物大战僵尸即可免费领取!
复制成功

代码块
JavaScript
自动换行
复制代码
还有海量学习资源等你来选,感兴趣的同学可以私聊了解详情哦~
复制成功