目前的跳跃动画包括了跳起来和落下,我们单独抽象出掉落状态接管Jump空中以及落地的逻辑,这样移动时突然掉落也可以复用掉落状态的逻辑。

public enum PlayerState
{
    Idle,
    Move,
    Jump,
    AirDown
}

增添空中下落状态。

public class PlayerJumpState : PlayerStateBase
{
    public override void Enter()
    {
        player.PlayAnimation("Jump");
        player.PlayerModel.SetRootMotionAction(OnRootMotion);
    }

    public override void Update()
    {
        if (CheckAnimatorStateName("JumpStart", out float animationTime) && animationTime >= 0.9f)
        {
            player.ChangeState(PlayerState.AirDown);
        }
    }
    public override void Exit()
    {
        player.PlayerModel.ClearRootMotion();
    }
    public void OnRootMotion(Vector3 deltaPosition, Quaternion deltaRotation)
    {
        deltaPosition.y *= player.jumpForce;
        player.CharacterController.Move(deltaPosition);
    }
}

由于单独抽象出来了下落状态,所以原来跳跃只负责起跳,空中位移部分移至下落状态处理。

	//PlayerAirDownState
    private void AirControll()
    {
        //处理空中位移
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        Vector3 motion = new Vector3(0, player.gravity * Time.deltaTime, 0);

        if (h != 0 || v != 0)
        {
            Vector3 input = new Vector3(h, 0, v);
            Vector3 dir = Camera.main.transform.TransformDirection(input);
            motion.x = player.moveSpeedForJump * Time.deltaTime * dir.x;
            motion.z = player.moveSpeedForJump * Time.deltaTime * dir.z;
            //处理旋转
            //获取相机的旋转值y
            float y = Camera.main.transform.rotation.eulerAngles.y;
            //让四元数和向量相乘,表示这个向量按照这个四元数所表达的角度进行旋转后得到的向量
            Vector3 targetDir = Quaternion.Euler(0, y, 0) * input;
            //Slerp插值
            player.PlayerModel.transform.rotation = Quaternion.Slerp(player.PlayerModel.transform.rotation, Quaternion.LookRotation(targetDir), Time.deltaTime * player.rotateSpeed);
        }

        //Vector3 dir2 = Quaternion.Euler(0, Camera.main.transform.rotation.eulerAngles.y, 0) * new Vector3(h, 0, v);
        //player.CharacterController.Move();
        player.CharacterController.Move(motion);

    }

下落时位移不再使用RootMotion(为了能够在非跳跃下落时也复用),所以位移部分需要添加motion y方向的重力矢量。

        // 检测下落
        if (player.CharacterController.isGrounded == false)
        {
            player.ChangeState(PlayerState.AirDown);
            return;
        }

分别在角色Idle,Ground状态添加下落检测逻辑。

    private float playEndAnimationHeight = 1.6f;   // 如果空中检测到距离地面有3米则启动翻滚
    private float endAnimationHeight = 1.5f;     // End动画播放需要的高度,从这个高度开始播放

    private LayerMask groundLayerMask = LayerMask.GetMask("Env");
    private bool needEndAnimation;
    private AirDownChildState airDownState;
    private AirDownChildState AirDownState
    {
        get => airDownState;
        set
        {
            airDownState = value;
            switch (airDownState)
            {
                case AirDownChildState.Loop:
                    player.PlayAnimation("JumpLoop");
                    break;
                case AirDownChildState.End:
                    player.PlayAnimation("JumpEnd");
                    break;
            }
        }
    }

    public override void Enter()
    {
        AirDownState = AirDownChildState.Loop;
        // 判断当前角色的高度是否有可能切换到End
        needEndAnimation = !Physics.Raycast(player.transform.position + new Vector3(0, 0.5f, 0), player.transform.up * -1, playEndAnimationHeight + 0.5f, groundLayerMask);
    }

    public override void Update()
    {
        switch (airDownState)
        {
            case AirDownChildState.Loop:

                if (needEndAnimation)
                {
                    // 高度检测
                    if (Physics.Raycast(player.transform.position + new Vector3(0, 0.5f, 0), player.transform.up * -1, endAnimationHeight + 0.5f, groundLayerMask))
                    {
                        AirDownState = AirDownChildState.End;
                        return;
                    }
                }
                else
                {
                    if (player.CharacterController.isGrounded)
                    {
                        player.ChangeState(PlayerState.Idle);
                        return;
                    }
                }
                AirControll();

                break;
            case AirDownChildState.End:
                if (CheckAnimatorStateName("JumpEnd", out float animationTime))
                {
                    if (animationTime >= 0.8f)
                    {
                        // 此时依然踩空了,继续下坠
                        if (player.CharacterController.isGrounded == false)
                        {
                            AirDownState = AirDownChildState.Loop;
                        }
                        else
                        {
                            player.ChangeState(PlayerState.Idle);
                        }
                    }
                    else if (animationTime < 0.6f)
                    {
                        AirControll();
                    }
                }

                break;
        }
    }
  • 将空中下落分为两个子状态,空中下落循环和接近地面翻滚。
  • 在循环下落过程中进行高度检测来切换子状态,要发两次射线检测。
  • 第一次检测playEndAnimationHeight是切换至翻滚的当前高度前提,射线检测不触发,代表当前高度大于此高度,这么设置是为了避免角色从较低位置下落也会触发翻滚运动,必须是由高处下落才会触发。
  • 第二次检测endAnimationHeight是开始播放翻滚动画的高度,小于这个高度时播放触发翻滚运动。
  • 由大于playEndAnimationHeight的高处下落至endAnimationHeight触发翻滚,小于等于playEndAnimationHeight的低处下落则执行空中位移逻辑,如果落地则切换至待机状态。
  • 翻滚播放进度80%时检测地面,没有地面则回到空中循环下落状态,否则切换至待机,小于60%进度时保持空中位移。

最终效果如下:

alt