十分钟教你写一个简单的第三人称控制器

261 阅读3分钟

写之前还是构思我们的流程,先思考一下一个第三人称控制器需要什么

  • 角色根据键盘事件进行移动
  • 相机跟随角色的移动

不考虑物理系统的话,貌似只有这两大方面。来我们逐个来拆解实现

人物移动

这块其实也很简单,我们关键用几个向量运算就可以实现

首先,事件绑定不用说了吧。我相信这个大家没有不会的。接下来我们直接写关键代码

假设我们的事件绑定已经写好

 const { forward, backward, left, right } = input

思考一下:我们常规逻辑怎么使一个物体移动呢

  1. 我们肯定需要先获取角色当前的朝向方向
  2. 当键盘事件触发时,角色沿着当前的朝向前进

获取角色当前朝向,threejs 提供有现成的api。我们只需要将它归一化,同时这里简单点,我们不考虑y方向上的,只在地面上行走


  const forwardVector = new Vector3()
  this.player.getWorldDirection(forwardVector)

  forwardVector.y = 0
  forwardVector.normalize()

其实这个时候,我们的前进后退就已经可以实现了。

因为我们已经有了朝向向量,角色的移动我们只需要每一帧让角色从当前位置,沿着朝向方向走一段距离就可以了

  
  const moveVector = forwardVector.multiplyScalar(16 * deltaT)

  this.player.position.add(moveVector)

因为有四个方向,我们现在暂时定义一个中间向量记录一下方向吧

 const moveDirection = new Vector3()

 if (forward)
    moveDirection.add(forwardVector)

  const moveVector = moveDirection.normalize().multiplyScalar(16 * deltaT)
  this.player.position.add(moveVector)

往后走,那就是moveDirection减去forwardVector咯

  const moveDirection = new Vector3()

  if (forward)
    moveDirection.add(forwardVector)
  if (backward)
    moveDirection.sub(forwardVector)

左右呢? 首先看一下我们目前有什么

假设x就是我们目前的朝向向量,我们想要获取z。同时y这个正上的向量我们也是已知了。那么z很明显就可以通过x和y的叉积得到

const vectorUp = new Vector3(0, 1, 0)
const vectorRight = new Vector3().crossVectors(vectorUp, forwardVector).normalize()

获取到了这个右向向量,后面的逻辑不就是上面的复制粘贴么

const vectorUp = new Vector3(0, 1, 0)
const vectorRight = new Vector3().crossVectors(vectorUp, forwardVector).normalize()

const moveDirection = new Vector3()

if (forward)
  moveDirection.add(forwardVector)
if (backward)
  moveDirection.sub(forwardVector)
if (left)
  moveDirection.add(vectorRight)
if (right)
  moveDirection.sub(vectorRight)

那么到这里人物移动的核心代码就搞定了

相机跟随

这里要做什么呢?

  1. 角色移动的时候,相机要始终在角色屁股后面的某一个位置(先不引入弹簧臂的概念,本身也是比较简单的东西)
  2. 相机控制旋转的时候,角色也需要跟着旋转

相机的控制,这里我们是没有必要自己再写一套的。我们完全可以借助three的轨道控制器

先来实现1,这个其实就更简单了,我们可以搞一个容器,包裹角色。然后利用容器的相对位置关系找一个位置放置相机,然后将上面在移动时候更新角色的逻辑调整为更新容器

再来实现2,其实主要目的就是要让相机始终被对角色(除了左右移动)

思考一下这里我们可以拿到什么

  1. 角色位置
  2. 相机位置

有这些我们就可以让2-1,获取到角色指向相机的向量。上面说了我们暂时先只考虑xz平面

为了简单化,我们假设这个“角色指向相机的向量” 为(5,5,5),不考虑y方向即(5,0,5)。由xz平面做二维坐标系如下

可以看到α 其实就是我们最终的旋转值。也即在帧循环中 this.character.rotation.y = α

那可以看到这里关键就是计算 α ,由图可以清晰的看到 α = β + Math.PI 。那么关键又来到了怎么计算Math.PI 了。

可能数学功底比较好的同学一眼就看看出来了,这边就是一个atan2

即最终代码

    const cameraPosition = new Vector3()
    this.camera.getWorldPosition(cameraPosition)

    const characterPosition = new Vector3()
    this.character.getWorldPosition(characterPosition)

    const direction = new Vector3()
    direction.subVectors(cameraPosition, characterPosition)
    direction.y = 0

    const angle = Math.atan2(direction.x, direction.z)

    this.character.rotation.y = angle + Math.PI