从此

游戏开发引擎、资源文件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 下。