我们先来看这个主要的计算移动的函数
protected override void OnStep(Player player){// 获取当前输入的移动方向(相对于相机)var inputDirection = player.inputs.GetMovementCameraDirection();// 判断是否有输入if (inputDirection.sqrMagnitude > 0){// 获取输入移动方向相对于当前移动方向的偏移量var dot = Vector3.Dot(inputDirection, player.lateralVelocity);// 这里是触发制动状态的条件判断,下面画图的时候进行讲解if (dot >= player.stats.current.brakeThreshold){player.Accelerate(inputDirection);player.FaceDirectionSmooth(player.lateralVelocity);} else{player.states.Change<BrakePlayerState>();}}}
首先根据注释我们讲这个计算速率分成几个步骤
- 通过InputSystem获取Vecotr2输入值算出当前输入相对于摄像机的移动方向
- 在非制动状态下通过
Accelerate(Vector3 direction)
方法计算速率 - 平滑转向指定方向
进入函数
获取方向
// 这个函数的作用是:获取玩家的输入方向,并将其转换为相对于摄像机朝向的方向。
public virtual Vector3 GetMovementCameraDirection(){// 获取移动方向var direction = GetMovementDirection();// 由于direction返回的是一个Vector3,这里就取模长if (direction.sqrMagnitude > 0){// 这里计算出相机在Y轴上的偏航角度(Yaw)var rotation = Quaternion.AngleAxis(m_camera.transform.eulerAngles.y, Vector3.up);// 根据摄像机的朝向修改人物的移动方向,例如人物现在按W,摄像机看向相对右手边,此时人物不是绝对移动向前,而是向摄像机的偏航角方向移动direction = rotation * direction;// 归一化,只取方向direction = direction.normalized;}return direction;}
public virtual Vector3 GetMovementDirection(){if (Time.time < m_movementDirctionUnlock){return Vector3.zero;}// 从InputSystem里获取Vector2var value = m_movement.ReadValue<Vector2>();return GetAxisWithCrossDeadZone(value);}
public virtual Vector3 GetAxisWithCrossDeadZone(Vector2 axis){var deadZone = InputSystem.settings.defaultDeadzoneMin;// Abs仅仅判断输入的值是否大于死区的值axis.x = Mathf.Abs(axis.x) > deadZone ? RemapToDeadzone(axis.x, deadZone) : 0;axis.y = Mathf.Abs(axis.y) > deadZone ? RemapToDeadzone(axis.y, deadZone) : 0;// 这里仅控制平面移动return new Vector3(axis.x, 0, axis.y);}
// 死区的值如果设置成0.2,当ABS==0.21时,如果不调用这个方法则启动时输入的值就是0.21,会非常的突兀
// 这里除法的作用就是归一化,防止出现速度比预期少,例如输入value为1,没有除法的话最大速度就是(value - deadzone)
private float RemapToDeadzone(float value, float deadzone) => (value - deadzone) / (1 - deadzone);
计算速率
有了Vector3方向之后我们就可以进行速率的计算了
让我们进入Accelerate
方法当中去
public virtual void Accelerate(Vector3 direction){// 转向时的摩擦力var turningDrag = isGrounded && inputs.GetRun()? stats.current.runningTurnningDrag: stats.current.turningDrag;// 速率var acceleration = isGrounded && inputs.GetRun()? stats.current.runningAcceleration: stats.current.acceleration;// 最快速度var topSpeed = inputs.GetRun()? stats.current.runningTopSpeed: stats.current.topSpeed;// 最终得到的速率var finalAcceleration = isGrounded ? acceleration : stats.current.airAcceleration;Accelerate(direction, turningDrag, finalAcceleration, topSpeed);}
public virtual void Accelerate(Vector3 direction, float turningDrag, float acceleration, float topSpeed){if (direction.sqrMagnitude > 0){// 代表“我当前的速度(lateralVelocity),有多‘顺着’我想去的新方向(direction)”。// 如果数字很大很正:非常顺路。// 如果数字是0:正好垂直,不顺路也不逆路。// 如果数字是负数:完全是反方向,在“开倒车”。// speed 的初始值,就是你当前速度 lateralVelocity 在你想去的新方向 direction 上的“贡献值”。var speed = Vector3.Dot(direction, lateralVelocity);// direction 是一个只有方向没有大小的“路标”,speed 是一个只有大小没有方向的“油门大小”。两者一乘,就得到了一个既有正确方向,又有合适大小 的速度向量。这就是我们分解出的“好速度”。var velocity = direction * speed;// 坏速度,需要被摩擦力抵消的速度var turningVelocity = lateralVelocity - velocity;var turiningDelta = turningDrag * turningDragMultiplier * Time.deltaTime;// 当前能达到的最大移速var targetTopSpeed = topSpeed * topSpeedMultiplier;// 如果当前速度未达到最高移速或者速度小于0if (lateralVelocity.magnitude < targetTopSpeed || speed < 0){// 持续加速speed += acceleration * acclerationMultiplier * Time.deltaTime;// 限制速度,在当前可达到最大速度区间speed = Mathf.Clamp(speed, -targetTopSpeed, targetTopSpeed);}// 当前的移动向量(这里不仅需要方向,而且要有大小了)velocity = direction * speed;// 每次调用方法将当前的坏速率向量慢慢消除turningVelocity = Vector3.MoveTowards(turningVelocity, Vector3.zero, turiningDelta);// 将当前的速率更新为计算过后的速率lateralVelocity = velocity + turningVelocity;}}
这里Dot之前一直存在一个误区,单纯把Dot拿来当作判断位置的值,但其实dot值的大小还是受到模长的影响,并非只有[-1,1]
- 它的正负号 依然告诉我们方向关系(前方(>0)还是后方(<0)或者是垂直(=0))。
- 它的绝对值 告诉我们“有多顺路”或“有多逆路”,这个值的大小直接受到 lateralVelocity 模长的影响。