移动状态切换

创建PlayerMoveState状态类和对应的枚举,在PlayerController中添加切换逻辑。

public class PlayerMoveState : PlayerStateBase
{

}
public enum PlayerState
{
    Idle,
    Move
}
//PlayerController
public void ChangeState(PlayerState playerState)
{
	switch (playerState)
	{
	...
	case PlayerState.Move:
	stateMachine.ChangeState<PlayerMoveState>();
	break;
	...
	}
}

为角色添加CharacterController,配置重力,旋转速度。

	//PlayerController
    #region 配置信息
    [Header("配置信息")]
    public float gravity = -9.8f;
    public float rotateSpeed = 5f;
    #endregion
    [SerializeField] private CharacterController characterController;
    public CharacterController CharacterController{ get => characterController; }

在PlayerIdleState检测玩家输入,以转换到MoveState,并模拟重力。

	//PlayerIdleState
    public override void Update()
    {
        //检测玩家移动
        player.CharacterController.Move(new Vector3(0, player.gravity * Time.deltaTime, 0));
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        if(h!=0 || v!=0)
        {
            player.ChangeState(PlayerState.Move);
        }
    }

PlayerMoveState同理。

	//PlayerMoveState
    public override void Update()
    {   
        //停止运动回到Idle
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        player.CharacterController.Move(new Vector3(0, player.gravity * Time.deltaTime, 0));
        if (h == 0 && v == 0)
        {
            player.ChangeState(PlayerState.Idle);
        }
    }

基于动画根运动的移动

动画根运动逻辑在PlayerModel层,为此首先在PlayerController中添加属性使得状态类能够通过宿主调用PlayerModel层逻辑。

//PlayerController
public PlayerModel PlayerModel{ get => playerModel; }

根运动相关逻辑,获取动画每一帧位置和偏移量,通过Action回调参数传递给状态使用。

	//PlayerModel
    #region 根运动
    private Action<Vector3, Quaternion> rootMotionAction;
    public void SetRootMotionAction(Action<Vector3, Quaternion> rootMotionAction)
    {
        this.rootMotionAction = rootMotionAction;
    }

    public void ClearRootMotion()
    {
        rootMotionAction = null;
    }
    private void OnAnimatorMove()
    {     
        rootMotionAction?.Invoke(animator.deltaPosition,animator.deltaRotation);
    }
    #endregion

为PlayerMoveState添加进入播放移动动画树,根运动,退出清空rootMotionAction。

	//PlayerMoveState
    public override void Enter()
    {
        player.PlayAnimation("Move");
        player.PlayerModel.SetRootMotionAction(OnRootMotion);
    }

    public override void Exit()
    {
        player.PlayerModel.ClearRootMotion();
    }

    public void OnRootMotion(Vector3 deltaPosition, Quaternion deltaRotation)
    {
        player.CharacterController.Move(deltaPosition);
    }

这里多说一嘴,这个Demo是自由镜头视角,所以移动状态机的动画只有单向的走和跑(简单起见Idle也放到混合树里是可以的,但之后要做急停,所以运动和不运动的动画过渡需要自己控制),如果是固定镜头视角,有8(9)向动画,可以去看看我写的3C分析,里面有对应的案例。

alt

角色转向移动的方向

让角色移动方向与WASD输入在相机坐标空间的方向一致。

	//PlayerMoveState
    public override void Update()
    {   
        //停止运动回到Idle
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        if (h == 0 && v == 0)
        {
            player.ChangeState(PlayerState.Idle);
        }
        else
        {
            //角色旋转
            Vector3 input = new Vector3(h, 0, v);
            //获取相机的旋转值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);
        }
    }

奔跑实现

添加跑步动画过渡逻辑。

	//PlayerMoveState
    public override void Update()
    {   
      	player.CharacterController.Move(new Vector3(0, player.gravity * Time.deltaTime, 0))
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        if (h == 0 && v == 0)
        {
            //停止运动回到Idle
            player.ChangeState(PlayerState.Idle);
        }
        else
        {
            //走到跑的过渡
            if (Input.GetKey(KeyCode.LeftShift))
            {
                walk2RunTranstion = Mathf.Clamp(walk2RunTranstion + Time.deltaTime * player.walk2Transition, 0, 1);
            }
            else
            {
                walk2RunTranstion = Mathf.Clamp(walk2RunTranstion - Time.deltaTime * player.walk2Transition, 0, 1);
            }
            player.PlayerModel.Animator.SetFloat("Move", walk2RunTranstion);
            //影响动画播放速度,来影响rootmotion位移速度
            player.PlayerModel.Animator.speed = Mathf.Lerp(player.walkSpeed, player.runSpeed, walk2RunTranstion);
			//...角色转向
        }
    }
    public override void Exit()
    {	
      	//退出状态则转换累加速度归零,动画速度恢复正常
        walk2RunTranstion = 0f;
        player.PlayerModel.Animator.speed = 1f;
        player.PlayerModel.ClearRootMotion();
    }

其中,需要控制走路,跑步,走跑过渡速度(随时间累加/累减)。

	//PlayerController
    [Header("配置信息")]
    public float gravity = -9.8f;
    public float rotateSpeed = 8f;
    public float walk2Transition = 1f;
    public float walkSpeed = 1f;
    public float runSpeed = 1f;

脚步声外包给

使用动画事件调用footStep方法,和根运动的实现类似,通过Action将具体的功能外包给PlayerController实现。

	//PlayerModel
 	private Action footStepAction;
    public void Init(Action action)
    {
        this.footStepAction = action;
    }
    public void FootStep()
    {
        footStepAction?.Invoke();
    }

	//PlayerController
	public AudioClip[] footStepAudioClips;
    void Start()
    {
        PlayerModel.Init(FootStep);
        stateMachine = new StateMachine();
        stateMachine.Init<PlayerIdleState>(this);
    }
	    public void FootStep()
    {
        audioSource.PlayOneShot(footStepAudioClips[Random.Range(0, footStepAudioClips.Length)]);
    }

在PlayerController中将脚步声播放的方法传给PlayerModel中对应的委托,并由动画事件调用委托方法执行,播放脚步声。

本节最终效果如下。

alt