写之前还是构思我们的流程,先思考一下一个第三人称控制器需要什么
- 角色根据键盘事件进行移动
- 相机跟随角色的移动
不考虑物理系统的话,貌似只有这两大方面。来我们逐个来拆解实现
人物移动
这块其实也很简单,我们关键用几个向量运算就可以实现
首先,事件绑定不用说了吧。我相信这个大家没有不会的。接下来我们直接写关键代码
假设我们的事件绑定已经写好
const { forward, backward, left, right } = input
思考一下:我们常规逻辑怎么使一个物体移动呢
- 我们肯定需要先获取角色当前的朝向方向
- 当键盘事件触发时,角色沿着当前的朝向前进
获取角色当前朝向,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)
那么到这里人物移动的核心代码就搞定了
相机跟随
这里要做什么呢?
- 角色移动的时候,相机要始终在角色屁股后面的某一个位置(先不引入弹簧臂的概念,本身也是比较简单的东西)
- 相机控制旋转的时候,角色也需要跟着旋转
相机的控制,这里我们是没有必要自己再写一套的。我们完全可以借助three的轨道控制器
先来实现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