在上一篇文章留了一個(gè)FSM有限自動(dòng)機(jī)狀態(tài)機(jī)的坑,在這里填上,對(duì)這部分不感興趣的可以直接看后面的API和使用方法
FSM有限自動(dòng)狀態(tài)機(jī)
簡(jiǎn)單來(lái)說(shuō)狀態(tài)機(jī)是一種維護(hù)狀態(tài)遷移的精簡(jiǎn)并邏輯性很強(qiáng)的模式,在游戲開(kāi)發(fā)中被廣泛運(yùn)用在AI邏輯和狀態(tài)變化中,設(shè)想一種情況,我們需要維護(hù)角色運(yùn)動(dòng)狀態(tài)的變化,如行走,跳躍,掉落等等,一種可行的方式是用一些if,else語(yǔ)句,如這種在某些教程中經(jīng)常出現(xiàn)的代碼:
if(Input.GetKeyDown(KeyCode.W))
{
transform.positon += new Vector3(0,0,1);
}
if(Input.GetKeyDown(KeyCode.S))
{
transform.positon += new Vector3(0,0,-1);
}
這樣的代碼在處理單純的行走時(shí)是可行的,但是當(dāng)動(dòng)作增多、邏輯增多以后這樣的代碼變得不可行,就算只有行走、跳躍、掉落這三種動(dòng)作,帶來(lái)的邏輯也是相當(dāng)多的,比如角色在輸入跳躍后角色將跳躍,角色在空中時(shí)不可再跳躍,跳躍著地以后才能繼續(xù)行走等等等
為了解決大量的狀態(tài)變化,我們引入了狀態(tài)機(jī)
事實(shí)上我相信每個(gè)Unity開(kāi)發(fā)人員都接觸過(guò)狀態(tài)機(jī),Unity編輯器中對(duì)動(dòng)畫(huà)的處理就是采用狀態(tài)機(jī)的(5.x版本以后)

