游戏开发引擎、3D建模、有限状态机(FSM)、ECS
游戏实战
状态:
Idle(立)、Walk(走)、Run(跑)、Jump(跳)、Fall(落)、Attack(攻)、Defend(防)、Die(死)。
关系:死与所有状态互斥;攻防互斥。
Jump - 空格;跳起后直接通过离地和落地(无需MaxActiveDuration),来开始及结束状态,并回退至Idle。
Attack - 鼠标左键;攻击需要MinActiveDuration和MaxActiveDuration来管理再入前摇和后摇,且不会回退。
Defend - 鼠标右键;
实务:
动画播放时为true(临时解决连点时总处于首帧/或用动画完成回调): player.GetNode("AnimationTree").Get("parameters/OneShot/active").AsBool();
退出游戏:
public override void _UnhandledInput(InputEvent @event)
{
if (@event is InputEventKey eventKey)
{
if (eventKey.Pressed && eventKey.Keycode == Key.Escape) { GetTree().Quit(); }
}
}
状态机:
枚举状态机 - 通过 enum + switch case 集中化管控,状态间具有排他性。
有限状态机(Finite State Machine/FSM) - 分散为继承基类的子类状态对象。
复合方式实现边走边攻击(缺点是状态较多):Idle、Walk、Attack、Walk_Attack
并发状态机(Concurrent State Machine) - 状态间可并存;可换用行为树的 Parallel 节点。
FSM 多实例方式(不优雅) - https://developer.unity.cn/projects/67399c19edbc2a001e3f7076
多个 IState 字段方式 - https://info.congci.com/main/infomations/articles/8df94b5f-c8cd-11ee-904e-592f6ee49b9d#concurrent
states 数组方式 - project-templates\programming\godot\projects\csharp\single-player\csm\csm-blend\
https://www.cnblogs.com/gamedaybyday/p/18993996#t3
通过代码添加跳转规则? fsm.AddTransition("State", "State2");
层次状态机(Hierarchical State Machine) - 状态分组,子状态继承父状态的通用处理逻辑。
CurrentState 存储最深子状态,可反查至根类状态,子状态不处理就向上执行,继承链上均为活跃状态。
分层状态机原理 - https://zhuanlan.zhihu.com/p/558422986
override 方式 - https://blog.csdn.net/weixin_42216813/article/details/146218878
储存继承链方式 - https://github.com/olafvisker/hfsm/blob/main/HFSM.cs
切换判定,可重写默认的 Finished() { return true; }
Godot引用Unity HFSM库 - https://github.com/BangL/GodotHFSM-Samples/blob/master/GodotHFSM-Samples/GuardAI/PlayerController.cs
Godot HFSM2 - https://github.com/Daylily-Zeleen/HFSM2/blob/main/addons/com.daylily_zeleen.hfsm2/CSharpWrappers/State.cs
下推自动机(Pushdown Automata/PDA) - 堆栈式: https://github.com/AnAdisorn/Godot4-Push-Down-Automata-HFSM/blob/main/state_machine/StateMachine.cs
分层并发有限状态机 Hierarchical concurrent finite state machine(HCFSM) - 结合HSM和CSM。
状态机通用模板:
public class StateMachine
{
private State currentState; // 并发状态机则应改为多项值 activeStates。
public State CurrentState { get => currentState; }
public void changeState(State state)
{
if (currentState != null)
currentState.Exit(); // 老状态离开时触发。
currentState = state;
currentState.Enter(); // 新状态进入时触发。
}
public void Tick()
{
currentState.Tick(); // FSM 只此一行,而分层状态机则要增加前置或后置处理:
/*
// 层次状态机 - https://github.com/olafvisker/hfsm/blob/main/HFSM.cs
var entry = currentState.GetFinalEntryState(); // 递归首个子状态,直至最末端作为入口状态。
if (entry != null) changeState(entry);
currentState.Tick();
// [以下可选] 注册自动跳转条件 hfsm.To(idle, move, ()=>true); 根据Lambda返回值自动从idle跳至move状态,多项则全部为true才会跳转。
var to = currentState.GetTransitionState();
if (to != null) changeState(to);
*/
}
}
/* 分层状态机用法:
//刷新 hfsm.Tick(delta);
var two = new Two();
// 继承后 public class Three : Two { } 可触发基类 Update(double delta) { base.Update(delta); }
var three = new Three();
two.AddChildren(three);
var two2 = new Two2();
var one = new One();
one.AddChildren(two, two2);
//one.SetEntryState(two2); // 设置非首项入口状态。
hfsm.changeState(one); // 必须是已存在实例。
hfsm.changeState(two2);
*/
Godot官方状态机实例(HSM+PDA)
var states_stack := [] // 比如跳起普攻 [Attack, Jump ,Idle]
普攻执行完成后会清除栈顶 finished.emit(PLAYER_STATE.previous),变为 [Jump ,Idle],跳完再执行 states_stack.pop_front() 清顶为 [Idle]。
连招独立状态机(switch形式) - finite_state_machine/player/weapon/sword.gd
连招超时通过动画树函数计步:起手set_attack_input_listening、播完set_ready_for_next_attack。
行为树
C#行为树插件+类库: 暂未用上的行为树插件
<ItemGroup><PackageReference Include="GroveGames.BehaviourTree" Version="0.4.10" /></ItemGroup>
解压插件 behaviour-tree-0.4.10-godot-addon.zip
编辑器Debugger选项卡Behaviour Tree Debugger启用:运行 -> 远程 -> 选中 GodotBehaviourTree 节点 -> 勾上 Debuggable;后续估计会在 项目设置 里添加该插件选项。
简述之 - 逐帧执行 GodotBehaviourTree 类 SetupTree() 链式添加的各种节点:
Sequence(整体成功方Success、首个失败即短路Failure),用于严格匹配。
Selector(整体失败方Failure、首个成功即短路Success),用于宽松回退。
Parallel(非线性/全部执行并符合传入ParallelPolicy),解决Sequence当前节点返回Running时,下一节点执行不到的场景。
可取代并发状态机!
Decorator(纯条件判定节点/子类Conditional等)。
bt.Abort() - 中断Running状态节点。
var rs = Root.Selector(); // 直接节点用 rs.Conditional(() => true)...,其他节点类型可链式构造:
var sr = rs.Sequence().Conditional(() => true).Cooldown(1f).Repeater(RepeatMode.UntilSuccess);
sr.Attach(new Attack(sr)); // 成功则执行相应动作:通常为 Execute()、Evaluate(float delta) 方法名。
// 或显式包装 seq.Attach(new HasEnemy(_enemy, _entity, seq));
var seq = rs.Sequence(); seq = seq.Attach(new Conditional(seq, () => true)).Cooldown(1f)...
关键行 - public override void _Process(double delta) => bt.Tick((float)delta); // 牵扯物理计算可换至 _PhysicsProcess。
上下文 - Blackboard.SetValue("target_pos_enemey", entity.GetParent().GetNode("entity")); // 存储玩家角色位置供NPC敌人走近。
实战图 -
行为参考 https://blog.csdn.net/weixin_42216813/article/details/146218878#3_HFSM__44
Selector
Selector - 要么走、要么攻击。
Walk、Attack
Idle - 不走不攻击则回退至站立。
架构
ECS: Entity component system
目的 - 使行为能add/remove,提升灵活度,分解复杂度,而CPU L1\L2内存连续的读取性能提升只是附带。
场景 - OOP对象同时提供数据和行为,整体载入会拖累性能,因此ECS采用了分拆组合的方式,将数据和行为进行了分离,甚至对两者按业务再次划分,使各自关注点的噪音更小。
实体是游戏对象的容器,它没有任何行为或属性,只是一个标识符。组件是游戏对象的属性或行为,例如位置、速度、生命值等。系统是游戏对象的行为逻辑,例如移动、攻击、碰撞检测等。
内存连续原理 - https://blog.csdn.net/ylmbtm/article/details/121430868
ECS结合FSM、行为树用法 - https://pixelmatic.github.io/articles/2020/05/13/ecs-and-ai.html
C# ECS框架(支持Godot) - https://github.com/genaray/Arch/wiki/Integration-Guides#godot
用法 - https://github.com/Neerti/Arch-ECS-Godot-Demo/blob/main/Demo/EntityRenderer.cs
创建Entity: World.Create(new Position {Vec2 = new Vector2(GD.Randf() * GetViewportRect().Size.X, GD.Randf() * GetViewportRect().Size.Y)},
new Velocity {Vec2 = new Vector2((float)GD.RandRange(-1.0f, 1.0f) * 200f, (float)GD.RandRange(-1.0f, 1.0f) * 200f)},
new Sprite {SpriteColor = new Color(GD.Randf(), GD.Randf(), GD.Randf())});
[清除Entities] World.Clear();
var sys = new MovementSystem(World.Create(), new Rect2I(0, 0, GetTree().Root.Size));
_PhysicsProcess 调用 sys.Update(delta); sys2.Update(delta);
GDS ECS框架 - https://godotengine.org/asset-library/asset/3481
运行逻辑 - 某个业务系统筛选出拥有这个业务系统相关组件的实体,对这些实体的相关组件进行处理更新。www.cnblogs.com/hggzhang/p/17161722.html
即 - sys.add(comp); systems.add(sys); while (true) { for systems.run() }
C#默认堆不便实现内存连续,故要自行处理,可参考Arch ECS框架实现:
https://github.com/genaray/Arch/blob/master/src/Arch/Core/Chunk.cs
●Entity: 表示一个游戏对象,它通常只包含一个表示对象ID的唯一标识符。它拥有一个Component的集合,这些不同类型的Component集合构成一个特定类型的游戏对象。此外,Entity 也可用来作为属性间通信的枢纽。
●Component: 表示一个属性的数据部分,例如HealthComponent 表示一个血量属性的数据。通常Component是不包含实例方法的,除了一些方便的get/set,或者内部数据处理方法。
●System: 表示一个属性的行为部分,例如HealthSystem表示血量属性的行为,它会检测HealthComponent的值,如果其血量小于或等于0时则向Entity发出死亡通知事件。System 通常由游戏循环驱动来修改游戏对象的状态,典型的System 包含一个update (更新)方法。
1. ECS 结合 FSM 的实现方式
FSM 在 ECS 中通常有两种主要的实现方法,它们都遵循 ECS 的原则:数据驱动逻辑。
方法一:状态作为 Component Tag(最符合 ECS 理念)
状态(State) 被定义为一个 空 Component (Tag Component) 或一个只包含少量数据的 Component。
转换(Transition) 由专门的 System 来处理。
例如,要让一个实体进入“攻击”状态,只需给它添加一个 AttackStateComponent,并移除旧的 IdleStateComponent。
逻辑(Behavior) 分散在不同的 System 中。
IdleSystem 只查询拥有 IdleStateComponent 的实体,并执行空闲逻辑(如巡逻)。
AttackSystem 只查询拥有 AttackStateComponent 的实体,并执行攻击逻辑。
优点: 性能高,数据紧凑。System 的查询(Query)直接筛选出了处于特定状态的所有实体,非常适合并行计算。
缺点: 状态过多时会导致 System 和 Component 数量爆炸,以及 Archetype(数据块)的碎片化。
方法二:状态作为 Component 字段(更像传统 FSM)
状态(State) 被存储在实体的一个 AIStateComponent 的枚举(Enum)字段中。
FSM System 负责所有实体的状态转换,根据实体的数据和事件来改变这个枚举值。
行为逻辑 仍然分散在不同的 System 中,但这些 System 需要在内部检查状态枚举值。
AISystem 查询所有拥有 AIStateComponent 的实体,然后内部用 switch/case 语句根据状态枚举来调用不同的逻辑函数。
优点: 易于理解和实现,更像传统的 FSM,状态管理集中。
缺点: 破坏了 ECS 的**“最小化数据”**原则,System 必须处理不相关的数据(即在 switch 中跳过很多状态),不利于最极致的并行优化。
游戏引擎
UPBGE (Uchronia Project Blender Game Engine) - Blender Game Engine (BGE) 分支,与 Blender 深度集成,通过 Python 编写游戏的 3D 游戏引擎。
Godot 布娃娃:
选中Skeleton3D -> Skeleton -> Create Physical Skeleton:将创建PhysicalBoneSimulator3D和自动生成子节点们PhysicalBone3D。
执行瘫软姿势:PhysicalBoneSimulator3D.physical_bones_start_simulation();
其他
只能鼠标点击,不响应Enter等确认键:Button.FocusMode = FocusModeEnum.None;
Blender 为动画批量增加根骨骼的 plugin - https://github.com/snougo/MixamoTool
行为树实例 -
https://github.com/grovegs/BehaviourTree/blob/main/sandbox/GodotApplication/node_3d.tscn
TestSceneController 类:
多处 - Node 改为 BehaviourNode
一处 - Tree 改为 GodotBehaviourTree、Root 改为 BehaviourRoot
GodotBehaviourTree.Debug 类:
底部 Nodes.Node.Empty 改为 Nodes.BehaviourNode.Empty