【游戏编程方法】01 FSM有限状态机--最清晰易懂的游戏动画设计思路

 

即使停下脚步,徘徊不前,时间也不会停止流逝,它才不会驻留在你身旁,陪着你一起呼天抢地。

序言

2024年了,一年学习下来匆匆忙忙,却又始终感觉一事无成。这一年跟着学了不少教程,但始终没有留下太深的印象在脑海里。因此,我准备以后坚持写自己的专栏,在记录自己学习过程的同时,也能让未来的自己能快速复习已学的知识。

当然,如果我的文章能给大家带来帮助,帮大家少踩一些坑,那也就再好不过了~

那么接下来,就让我们的游戏编程之路开始吧!!!

前言

很多新手同学开始制作2D游戏时,常常使用引擎自带的动画系统来完成动画功能。在动画量较少的情况下,这确实能非常直观地体现出游戏的整体动画流程。然而,一但动画量变多,动画实现的思路就会越来越复杂,同时代码的耦合性将会变得越来越严重。

一般的if/else方法和switch方法往往会产生以下问题:

  • 只要增加一个状态,几乎所有的其他状态相关代码中都要加入与新状态切换、交互的相关逻辑。在程序功能逐渐丰富的同时,if-else数量会增长的越来越快。

  • 每一个状态相关的对象,都保留在总控制代码中。当对象被多个状态共享时,极有可能会产生混淆,不太容易分辨被哪个状态控制,调试会变得困难。

  • 每一个状态可能使用不同的类对象,从而造成总控制类过度依赖其他类,难以移植。

像我第一次做自己的游戏时,自己撸的一个动画控制:

以及用了上百个变量,耦合了各种状态转换、条件判断的代码上千行:

当时还觉得自己凭借自己的能力完整写出来很厉害TT,现在想想简直笨死了()

实际上,我们设计2D动画基本不需要使用unity的动画系统。使用简单的FSM便能实现以下几个优点:

  • 无需动画转换过渡,2D动画一般是逐帧,我们只需要直接切换动作片段就好。

  • 减少flag的使用,这也是普通动画判定方法在代码中耦合性体现最强的一点,因为我们执行动画时,需要对人物所在的各种情况进行反复判断。

  • 编程简单、修改灵活、易于调试。程序更加符合人类的思维逻辑,能将一类相关的事件封装在一个状态中,并通过控制类进行状态切换。

那究竟什么是有限状态机FSM(finite-state macine)?

FSM是一种数据结构,主要由以下几个部分组成:

  • 有限,即存在有限个状态

  • 确定的输入条件

  • 状态之间的相互转换关系

FSM是状态模式的一个非常重要的应用,但他并不是一个特别复杂的理论或者技术,只需确定好”状态“、“转换规则”,就能快速完成AI的功能。

实现过程

IState接口

首先,我们需要定义一个所有状态机的规范。

我们创建一个脚本,删除所有内容,并将该脚本改为接口,命名为IState(名称前带I,代表这是一个接口),该接口有三个方法:

  • OnEnter()方法:代表着状态的进入

  • OnUpdate()方法:代表着状态的更新

  • OnExit()方法:代表着状态的退出

对于所有继承该接口的类,必须对这三个方法进行重写。相关代码如下:

public interface IState
{
    void OnEnter();
    void OnUpdate();
    void OnExit(); 
}

状态类

写完IState接口后,我们需要做的就是实现继承接口的状态类了。

我们需要将每个状态单独储存成一个类,重写接口中的三个函数并提供带有控制器参数的构造方法。OnEnter函数可以类比为Start,每次调用时默认先调用一次。OnExit函数则可以类比为析构函数,在状态退出时默认调用一次。

这里以IdleState状态机为例子,其他状态类可以根据此来扩展。

public class IdleState : IState
{
    private FSM manager;
​
    private Parameter parameter;
​
    private float timer;
​
​
    public IdleState(FSM manager)
    {
        this.manager = manager;
        this.parameter = manager.parameter; 
    }
    public void OnEnter()
    {
        parameter.animator.Play("E003Idle");
​
    }
    public void OnUpdate() 
    {
        timer += Time.deltaTime;
        //Debug.Log(timer);
  
        if(parameter.target != null &&
            parameter.target.position.x >= parameter.chasePoints[0].position.x && 
            parameter.target.position.x <= parameter.chasePoints[1].position.x) 
        {
            manager.TransitonState(StateType.Chase);
        }
  
        if(timer >= parameter.idleTime)
        {
            manager.TransitonState(StateType.Patrol);
        }
    }
    public void OnExit() 
    {
        timer = 0;
    } 
}
 

