从此

游戏开发引擎、资源文件I/O、架构模式、有限状态机(FSM)、行为树、ECS


综合/最新

Godot游戏开发 | 知名游戏作品 | 游戏开发文章

新: RN + Godot - https://github.com/borndotcom/react-native-godot

资源文件 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"); } // 存在则覆盖

游戏实战

节点命名

2D节点命名: All - 根节点,用Node3D,Node2D,特征不明显可用Node;通常不挂脚本。 Stage - 主舞台,用Node3D,Node2D;脚本名为Stage.cs;命名备选arena竞技场。 Player - 玩家角色,player.tscn,脚本名为Player.cs 3D(Three-dimensional)节点命名: 说明 - 场景根节点均加 Owner 或 Scope/Domain、Branch、Reuse 后缀。 Entrance.tscn EntranceOwner - 放个“Play”按钮,GetTree().ChangeSceneToFile("res://Game.tscn"); Game.tscn GameOwner(or All) - 根节点,用Node3D,Node2D,特征不明显可用Node;通常不挂脚本。 Stage - 主舞台,用Node3D,Node2D;脚本名为Stage.cs;命名备选arena竞技场。 Character PlayerOwner - 玩家角色,封装至player.tscn,根类型用CharacterBody3D,锁定入眼的全局方位,只负责 MoveAndSlide() 平移,旋转交给子对象PlayerModel,脚本名为Player.cs,挂载至player.tscn内,在当前节点右侧会显示脚本图标;演示暂用跟人类躯体相像的圆柱体(CapsuleMesh)。 Body - 3D模型,内含骨骼动画等,只负责旋转,平移交给Player,Godot tps-demo 就是这种做法。 非可视化节点(CharacterBody3D等)无边界框,但可视化节点(MeshInstance3D等)则有,取其高度:((VisualInstance3D)node.GetNode("Body/GeneralSkeleton/MeshInstance3D")).GetAabb().Size.Y; RayCast3D - 需Z轴放至Body肚皮之外,否则会被自身碰撞体遮挡。 SpringArm3D/Camera3D - 摇臂避免摄像头嵌入建筑物而看不到Player。 RemoteTransform3D - 【可选】指向 Player 外部的 Camera3D,瞬移场景用;或 RootMotion + at.GetRootMotionRotation() 修改骨骼朝向。 NPCs - 非玩家角色。 HostileOwner - 敌对NPC。 Top - 头顶血条。 PureNPCOwner - 中性NPC,或 Neutral、FairNPC。 PartnerOwner - 友好NPC,或叫 Amiable、Friendly。 NPC - 首个非玩家角色;根据 res://NPC.tscn 动态创建。 Thing - 地面门窗等,用StaticBody3D+子节点CollisionShape3D(设置其shape属性为无限平面WorldBoundaryShape3D),由于物体多,故根节点类型用Node3D隔离下; Ground - 地面用土色 new Color(0.5f, 0.4f, 0.2f),用StaticBody3D;Door - 门窗,用AnimatableBody3D。 Grass - 可用 QuadMesh(或修改 PlaneMesh.orientation) 制作。 Other Camera3D - 摄像头,2D用Camera2D。 WorldEnvironment - 光照。 HUD - Heads-up display,用CanvasLayer(通过Layer数字决定是否为最外层/跟节点出现顺序无关)或Node节点;Node2D会导致Control锚点失效。 Surface/SurfaceOwner - 最外层全屏窗口,用CanvasLayer;或用 Outermost;暂停和恢复游戏:GetTree().Paused = true; // 豁免 GetTree().Root.GetNode("All/Surface").ProcessMode = ProcessModeEnum.Always;

项目模板

  项目:至少存在一个 project.godot 文件。
config_version=5 [application] config/name="g1" config/features=PackedStringArray("4.3") run/main_scene="res://control.tscn" [dotnet] project/assembly_name="g1"
说明: config_version=5 即 Godot 4.x;config_version=4 则为 3.x。 project/assembly_name="g1" 决定了 g1.csproj 和 g1.sln 文件名及C#程序集名。 config/features 不写则自动生成当前IDE版本,且渲染模式回落至 "Forward Plus"。 run/main_scene="uid://qkbsbukvmysy" 也支持uid前缀。 C#方案生成(g1.csproj、不常改动的g1.sln): 项目 -> 工具 -> C# -> Create C# solution
<Project Sdk="Godot.NET.Sdk/4.5.0"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <EnableDynamicLoading>true</EnableDynamicLoading> </PropertyGroup> </Project>
C#脚本默认模板:.NET 6 默认 C# 版本为 10; v4.4 = net8.0 + C#12。
using Godot; using System; // 节点类定义必须声明为partial; // 空类也可 - public partial class MyNode : Control { } public partial class MyNode : Control { // 区别? new MyNode() Vs. Script.New(); public MyNode() { GD.Print("GD.Load<CSharpScript>("res://NewScript.cs").New();"); } // C#构造函数名即类名 public MyNode(int x) { GD.Print("GD.Load<GDScript>("res://new_script.gd").New(123);"); } // GDS构造函数名为_init(p) // this.AddChild(node) 执行后触发 _Ready()。 public override void _Ready() { GD.Print("_Ready()"); if (GetTree().GetFrame() > 0) // p.AddChild(node) 时触发: { await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); } else // 等待所属场景载入完毕,Owner默认为主场景的根节点;run/main_scene 时触发: { await ToSignal(Owner ?? GetParent(), Node.SignalName.Ready); } OS.Alert("Ready."); } public override void _Process(double delta) { } }

游戏架构、设计模式

状态:
  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(); }
    }
   }

状态机:
  枚举状态机 - 通过 enum + switch case 集中化管控,状态间具有排他性;取代最原始的 if + flag 方式。
  有限状态机(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 下。