走进互动营销二:so easy实现一个贪吃蛇

645 阅读5分钟

「本文已参与好文召集令活动,点击查看: 后端、大前端双赛道投稿,2万元奖池等你挑战!

走进互动营销二:使用phaserjs实现一个贪吃蛇

技术栈:canvasphaserTS
本篇完成项目地址完整代码

听到实现个小游戏就很慌?duck不必! ,这要学不会,你来打我!
今天还是初级文章,手把手带大家实现一个贪吃蛇。大佬绕过。你可以get:

  • phaserjs的简单使用
  • 一个游戏的模板仓库 phaser + ts + webpack5
  • 一条可爱的蛇蛇

废话不多说,开干!

一、分析需求(小伙子,别慌!)

这个游戏可以拆成以下模块:

  • 一条可爱的蛇蛇
  • 随机生成的水果
  • wasd键盘控制上下左右
  • 吃水果动作

二、基础环境搭建(TS+phaser)

基础框架

比较干净没有什么东西。提前给大家准备了一个干净的模板。phaser + ts + webpack5项目模板

所需素材

就两张方块图片,大家可以去完整的项目里面去下载。完整代码

素材配置以及预加载图片

比较基础,不再赘述了,之前文章有 讲过。传送门走进互动营销一:使用canvas引擎phaser实现一个推箱子h5游戏实战

三、游戏场景

我们来分析一下:

蛇是有一节一节组成的,每一节都是个sprite。所以蛇应该是一个数组。

水果

每个水果就是也是一个节点。方便后面扩展,我么也把水果初始化成数组

方向

wasd 分别为上下左右4个方向。我们用向量进行表示。 当然你可以用x,y也可以的。后面会讲。

创建主场景MainScene;

新建 src/scenes/mainScene.ts

import FpsText from '../objects/fpsText'
export default class MainScene extends Phaser.Scene {
  fpsText
  /** 鼠标事件 */
  public cursors: Phaser.Types.Input.Keyboard.CursorKeys
  /** 蛇蛇 */
  private snakes: Phaser.GameObjects.Sprite[] = []
  /** 果果 */
  private fruits: Phaser.GameObjects.Sprite[] = []
  /** 方向 */
  private moveVec: Phaser.Math.Vector2
  private updateDelay: number = 0
  constructor() {
    super({ key: 'MainScene' })
  }

  create() {
    this.fpsText = new FpsText(this)
    this.moveVec = new Phaser.Math.Vector2(0, 0)
    this.initGame()
  }

  /** 初始化游戏 */
  initGame() {}

  /** 重新开始本关 */
  resetGame() {
    this.initGame()
  }

  /** 判断是否结束 */
  private gameOver() { }
  /**帧 */
  update() {
    this.fpsText.update()
  }
}

这个时候不出意外,就可以看到一个空白的场景,左上角有个FPS.

四、画一条小蛇蛇

我么先定义一下每一节蛇蛇的宽高BOX_SIZE
以及初始化一条长度为SNAKE_START_LENGTH 蛇蛇。

新建 src/constants/config.ts


export const CONFIG = {
  /** 格子大小 */
  BOX_SIZE: 40,
  /** 初始的蛇蛇长度 */
  SNAKE_START_LENGTH: 4
}

我们新增两个方法。

  • initSnake: 初始化蛇蛇。循环SNAKE_START_LENGTH,生成坐标,然后去调用下面
  • genneratorSnakeItem:根据坐标生成蛇的一节item
 /** 初始化蛇蛇 */
  private initSnake() {
    // 初始化 蛇 4个元素, 从4, 4开始
    for (var i = 0; i < CONFIG.SNAKE_START_LENGTH; i++) {
      this.snakes[i] = this.genneratorSnakeItem({ x: (4 + i) * CONFIG.BOX_SIZE, y: 4 * CONFIG.BOX_SIZE })
    }
  }

  /** 生成蛇蛇的item */
  private genneratorSnakeItem(pos: IPOS) {
    return this.add.sprite(pos.x, pos.y, 'snake').setDisplaySize(CONFIG.BOX_SIZE, CONFIG.BOX_SIZE).setOrigin(0,0)
  }

我们在initGame执行this.initSnake() 此时应该可以看到一条长度为4的小蛇,下一步就让小蛇动起来:

image.png

五、让小蛇蛇跑起来

小蛇就这么躺着肯定不行,怎么让它动起来呢?

  • 监听wasd方向键
  • 小蛇根据方向,进行移动

先说方向

不管wasd哪个键,我们都用是坐标x,y来表示方向。

  • w向上,那么{x: 0 , y: -1}
  • a向左,那么{x: -1 , y: 0}
  • s向下,那么{x: 0 , y: 1}
  • d向上,那么{x: 1 , y: -0} 在帧事件里面我们去把小蛇的坐标加上对应的方向坐标。小蛇就可以移动了。

蛇移动

这里再重新强调下:数组的最后一项为蛇头
举个栗子:我们按了d键,让蛇向移动。我们只需要移动蛇尾就可以达到效果了。蛇尾的坐标为蛇头的坐标 + 上方向向量

我亲手画个美妙的示例图: 蛇方向解释.jpg

那就上代码吧:
首先create生命周期里面把键盘事件监听以及方向向量初始化掉。

create() {
    this.fpsText = new FpsText(this)
    this.cursors = this.input.keyboard.createCursorKeys()
    this.input.keyboard.on('keydown', this.onListenerKeyDown.bind(this))
    this.moveVec = new Phaser.Math.Vector2(0, 0)
    this.initGame()
}