這張圖很好的說(shuō)明了狀態(tài)機(jī)在做什么,其實(shí)就是在管理狀態(tài)以及狀態(tài)之間的遷移,Idle狀態(tài)在輸入前進(jìn)方向后會(huì)遷移到Walk狀態(tài),而Walk狀態(tài)在脫離地面則會(huì)進(jìn)入Fall狀態(tài)等等
有兩條原則是確保狀態(tài)機(jī)可以簡(jiǎn)單實(shí)現(xiàn)的:
- 每一時(shí)刻狀態(tài)機(jī)的當(dāng)前狀態(tài)只能是確定的一種狀態(tài)
-
對(duì)當(dāng)前狀態(tài)進(jìn)行遷移時(shí)必須遷移至確定的狀態(tài)
下面是實(shí)現(xiàn)的設(shè)計(jì)架構(gòu):
即一個(gè)StateMachine類控制狀態(tài),一個(gè)State的基類表示狀態(tài),所有具體的狀態(tài)繼承自這個(gè)基類
使用方法
添加動(dòng)作
在StateMachineEnum腳本中為StateID枚舉類添加新的狀態(tài)項(xiàng)
-
在CharacterStateMachine腳本中添加自定義狀態(tài)類并繼承FSMState
public class CharacterIdelState : FSMState{ public CharacterIdleState() { stateID = StateID.CharacterIdle; } public override void Reason() { } public override void Act() { } }其中需要重載構(gòu)造函數(shù)、Reason函數(shù)、Act函數(shù),構(gòu)造函數(shù)中為stateID賦值為自定義添加的狀態(tài)值
Reason函數(shù)代表狀態(tài)經(jīng)過(guò)特定的事件會(huì)發(fā)生遷移
Act函數(shù)代表該狀態(tài)下執(zhí)行的行為
-
在CharacterStateMachine類的構(gòu)造函數(shù)中執(zhí)行AddState()函數(shù),參數(shù)為新建狀態(tài)的實(shí)例
AddState(new CharacterIdleState());
同樣在SuperMachineEnum類中Transition枚舉類中添加自定義的狀態(tài)遷移
在自定義狀態(tài)的類的初始化函數(shù)中添加AddTransition(),參數(shù)分別為狀態(tài)遷移值和目標(biāo)狀態(tài)值
API
SuperCharacterController:
EnableClamping():使角色吸附到地面
DisableClamping():使角色不吸附地面
EnableSlopeLimit():使角色計(jì)算坡度限制
DisableSlopeLimit():使角色不計(jì)算坡度限制
IsClamping():返回角色是否吸附地面
MoveHorizontal(Vector2 direction,float speed,float WalkAcceleration):朝一個(gè)方向按一定速度移動(dòng)(以一定加速度加速,方向?yàn)橄鄬?duì)于角色的方向)
MoveVertical(float Acceleration,float finalSpeed):在角色垂直值按一定最終速度進(jìn)行加速移動(dòng)
Ronate(Quaternion target,float maxDelta):以給定的角速度旋轉(zhuǎn)到目標(biāo)方向(四元數(shù)表示)
GetRight():獲得右方向
GetForword():獲得前方向
GetUp():獲得上方向
AcquiringGround():返回是否接觸地面
MaintainingGround():返回是否接近地面
PointBelowHead(Vector3 point):返回點(diǎn)是否在頭部以下
PointAboveFeet(Vector3 point):返回點(diǎn)是否在腳以下
MoveToTarget(Vector3 target):將角色移動(dòng)到目標(biāo)位置
在跳躍和降落狀態(tài)前記得應(yīng)用
controller.DisableClamping();
controller.DisableSlopeLimit();
FSM:
FSMSystem:
- AddState(FSMState s):為狀態(tài)機(jī)添加一個(gè)新的狀態(tài)
- DeleteState(StateID id):刪除StateId為id的狀態(tài)
- PerformTransition(Transition trans):嘗試對(duì)當(dāng)前狀態(tài)執(zhí)行trans的狀態(tài)遷移
FSMState:
- AddTransition(Transition trans, StateID id):為狀態(tài)添加一個(gè)trans狀態(tài)遷移,目標(biāo)狀態(tài)的StateID為id
- DeleteTransition(Transition trans):刪除trans的狀態(tài)遷移
- GetOutputState(Transition trans):獲得trans狀態(tài)遷移的目標(biāo)狀態(tài)
- DoBeforeEntering():重載,在進(jìn)入狀態(tài)前執(zhí)行
- DoBeforeLeaving():重載,在狀態(tài)轉(zhuǎn)移前執(zhí)行
- Reason():必需重載,代表狀態(tài)何時(shí)遷移的代碼
- Act():必需重載,代表狀態(tài)執(zhí)行的邏輯代碼
使用
FSMSystem fsm = new FSMSystem();
void Update()
{
fsm.CurrentState.Reason();
fsm.CurrentState.Act();
}
示例
下面我拿Walk狀態(tài)舉例
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));
}
}
首先我們需要定義一個(gè)類來(lái)繼承FSMSystem類,并定義構(gòu)造函數(shù),在構(gòu)造函數(shù)中添加所有我們需要的狀態(tài)
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;//模擬照相機(jī)
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();
}
}
接下來(lái)定義一個(gè)Walk類繼承自FSMState類,在構(gòu)造函數(shù)中使用AddTransiton方法將所有可能的狀態(tài)遷移添加進(jìn)去。
重載Reason和Act函數(shù),這兩個(gè)函數(shù)分別代表狀態(tài)在經(jīng)歷什么樣的事件會(huì)進(jìn)行狀態(tài)遷移以及在這個(gè)狀態(tài)下會(huì)執(zhí)行什么樣的邏輯代碼
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函數(shù)的邏輯很簡(jiǎn)單,如果我們的角色脫離了地面將進(jìn)入Fall狀態(tài),如果輸入了跳躍動(dòng)作將進(jìn)入跳躍狀態(tài),如果輸入的行走向量長(zhǎng)度過(guò)小將進(jìn)入靜止?fàn)顟B(tài)
public override void Act()
{
float walkSpeed = 5;
float walkAcc = 1;
float angleDelta = 30;
Vector2 inputV = InputController.GetKey<Vector2>("inputV");
Transform camera = Camera.main.transform;//模擬照相機(jī)
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();
}
}
旋轉(zhuǎn)
Act函數(shù)中執(zhí)行了使角色移動(dòng)的邏輯代碼,我采用了模擬搖桿的方式,首先計(jì)算出從向量(0,0,1)到輸入的搖桿向量的旋轉(zhuǎn)變化值inputQua(對(duì)四元數(shù)不太清楚的讀者可以暫時(shí)跳過(guò)),然后通過(guò)inputQua * pScreenForword計(jì)算要旋轉(zhuǎn)到的目的方向(pScreenForword表示從攝像機(jī)位置到角色位置的向量在xz平面上的投影,如果角色處在攝像機(jī)中心位置的話,這樣的旋轉(zhuǎn)方式是很人性化的),最后使用Quaternion.FromToRotation(Vector3.forward, target)計(jì)算出這一旋轉(zhuǎn)對(duì)應(yīng)的四元數(shù)值,并調(diào)用Ronate方法進(jìn)行旋轉(zhuǎn)
移動(dòng)
移動(dòng)要簡(jiǎn)單得多,就是以一定速度和加速度調(diào)用MoveHorizontal方法使角色朝前方移動(dòng)
最后的DoBeforeEntering即是字面理解的意思,這里使角色啟用附著地面和坡度限制
