【unity实战】在Unity中使用有限状态机制作一个敌人AI

最终效果

在这里插入图片描述

文章目录

  • 最终效果
  • 前言
  • 有限状态机的主要作用和意义
  • 素材下载
  • 逻辑图
  • 敌人动画配置
  • 优雅的代码文件目录
  • 状态机代码
  • 定义敌人不同状态切换
  • 创建敌人
  • 效果
  • 更多的敌人
  • 参考
  • 源码
  • 完结

前言

有限状态机以前的我嗤之以鼻,现在的我逐帧分析。其实之前我就了解过有限状态机,但是奈何那时能力不够,并不能理解其中的奥秘,只觉得麻烦。直到我项目需要越来越多的去编写敌人的AI,大量的if else让我头晕目眩,各种状态的切换和调试耗费我大量的时间。于是我又重新查找一些状态机的教程进行深入学习。以下我我的学习记录,希望对你有帮助。

如果后续项目使用时存在任何问题我还会回来补充和调整,文章的代码我也会尽量保持完整分享,以便大家可以复制粘贴盗自己的项目中即可使用。

有限状态机的主要作用和意义

有限状态机(Finite State Machine,FSM)是一种在计算机科学和工程中常用的模型,用于描述对象或系统在有限状态集合中的行为和状态转换。它的主要作用和意义包括:

  1. 行为管理与控制: FSM通过定义有限数量的状态和状态之间的转换规则,可以有效管理和控制对象或系统的行为。每个状态代表对象可能处于的一种特定状态,例如待机、行走、攻击、受伤等,而状态之间的转换则定义了这些行为如何响应外部事件或条件变化。

  2. 简化复杂性: 将复杂的行为分解为简单的状态和状态转换,使得程序员可以更容易地理解和管理系统的行为逻辑。这种分解也有助于减少错误和提高代码的可维护性。

  3. 灵活性和扩展性: FSM可以根据具体需求进行灵活的定制和扩展。通过修改状态和状态转换规则,可以快速调整和扩展系统的行为,而无需大规模重构代码。

  4. 行为预测和调试: FSM的结构使得系统的行为预测变得相对容易,因为每个状态和转换的行为是明确定义的。这种结构也有助于调试和排查问题,因为可以更容易地追踪和理解系统在特定状态下的行为。

  5. 应用领域广泛: 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);    // 初始状态为IdlecurrentState.OnEnter();}void Update(){parameter.animatorStateInfo = parameter.animator.GetCurrentAnimatorStateInfo(0);// 获取当前动画状态信息currentState.OnUpdate();    // 每帧更新当前状态//TODO:用于测试 如果按下回车键,设置被击中状态为trueif (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是生活!如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/39769.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2.(vue3.x+vite)调用iframe的方法(vue编码)

1、效果预览 2.编写代码 (1)主页面 <template><div><button @click="sendMessage">调用iframe,并发送信息

【udp报文】udp报文未自动分片,报文过长被拦截问题定位

问题现象 某局点出现一个奇怪的现象&#xff0c;客户端给服务端发送消息&#xff0c;服务端仅能收到小部分消息&#xff0c;大部分消息从客户端发出后&#xff0c;服务端都未收到。 问题定位 初步分析 根据现象初步分析&#xff0c;有可能是网络原因导致消息到服务端不可达&a…

【C语言】文件的顺序读写

©作者:末央&#xff06; ©系列:C语言初阶(适合小白入门) ©说明:以凡人之笔墨&#xff0c;书写未来之大梦 目录 前言字符输入输出函数 - fgetc和fputc文本行输入输出函数 - fgets和fputs格式化输入输出函数 - fscanf和fprintf 前言 对文件数据的读写可以分为顺序…

Seal^_^【送书活动第8期】——《ChatGLM3大模型本地化部署、应用开发与微调》

Seal^_^【送书活动第8期】——《ChatGLM3大模型本地化部署、应用开发与微调》 一、参与方式二、本期推荐图书2.1 作者建语2.2 编辑推建2.3 图书简介2.4 前 言2.5 目 录 三、正版购买 大模型领域 既是繁星点点的未知宇宙&#xff0c;也是蕴含无数可能的广阔天地&#xff0c; 正…

idea创建自定义的maven spark scala archetype脚手架

一&#xff1a;先创建一个Maven项目net.alchim31.maven&#xff08;选该模板&#xff0c;得要等一会儿才能加载出来&#xff09; 之后将自己的目录结构建立好&#xff0c;最好不要有空目录&#xff0c;可能会因为没有文件在install的时候编译不进去 pom中内容也按照自己的需要改…

Stable Diffusion web UI 插件

2024.7.3更新&#xff0c;持续更新中 如果需要在linux上自己安装sd&#xff0c;参考&#xff1a;stable diffusion linux安装 插件复制到 /stable-diffusion-webui/extensions 目录下&#xff0c;然后重新启动sd即可 一、插件安装方法 每种插件的安装方法可能略有不同&#xf…

苹果p12证书最简单最新申请流程

使用uniapp打包&#xff0c;在ios上打正式包需要苹果的p12证书和证书profile文件&#xff0c;点进去uniapp的ios证书申请教程&#xff0c;通篇就是使用mac电脑申请的教程&#xff0c;假如没有mac电脑就无法继续了。 因此&#xff0c;假如没有mac电脑的同志们&#xff0c;可以参…

