ThreeJS实现推箱子小游戏【二】

在线预览地址

前言

上一章主要介绍了怎么创建基础的场景以及推箱子的人物,这一章主要介绍整个游戏场景的创建,以及游戏的基本逻辑。

正文

游戏的所有内容都是通过ThreeJs的立体几何来创建的,整个场景分为了游戏区域以及环境区域,游戏区域一共有五种类型:人物、围墙、箱子、目标点、空白区域。

首先定义五种类型:

ts 复制代码
export const EMPTY = 'empty'
export const WALL = 'wall'
export const TARGET = 'TARGET'
export const BOX = 'box'
export const PLAYER = 'player'

类型定义好之后,我们需要定义整个游戏关卡的布局,推箱子的游戏掘金上也有很多,我看了设置布局的方式多种多样,我选择一种比较容易理解也比较简单的数据结构,就是用双层数组结构来表示每一种元素对应所在的位置。并且我把目标点的位置没有放在整个游戏的布局数据里面,而是单独存起来,这样做是因为player移动之后我们需要实时的去维护这个布局数据,所以少一种类型的话我们会简化很多判断逻辑。

ts 复制代码
export const firstLevelDataSource: LevelDataSource = {
  layout: [
    [WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL],
    [WALL, PLAYER, EMPTY, EMPTY, WALL, WALL, WALL, WALL, WALL],
    [WALL, EMPTY, BOX, BOX, WALL, WALL, WALL, WALL, WALL],
    [WALL, EMPTY, BOX, EMPTY, WALL, WALL, WALL, EMPTY, WALL],
    [WALL, WALL, WALL, EMPTY, WALL, WALL, WALL, EMPTY, WALL],
    [WALL, WALL, WALL, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, WALL],
    [WALL, WALL, EMPTY, EMPTY, EMPTY, WALL, EMPTY, EMPTY, WALL],
    [WALL, WALL, EMPTY, EMPTY, EMPTY, WALL, WALL, WALL, WALL],
    [WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL]
  ],
  targets: [
    [3, 7],
    [4, 7],
    [5, 7]
  ]
}

layout就表示游戏的布局数据,后面我们循环加载的时候按照类型来对应加载就行了。

接着我们开始加载游戏的基本数据。

ts 复制代码
/**
 * 创建类型网格
 */
private createTypeMesh(cell: CellType, x: number, y: number) {
  if (cell === WALL) {
    this.createWallMesh(x, y)
  } else if (cell === BOX) {
    this.createBoxMesh(x, y)
  } else if (cell === PLAYER) {
    this.createPlayerMesh(x, y)
  }
}

这里的x,y实际就对应当前几何体所在的位置,需要注意的就是在加载箱子的时候,需要判断一下,当前箱子的位置是不是在目标点上,如果在目标点上的话就需要把箱子的颜色设置为激活的颜色。

ts 复制代码
private createBoxMesh(x: number, y: number) {
  const isTarget = this.elementManager.isTargetPosition(x, y)
  const color = isTarget ? theme.coincide : theme.box
  const boxGraphic = new BoxGraphic(color)
  boxGraphic.mesh.position.x = x
  boxGraphic.mesh.position.z = y
  this.entities.push(boxGraphic)
  this.scene.add(boxGraphic.mesh)
}

这里我还创建了一个elementManager管理工具,专门用来存当前关卡的布局数据以及用来移动几何体的位置。 创建出来的基础游戏场景就是这样。

基础布局创建完之后,添加上键盘事件,主要用来控制人物和箱子的移动。

ts 复制代码
private bindKeyboardEvent() {
  window.addEventListener('keyup', (e: KeyboardEvent) => {
    if (!this.isPlaying) return

    const keyCode = e.code
    const playerPos = this.elementManager.playerPos

    const nextPos = this.getNextPositon(playerPos, keyCode) as Vector3
    const nextTwoPos = this.getNextPositon(nextPos, keyCode) as Vector3
    const nextElement = this.elementManager.layout[nextPos.z][nextPos.x]

    const nextTwoElement =
      this.elementManager.layout[nextTwoPos.z][nextTwoPos.x]

    if (nextElement === EMPTY) {
      this.elementManager.movePlayer(nextPos)
    } else if (nextElement === BOX) {
      if (nextTwoElement === WALL || nextTwoElement === BOX) return
      this.elementManager.moveBox(nextPos, nextTwoPos)
      this.elementManager.movePlayer(nextPos)
    }
  })
}

