游戏开发引擎、资源文件I/O、架构模式、有限状态机(FSM)、行为树、ECS
综合/最新
Godot游戏开发 | 知名游戏作品 | 游戏开发文章
新:
RN + Godot(只支持Android和iOS/不支持PC和Web) - https://github.com/borndotcom/react-native-godot
总:
Godot Renderer - Forward+(全能力)、Forward Mobile(均衡)、Compatibility(基本废弃)
游戏引擎
虚幻:
编程语言为C++,也支持可视化蓝图方式。
UE AnimMontage(动画蒙太奇/类似Godot动画树)
动画插槽管理器(Anim Slot Manager) - 实现混合动画(不如全身动画身体协调/不适合混合的动作:翻滚等)
创建俩插槽(即Godot的AnimationNodeOneShot端口in、shot):
DefaultGroup.DefaultSlot(WASD移动)、DefaultGroup.UpperBody(攻击、格挡)
将 DefaultSlot 完全体状态机姿势存储为 MotionCache,以便上下文内的下半身插槽重用。
右键点 AnimMontage 空白处选择 Use cached pose 'MotionCache',添加 Layered blend per bone 并设置 Blend Pose 0 属性,在 Details -> Layer Setup -> Branch Filters 屏蔽骨头。
右键选 Slot UpperBody 添加至蒙太奇视图,左侧连 Use cached pose 'MotionCache',右侧连 Layered blend per bone 节点。
混合动画播放 AnimInstance->Montage_Play(UpperBodyMontage, 1.0f);
原则 - 若上半身动作需要管理生命周期,则最好是加个独立状态机控制;若移动和攻击互斥(硬核游戏),则单个状态机即可。
资源文件 I/O
文件存储:
只读前缀 res:// 或 可写前缀 user:// 示例 user://x.txt,以及资源唯一标识前缀 uid://
相对路径 - ResourceLoader.Load(...) 不识别 C:\x.txt 路径,未填写则自动补 res://。
绝对路径 - Image.LoadFromFile(@"D:\x\x.jpg"); 或 _FontFile.Data = FileAccess.GetFileAsBytes("C:\x.ttc");
转绝对路径 - ProjectSettings.GlobalizePath("res://x.dll")
FileAccess高级用法:
读文本 - FileAccess.Open(fPath, FileAccess.ModeFlags.Read).GetAsText();
var fa = FileAccess.Open(fPath, FileAccess.ModeFlags.Read);
var bytes = fa.GetBuffer((long)fa.GetLength()); fa.Close();
简单k/v存储:
var cf = new ConfigFile();
cf.SetValue("path", "k", "v"); cf.SetValue("path", "k2", v2); // 值支持数组、对象等
cf.Save("user://x.ini"); // 存在则覆盖
if(cf.Load("user://x.ini")==Error.Ok){var v=(string)cf.GetValue("path", "k");}
// 以下为读取配置文件:
if (cf.Load("user://default.ini") == Error.Ok && cf.HasSection("a.b"))
{ cf.EraseSection("a.b"); cf.Save("user://default.ini"); } // 存在则覆盖
游戏架构、设计模式
状态:
Idle(立)、Walk(走)、Run(跑)、Jump(跳)、Fall(落)、Attack(攻)、Defend(防)、Die(死)。
关系:死与所有状态互斥;攻防互斥。
Jump - 空格;跳起后直接通过离地和落地(无需MaxActiveDuration)来开始及结束状态,并回退至Idle,若有位移,则幅度比Walk较小,且不要与Walk通过并发实现。
Attack - 鼠标左键;攻击需要MinActiveDuration和MaxActiveDuration来管理再入前摇和后摇,且不会回退。
Defend - 鼠标右键;
并发:
1. 下半身/移动层: Walk, Run, Jump
2. 上半身/动作层: Attack, Defend
角色可同时处于 Walk(移动层)和 Attack(动作层)状态。
实务:
动画播放时为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(); }
}
}
状态机:
属性拦截 - 各状态间flag残留混杂。
private Phase _currentPhase = Phase.None;
public Phase CurrentPhase
{
get => _currentPhase;
private set
{
if (_currentPhase != value)
{
_currentPhase = value;
EmitSignal(SignalName.PhaseChanged, (int)_currentPhase);
}
}
}
枚举状态机 - 通过 enum + switch case 集中化管控,状态间具有排他性;取代最原始的 if + flag 方式。
线性序列机(Linear Sequence Machine) - 如果状态按序执行,可通过声明个 List 并切换至下个阶段 _phases[_currentPhaseIndex].OnUpdate(delta);
比FSM心智负担低一些,毕竟是有序执行,即用 nextState() 换掉了 FSM 的 changeState(状态名);缺点是只能走 Duration,而适合确切时间点。
有限状态机(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://www.bilibili.com/opus/907564063269060614
储存继承链方式 - 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
状态图(Harel Statecharts/支持GDS+C#) - https://github.com/derkork/godot-statecharts
也叫 分层并发有限状态机 Hierarchical concurrent finite state machine(HCFSM) - 结合HSM和CSM。
其正交区域类似分层状态机,每区只能处于1个最终子状态,由树形结构组成。
插件包用法 - https://www.bilibili.com/video/BV14E421A7G6/
库 - Spring Statemachine
状态机通用模板:
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。
有限状态机(Finite State Machine):
N种状态下处于单一状态 - 比如老写法currentState = enumState.IDLE;switch (currentState) { case enumState.IDLE: ... }
FSM写法(适合于状态多于3种的情况) - 将enumState的每个状态分散到单个(基于StateBase)子类中单独控制。
Godot+C#状态机实例(相同状态时应改为跳过) - https://github.com/spaceyjase/sr-6 https://www.bilibili.com/video/BV1z34y1A7rg/
3D移动人物FSM - https://www.bilibili.com/video/BV1de411i7Lh/ C#非移动人物版 - https://www.youtube.com/watch?v=Kcg1SEgDqyk
多层次状态机原理 - https://zhuanlan.zhihu.com/p/662567305
并发状态机 - 即两个状态机分别负责腿部动作(站立,下蹲,奔跑)和手部动作(持枪,空手,瞄准)
把攻击动画的控制骨骼限制在上身,下身依然执行的是老动画walk。
并发状态机参考示例 - https://info.congci.com/main/infomations/articles/8df94b5f-c8cd-11ee-904e-592f6ee49b9d
行为树
机制 - Success 和 Failure 均会导致自身停止,区分为2个词是为了告知父节点执行情况,递归回Root后方决定是否停止“整个系统”,若返回 Running 则预示着下一 Tick 从该记忆点跨帧执行,跳过父级至根级的Conditional,但依然检查自身Conditional,无需整体重来。
插件 - Godot C# 行为树
架构
ECS: Entity component system
目的 - 使行为能add/remove,提升灵活度,分解复杂度,而CPU L1\L2内存连续的读取性能提升只是附带。
场景 - OOP对象同时提供数据和行为,整体载入会拖累性能,因此ECS采用了分拆组合的方式,将数据和行为进行了分离,甚至对两者按业务再次划分,使各自关注点的噪音更小。
实体是游戏对象的容器,它没有任何行为或属性,只是一个标识符。组件是游戏对象的属性或行为,例如位置、速度、生命值等。系统是游戏对象的行为逻辑,例如移动、攻击、碰撞检测等。
类似 - 《双影奇境》Capabilities并行架构。
内存连续原理 - 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 (更新)方法。
游戏引擎
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();
GDS导出加密:
流程:
编译 android 导出模板(调试则命名为android_debug.apk)后覆盖至 C:\Users\[user]\AppData\Roaming\Godot\export_templates\4.5.1.stable.mono\android_release.apk;或 Android“自定义模板” 指向该文件。
接着重新安装 game_project/android/ 目录(可选?),[仅用于Android加密]启用apk扩展(apk_expansion/enable),并勾上"高级选项"->"加密"->“加密导出的PCK”,如果未提供 Google Play 公钥,Godot 会用代码方式加解密 _StorageManager.mountObb(*.obb文件, null, obbListener)。
视频教程 - https://www.bilibili.com/video/BV1VsQQYuEpB
GDS混淆 - https://github.com/cherriesandmochi/gdmaim https://github.com/June-Tree/Godot-Source-Code-Obfuscator
其他
只能鼠标点击,不响应Enter等确认键:Button.FocusMode = FocusModeEnum.None;
Blender 为动画批量增加根骨骼的 plugin - https://github.com/snougo/MixamoTool
C# GDExtension 插件 -
https://github.com/Delsin-Yu/CSharp-Wrapper-Generator-for-GDExtension
https://godotengine.org/asset-library/asset/3832
游戏模型资源 - https://www.cnblogs.com/Mr147/p/19074904
IState 类未定义在同名类文件 IState.cs 则报? - TryReloadRegisteredScriptWithClass ... An item with the same key has already been added. Key: GameFSM.IState
Godot 4.x起非数字区快捷键也可用了:前视图=Num 1等。
反射常量名 - get_script().get_script_constant_map().keys()
自动锁敌视角攻击 - https://zhuanlan.zhihu.com/p/371989026
行为树实例 -
https://github.com/grovegs/BehaviourTree/blob/main/sandbox/GodotApplication/node_3d.tscn
TestSceneController 类:
多处 - Node 改为 BehaviourNode
一处 - Tree 改为 GodotBehaviourTree、Root 改为 BehaviourRoot
GodotBehaviourTree.Debug 类:
底部2处 Nodes.Node.Empty 改为 Nodes.BehaviourNode.Empty;2处 Root 改为 BehaviourRoot;修改后 rebuild 下。