Pytest+Allure+Yaml+PyMsql+Jenkins+Gitlab接口自动化(五)Jenkins配置

一、背景 Jenkins&#xff08;本地宿主机搭建&#xff09; 拉取GitLab(服务器)代码到在Jenkins工作空间本地运行并生成Allure测试报告 二、框架改动点 框架主运行程序需要先注释掉运行代码&#xff08;可不改&#xff0c;如果运行报allure找不到就直接注释掉&#xff09; …

鸿蒙应用开发-时间屏幕

点击下载源码&#xff1a; https://download.csdn.net/download/liuhaikang/89509449 做一个时间屏幕&#xff0c;可以点击切换白色和黑色&#xff0c;有渐变效果&#xff0c;使用到了鸿蒙的动画效果。 在这个设计中&#xff0c;我们首先引入了通用能力包&#xff0c;以实现功…

Kubernetes 离线安装的坑我采了

Kubernetes 离线安装的坑我采了 一、Error from server: Get "https://xx.xx.xx.xx:10250/containerLogs/kube-system/calico-node-8dnvs/calico-node": tls: failed to verify certificate: x509: certificate signed by unknown authority二、calico 或 pod 启动正…

cesium公交车轨迹漫游

个人博客&#xff1a;CSDN 博客-满分观察网友 z 演示地址&#xff1a;哔哩哔哩-满分观察网友 z 这是一个用 Cesium.js 做的公交车轨迹漫游&#xff0c;实现的功能有加载站点和道路轨迹点数据、监听车辆的实时位置、车辆控制器。滚动屏等等。 文章目录 1. 地图初始化2. 数据渲…

【高中数学/基本不等式】已知:x,y均为正实数,且xy+2x+y=4 求:x+y的最小值?

【问题】 已知&#xff1a;x,y均为正实数&#xff0c;且xy2xy4 求&#xff1a;xy的最小值&#xff1f; 【来源】 https://www.ixigua.com/7147585275823292942?logTagf25494de7fce23a3a3d0 【解答】 解&#xff1a; 由xy2xy4 两边加二得 xy2xy24 2 分解因式得 (x1)(…

【WEB前端2024】3D智体编程:乔布斯3D纪念馆-第53课-语音指令跳舞

【WEB前端2024】3D智体编程&#xff1a;乔布斯3D纪念馆-第53课-语音指令跳舞 使用dtns.network德塔世界&#xff08;开源的智体世界引擎&#xff09;&#xff0c;策划和设计《乔布斯超大型的开源3D纪念馆》的系列教程。dtns.network是一款主要由JavaScript编写的智体世界引擎&…

面试题springboot面试

文章目录 Spring的依赖注入构造器注入stetter注入属性注入 springboot的优势第一开箱即用约定大于配置内嵌tomcat服务器 javaweb的三大组件springboot的自动配置原理SpringIoc的实现机制springmvcspring如何简化开发 Spring的依赖注入 构造器注入 stetter注入 属性注入 使用…

前端进阶:Vue.js

目录 框架&#xff1a; 助解&#xff1a; 框架&#xff1a; VUE 什么是Vue.js? Vue.js优点 Vue安装 方式一&#xff1a;直接用<script>引入 方式二&#xff1a;命令行工具 第一个Vue程序 代码 代码解释&#xff1a; 运行 Vue指令 v-text v-html v-tex…

Mysql和ES使用汇总

一、mysql和ES在业务上的配合使用 一般使用时使用ES 中存储全文检索的关键字与获取的商品详情的id&#xff0c;通过ES查询获取查询商品的列表中展示的数据&#xff0c;通过展示id 操作去获取展示商品的所有信息。mysql根据id去查询数据库数据是很快的&#xff1b; 为什么ES一般…

10 - Python文件编程和异常

文件和异常 在实际开发中&#xff0c;常常需要对程序中的数据进行持久化操作&#xff0c;而实现数据持久化最直接简单的方式就是将数据保存到文件中。说到“文件”这个词&#xff0c;可能需要先科普一下关于文件系统的知识&#xff0c;对于这个概念&#xff0c;维基百科上给出…

亚马逊云科技AWS免费大热AI应用开发证书(含题库、开卷)

亚马逊云科技AWS官方生成式AI免费证书来了&#xff01;内含免费AI基础课程&#xff01;快速掌握AWS的前沿AI技术&#xff0c;后端开发程序员也可以速成AI专家&#xff0c;了解当下最&#x1f525;的AWS AI架构解决方案&#xff01; 本证书内容包括AWS上的AI基础知识&#xff0c…

剖析DeFi交易产品之UniswapV4:Swap

文章首发于公众号&#xff1a;Keegan小钢 Swap 可分为两种场景&#xff1a;单池交易和跨池交易。在 PoolManager 合约里&#xff0c;要完成交易流程&#xff0c;会涉及到 lock()、swap()、settle()、take() 四个函数。单池交易时只需要调一次 swap() 函数&#xff0c;而跨池交易…

【面向就业的Linux基础】从入门到熟练,探索Linux的秘密(七)-shell语法(5)

shell语法的一些知识和练习&#xff0c;可以当作笔记收藏一下&#xff01;&#xff01; 文章目录 前言 一、shell 二、shell语法 1.文件重定向 2.引入外部脚本 3.作业 总结 前言 shell语法的一些知识和练习&#xff0c;可以当作笔记收藏一下&#xff01;&#xff01; 提示&…