这里主要做了两件事,首先把下个和下下个的位置和位置所在的mesh类型查找出来,计算位置很简单,用当前player所在的位置加上键盘按下的方向计算出来就行。

ts 复制代码
if (newDirection) {
   const mesh = this.sceneRenderManager.playerMesh
   mesh.lookAt(mesh.position.clone().add(newDirection))
   return position.clone().add(newDirection)
 }

查找坐标所在的mesh直接用当前位置所在的坐标x,y,就能在elementManager上获取到。

ts 复制代码
const nextElement = this.elementManager.layout[nextPos.z][nextPos.x]

然后我们接着判断下个坐标以及下下个坐标的类型,来决定player和箱子是否可以移动。

ts 复制代码
if (nextElement === EMPTY) {
  this.elementManager.movePlayer(nextPos)
} else if (nextElement === BOX) {
  if (nextTwoElement === WALL || nextTwoElement === BOX) return
  this.elementManager.moveBox(nextPos, nextTwoPos)
  this.elementManager.movePlayer(nextPos)
}

在elementManager里面更新mesh的位置,首先是根据坐标把对应的mesh查找出来,然后把mesh坐标设置为下一个坐标,并且还需要维护this.levelDataSource.layout布局数据,因为这个数据是随着玩家的操作实时更新的。

ts 复制代码
/**
 * 更新实体位置
 */
private updateEntityPosotion(curPos: Vector3, nextPos: Vector3) {
  const entity = this.scene.children.find(
    (mesh) =>
      mesh.position.x === curPos.x &&
      mesh.position.y === curPos.y &&
      mesh.position.z === curPos.z &&
      mesh.name !== TARGET
  ) as Mesh

  if (entity) {
    const position = new Vector3(nextPos.x, entity.position.y, nextPos.z)
    entity.position.copy(position)
  }
  // 如果实体是箱子,需要判断是否是目标位置
  if (entity?.name === BOX) this.updateBoxMaterial(nextPos, entity)
}

最后在每一步键盘操作之后都需要判断当前游戏是否结束,只需要判断所有的box所在的位置是否全部都在目标点的位置上就行。

ts 复制代码
/**
* 判断游戏是否结束
*/
public isGameOver() {
 // 第一部找出所有箱子的位置,然后判断箱子的位置是否全部在目标点上
 const boxPositions: Vector3[] = []
 this.layout.forEach((row, y) => {
   row.forEach((cell, x) => {
     if (cell === BOX) boxPositions.push(new Vector3(x, 0, y))
   })
 })
 return boxPositions.every((position) =>
   this.isTargetPosition(position.x, position.z)
 )
}
相关推荐
巴巴博一5 小时前
keep-alive缓存
前端·javascript·vue.js·缓存·typescript
爱看书的小沐6 小时前
【小沐杂货铺】基于Three.JS绘制太阳系Solar System(GIS 、WebGL、vue、react)
javascript·vue.js·webgl·three.js·地球·太阳系·三维地球
Hamm6 小时前
用一种全新的方式来实现i18n,和魔法字符串彻底说拜拜
前端·vue.js·typescript
Mintopia8 小时前
深入理解 Three.js 中的 Mesh:构建 3D 世界的基石
前端·javascript·three.js
Mintopia1 天前
深入理解 Three.js 中的 PerspectiveCamera
前端·javascript·three.js
念九_ysl1 天前
基数排序算法解析与TypeScript实现
前端·算法·typescript·排序算法
飞哥数智坊1 天前
AI编程实战:30分钟实现Web 3D船舶航行效果
人工智能·three.js
安分小尧1 天前
[特殊字符] 使用 Handsontable 构建一个支持 Excel 公式计算的动态表格
前端·javascript·react.js·typescript·excel
yanxy5122 天前
【TS学习】(18)分发逆变推断
前端·学习·typescript
Mintopia2 天前
Three.js深度解析:InstancedBufferGeometry实现动态星空特效 ——高效渲染十万粒子的底层奥秘
前端·javascript·three.js