在上一篇文章留了一个FSM有限自动机状态机的坑,在这里填上,对这部分不感兴趣的可以直接看后面的API和使用方法
FSM有限自动状态机
简单来说状态机是一种维护状态迁移的精简并逻辑性很强的模式,在游戏开发中被广泛运用在AI逻辑和状态变化中,设想一种情况,我们需要维护角色运动状态的变化,如行走,跳跃,掉落等等,一种可行的方式是用一些if,else语句,如这种在某些教程中经常出现的代码:
if(Input.GetKeyDown(KeyCode.W))
{
transform.positon += new Vector3(0,0,1);
}
if(Input.GetKeyDown(KeyCode.S))
{
transform.positon += new Vector3(0,0,-1);
}
这样的代码在处理单纯的行走时是可行的,但是当动作增多、逻辑增多以后这样的代码变得不可行,就算只有行走、跳跃、掉落这三种动作,带来的逻辑也是相当多的,比如角色在输入跳跃后角色将跳跃,角色在空中时不可再跳跃,跳跃着地以后才能继续行走等等等
为了解决大量的状态变化,我们引入了状态机
事实上我相信每个Unity开发人员都接触过状态机,Unity编辑器中对动画的处理就是采用状态机的(5.x版本以后)
这张图很好的说明了状态机在做什么,其实就是在管理状态以及状态之间的迁移,Idle状态在输入前进方向后会迁移到Walk状态,而Walk状态在脱离地面则会进入Fall状态等等
有两条原则是确保状态机可以简单实现的:
- 每一时刻状态机的当前状态只能是确定的一种状态
-
对当前状态进行迁移时必须迁移至确定的状态
下面是实现的设计架构:
即一个StateMachine类控制状态,一个State的基类表示状态,所有具体的状态继承自这个基类
使用方法
添加动作
在StateMachineEnum脚本中为StateID枚举类添加新的状态项
-
在CharacterStateMachine脚本中添加自定义状态类并继承FSMState
public class CharacterIdelState : FSMState{ public CharacterIdleState() { stateID = StateID.CharacterIdle; } public override void Reason() { } public override void Act() { } }
其中需要重载构造函数、Reason函数、Act函数,构造函数中为stateID赋值为自定义添加的状态值
Reason函数代表状态经过特定的事件会发生迁移
Act函数代表该状态下执行的行为
-
在CharacterStateMachine类的构造函数中执行AddState()函数,参数为新建状态的实例
AddState(new CharacterIdleState());
同样在SuperMachineEnum类中Transition枚举类中添加自定义的状态迁移
在自定义状态的类的初始化函数中添加AddTransition(),参数分别为状态迁移值和目标状态值
API
SuperCharacterController:
EnableClamping():使角色吸附到地面
DisableClamping():使角色不吸附地面
EnableSlopeLimit():使角色计算坡度限制
DisableSlopeLimit():使角色不计算坡度限制
IsClamping():返回角色是否吸附地面
MoveHorizontal(Vector2 direction,float speed,float WalkAcceleration):朝一个方向按一定速度移动(以一定加速度加速,方向为相对于角色的方向)
MoveVertical(float Acceleration,float finalSpeed):在角色垂直值按一定最终速度进行加速移动
Ronate(Quaternion target,float maxDelta):以给定的角速度旋转到目标方向(四元数表示)
GetRight():获得右方向
GetForword():获得前方向
GetUp():获得上方向
AcquiringGround():返回是否接触地面
MaintainingGround():返回是否接近地面
PointBelowHead(Vector3 point):返回点是否在头部以下
PointAboveFeet(Vector3 point):返回点是否在脚以下
MoveToTarget(Vector3 target):将角色移动到目标位置
在跳跃和降落状态前记得应用
controller.DisableClamping();
controller.DisableSlopeLimit();
FSM:
FSMSystem:
- AddState(FSMState s):为状态机添加一个新的状态
- DeleteState(StateID id):删除StateId为id的状态
- PerformTransition(Transition trans):尝试对当前状态执行trans的状态迁移
FSMState:
- AddTransition(Transition trans, StateID id):为状态添加一个trans状态迁移,目标状态的StateID为id
- DeleteTransition(Transition trans):删除trans的状态迁移
- GetOutputState(Transition trans):获得trans状态迁移的目标状态
- DoBeforeEntering():重载,在进入状态前执行
- DoBeforeLeaving():重载,在状态转移前执行
- Reason():必需重载,代表状态何时迁移的代码
- Act():必需重载,代表状态执行的逻辑代码
使用
FSMSystem fsm = new FSMSystem();
void Update()
{
fsm.CurrentState.Reason();
fsm.CurrentState.Act();
}
示例
下面我拿Walk状态举例
public class CharacterStateMachine : FSMSystem {
public CharacterStateMachine(SuperCharacterController controller)
{
AddState(new CharacterIdleState(controller,this));
AddState(new CharacterWalkState(controller, this));
AddState(new CharacterJumpState(controller, this));
AddState(new CharacterFallState(controller, this));
}
}
首先我们需要定义一个类来继承FSMSystem类,并定义构造函数,在构造函数中添加所有我们需要的状态
public class CharacterWalkState : FSMState
{
public SuperCharacterController controller;
public CharacterWalkState(SuperCharacterController c, FSMSystem f)
{
fsm = f;
controller = c;
stateID = StateID.CharacterWalk;
AddTransition(Transition.CharacterWalkToIdle, StateID.CharacterIdle);
AddTransition(Transition.CharacterJump, StateID.CharacterJump);
AddTransition(Transition.CharacterFall, StateID.CharacterFall);
}
public override void Reason()
{
if (!controller.MaintainingGround())
{
fsm.PerformTransition(Transition.CharacterFall);
}
if (InputController.GetKey<bool>("Jump"))
{
fsm.PerformTransition(Transition.CharacterJump);
}
if (InputController.GetKey<Vector2>("inputV").magnitude <= 0.1f)
{
fsm.PerformTransition(Transition.CharacterWalkToIdle);
}
}
public override void Act()
{
float walkSpeed = 5;
float walkAcc = 1;
float angleDelta = 30;
Vector2 inputV = InputController.GetKey<Vector2>("inputV");
Transform camera = Camera.main.transform;//模拟照相机
Vector3 screenForword = controller.transform.position - camera.position;
Vector3 pScreenForword = Math3d.ProjectVectorOnPlane(controller.up, screenForword);
Quaternion inputQua = Quaternion.FromToRotation(new Vector3(0,0,1), new Vector3(inputV.x, 0, inputV.y));
if(inputV == new Vector2(0,-1))
{
inputQua = Quaternion.AngleAxis(180, controller.up);
}
Vector3 target = inputQua * pScreenForword;
controller.Ronate(Quaternion.FromToRotation(Vector3.forward, target), angleDelta);
Vector2 direction = new Vector2(0, 1);
controller.MoveHorizontal(direction, walkSpeed, walkAcc);
}
public override void DoBeforeEntering()
{
controller.EnableClamping();
controller.EnableSlopeLimit();
}
}
接下来定义一个Walk类继承自FSMState类,在构造函数中使用AddTransiton方法将所有可能的状态迁移添加进去。
重载Reason和Act函数,这两个函数分别代表状态在经历什么样的事件会进行状态迁移以及在这个状态下会执行什么样的逻辑代码
public override void Reason()
{
if (!controller.MaintainingGround())
{
fsm.PerformTransition(Transition.CharacterFall);
}
if (InputController.GetKey<bool>("Jump"))
{
fsm.PerformTransition(Transition.CharacterJump);
}
if (InputController.GetKey<Vector2>("inputV").magnitude <= 0.1f)
{
fsm.PerformTransition(Transition.CharacterWalkToIdle);
}
}
Reason函数的逻辑很简单,如果我们的角色脱离了地面将进入Fall状态,如果输入了跳跃动作将进入跳跃状态,如果输入的行走向量长度过小将进入静止状态
public override void Act()
{
float walkSpeed = 5;
float walkAcc = 1;
float angleDelta = 30;
Vector2 inputV = InputController.GetKey<Vector2>("inputV");
Transform camera = Camera.main.transform;//模拟照相机
Vector3 screenForword = controller.transform.position - camera.position;
Vector3 pScreenForword = Math3d.ProjectVectorOnPlane(controller.up, screenForword);
Quaternion inputQua = Quaternion.FromToRotation(new Vector3(0,0,1), new Vector3(inputV.x, 0, inputV.y));
if(inputV == new Vector2(0,-1))
{
inputQua = Quaternion.AngleAxis(180, controller.up);
}
Vector3 target = inputQua * pScreenForword;
controller.Ronate(Quaternion.FromToRotation(Vector3.forward, target), angleDelta);
Vector2 direction = new Vector2(0, 1);
controller.MoveHorizontal(direction, walkSpeed, walkAcc);
}
public override void DoBeforeEntering()
{
controller.EnableClamping();
controller.EnableSlopeLimit();
}
}
旋转
Act函数中执行了使角色移动的逻辑代码,我采用了模拟摇杆的方式,首先计算出从向量(0,0,1)到输入的摇杆向量的旋转变化值inputQua(对四元数不太清楚的读者可以暂时跳过),然后通过inputQua * pScreenForword计算要旋转到的目的方向(pScreenForword表示从摄像机位置到角色位置的向量在xz平面上的投影,如果角色处在摄像机中心位置的话,这样的旋转方式是很人性化的),最后使用Quaternion.FromToRotation(Vector3.forward, target)计算出这一旋转对应的四元数值,并调用Ronate方法进行旋转
移动
移动要简单得多,就是以一定速度和加速度调用MoveHorizontal方法使角色朝前方移动
最后的DoBeforeEntering即是字面理解的意思,这里使角色启用附着地面和坡度限制