FSM控制脚本

FSM脚本则是我们整个动画控制的核心。为使逻辑更加明确,我们将该脚本分成枚举类、参数类和功能类。

枚举类

我们为所有的状态做一个枚举,我们需要初始化时以枚举参数为索引,将不同的状态对象一起插入字典中。这也我们同样可以快速根据枚举类型来查找我们的状态对象。

例如:

public enum StateType
{
    Idle,Patrol,Chase,Attack
}

参数类

顾名思义,参数类即存储参数的类。这里我们基本上将所有与该脚本控制的游戏物体(比如一只小怪的血量、动画机等等......)相关参数都存储在这里。

例如:

public class Parameter
{
    public int health;
    public float moveSpeed;
    public float chaseSpeed;
    public float idleTime;
    public Transform[] patrolPoints;
    public Transform[] chasePoints;
    public Transform target;
    public LayerMask targetLayer;
    public Transform attackPoint;
    public float attackArea;
    public Animator animator;
}

为了使调整更加方便,我们可以对该类进行序列化,这样我们可以在场景中实时修改参数。

using UnityEngine;
​
[Serializable]
public class Parameter
{
    ...
}

这样我们就能非常方便的在引擎中对参数进行调控了。

控制类

控制类即控制不同各种动画和行为方式的类。要实现FSM的功能,一般做要先处理以下函数:

  • Start():确保在角色动画执行的开始,初始化动画器,并将所有状态与其枚举存入字典中,并将当前的状态转换到Idle(即直立)的最初状态。

  • Update():让当前状态的onUpdate()方法保持持续的更新。

  • TranstionState(StateType type):判断当前状态是否为空,若不为空则退出当前状态。并将状态重新赋值并执行。

    此外,一些公用的方法也可以写在这里,比如转向判断,攻击范围判定,碰撞等等······

public class FSM : MonoBehaviour
{
    public Parameter parameter;
​
    private IState currentState;
​
    private Dictionary<StateType, IState> states = new Dictionary<StateType, IState>();
​
    // Start is called before the first frame update
    void Start()
    {
        parameter.animator = GetComponent<Animator>();
​
        states.Add(StateType.Idle, new IdleState(this));
        states.Add(StateType.Patrol, new PatrolState(this));
        states.Add(StateType.Chase, new ChaseState(this));
        states.Add(StateType.Attack, new AttackState(this));
​
        TransitonState(StateType.Idle);
​
​
  }
​
    // Update is called once per frame
    void Update()
    {
        currentState.OnUpdate();
    }
​
    public void TransitonState(StateType type)
    {
        if (currentState! != null)
            currentState.OnExit();
        currentState = states[type];
        currentState.OnEnter();
    }
​
    //共用的方法:
    //比如FlipTo:保持动画方向正常的方法
    public void FlipTo(Transform target)
    {
        if(target != null)
        {
            if(transform.position.x > target.position.x)
            {
                transform.localScale = new Vector3(-1, 1, 1);
            }
            else if (transform.position.x < target.position.x)
            {
                transform.localScale = new Vector3(1, 1, 1);
            }
        }
    }
​

效果实现

使用四个状态:站立、巡逻、追击、攻击。完成了一个简单的敌人AI,相关代码已发布到Github:

JcMarical/GameDevlopmentFunction: A summary of game devlopment (github.com)

后言

这样看来,有限状态机的AI方法是不是变得非常清晰呢?

当然,我们同样可以用状态模式和有限自动状态机的思想运用在其他场景中,比如:

  • 场景控制:我们可以把每个场景都视为一个状态,场景的切换即转换关系。

  • 菜单控制:不同UI内容也可以视为一个状态,菜单间的切换即转换关系。

不过初学者一般没有大量的场景和菜单切换需求,不使用设计模式也能轻松写出功能出来。如果后续有大型工程项目需求,希望这篇文章的思想能提供一些帮助。

Reference

[1] 【游戏设计模式】之三 状态模式、有限状态机 & Unity版本实现 - 知乎 (zhihu.com)

[2] 在Unity中使用状态机制作一个敌人AI哔哩哔哩bilibili

[3]《设计模式与游戏完美开发》 蔡升达 著

(作者也是第一次写技术型文章,定有不少缺漏之处,欢迎各位读者们指正并留下宝贵的意见!!!)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值