/** 监听键盘事件 */
private onListenerKeyDown(e) {
    if (!Object.values(KEY_DIR).includes(e.key)) return
    let vec = new Phaser.Math.Vector2(0, 0)
    switch (e?.key) {
      case KEY_DIR.UP:
        vec.y = -1
        break
      case KEY_DIR.LEFT:
        vec.x = -1
        break
      case KEY_DIR.DOWN:
        vec.y = 1
        break
      case KEY_DIR.RIGHT:
        vec.x = 1
        break
    }
    this.moveVec = vec
}

然后关键代码来了。
为了控制速度,我们增加一个updateDelay的数字。每一帧+1。然后只有在20帧才真正渲染一次。这个20可以后面扩展成为速度。
update事件里面添加如下代码

  update(dt) {
    this.fpsText.update()
    this.updateDelay++
    if (this.moveVec.x === 0 && this.moveVec.y === 0) return
    if (this.updateDelay % 20 === 0) {
      this.updateDelay = 0;
      // 数组最后一项为 蛇头
      let snakesFirst = this.snakes[this.snakes.length - 1]
      let snakesLast = this.snakes.shift()
      //获取本次预移动的点坐标
      snakesLast!.x = snakesFirst.x + this.moveVec.x * CONFIG.BOX_SIZE
      snakesLast!.y = snakesFirst.y + this.moveVec.y * CONFIG.BOX_SIZE
      //添加到头部
      // this.physics.add.overlap(snakesLast, this.fruits, this.collectFruit, null, this)
      this.snakes.push(snakesLast!)
    }
  }

此时此刻,你的小蛇蛇应该可以移动了。这部分代码比较长,如果不好写,可以直接参考源码:完整代码

六、吃水果

生成水果

我们生成水果的时候,无非就是随机坐标放置一个水果。
随机坐标的学问在于:

  • 坐标需要和蛇的格子一样。我们需要计算出,整个屏幕大概x,y分别可以放置多少个水果maxX,maxY。然后再这个里面取随机数。
  • 新生成的水果,不能生成在蛇身上。如果落到蛇身上了,继续递归一个新的坐标。

新增genneratorFruit以及获取随机坐标getNewFruitPos。为了吃到第一个水果,记得在initGame方法里面调用一次this.genneratorFruit()

 /** 生成水果的位置,不要出现在蛇的身上 */
  private getNewFruitPos():IPOS {
    let maxX = Math.floor(+this.game.config.width / CONFIG.BOX_SIZE)
    let maxY = Math.floor(+this.game.config.height / CONFIG.BOX_SIZE / 3 * 2)
    let x = Phaser.Math.Between(1, maxX) * CONFIG.BOX_SIZE
    let y = Phaser.Math.Between(1, maxY) * CONFIG.BOX_SIZE
    let isOnSnake = this.snakes.some(item => item.x === x && item.y === y)
    return isOnSnake ? this.getNewFruitPos() : { x, y }
  }

  /** 随机果果 */
  private genneratorFruit() {
    let pos = this.getNewFruitPos()
    let fruit = this.add
      .sprite(pos.x, pos.y, 'fruit')
      .setDisplaySize(CONFIG.BOX_SIZE, CONFIG.BOX_SIZE).setOrigin(0,0)
    this.fruits.push(fruit)
  }

吃水果

前面我们已经把水果和蛇的格子坐标统一了,这里的吃水果就好做了。

  • 1、要判断蛇头有没有碰到水果
  • 2、在水果坐标这里,新生成一个蛇的节item
  • 3、销毁当前水果,重新生成一个

update 方法里面新增检测是否碰到水果this.collectFruit()。 创建collectFruit方法

/** 吃到水果 */
  private collectFruit() {
    this.fruits.map((_fruit, index) => {
      if (_fruit.x === this.snakes[this.snakes.length - 1].x && _fruit.y === this.snakes[this.snakes.length - 1].y) {
        // 根据当前水果坐标,新增一个蛇蛇的item
        this.snakes.unshift(this.genneratorSnakeItem({ x: _fruit.x, y: _fruit.y }))
        //销毁这个水果
        this.fruits.splice(index)[0]
        _fruit.destroy()
        // 生成下一个水果
        this.genneratorFruit()
      }
    })
  }

打开浏览器看看? 你的贪吃蛇就是不是就完成了!

碰撞自己及碰墙

我们需要在update方法里面新增两个检测,因为蛇头在最前面,我们只需要判断蛇头这个元素坐标就OK了。

  • 不能碰到自己this.checkSelf(snakesFirst)
  • 不能撞墙this.checkWall(snakesFirst)
/** 是否碰到自己 */
  private checkSelf(snakesFirst) {
    let isCheckSelf = this.snakes.filter(item => item.x === snakesFirst.x && item.y === snakesFirst.y);
    // 取个巧,找出坐标和蛇头一样的元素,因为蛇头肯定等于蛇头,所以要 匹配List>1
    if(isCheckSelf.length > 1) alert('碰到的自己啦')
  }

   /** 是否碰墙 */
  private checkWall(snakesFirst) {
    let isCheckWall = snakesFirst.x >=  this.game.config.width  || snakesFirst.x < 0 || snakesFirst.y >= this.game.config.height || snakesFirst.y < 0;
    // 取个巧,找出坐标和蛇头一样的元素,因为蛇头肯定等于蛇头,所以要 匹配List>1
    if(isCheckWall) alert('撞墙啦')
  }
  

结语

一个简单的小游戏就完成了。 phaserjs里面封装好的碰撞检测。可以直接用。因为这里的碰撞很简单,我们就自己实现了。

如果大家对移动端互动营销h5游戏感兴趣,点个赞,点个关注。后续会难度会陆续增加。