FSM状态机以及超级控制器API的使用

在上一篇文章留了一个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即是字面理解的意思,这里使角色启用附着地面和坡度限制

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,100评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,862评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,993评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,309评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,303评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,421评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,830评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,501评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,689评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,506评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,564评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,286评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,826评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,875评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,114评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,705评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,269评论 2 341

推荐阅读更多精彩内容