C#程式状态机及其Godot实践
前言
今天是周日,马上就要迎来新的一周了,前几周都没干什么事,为了减缓偷懒症状,立个Flag从今往后每周至少更新两次文章。内容虽然无法保证优质,但重在坚持,全当写周记了。希望不要三分钟热度吧。
今天记录的是老生常谈的状态机,灵感来源于我的新项目中为了编写人物逻辑采用的方式。
关于状态机
简述
状态机,StateMachine,是游戏开发中常用的AI之一。其具体又可以分为有限状态机,也就是FSM,还有分层状态机HSM,无限状态机等等。
个人以为状态机的核心就在于其状态的唯一性和隔离性,表示为同一时刻只能有一种状态,和状态之间的不·共享。
什么时候要应该用到状态机?我想大概就是发现控制逻辑中有大量Bool和if语句的时候。
Godot中的状态机
虽然Godot目前没有直接的AI支持。但是还是有多种途径去实现一个状态机功能的。比如:
1.通过节点实现。把每个状态以节点表示,在对应的节点上编写每个状态的具体逻辑,状态之间的切换就对相当于“使用哪个节点的上处理逻辑”,其他节点不用管。可以适当的抽象出状态和状态机脚本。采用普通的Node节点即可,因为我们不需要太多多余信息,只需要让节点能够被处理即可。
这种方式的好处就是开发效率高和比较直观。但是个人认为目前仅仅适用于GDScript开发,因为GDs可以快速获取节点结构,在编写每个状态的逻辑时可以很方便的引用场景中的其他节点,还有GDs支持内嵌脚本,可以直接为对应状态内嵌一个脚本给对应节点,方便文件管理。而C#没有这些优势,希望以后能支持吧。
2.采用AnimationTree实现,就是那个动画树节点。虽然我没有实操过,但是鉴于Unity的使用经验,理论上应该是可行的。因为动画树里确实内嵌了状态机这个东西,以后有时间再研究。
3.Resource实现。这算是我踩过的一个小坑,Godot的Resource很强大,个人认为比Unity的SO好些,因为可以方便的在Inspector中删改。但是如果要用到像状态机这种极大依赖于容器数据的场合,还是需要慎重考虑,什么意思呢?这里所说的“容器”就是指包含状态机的那个对象,比如我们的角色控制器,包含一个状态机。那么这个状态机必然依赖于控制器本身的一些数据,比如移动状态需要移动速度,跳跃状态需要跳跃速度之类的。
而Resource在这方面显得无力,因为目前Godot不支持在Resource中Export出节点,仅作为数据逻辑容器,而不知道什么东西将会用到它,必须依靠依赖注入才能获取上层数据。虽然对于上述情况还有解法,比如每个状态管理各自需要的数据,但又因为通过Resource实现的状态之间不好切换,或者每个状态都要对应一种资源,可能导致难于管理等等原因,所以Pass掉了。
不过也能看出,Resource在很多系统上大有可为,比如什么能力系统,组件系统,可以重点研究一下。
程式状态机
这个其实没有什么特别的说法,单纯就是我为了表示仅有代码实现状态机,没有任何可视化才这样叫的。
有时候状态机需求很低,仅仅两三个状态,用不得大动干戈,仅靠代码实现反而有助于开发。
using System;
using System.Collections.Generic;namespace AdamDontCry;public sealed class StateMachine<T>
{public T Owner { get; private set; }private readonly Dictionary<Type, State<T>> m_states = [];public State<T> CurrentState { get; private set; }public void Initialize<S>(T owner) where S : State<T>{Owner = owner;ChangeState<S>();}public void ChangeState<S>() where S : State<T>{State<T> _ChangeState(State<T> state){CurrentState?.Exit();CurrentState = state;CurrentState.Enter();return state;}if (m_states.TryGetValue(typeof(S), out var state)){_ChangeState(state);}else{var newState = Activator.CreateInstance<S>();newState.Initialize(this);m_states.Add(typeof(S), _ChangeState(newState));}}// Seems no need to add or remove ?// public void AddState<S>() where S : State<T>, new()// {// if (m_states.ContainsKey(typeof(S))) return;// else m_states.Add(typeof(S), new S());// }// public void RemoveState<S>() where S : State<T> => m_states.Remove(typeof(S));public void Process(double delta) => CurrentState.Process(delta);
}public abstract class State<T>()
{public T Owner { get; private set; }public StateMachine<T> StateMachine { get; private set; }// TODO: Find a way to use constructor instead a new method?// public State(T owner, StateMachine<T> stateMachine) : this()// {// Owner = owner;// StateMachine = stateMachine;// }public void Initialize(StateMachine<T> stateMachine){Owner = stateMachine.Owner;StateMachine = stateMachine;}public virtual void Enter() { }public virtual void Process(double delta) { }public virtual void Exit() { }public void ChangeState<S>() where S : State<T> => StateMachine.ChangeState<S>();
}
其实想法非常简单,就是通过泛型实现依赖注入以及约束状态范围。状态之间的切换直接对应于类型切换,需要为每个状态编写一种类型。
值得一提的是这里原本是想通过构造函数建立状态机和状态之间的依赖,结果发现一些关于泛型构造函数的尚不能解决的问题,遂转用一个“初始化”方法代替。
现在看不出什么还是得结合实践。
实践
下面是实际应用的例子。
public partial class Player : Unit
{[ExportGroup("Properties")][Export] public float Speed = 5000.0f;[Export] public float JumpVelocity = 300.0f;[ExportGroup("Animation")][Export] public AnimatedSprite2D Sprite;public override void _Ready(){StateMachine.Initialize<IdleState>(this);}public override void _PhysicsProcess(double delta){StateMachine.Process(delta);ApplyGravity(delta);}
}public partial class Player : Unit
{public StateMachine<Player> StateMachine { get; private set; } = new();public class IdleState : State<Player>{public override void Enter() => Owner.Sprite.Play("idle");public override void Process(double delta){if (Input.IsActionJustPressed("jump") && Owner.IsOnFloor()){ChangeState<JumpState>(); return;}if (Mathf.Abs(Input.GetAxis("move_left", "move_right")) > 0.25){ChangeState<RunState>(); return;}if (Input.IsActionJustPressed("pick")){ChangeState<PickState>(); return;}}}public class RunState : State<Player>{public override void Enter() => Owner.Sprite.Play("run");public override void Process(double delta){if (Input.IsActionJustPressed("jump") && Owner.IsOnFloor()){ChangeState<JumpState>(); return;}Vector2 velocity = Owner.Velocity;var _delta = (float)delta;Vector2 direction = new(Input.GetAxis("move_left", "move_right"), 0);if (direction != Vector2.Zero){velocity.X = direction.Normalized().X * Owner.Speed * _delta;Owner.Sprite.FlipH = velocity.X < 0;}else{ChangeState<IdleState>(); return;}Owner.Velocity = velocity;Owner.MoveAndSlide();}}public class JumpState : State<Player>{public const double max_timer = 1.0;public double timer = 0;public bool isJumped = false;public override void Enter(){Owner.Sprite.Play("jump_prepare");timer = 0;isJumped = false;}public override void Process(double delta){timer += delta;if (isJumped && Owner.IsOnFloor()){ChangeState<IdleState>(); return;}if (!isJumped && Input.IsActionJustReleased("jump")){Owner.Sprite.Play("jump");Owner.Velocity =Input.GetVector("move_left", "move_right", "move_up", "move_down").Normalized() *Owner.JumpVelocity * (float)Mathf.Min(timer / max_timer, 1.0);Owner.MoveAndSlide();isJumped = true;}}}
为了美观采用了部分类的写法。然后为每个需要的状态编写一个类,只需继承容器对应的泛型状态,这里我直接写成内部类。
这里有一些设计上的小缺陷,就是每次调用切换状态方法后,实际还需要执行完当前状态剩下的代码,所以需要每次切换后return一下,避免后面的代码影响到整个逻辑流。
虽然看起来每次切换都要调用很麻烦,但是就算是可视化操作也得一个一个“连连看”呢,所以似乎可以接收。
还有既然来都来了就顺便记录移动处理那方面的内容:比如对于有输入强度相关的情况,就像上面的获取移动输入的向量值,其对应的是一个依赖输入强度的可变向量,即使方向不变,输入时强度改变(比如使用手柄摇杆)也会影响其计算结果,所以这里用了Normalized把向量值变得只与方向有关,从而避免了输入强度带来的影响,或者“强度”这个说法不太准确,实际上只要跟输入量化有关的都需要留意是否有对应需求。
结语
之后的文章可能会比较直接简洁,因为我觉得写文章也好累好麻烦,所以可能效率至上突出重点好点。
相关文章:
C#程式状态机及其Godot实践
前言 今天是周日,马上就要迎来新的一周了,前几周都没干什么事,为了减缓偷懒症状,立个Flag从今往后每周至少更新两次文章。内容虽然无法保证优质,但重在坚持,全当写周记了。希望不要三分钟热度吧。 今天记录…...
Windows 系统下使用 Ollama 离线部署 DeepSeek - R1 模型指南
引言 随着人工智能技术的飞速发展,各类大语言模型层出不穷。DeepSeek - R1 凭借其出色的语言理解和生成能力,受到了广泛关注。而 Ollama 作为一款便捷的模型管理和部署工具,能够帮助我们轻松地在本地环境中部署和使用模型。本文将详细介绍如…...
Docker、Ollama、Dify 及 DeepSeek 安装配置与搭建企业级本地私有化知识库实践
在现代企业中,管理和快速访问知识库是提升工作效率、促进创新的关键。为了满足这些需求,企业越来越倾向于构建本地私有化的知识库系统,这样可以更好地保护企业数据的安全性和隐私性。本文将介绍如何利用 **Docker**、**Ollama**、**Dify** 和…...
【漫话机器学习系列】087.常见的神经网络最优化算法(Common Optimizers Of Neural Nets)
常见的神经网络优化算法 1. 引言 在深度学习中,优化算法(Optimizers)用于更新神经网络的权重,以最小化损失函数(Loss Function)。一个高效的优化算法可以加速训练过程,并提高模型的性能和稳定…...
react-native fetch在具有http远程服务器后端的Android设备上抛出“Network request failed“错误
问题描述: 在具有http远程服务器后端的Android设备上,使用react-native fetch时抛出"Network request failed"错误。 回答: "Network request failed"错误通常表示在进行网络请求时出现了问题。可能的原因包括网络连接…...
【JVM详解四】执行引擎
一、概述 Java程序运行时,JVM会加载.class字节码文件,但是字节码并不能直接运行在操作系统之上,而JVM中的执行引擎就是负责将字节码转化为对应平台的机器码让CPU运行的组件。 执行引擎是JVM核心的组成部分之一。可以把JVM架构分成三部分&am…...
route 与 router 之间的差别
简述: router:主要用于处理一些动作, route:主要获得或处理一些数据,比如地址、参数等 例: videoInfo1.vue: <template><div class"video-info"><h3>二级组件…...
[vue3] Ref Reactive
【b站-【前端面试】Vue3 ref 与 reactive 区别】 Ref:Ref用于创建一个响应式的基本数据类型,比如数字、字符串等。它将普通的数据变成响应式数据,可以监听数据的变化。使用Ref时,我们可以通过.value来访问和修改数据的值。 Reac…...
SamWaf开源轻量级的网站应用防火墙(安装包),私有化部署,加密本地存储的数据,易于启动,并支持 Linux 和 Windows 64 位和 Arm64
一、SamWaf轻量级开源防火墙介绍 (文末提供下载) SamWaf网站防火墙是一款适用于小公司、工作室和个人网站的开源轻量级网站防火墙,完全私有化部署,数据加密且仅保存本地,一键启动,支持Linux,Wi…...
极客说|利用 Azure AI Agent Service 创建自定义 VS Code Chat participant
作者:卢建晖 - 微软高级云技术布道师 「极客说」 是一档专注 AI 时代开发者分享的专栏,我们邀请来自微软以及技术社区专家,带来最前沿的技术干货与实践经验。在这里,您将看到深度教程、最佳实践和创新解决方案。关注「极客说」&a…...
22.2、Apache安全分析与增强
目录 Apache Web安全分析与增强 - Apache Web概述Apache Web安全分析与增强 - Apache Web安全威胁Apache Web安全机制Apache Web安全增强 Apache Web安全分析与增强 - Apache Web概述 阿帕奇是一个用于搭建WEB服务器的应用程序,它是开源的,它的配置文件…...
理邦仪器嵌入式(C/C++开发)开发面试题及参考答案
C++ 虚函数的概念和作用 C++ 中的虚函数是一种非常重要的机制,它在实现多态性方面起着关键作用。 概念上来说,虚函数是在基类中使用关键字 virtual 声明的成员函数。当基类的指针或引用指向派生类的对象时,通过这个基类的指针或引用调用虚函数,实际执行的是派生类中重写的该…...
windows + visual studio 2019 使用cmake 编译构建静、动态库并调用详解
环境 windows visual studio 2019 visual studio 2019创建cmake工程 1. 静态库.lib 1.1 静态库编译生成 以下是我创建的cmake工程文件结构,只关注高亮文件夹部分 libout 存放编译生成的.lib文件libsrc 存放编译用的源代码和头文件CMakeLists.txt 此次编译CMak…...
Chrome 浏览器 支持多账号登录和管理的浏览器容器解决方案
根据搜索结果,目前没有直接提到名为“chrometable”的浏览器容器或插件。不过,从功能描述来看,您可能需要的是一个能够支持多账号登录和管理的浏览器容器解决方案。以下是一些可能的实现方式: 1. 使用 Docker 容器化部署 Chrome …...
GrassWebProxy
GrassWebProxy第一版: using System; using System.Collections.Generic; using System.Linq; using System.Net.Sockets; using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; using System.IO; using Newtonsoft.Json;…...
DeepSeek API 调用 - Spring Boot 实现
DeepSeek API 调用 - Spring Boot 实现 1. 项目依赖 在 pom.xml 中添加以下依赖: <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></depe…...
【DeepSeek】Deepseek辅组编程-通过卫星轨道计算终端距离、相对速度和多普勒频移
引言 笔者在前面的文章中,介绍了基于卫星轨道参数如何计算终端和卫星的距离,相对速度和多普勒频移。 【一文读懂】卫星轨道的轨道参数(六根数)和位置速度矢量转换及其在终端距离、相对速度和多普勒频移计算中的应用 Matlab程序 …...
【kafka实战】05 Kafka消费者消费消息过程源码剖析
1. 概述 Kafka消费者(Consumer)是Kafka系统中负责从Kafka集群中拉取消息的客户端组件。消费者消费消息的过程涉及多个步骤,包括消费者组的协调、分区分配、消息拉取、消息处理等。本文将深入剖析Kafka消费者消费消息的源码,并结合…...
[EAI-033] SFT 记忆,RL 泛化,LLM和VLM的消融研究
Paper Card 论文标题:SFT Memorizes, RL Generalizes: A Comparative Study of Foundation Model Post-training 论文作者:Tianzhe Chu, Yuexiang Zhai, Jihan Yang, Shengbang Tong, Saining Xie, Dale Schuurmans, Quoc V. Le, Sergey Levine, Yi Ma 论…...
算法与数据结构(字符串相乘)
题目 思路 这道题我们可以使用竖式乘法,从右往左遍历每个乘数,将其相乘,并且把乘完的数记录在nums数组中,然后再进行进位运算,将同一列的数进行相加,进位。 解题过程 首先求出两个数组的长度,…...
DeepSeek从入门到精通:全面掌握AI大模型的核心能力
文章目录 一、DeepSeek是什么?性能对齐OpenAI-o1正式版 二、Deepseek可以做什么?能力图谱文本生成自然语言理解与分析编程与代码相关常规绘图 三、如何使用DeepSeek?四、DeepSeek从入门到精通推理模型推理大模型非推理大模型 快思慢想&#x…...
【Pytorch函数】PyTorch随机数生成全解析 | torch.rand()家族函数使用指南
🌟 PyTorch随机数生成全解析 | torch.rand()家族函数使用指南 🌟 📌 一、核心函数参数详解 PyTorch提供多种随机数生成函数(注意:无直接torch.random()函数),以下是常用函数及参数:…...
vue print 打印
vue 点击打印页面部分内容,或者打印弹窗内的内容 打印页面部分内容 <template><div><div id"print"><div class"info"><div class"bx_title">费用报销单<span class"code">NO.<s…...
【异常解决】在idea中提示 hutool 提示 HttpResponse used withoud try-with-resources statement
博主介绍:✌全网粉丝22W,CSDN博客专家、Java领域优质创作者,掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域✌ 技术范围:SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大数据、物…...
【Uniapp-Vue3】UniCloud云数据库获取指定字段的数据
使用where方法可以获取指定的字段: let db uniCloud.database(); db.collection("数据表").where({字段名1:数据, 字段名2:数据}).get({getOne:true}) 如果我们不在get中添加{getOne:true},在只获取到一个数据res.result.data将会是一个数组&…...
信息科技伦理与道德3-2:智能决策
2.2 智能推荐 推荐算法介绍 推荐系统:猜你喜欢 https://blog.csdn.net/search_129_hr/article/details/120468187 推荐系统–矩阵分解 https://blog.csdn.net/search_129_hr/article/details/121598087 案例一:YouTube推荐算法向儿童推荐不适宜视频 …...
openssl使用
openssl使用 提取密钥对 数字证书pfx包含公钥和私钥,而cer证书只包含公钥。提取需输入证书保护密码 openssl pkcs12 -in xxx.pfx -nocerts -nodes -out pare.key提取私钥 openssl rsa -in pare.key -out pri.key提取公钥 openssl rsa -in pare.key -pubout -ou…...
Visual Studio 2022 中使用 Google Test
要在 Visual Studio 2022 中使用 Google Test (gtest),可以按照以下步骤进行: 安装 Google Test:确保你已经安装了 Google Test。如果没有安装,可以通过 Visual Studio Installer 安装。在安装程序中,找到并选择 Googl…...
SpringBoot3 + Jedis5 + Redis集群 如何通过scan方法分页获取所有keys
背景: 由于需要升级老项目代码,从SpringBoot1.5.x 升级到 SpringBoot3.3.x,框架中引用的Jedis自动升级到了 5.x;正好代码中有需要获取Redis集群的所有keys的需求存在;代码就不适用了,修改如下: POM 由于…...
WGCLOUD监控系统部署教程
官网地址:下载WGCLOUD安装包 - WGCLOUD官网 第一步、环境配置 #安装jdk 1、安装 EPEL 仓库: sudo yum install -y epel-release 2、安装 OpenJDK 11: sudo yum install java-11-openjdk-devel 3、如果成功,你可以通过运行 java …...
