在上一篇文章留了一個FSM有限自動機狀態(tài)機的坑惭嚣,在這里填上必尼,對這部分不感興趣的可以直接看后面的API和使用方法
FSM有限自動狀態(tài)機
簡單來說狀態(tài)機是一種維護狀態(tài)遷移的精簡并邏輯性很強的模式窜醉,在游戲開發(fā)中被廣泛運用在AI邏輯和狀態(tài)變化中涎永,設想一種情況桑涎,我們需要維護角色運動狀態(tài)的變化倔叼,如行走竭宰,跳躍空郊,掉落等等份招,一種可行的方式是用一些if,else語句,如這種在某些教程中經常出現(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);
}
這樣的代碼在處理單純的行走時是可行的狞甚,但是當動作增多锁摔、邏輯增多以后這樣的代碼變得不可行,就算只有行走哼审、跳躍谐腰、掉落這三種動作,帶來的邏輯也是相當多的涩盾,比如角色在輸入跳躍后角色將跳躍十气,角色在空中時不可再跳躍,跳躍著地以后才能繼續(xù)行走等等等
為了解決大量的狀態(tài)變化春霍,我們引入了狀態(tài)機
事實上我相信每個Unity開發(fā)人員都接觸過狀態(tài)機砸西,Unity編輯器中對動畫的處理就是采用狀態(tài)機的(5.x版本以后)
這張圖很好的說明了狀態(tài)機在做什么,其實就是在管理狀態(tài)以及狀態(tài)之間的遷移址儒,Idle狀態(tài)在輸入前進方向后會遷移到Walk狀態(tài)芹枷,而Walk狀態(tài)在脫離地面則會進入Fall狀態(tài)等等
有兩條原則是確保狀態(tài)機可以簡單實現(xiàn)的:
- 每一時刻狀態(tài)機的當前狀態(tài)只能是確定的一種狀態(tài)
-
對當前狀態(tài)進行遷移時必須遷移至確定的狀態(tài)
下面是實現(xiàn)的設計架構:
即一個StateMachine類控制狀態(tài),一個State的基類表示狀態(tài)莲趣,所有具體的狀態(tài)繼承自這個基類
使用方法
添加動作
在StateMachineEnum腳本中為StateID枚舉類添加新的狀態(tài)項
-
在CharacterStateMachine腳本中添加自定義狀態(tài)類并繼承FSMState
public class CharacterIdelState : FSMState{ public CharacterIdleState() { stateID = StateID.CharacterIdle; } public override void Reason() { } public override void Act() { } }
其中需要重載構造函數(shù)杖狼、Reason函數(shù)、Act函數(shù)妖爷,構造函數(shù)中為stateID賦值為自定義添加的狀態(tài)值
Reason函數(shù)代表狀態(tài)經過特定的事件會發(fā)生遷移
Act函數(shù)代表該狀態(tài)下執(zhí)行的行為
-
在CharacterStateMachine類的構造函數(shù)中執(zhí)行AddState()函數(shù)蝶涩,參數(shù)為新建狀態(tài)的實例
AddState(new CharacterIdleState());
同樣在SuperMachineEnum類中Transition枚舉類中添加自定義的狀態(tài)遷移
在自定義狀態(tài)的類的初始化函數(shù)中添加AddTransition(),參數(shù)分別為狀態(tài)遷移值和目標狀態(tài)值
API
SuperCharacterController:
EnableClamping():使角色吸附到地面
DisableClamping():使角色不吸附地面
EnableSlopeLimit():使角色計算坡度限制
DisableSlopeLimit():使角色不計算坡度限制
IsClamping():返回角色是否吸附地面
MoveHorizontal(Vector2 direction,float speed,float WalkAcceleration):朝一個方向按一定速度移動(以一定加速度加速絮识,方向為相對于角色的方向)
MoveVertical(float Acceleration,float finalSpeed):在角色垂直值按一定最終速度進行加速移動
Ronate(Quaternion target,float maxDelta):以給定的角速度旋轉到目標方向(四元數(shù)表示)
GetRight():獲得右方向
GetForword():獲得前方向
GetUp():獲得上方向
AcquiringGround():返回是否接觸地面
MaintainingGround():返回是否接近地面
PointBelowHead(Vector3 point):返回點是否在頭部以下
PointAboveFeet(Vector3 point):返回點是否在腳以下
MoveToTarget(Vector3 target):將角色移動到目標位置
在跳躍和降落狀態(tài)前記得應用
controller.DisableClamping();
controller.DisableSlopeLimit();
FSM:
FSMSystem:
- AddState(FSMState s):為狀態(tài)機添加一個新的狀態(tài)
- DeleteState(StateID id):刪除StateId為id的狀態(tài)
- PerformTransition(Transition trans):嘗試對當前狀態(tài)執(zhí)行trans的狀態(tài)遷移
FSMState:
- AddTransition(Transition trans, StateID id):為狀態(tài)添加一個trans狀態(tài)遷移绿聘,目標狀態(tài)的StateID為id
- DeleteTransition(Transition trans):刪除trans的狀態(tài)遷移
- GetOutputState(Transition trans):獲得trans狀態(tài)遷移的目標狀態(tài)
- DoBeforeEntering():重載,在進入狀態(tài)前執(zhí)行
- DoBeforeLeaving():重載次舌,在狀態(tài)轉移前執(zhí)行
- Reason():必需重載熄攘,代表狀態(tài)何時遷移的代碼
- 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));
}
}
首先我們需要定義一個類來繼承FSMSystem類彼念,并定義構造函數(shù)挪圾,在構造函數(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;//模擬照相機
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類,在構造函數(shù)中使用AddTransiton方法將所有可能的狀態(tài)遷移添加進去逐沙。
重載Reason和Act函數(shù)哲思,這兩個函數(shù)分別代表狀態(tài)在經歷什么樣的事件會進行狀態(tài)遷移以及在這個狀態(tài)下會執(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ù)的邏輯很簡單,如果我們的角色脫離了地面將進入Fall狀態(tài)吩案,如果輸入了跳躍動作將進入跳躍狀態(tài)棚赔,如果輸入的行走向量長度過小將進入靜止狀態(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;//模擬照相機
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函數(shù)中執(zhí)行了使角色移動的邏輯代碼,我采用了模擬搖桿的方式,首先計算出從向量(0靠益,0丧肴,1)到輸入的搖桿向量的旋轉變化值inputQua(對四元數(shù)不太清楚的讀者可以暫時跳過),然后通過inputQua * pScreenForword計算要旋轉到的目的方向(pScreenForword表示從攝像機位置到角色位置的向量在xz平面上的投影胧后,如果角色處在攝像機中心位置的話芋浮,這樣的旋轉方式是很人性化的),最后使用Quaternion.FromToRotation(Vector3.forward, target)計算出這一旋轉對應的四元數(shù)值壳快,并調用Ronate方法進行旋轉
移動
移動要簡單得多纸巷,就是以一定速度和加速度調用MoveHorizontal方法使角色朝前方移動
最后的DoBeforeEntering即是字面理解的意思,這里使角色啟用附著地面和坡度限制