【《游戏编程模式》实战04】状态模式实现敌人AI
目录
1、状态模式
2、使用工具
3、状态模式适用范围
4、实现内容
5、代码及思路
Enemy.cs
EnemyState.cs
6、unity里的设置
7、运行效果展示
1、状态模式
“允许一个对象在其内部状态改变时改变自身的行为。对象看起来好像是在修改自身类。”
就是一个对象能随着自己的状态改变执行不同的方法吧,而这个方法和状态并不是写在这个对象类里的,却能达到像是对象修改自身类的效果。
2、使用工具
Unity2022.3.51f1c1、visual studio code
3、状态模式适用范围
1)你有一个游戏实体,它的行为基于它的内部状态而改变
2)这些状态被严格分为相对数目较少的小集合
3)游戏实体随着时间的变化会响应用户输入或一些游戏事件
在游戏里广泛使用在ai里,也经常被应用于用户输入处理、浏览菜单屏幕、解析文件、网络协议和其他异步的行为
4、实现内容
实现敌人能在这几个状态之间根据距离远近自动切换,hp为0时死亡
三段距离:一段距离内攻击,一段距离内追逐,更远的距离会暂停
但是暂停、防御的触发、检验技能是否释放完毕等功能并没有实现,这里只是搭框架,原理都差不多,只要学会一个条件的状态转移其他的都很好做,就不赘述了。

5、代码及思路
书上的代码看着太复杂了,写的时候没有完全参考,我更倾向于围绕状态模式的目标——将每个状态相关的所有数据和行为封装到对应状态类里面 来写。
状态模式的基本模式:参考状态模式 | 菜鸟教程
- 上下文类(Enemy):它持有一个状态的引用,并在状态改变时更新其行为
- 状态类(EnemyState):定义所有可能的状态并为它们提供行为接口
- 具体状态类(IdleState\StandState...):实现状态接口的具体类,表示对象的某一具体状态。
具体来说在我的代码里结构是这样的

