FSM狀態(tài)機(jī)以及超級(jí)控制器API的使用

在上一篇文章留了一個(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版本以后)


一個(gè)狀態(tài)機(jī)的例子

這張圖很好的說(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即是字面理解的意思,這里使角色啟用附著地面和坡度限制

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容