移动状态切换
创建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分析,里面有对应的案例。
角色转向移动的方向
让角色移动方向与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中对应的委托,并由动画事件调用委托方法执行,播放脚步声。
本节最终效果如下。