Enemy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
// 每个敌人身上都该有
public class Enemy : MonoBehaviour
{[HideInInspector]public EnemyState state;public int distance;// 距离public int thresholdA=10;// 阈值Apublic int thresholdB=20;// 阈值Bpublic int hp=10;// 血量UnityEvent<Enemy> stateChangeEvent = new UnityEvent<Enemy>();//状态改变事件void Start(){ChangeState(EnemyState.IdleState);//设置为初始状态}public void ChangeState(EnemyState newState){if (state != null){stateChangeEvent.RemoveListener(state.handleData);//移除原本的方法引用}state = newState;//切换至新状态stateChangeEvent.AddListener(state.handleData);//添加新状态的方法引用stateChangeEvent.Invoke(this);//调用下一个状态的handleData}}
- 在Start方法中初始化了敌人的状态并调用初始状态的handleData方法,这样在开始时,敌人就会执行与IdleState状态相关的行为。
- 当敌人的状态改变时,ChangeState方法通过 stateChangeEvent.Invoke(this)触发事件,从而调用切换后状态的 handleData方法。(有一个坑是在使用AddListener的时候里面的参数传的其实是一个方法引用(地址值),因此在改变state的值以前,一定要把原本注册的方法移除,再添加新的,不然调用的还会是之前状态的方法/空引用异常。)
这本书这部分写得真不咋滴啊,对象类的函数不清不楚,没写如果把条件判断也加到状态类里去了,对象要怎么切换状态,这部分是我自己想的。
EnemyState.cs
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using Unity.VisualScripting.Antlr3.Runtime;
using UnityEditor;
using UnityEditor.EditorTools;
using UnityEngine;public class EnemyState :MonoBehaviour
{// 定义所有可能的状态public static IdleState IdleState; // 空闲状态public static StandState StandState; // 站立状态public static MoveState MoveState; // 移动状态public static AttackState AttackState; // 攻击状态public static DieState DieState; // 死亡状态public static DefenseState DefenseState;// 防御状态void Awake(){// Awake方法在对象被激活时调用且会被派生类继承// 派生类生成时也会被调用,所以一定要加条件防止重复添加if (gameObject.GetComponent<IdleState>() == null){// 添加各个状态的组件实例// 用add的原因是继承自MonoBehaviour的类不能直接newIdleState = gameObject.AddComponent<IdleState>();StandState = gameObject.AddComponent<StandState>();MoveState = gameObject.AddComponent<MoveState>();AttackState = gameObject.AddComponent<AttackState>();DieState = gameObject.AddComponent<DieState>();DefenseState = gameObject.AddComponent<DefenseState>();}}// 虚拟的handleData方法,用于每个状态具体的处理逻辑public virtual void HandleData(Enemy enemy){// 检查敌人的血量StartCoroutine(UpdateDieState(enemy));}// 虚拟的状态更新方法,每个状态可以自定义自己的更新逻辑public virtual IEnumerator UpdateState(Enemy enemy){yield return null;}// 死亡状态的更新方法,持续检查敌人的血量是否小于等于零public IEnumerator UpdateDieState(Enemy enemy)//这个只需要在初始状态调一次{while (true){if (enemy.hp <= 0){enemy.ChangeState(EnemyState.DieState);break;}yield return null;}}}
public class IdleState :EnemyState
{public override void HandleData(Enemy enemy){base.HandleData(enemy);// 调用基类的处理方法,检查死亡状态Debug.Log("初始状态");enemy.ChangeState(EnemyState.StandState);}
}
public class StandState : EnemyState
{public override void HandleData(Enemy enemy){Debug.Log("站立状态");StartCoroutine(UpdateState(enemy));// 开始更新状态,判断是否切换到其他状态}public override IEnumerator UpdateState(Enemy enemy){while (true){if (enemy.distance < enemy.thresholdA) // 距离小于阈值A,进入攻击状态{enemy.ChangeState(EnemyState.AttackState);yield break; // 结束当前协程}else if (enemy.distance > enemy.thresholdA) // 距离大于阈值A,进入移动状态{enemy.ChangeState(EnemyState.MoveState);yield break; // 结束当前协程}yield return null; // 每帧检查一次}}
}
public class MoveState : EnemyState
{public override void HandleData(Enemy enemy){Debug.Log("追逐状态");StartCoroutine(UpdateState(enemy)); // 开始更新状态,判断是否切换到其他状态}public override IEnumerator UpdateState(Enemy enemy){while (true){if (enemy.distance < enemy.thresholdA) // 距离小于阈值A,进入站立状态{enemy.ChangeState(EnemyState.StandState);break; // 结束当前协程}yield return null; // 每帧检查一次}}}
public class AttackState : EnemyState
{public override void HandleData(Enemy enemy){Debug.Log("攻击状态");StartCoroutine(UpdateState(enemy));// 开始更新状态,判断是否切换到其他状态}public override IEnumerator UpdateState(Enemy enemy){while (true){if (enemy.distance > enemy.thresholdA) // 距离大于阈值A,进入移动状态{enemy.ChangeState(EnemyState.MoveState);break; // 结束当前协程}yield return null; // 每帧检查一次}}
}
public class DieState : EnemyState
{public override void HandleData(Enemy enemy){GameObject.Destroy(enemy.gameObject);// 销毁敌人对象,表示死亡Debug.Log("死亡状态");}
}
public class DefenseState : EnemyState
{public override void HandleData(Enemy enemy){Debug.Log("防御状态");StartCoroutine(UpdateState(enemy));// 开始更新状态,判断是否切换到其他状态}public override IEnumerator UpdateState(Enemy enemy){while (true){//如果是释放过程中注意都要等防御技能结束才行//防御状态要切换到其他状态的条件应是被打断/成功防住if (enemy.distance > enemy.thresholdA)// 距离大于阈值A,进入移动状态{enemy.ChangeState(EnemyState.MoveState);break;// 结束当前协程}else if (enemy.distance < enemy.thresholdA)// 距离小于阈值A,进入攻击状态{enemy.ChangeState(EnemyState.AttackState);break;// 结束当前协程}yield return null;// 每帧检查一次}}
}
- 通过EnemyState 作为基类来定义所有可能的敌人状态(如 IdleState、MoveState、AttackState 等)。每个状态类都会实现 handleData方法和 UpdateState 协程方法,这样每个状态都可以自定义自己的行为和状态转移条件。
- 状态切换:通过 enemy.ChangeState(EnemyState.XXX) 来切换敌人的状态,每个状态都有自己的逻辑来判断何时切换到下一个状态。
- 血量和死亡判断:EnemyState类中的 UpdateDieState 协程用于实时检查敌人的血量,一旦血量降至 0 或以下,就会触发死亡状态并销毁敌人对象。
- 状态更新:handleData 方法负责初始化和启动状态检查,UpdateState 协程负责检查条件并更新状态。
6、unity里的设置
建一个空物体挂上EnemyState类(继承自monobehavior类的都需要挂到某物体上才能用)

建两个胶囊体挂上Enemy类,阈值和血量可以根据敌人的不同自定义,这里我随便设的值。

7、运行效果展示
由于这只是框架,所以我检查效果的方式就是直接在Inspector里调Distance来观察状态转换是否成功。对照着这个状态转移图来看:

运行后,两个胶囊体分别从初始进入站立状态,又由于d<a而进入攻击状态

将其中一个胶囊体的Distance调为11>a,攻击->追逐。另外可观察到另一个胶囊体状态不受影响

将胶囊体Distance调回0,按照状态转移图从追逐->站立->攻击

最后,将胶囊体的Hp调为0 ,该胶囊体切换至死亡状态后执行销毁行为,另一胶囊体不受影响。


相关文章:
【《游戏编程模式》实战04】状态模式实现敌人AI
目录 1、状态模式 2、使用工具 3、状态模式适用范围 4、实现内容 5、代码及思路 Enemy.cs EnemyState.cs 6、unity里的设置 7、运行效果展示 1、状态模式 “允许一个对象在其内部状态改变时改变自身的行为。对象看起来好像是在修改自身类。” 就是一个对象能随着自己…...
借助免费GIS工具箱轻松实现las点云格式到3dtiles格式的转换
在当今数字化浪潮下,地理信息系统(GIS)技术日新月异,广泛渗透到城市规划、地质勘探、文化遗产保护等诸多领域。而 GISBox 作为一款功能强大且易用的 GIS 工具箱,以轻量级、免费使用、操作便捷等诸多优势,为…...
科研绘图系列:R语言科研绘图之标记热图(heatmap)
禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍加载R包数据下载导入数据数据预处理画图系统信息参考介绍 科研绘图系列:R语言科研绘图之标记热图(heatmap) 加载R包 library(tidyverse) library(ggplot2) library(reshape)…...
【轻松学C:编程小白的大冒险】--- C语言简介 02
在编程的艺术世界里,代码和灵感需要寻找到最佳的交融点,才能打造出令人为之惊叹的作品。而在这座秋知叶i博客的殿堂里,我们将共同追寻这种完美结合,为未来的世界留下属于我们的独特印记。 【轻松学C:编程小白的大冒险】…...
《HeadFirst设计模式》笔记(上)
设计模式的目录: 1 设计模式介绍 要不断去学习如何利用其它开发人员的智慧与经验。学习前人的正统思想。 我们认为《Head First》的读者是一位学习者。 一些Head First的学习原则: 使其可视化将文字放在相关图形内部或附近,而不是放在底部…...
数据结构:ArrayList与顺序表
目录 📖一、什么是List 📖二、线性表 📖三、顺序表 🐬1、display()方法 🐬2、add(int data)方法 🐬3、add(int pos, int data)方法 🐬4、contains(int toFind)方法 🐬5、inde…...
SpringBoot之核心配置
学习目标: 1.熟悉Spring Boot全局配置文件的使用 2.掌握Spring Boot配置文件属性值注入 3.熟悉Spring Boot自定义配置 4.掌握Profile多环境配置 5.了解随机值设置以及参数间引用 1.全局配置文件 Spring Boot使用 application.properties 或者application.yaml 的文…...
EasyExcel上传校验文件错误信息放到文件里以Base64 返回给前端
产品需求: 前端上传个csv 或 excel 文件,文件共4列,验证文件大小,类型,文件名长度,文件内容,如果某行某个单元格数据验证不通过,就把错误信息放到这行第五列,然后把带有…...
单片机软件定时器V4.0
单片机软件定时器V4.0 用于单片机定时执行任务等,比如LED GPIO等定时控制,内置前后台工作模式 头文件有使用例子 #ifndef __SORFTIME_APP_H #define __SORFTIME_APP_H#ifdef __cplusplus extern "C" { #endif#include <stdint.h>// #…...
超完整Docker学习记录,Docker常用命令详解
前言 关于国内拉取不到docker镜像的问题,可以利用Github Action将需要的镜像转存到阿里云私有仓库,然后再通过阿里云私有仓库去拉取就可以了。 参考项目地址:使用Github Action将国外的Docker镜像转存到阿里云私有仓库 一、Docker简介 Do…...
C++ 入门第26天:文件与流操作基础
往期回顾: C 入门第23天:Lambda 表达式与标准库算法入门-CSDN博客 C 入门第24天:C11 多线程基础-CSDN博客 C 入门第25天:线程池(Thread Pool)基础-CSDN博客 C 入门第26天:文件与流操作基础 前言…...
使用python将多个Excel表合并成一个表
import pandas as pd# 定义要合并的Excel文件路径和名称 file_paths [file1.xlsx, file2.xlsx, file3.xlsx, file4.xlsx, file5.xlsx]# 创建一个空的DataFrame来存储合并后的数据 merged_data pd.DataFrame()# 循环遍历每个Excel文件,并读取其中的数据 for file_p…...
halcon三维点云数据处理(七)find_shape_model_3d_recompute_score
目录 一、find_shape_model_3d_recompute_score例程代码二、set_object_model_3d_attrib_mod函数三、prepare_object_model_3d 函数四、create_cube_shape_model_3d函数五、获得CamPose六、project_cube_image函数七、find_shape_model_3d函数八、project_shape_model_3d函数 一…...
vue js实现时钟以及刻度效果
2025.01.08今天我学习如何用js实现时钟样式,效果如下: 一、html代码如下: <template><!--圆圈--><div class"notice_border"><div class"notice_position notice_name_class" v-for"item in …...
unity学习15:预制体prefab
目录 1 创建多个gameobject 2 创建prefab 2.1 创建prefab (类) 2.2 prefab 是一个文件 2.3 prefab可以导出 3 创建prefab variant (子类) 3.1 除了创建多个独立的prefab, 还可以创建 prefab variant 3.2 他…...
基于Thinkphp6+uniapp的陪玩陪聊软件开发方案分析
使用uni-app框架进行前端开发。uni-app是一个使用Vue.js开发所有前端应用的框架,支持一次编写,多端发布,包括APP、小程序、H5等。 使用Thinkphp6框架进行后端开发。Thinkphp6是一个轻量级、高性能、面向对象的PHP开发框架,具有易…...
MySQL - 子查询和相关子查询详解
在SQL中,子查询(Subquery)和相关子查询(Correlated Subquery)是非常强大且灵活的工具,可以用于执行复杂的数据检索和操作。它们允许我们在一个查询中嵌套另一个查询,从而实现更复杂的逻辑和条件…...
Android 系统签名 keytool-importkeypair
要在 Android 项目中使用系统签名并将 APK 打包时与项目一起打包,可以按照以下步骤操作: 步骤 1:准备系统签名文件 从 Android 系统源码中获取系统签名文件,通常位于 build/target/product/security 目录下,包括 pla…...
安卓漏洞学习(十八):Android加固基本原理
APP加固技术发展历程 APK加固整体思路 加固整体思路:先解压apk文件,取出dex文件,对dex文件进行加密,然后组合壳中的dex文件(Android类加载机制),结合之前的apk资源(解压apk除dex以外…...
Docker 使用Dockerfile创建镜像
创建并且生成镜像 在当前目录下创建一个名为Dockerfile文件 vi Dockerfile填入下面配置 # 使用 CentOS 作为基础镜像 FROM centos:7# 设置工作目录 WORKDIR /app# 复制项目文件到容器中 COPY bin/ /app/bin/ COPY config/ /app/config/ COPY lib/ /app/lib/ COPY plugin/ /a…...
StructBERT情感分类镜像保姆级教程:GPU加速中文情感分析快速上手
StructBERT情感分类镜像保姆级教程:GPU加速中文情感分析快速上手 10分钟学会部署和使用专业级中文情感分析模型,让AI帮你读懂用户情绪 1. 前言:为什么要用StructBERT做情感分析? 你有没有遇到过这些情况? 电商平台上…...
SerialComProtocol:嵌入式双MCU轻量级串口事件驱动协议
1. SerialComProtocol:面向嵌入式双MCU串口通信的轻量级事件驱动协议栈SerialComProtocol 是一个专为资源受限嵌入式系统设计的零依赖、纯C实现的串口通信协议库。它不依赖任何RTOS、HAL抽象层或标准C库(如<string.h>或<stdlib.h>࿰…...
OpenClaw知识管理:千问3.5-9B构建个人知识图谱
OpenClaw知识管理:千问3.5-9B构建个人知识图谱 1. 为什么需要AI驱动的知识管理 作为一个长期与信息过载搏斗的技术从业者,我书架上有37本未拆封的技术书籍,浏览器收藏夹里堆积着600个"稍后阅读"的网页,笔记软件中散落…...
从H.264到AV1:主流视频编码标准的演进、选型与实战场景剖析
1. 视频编码标准的发展脉络 我第一次接触视频编码是在2005年,当时用手机拍摄的3GP视频文件总是模糊不清。后来才知道,这背后是MPEG-4 Part 2编码在起作用。如今视频编码技术已经迭代了好几代,从最早的MPEG-2到现在的AV1,每一次升级…...
ECharts地图可视化进阶:如何优雅处理GeoJSON中的飞地与特殊区域(以海南为例)
ECharts地图可视化进阶:GeoJSON飞地与特殊区域处理实战 当我们在使用ECharts进行地理数据可视化时,经常会遇到一些特殊的地理区域处理难题。比如海南省地图中的南海诸岛,由于与主岛距离较远且面积比例悬殊,直接展示会导致可视化效…...
Telemetrix4UnoR4:Arduino Uno R4的轻量级双向固件框架
1. 项目概述Telemetrix4UnoR4 是专为 Arduino Uno R4 系列开发板设计的嵌入式固件服务器框架,其核心目标是构建一个轻量、可靠、可扩展的双向通信桥梁,使 Python 主机端(运行telemetrix_uno_r4或telemetrix_uno_r4-aio库)能够以类…...
Navicat Premium 17 创建触发器保姆级教程
前言:触发器是MySQL中极具实用性的数据库对象,核心作用是“当表发生INSERT/UPDATE/DELETE操作时,自动执行预设SQL”,无需手动调用、无需程序介入,常用于自动填充时间、数据同步、日志记录、数据校验等场景。Navicat Pr…...
单片机IO口扩展方案全解析与应用实践
1. 单片机IO口扩展的必要性与挑战作为一名在嵌入式领域摸爬滚打多年的工程师,我经常遇到这样的场景:项目进行到一半,突然发现单片机GPIO口不够用了。这种"资源危机"在中小型项目中尤为常见,特别是当我们使用8位或低引脚…...
0欧姆电阻在电子设计中的关键应用与选型指南
1. 0欧姆电阻的实质与特性在电子工程实践中,0欧姆电阻(Zero-Ohm Resistor)是一种表面贴装或插装形式的特殊电子元件。虽然标称值为零欧姆,但实际测量时会发现其存在微小的阻值——典型值在20-50毫欧之间。这个特性使其既不同于理想…...
OpenClaw新手避坑指南:Qwen3-14b_int4_awq模型对接5大误区
OpenClaw新手避坑指南:Qwen3-14b_int4_awq模型对接5大误区 1. 为什么写这篇文章 上周我在本地部署OpenClaw对接Qwen3-14b_int4_awq模型时,踩了无数坑。从baseUrl格式错误到上下文窗口超限,几乎把所有新手可能犯的错误都犯了一遍。最痛苦的是…...
