一、关于状态和状态机
1.定义
状态:顾名思义就是角色当前的状态,例如“奔跑”“站立”“蹲下”等等,各状态独立
状态机:将当前状态转变到下一状态的一种方法
优点 | 缺点 |
每个状态都有自己独立的状态,互不干扰 | 每个状态都有一个自己的文件 |
便于状态的增添更改 | 玩家一次只能处于一种状态,例如不能同属处于射击和移动状态,不过只需要创建两个状态机即可解决 |
无法继承Unity的“MonoBehaviour”类 |
2.创建接口
因为状态机无法继承“MonoBehavior”类,我们需要先创建一个接口,这只需将Unity创建的脚本内“class”改为“interface”。并添加两个状态机中比较简单的方法Enter()和Exit(),这两个方法的逻辑在我们从一个状态转换到另一个状态时运行
Enter:每当状态变为当前状态时运行
Exit: 每当状态变为上一状态时运行
这将便于我们“进入”或“退出”状态时设置和重置数据
public interface IState
{
public void Enter();
public void Exit();
}
不过显然,这两种方法并不足以解决大多问题,我们通常有其他3种方法
public void HandleInput();
//允许我们运行任何关于读取输入的逻辑
public void Update();
//允许我们运行任何非物理相关的逻辑
public void PhysicsUpdate();
//允许我们运行任何物理相关的逻辑
顺带一提,“Update”和“PhysicsUpdate”等同于“MonoBehavior”中的“Update”和“FixedUpdate”
3.创建基本状态机抽象类
使其成为一个抽象类,并从创建保存当前状态的变量开始,这是我们创建的每个状态机都将继承的类,此处的Protected是为了使其可以访问从该类继承的类
public abstract class StateMachine
{
protected IState currentState;
}
ChangeState方法
Exit()方法可以重置前面的状态,但若状态为空,Exit()则不会被调用,导致报错,而在C#中有一个方便的运算符“null-conditional”便可以做到若返回null,则C#不会调用Exit方法
public void ChangeState(IState newState)
{
currentState?.Exit();
currentState = newState;
currentState.Enter();
}
再为每个状态逻辑创建一个方法,便可拥有一种方法可以从MonoBehaviour类调用当前状态的逻辑了
public void HandleInput()
{
currentState?.HandleInput();
}
public void Update()
{
currentState?.Update();
}
public void PhysicsUpdate()
{
currentState?.PhysicsUpdate();
}
二、创建运动状态机
1.关于“缓存状态”和“实例化新状态”
在ChangeState(IState newState)中,每当我们要调用ChangeState()方法,我们都需要传入我们的newState,我们可以通过每次更改状态时,创建新状态类中的新实例并缓存它的实例到我们的状态机中,在每次我们更改状态时创建一个新实例可以确保 如果一个状态未被使用,资源不会被不必要地浪费,因为他会被删除。
“缓存实例”将始终使用内存等资源,因为实例仍然是必须的且不会自动删除,但我们不需要每次都实例化新状态。此外,创建一个新实例总会再次创建变量,因此他们的值将被重置为初始默认值,而缓存实例将始终保持原样。
2.缓存初始状态
public class PlayerMovementStateMachine : StateMachine
{
public PlayerIdlingState IdlingState { get; }
public PlayerWalkingState WalkingState { get; }
public PlayerRunningState RunningState { get; }
public PlayerSprintingState SprintingState { get; }
}
并创建构造将其实例化
public PlayerMovementStateMachine(Player player)
{
IdlingState = new PlayerIdlingState();
WalkingState = new PlayerWalkingState();
RunningState = new PlayerRunningState();
SprintingState = new PlayerSprintingState();
}
3.创建玩家脚本
目前我们仍未继承MonoBehaviour类,那么他的逻辑在游戏中就永远不会运行,我们可以新建一个Player脚本,并创建一个状态机实例并调用其方法。创建Awake,Start,Update,FixedUpdate等常用方法,并将Player脚本挂载在Player物体上
三、创建玩家输入
创建好文件夹后在文件夹中Create一个“InputActions”,命名并双击打开,这是Unity的新输入系统,将创建的Movement里ActionType改为Value,Control Type改为Vector2,将创建Movement时自带的<No Binding>删除,并ADD一个2DVector,再根据UpDown等输入该方向移动的WASD键,详细方法为点击“Up”,并在右边的Path中点击Listen并输入自己希望的按键
现在我们已经保存了输入,那么就需要一种访问他们的方法,只需要在刚创建的InputAction中的Inspector中点击“Generate C# Class”即可自动生成一个脚本并提供一中方法
public class PlayerInput : MonoBehaviour
{
public PlayerInputActions InputActions { get; private set; }
public PlayerInputActions.PlayerActions PlayerActions { get; private set; }
private void Awake()
{
InputActions = new PlayerInputActions();
PlayerActions = InputActions.Player;
}
private void OnEnable()
{
InputActions.Enable();
}
private void OnDisable()
{
InputActions.Disable();
}
}
四、玩家移动
开始的开始,当然是为玩家添加RIgidBody组件以及一个胶囊碰撞器,方法可以看上一篇文章
通常,我们为玩家的移动添加一个力,都会设置刚体的“速度”变量,而在Unity的官方教程中,并不建议这么做,而是使用“AddForce”,因为添加力不是瞬间的,只会在下一次物理更新中发生,而Velocity是力的即使变化。但如果我们一直按下w键,玩家就会有一个一直向前的力,让我们的玩家速度无限加快,要解决该问题,我们只需从要添加的力,移除现有的速度
Vector3 currentPlayerHorizontalVelocity = GetPlayerHorizontalVelocity();
protected Vector3 GetPlayerHorizontalVelocity()
{
Vector3 playerHorizontalVelocity = stateMachine.Player.Rigidbody.velocity;
playerHorizontalVelocity.y = 0f;
return playerHorizontalVelocity;
}
五、添加相机
我们将使用cinemachine,它可以让我i们轻松添加一个相机系统,在Hierarchy>cinemachine>Virtual Camera中创建相机,后我们需要在玩家上新建一个CameraLookPoint以便我们可以实现相机跟随,更多跟cinemachine的可以参考cinemachine教程
六、玩家旋转
大多数游戏中,当我们旋转相机时,玩家将随之旋转,并向前移动到相机所注视的位置,因此,我们要让玩家朝着我们输入的方向加上相机旋转的总和移动
private float Rotate(Vector3 direction)
{
float directionAngle = UpdateTargetRotation(direction);
RotateTowardsTargetRotation();
return directionAngle;
}
protected float UpdateTargetRotation(Vector3 direction, bool shouldConsiderCameraRotation = true)
{
float directionAngle = GetDirectionAngle(direction);
if (shouldConsiderCameraRotation)
{
directionAngle = AddCameraRotationToAngle(directionAngle);
}
if (directionAngle != stateMachine.ReusableData.CurrentTargetRotation.y)
{
UpdateTargetRotationData(directionAngle);
}
return directionAngle;
}
private float GetDirectionAngle(Vector3 direction)
{
float directionAngle = Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg;
if (directionAngle < 0f)
{
directionAngle += 360f;
}
return directionAngle;
}
private float AddCameraRotationToAngle(float angle)
{
angle += stateMachine.Player.MainCameraTransform.eulerAngles.y;
if (angle > 360f)
{
angle -= 360f;
}
return angle;
}
private void UpdateTargetRotationData(float targetAngle)
{
stateMachine.ReusableData.CurrentTargetRotation.y = targetAngle;
stateMachine.ReusableData.DampedTargetRotationPassedTime.y = 0f;
}
protected void RotateTowardsTargetRotation()
{
float currentYAngle = stateMachine.Player.Rigidbody.rotation.eulerAngles.y;
if (currentYAngle == stateMachine.ReusableData.CurrentTargetRotation.y)
{
return;
}
float smoothedYAngle = Mathf.SmoothDampAngle(currentYAngle, stateMachine.ReusableData.CurrentTargetRotation.y, ref stateMachine.ReusableData.DampedTargetRotationCurrentVelocity.y, stateMachine.ReusableData.TimeToReachTargetRotation.y - stateMachine.ReusableData.DampedTargetRotationPassedTime.y);
stateMachine.ReusableData.DampedTargetRotationPassedTime.y += Time.deltaTime;
Quaternion targetRotation = Quaternion.Euler(0f, smoothedYAngle, 0f);
stateMachine.Player.Rigidbody.MoveRotation(targetRotation);
}
protected Vector3 GetTargetRotationDirection(float targetRotationAngle)
{
return Quaternion.Euler(0f, targetRotationAngle, 0f) * Vector3.forward;
}