最终效果
文章目录
- 最终效果
- 前言
- 有限状态机的主要作用和意义
- 素材下载
- 逻辑图
- 敌人动画配置
- 优雅的代码文件目录
- 状态机代码
- 定义敌人不同状态切换
- 创建敌人
- 效果
- 更多的敌人
- 参考
- 源码
- 完结
前言
有限状态机以前的我嗤之以鼻,现在的我逐帧分析。其实之前我就了解过有限状态机,但是奈何那时能力不够,并不能理解其中的奥秘,只觉得麻烦。直到我项目需要越来越多的去编写敌人的AI,大量的if else让我头晕目眩,各种状态的切换和调试耗费我大量的时间。于是我又重新查找一些状态机的教程进行深入学习。以下我我的学习记录,希望对你有帮助。
如果后续项目使用时存在任何问题我还会回来补充和调整,文章的代码我也会尽量保持完整分享,以便大家可以复制粘贴盗自己的项目中即可使用。
有限状态机的主要作用和意义
有限状态机(Finite State Machine,FSM)是一种在计算机科学和工程中常用的模型,用于描述对象或系统在有限状态集合中的行为和状态转换。它的主要作用和意义包括:
-
行为管理与控制: FSM通过定义有限数量的状态和状态之间的转换规则,可以有效管理和控制对象或系统的行为。每个状态代表对象可能处于的一种特定状态,例如待机、行走、攻击、受伤等,而状态之间的转换则定义了这些行为如何响应外部事件或条件变化。
-
简化复杂性: 将复杂的行为分解为简单的状态和状态转换,使得程序员可以更容易地理解和管理系统的行为逻辑。这种分解也有助于减少错误和提高代码的可维护性。
-
灵活性和扩展性: FSM可以根据具体需求进行灵活的定制和扩展。通过修改状态和状态转换规则,可以快速调整和扩展系统的行为,而无需大规模重构代码。
-
行为预测和调试: FSM的结构使得系统的行为预测变得相对容易,因为每个状态和转换的行为是明确定义的。这种结构也有助于调试和排查问题,因为可以更容易地追踪和理解系统在特定状态下的行为。
-
应用领域广泛: FSM不仅在游戏开发中常见,还在自动控制、工作流程管理、编程语言解析、通信协议等许多领域有着广泛的应用。其简单而强大的结构使得它成为许多复杂系统中行为管理的首选模型之一。
总之,有限状态机通过状态和状态转换的定义,提供了一种清晰且有效的方法来管理和控制对象或系统的复杂行为,为程序员和系统设计师提供了强大的工具,用于实现各种复杂的行为逻辑和控制流程。
素材下载
https://rvros.itch.io/animated-pixel-hero
https://jesse-m.itch.io/skeleton-pack
逻辑图
敌人动画配置
优雅的代码文件目录
状态机代码
新增StateType定义状态类型枚举
// 定义状态类型枚举
public enum StateType
{
Idle, //待机
Patrol, //巡逻
Chase, //追击
React, //反应
Attack, //攻击
Hit, //受击
Death //死亡
}
新增Parameter,可序列化的参数类,存储了角色的各种状态参数和配置
// 可序列化的参数类,存储了角色的各种状态参数和配置
using System;
using UnityEngine;
[Serializable]
public class Parameter
{
public int health; // 健康值
public float moveSpeed; // 移动速度
public float chaseSpeed; // 追击速度
public float idleTime; // 空闲时间
public Transform[] patrolPoints; // 巡逻点数组
public Transform[] chasePoints; // 追击点数组
[HideInInspector] public Transform target; // 目标对象
public LayerMask targetLayer; // 目标层
public Transform attackPoint; // 攻击点的位置
public float attackArea; // 攻击范围
[HideInInspector] public Animator animator; // 角色动画控制器
[HideInInspector] public bool getHit; // 是否被击中
[HideInInspector] public AnimatorStateInfo animatorStateInfo; // 动画状态信息
}
新增FSM有限状态机类
using System.Collections.Generic;
using UnityEngine;
// 有限状态机类
public class FSM : MonoBehaviour
{
private IState currentState; // 当前状态接口
protected Dictionary<StateType, IState> states = new Dictionary<StateType, IState>(); // 状态字典,存储各种状态
public Parameter parameter; // 状态机参数
protected virtual void Awake() {
}
protected virtual void OnEnable()
{
parameter.animator = transform.GetComponent<Animator>(); // 获取角色上的动画控制器组件
TransitionState(StateType.Idle); // 初始状态为Idle
currentState.OnEnter();
}
void Update()
{
parameter.animatorStateInfo = parameter.animator.GetCurrentAnimatorStateInfo(0);// 获取当前动画状态信息
currentState.OnUpdate(); // 每帧更新当前状态
//TODO:用于测试 如果按下回车键,设置被击中状态为true
if (Input.GetKeyDown(KeyCode.Return))
{
parameter.getHit = true;
}
}
void FixedUpdate()
{
currentState.OnFixedUpdate();
}
// 状态转换方法
public void TransitionState(StateType type)
{
if (currentState != null)
currentState.OnExit(); // 如果当前状态不为空,调用退出方法
currentState = states[type]; // 更新当前状态为指定类型的状态
currentState.OnEnter(); // 调用新状态的进入方法
}
// 翻转角色朝向方法,使其朝向目标
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); // 如果角色在目标右侧,翻转角色朝向为右
}
}
}
public void Destroy(){
Destroy(gameObject);
}
// 触发器进入事件,检测到玩家时设置目标为玩家
private void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Player"))
{
parameter.target = other.transform;
}
}
// 触发器离开事件,玩家离开时清空目标
private void OnTriggerExit2D(Collider2D other)
{
if (other.CompareTag("Player"))
{
parameter.target = null;
}
}
// 在Scene视图中绘制攻击范围的辅助图形
private void OnDrawGizmos()
{
Gizmos.DrawWireSphere(parameter.attackPoint.position, parameter.attackArea);
}
}
新增IState抽象基类,定义了所有状态类的基本结构
//抽象基类,定义了所有状态类的基本结构
public abstract class IState
{
protected FSM manager;// 当前状态机
protected Parameter parameter;// 参数
public abstract void OnEnter();// 进入状态时的方法
public abstract void OnUpdate();// 更新方法
public abstract void OnFixedUpdate();// 固定更新方法
public abstract void OnExit();// 退出状态时的方法
}
定义敌人不同状态切换
待机状态
using UnityEngine;
public class IdleState : IState
{
private float timer; // 计时器
public IdleState(FSM manager)
{
this.manager = manager;
this.parameter = manager.parameter;
}
public override void OnEnter()
{
parameter.animator.Play("Idle");
}
public override void OnUpdate()
{
timer += Time.deltaTime; // 计时器累加
// 如果被击中了,转换到受击状态
if (parameter.getHit)
{
manager.TransitionState(StateType.Hit);
}
// 如果有目标且目标在追逐范围内,则转换到反应状态
if (parameter.target != null &&
parameter.target.position.x >= parameter.chasePoints[0].position.x &&
parameter.target.position.x <= parameter.chasePoints[1].position.x)
{
manager.TransitionState(StateType.React);
}
// 如果达到空闲时间上限,则转换到巡逻状态
if (timer >= parameter.idleTime)
{
manager.TransitionState(StateType.Patrol);
}
}
public override void OnFixedUpdate()
{
}
public override void OnExit()
{
timer = 0; // 重置计时器
}
}
巡逻状态,每次敌人到达巡逻点都会在原地观察一段时间
using UnityEngine;
public class PatrolState : IState
{
private int patrolPosition; // 当前巡逻点索引
public PatrolState(FSM manager)
{
this.manager = manager;
this.parameter = manager.parameter;
}
public override void OnEnter()
{
parameter.animator.Play("Walk");
}
public override void OnUpdate()
{
// 如果被击中了,转换到受击状态
if (parameter.getHit)
{
manager.TransitionState(StateType.Hit);
}
// 如果有目标且目标在追逐范围内,则转换到反应状态
if (parameter.target != null &&
parameter.target.position.x >= parameter.chasePoints[0].position.x &&
parameter.target.position.x <= parameter.chasePoints[1].position.x)
{
manager.TransitionState(StateType.React);
}
// 如果已经接近当前巡逻点,则转换到空闲状态
if (Vector2.Distance(manager.transform.position, parameter.patrolPoints[patrolPosition].position) < .1f)
{
manager.TransitionState(StateType.Idle);
}
}
public override void OnFixedUpdate()
{
// 朝向当前巡逻点
manager.FlipTo(parameter.patrolPoints[patrolPosition]);
// 移动到当前巡逻点
manager.transform.position = Vector2.MoveTowards(manager.transform.position,
parameter.patrolPoints[patrolPosition].position, parameter.moveSpeed * Time.deltaTime);
}
public override void OnExit()
{
patrolPosition++; // 切换到下一个巡逻点
// 如果超过巡逻点数组长度,循环回到第一个巡逻点
if (patrolPosition >= parameter.patrolPoints.Length)
{
patrolPosition = 0;
}
}
}
反应状态
using UnityEngine;
public class ReactState : IState
{
public ReactState(FSM manager)
{
this.manager = manager;
this.parameter = manager.parameter;
}
public override void OnEnter()
{
parameter.animator.Play("React");
}
public override void OnUpdate()
{
// 如果被击中标志为true,转换到受击状态
if (parameter.getHit)
{
manager.TransitionState(StateType.Hit);
}
// 如果动画播放进度超过95%,转换到追逐状态
if (parameter.animatorStateInfo.normalizedTime >= 0.95f)
{
manager.TransitionState(StateType.Chase);
}
}
public override void OnFixedUpdate()
{
}
public override void OnExit()
{
}
}
追击状态
using UnityEngine;
public class ChaseState : IState
{
// 构造函数
public ChaseState(FSM manager)
{
this.manager = manager;
this.parameter = manager.parameter;
}
public override void OnEnter()
{
//TODO:如果有奔跑动画,当然切换为奔跑动画最好
parameter.animator.Play("Walk");
}
public override void OnUpdate()
{
// 如果被击中了,转换到受击状态
if (parameter.getHit)
{
manager.TransitionState(StateType.Hit);
}
// 如果目标不存在或者超出追逐范围,则转换到空闲状态
if (parameter.target == null ||
manager.transform.position.x < parameter.chasePoints[0].position.x ||
manager.transform.position.x > parameter.chasePoints[1].position.x)
{
manager.TransitionState(StateType.Idle);
}
// 如果检测到攻击范围内有目标,则转换到攻击状态
if (Physics2D.OverlapCircle(parameter.attackPoint.position, parameter.attackArea, parameter.targetLayer))
{
manager.TransitionState(StateType.Attack);
}
}
public override void OnFixedUpdate()
{
manager.FlipTo(parameter.target); // 面向目标
// 向目标位置移动
if (parameter.target != null)
{
manager.transform.position = Vector2.MoveTowards(manager.transform.position,
parameter.target.position, parameter.chaseSpeed * Time.deltaTime);
}
}
public override void OnExit()
{
}
}
攻击状态
public class AttackState : IState
{
public AttackState(FSM manager)
{
this.manager = manager;
this.parameter = manager.parameter;
}
public override void OnEnter()
{
parameter.animator.Play("Attack");
}
public override void OnUpdate()
{
if (parameter.getHit)
{
manager.TransitionState(StateType.Hit);
}
if (parameter.animatorStateInfo.normalizedTime >= .95f)
{
manager.TransitionState(StateType.Chase);
}
}
public override void OnFixedUpdate()
{
}
public override void OnExit()
{
}
}
受击状态
using UnityEngine;
public class HitState : IState
{
public HitState(FSM manager)
{
this.manager = manager;
this.parameter = manager.parameter;
}
public override void OnEnter()
{
parameter.animator.Play("Hit");
parameter.health--; // 减少角色生命值
}
public override void OnUpdate()
{
// 如果角色生命值小于等于0,转换到死亡状态
if (parameter.health <= 0)
{
manager.TransitionState(StateType.Death);
}
// 如果动画播放进度超过95%,重新寻找玩家目标并转换到追逐状态
if (parameter.animatorStateInfo.normalizedTime >= 0.95f)
{
parameter.target = GameObject.FindWithTag("Player").transform; // 寻找标签为Player的目标
manager.TransitionState(StateType.Chase); // 转换到追逐状态
}
}
public override void OnFixedUpdate()
{
}
public override void OnExit()
{
parameter.getHit = false; // 离开状态时重置受击标志
}
}
死亡状态
using UnityEngine;
public class DeathState : IState
{
public DeathState(FSM manager)
{
this.manager = manager;
this.parameter = manager.parameter;
}
public override void OnEnter()
{
parameter.animator.Play("Dead");
}
public override void OnUpdate()
{
// 如果动画播放进度超过95%,销毁敌人
if (parameter.animatorStateInfo.normalizedTime >= 0.95f)
{
manager.Destroy();
}
}
public override void OnFixedUpdate()
{
}
public override void OnExit()
{
}
}
创建敌人
比如新增Skull 骷髅怪,继承FSM
public class Skull : FSM {
protected override void Awake() {
// 初始化各个状态,并添加到状态字典中
states.Add(StateType.Idle, new IdleState(this));
states.Add(StateType.Patrol, new PatrolState(this));
states.Add(StateType.Chase, new ChaseState(this));
states.Add(StateType.React, new ReactState(this));
states.Add(StateType.Attack, new AttackState(this));
states.Add(StateType.Hit, new HitState(this));
states.Add(StateType.Death, new DeathState(this));
}
}
配置
效果
现在运行游戏,可以看到敌人现在可以发现玩家并进行追击,在进入攻击范围后攻击玩家,玩家从视野中消失或者超出追击范围后恢复到巡逻状态
我们很快的就搭建好了一个简单的敌人逻辑,而且代码也不显得杂乱,这就是使用有限状态机编写代码的好处,而且添加新的状态也很方便,只要在有关的状态中设置好切换条件,注册好新建的状态,然后编写自身的状态代码即可。
更多的敌人
其他敌人只要都继承这个FSM状态机即可实现代码的复用,比如我再创建不同类型的敌人哥布林
public class Goblin : FSM {
protected override void Awake() {
// 初始化各个状态,并添加到状态字典中
states.Add(StateType.Idle, new GoblinIdleState(this));
states.Add(StateType.Patrol, new GoblinPatrolState(this));
states.Add(StateType.Chase, new GoblinChaseState(this));
states.Add(StateType.React, new GoblinReactState(this));
states.Add(StateType.Attack, new GoblinAttackState(this));
states.Add(StateType.Hit, new GoblinHitState(this));
states.Add(StateType.Death, new GoblinDeathState(this));
}
}
参考
https://www.bilibili.com/video/BV1k1421Z7g8
https://www.bilibili.com/video/BV1zf4y1r7FJ
https://www.bilibili.com/video/BV1xp4y137Xr
源码
https://gitcode.net/unity1/fsm
完结
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,以便我第一时间收到反馈,你的每一次支持
都是我不断创作的最大动力。当然如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!
好了,我是向宇
,https://xiangyu.blog.csdn.net
一位在小公司默默奋斗的开发者,出于兴趣爱好,最近开始自学unity,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!php是工作,unity是生活!如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~