核心:
新:
设置相对于该控件的鼠标坐标位置 - Control.WarpMouse(Vector2.Zero)
综合
死记:
Godot 倡导游戏即使报错也不应该崩溃,故在记录错误后会继续执行,但C#层异常则直接崩,不再向下执行。
GDS load函数加载*.gd脚本用D:\\盘符和res://前缀均可,但GDS load("x.cs")则不支持D:\\盘符前缀方式,未测pck.dll方式。
Godot 场景文件可存为*.tscn和*.res,不支持*.tres,由于*.res无法通过IDE转回*.tscn,故最好选择*.tscn存储PackedScene。
图片导入后均会转化为ctex位图(CompressedTexture2D),SVG则只能通过load_svg_from_string(...)进行光栅化,但要先用inkscape命令将文本处理为矢量路径。
特殊:
Godot 4.4 C#版IDE未支持Web导出,预计集成思路是 采用 Blazor WebAssembly 方式(LibGodot)。
每次重装系统后,应 开启Windows长路径 支持,避免 .godot/ 目录内临时文件名过长。
C#脚本无GDS的全局函数,故将其分散到了Godot.Mathf、GD.Print(1)等类中;GDS函数名去掉下划线+驼峰式大小写,即为C#方法名。
C# as转换不能用于Godot Variant,需改为 - variant.As<MyEnum>() 或 (String)variant;
C#对应GDS特殊情况 - dict.ContainsKey(k)即GDS的dict.has(k);
Button节点无法与其他节点同时点按,故应换为支持多点触控的TouchScreenButton(设置texture_normal后方可见)。
场景节点类型为主对象,附加的脚本则类似临时容器,游戏引擎会将用户编写的脚本成员添加到主对象中。
脚本继承的类型只能与场景节点类型相同,或是其基类,不能是子类,否则报:
Script inherits from native type 'Node2D', so it can't be assigned to an object of type: 'Node'
包含float类型属性值的Resource对象,保存为*.tres时会因 var_to_str(float小数) 返回科学计数法,而使部分尾数变0:
不经过var_to_str函数的*.res二进制格式则尾数正常:ResourceSaver.save(res,"x.res"); load("x.res");
当保存为*.tres文本格式时,@export var x:float = 1686693128 数字load出来变 1686690000 !?
Control 的 this.Theme 对应的是完整的资源对象,而 Theme Overrides 则按主题项名字单独覆盖。
AddThemeColorOverride("font_color", Godot.Colors.Red);
AddThemeColorOverride("font_hover_color", Godot.Colors.Tan);
主题样式 - IDE 中 Theme Styles 属性在代码里叫 StyleBox。
主题默认字体:
var th = new Theme(); th.DefaultFontSize = 100;
// 会影响子节点,适用于包含了Button子节点的ConfirmationDialog或AcceptDialog
dlg.Theme = th; GD.Print(dlg.Theme);
节点高度控制:
占满行高 - confirmationDialog.AddChild(new LineEdit()); // [无效] lineEdit.Size = new Vector2(10, 10);
正常行高 - var hbc = new VBoxContainer(); dlg.AddChild(hbc); hbc.AddChild(new LineEdit());
网络:
HttpWebRequest 响应302报 WebException,虽说可通过 AllowAutoRedirect=false 抑制,但不甚顺滑;故首选C# System.Net.Http.HttpClient。
或用 AddChild(new HttpRequest());
或用 Godot内置网络请求:var hc = new Godot.HttpClient(); hc.ConnectToHost("example.com"); hc.Request(HttpClient.Method.Get, "/index.html", null);
其他:
Godot v4.4+ 支持了 PCK Patches? - https://github.com/godotengine/godot/pull/97118
项目:至少存在一个 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"。
C#方案生成(g1.csproj、不常改动的g1.sln): 项目 -> 工具 -> C# -> Create C# solution
<Project Sdk="Godot.NET.Sdk/4.3.0">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'android' ">net7.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'ios' ">net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
</Project>
C#脚本默认模板:.NET 6 默认 C# 版本为 10。
using Godot;
using System;
// 节点类定义必须声明为partial
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)
{
}
}
实时物理引擎:
普通游戏项目用内置物理引擎即可,但性能要求高的可无缝换用 Godot Jolt扩展(将来可能会替代Godot内置物理引擎)。
架构:
只有1%的游戏类型(策略游戏万人同屏)才可能会通过 ECS 来提升性能,故用常规的OOP做游戏即可。
可观察性:
列出节点树 - GetTree().Root.PrintTreePretty();
打印孤儿节点 - GetTree().Root.PrintStrayNodes();
调试模式 - OS.IsDebugBuild(); 查看本地化 - OS.GetLocale(); // zh_CN
帧率/秒 - Engine.GetFramesPerSecond();
所有传入参数 - OS.GetCmdlineArgs(); // --print-fps 可用 org.godotengine.godot.utils.CommandLineFileParser 解析 assets/_cl_。
用户自定义参数 - OS.GetCmdlineUserArgs(); // 双加号开头 ++k=v
判断运行时引擎版本:
Engine.GetVersionInfo()["string"]; // 4.4-dev3 (official)
var ev = (int)Engine.GetVersionInfo()["hex"];
ev >= 0x040400 // int263168=0x040400
线程:
当前线程Id - OS.GetThreadCallerId(); 主线程Id - OS.GetMainThreadId();
单 Thread 不好管理,可交由性能更好的 WorkerThreadPool 统一控制。
call_deferred指运行在线性交替的_physics_process(优先执行)、_process(idle)函数最底部的_call_idle_callbacks()。
节点线程组 - 4.1+ 允许节点运行在主线程之外,利用多核提升帧率:
this.ProcessThreadGroup = ProcessThreadGroupEnum.SubThread; // 默认MainThread
call_thread_safe指若当前线程与node处于同线程则直调call,否则就通过call_deferred_thread_group切换至node所在线程调用call。
层次结构:
SceneTree - 即唯一MainLoop:(SceneTree)GetTree();
Viewport - 根视图,由SceneTree内置,无需手动创建:Window viewport = GetTree().Root、GetNode<Node>("/root")、GetViewport() as Viewport
Scene - 即GetTree().CurrentScene场景;游戏启动时由 project.godot 的 run/main_scene 决定。
AutoLoad - 其他非current场景。
Viewport分屏视图 - 可通过用户创建多个分屏视图。
基类:
Godot.GodotObject - Godot所有对象的基类,即GDScript中的Object类;若不与Godot传入传出,则可用C#的System.Object。
Godot.Variant - 限定传入对象为Godot底层能处理的数据类型;与Godot交互的自定义类型应该继承自 GodotObject。
GDScript变量声明即为var x:Variant; 无需额外包装;C#则用object? Obj属性存放,类似C#无限定的dynamic。
C#通过隐式约束来限定赋值到Obj属性的数据类型(比如 System.Decimal 就未兼容):
public static implicit operator Variant(string from) { /* ... */ }
Variant限定特性 - public void m<[MustBeVariant] T>(T variant) { }
数组对应关系 - GDScript的Array类型int[]即C#的Godot.Collections.Array<int>,而PackedStringArray则为C#的string[]。
注解导出 Test[] 必须换用 Variant-compatible 的 [Export] Godot.Collections.Array<Test> enums;
// Call 入参 method 名首选 GodotObject.MethodName 常量,或用小写+下划线字符串
var r = new GodotObject().Call(GodotObject.MethodName.GetClass); // get_class
// Call 返回类型为 Variant,链式调用前应先转换为 .AsGodotObject() 或 具体类型。
GD.Print(r.AsString()); GD.Print(r.As<String>()); // 两者等同。
// String 和 int 并非 GodotObject 子类,但都可以与 Variant 互转。
Variant.From<int>(1).AsInt32(); // 包装为 Variant;只比 CreateFrom 多个泛型限定。
// 防抛异常转换可改 Variant.From(new Node()).As<Node2D>() 为:
if (Variant.From(new Node()).As<GodotObject>() is Node n) { GD.Print(n); }
var r = new GodotObject().Get("script"); // GodotObject.PropertyName 无常量;
GD.Print(r.VariantType == Variant.Type.Nil); // 单值;等同 new Variant()
GD.Print(r.VariantType.HasFlag(Variant.Type.Array | Variant.Type.Int)); // 多值 and 判断:
// VariantType 取大分类,转换为GodotObject才能获取实际类型,比如 CSharpScript。
if (r.VariantType == Variant.Type.Object) { GD.Print(r.AsGodotObject().GetType().Name); }
调用链入口:- project.godot -> run/main_scene="res://node.tscn"
- node.tscn -> [gd_scene load_steps=4 format=3 uid="uid://r6vkfmwdyxh5"]
后跟 [node name="Main" type="Node"]、[connection signal="pressed" ...]
或 外部资源[ext_resource ...] 及 内部资源[sub_resource ...]
Node 和 PackedScene 层次关系通过 parent="Player/Head" 决定,根节点无此属性。
- 解析外部资源 -> [ext_resource type="PackedScene" uid="uid://ceqmi2m1ad1kr" path="res://ext.tscn" id="4"]
- 同上第二步 -> 遍历至无外部PackedScene资源为止。
注 - load_steps=3指*.tscn文件内,外部资源+内部资源 的总条数加一。
唯一化 - 即 texture_normal = ExtResource("2_6ipn4") 改为 SubResource("CompressedTexture2D_2ycfj"),可指定同一资源的不同属性。
IDE编辑器设置属性 ResourceLocalToScene = true; 等同运行时调 res.Duplicate(); // 图片材质对象类型为CompressedTexture2D。
构件基类:
综述 - 节点(Node)可串联为树状方式的场景(Scene),自身属性值通过资源(Resource)来设置,附加脚本(Script)扩展游戏操作。
编辑器 *.tscn 的“场景 -> 添加子节点(Ctrl+A)”最底层只能选择Node对象,无法再深入其基类Object。
场景声明节点的组成 - scene.tscn;脚本则用命令式代码添加行为 - x.cs。
脚本方式 - GD.Load<CSharpScript>("res://x/MyNode.cs").New().As<Node>();
声明方式 - GD.Load<PackedScene>("res://x/MyScene.tscn").Instantiate();
更改脚本 - Object.set_script(script或nil对象"new Variant()"),设置后即触发其_init(),且属性成为该对象成员。
异同:
GetTree().Root Vs. GetTree().Root.Get("Main") - 前者是在“远程场景”中可见的根视图Viewport,路径已写死为"/root",同层级可添加分屏视图,后者可与AutoLoad节点并列,但节点名或路径必须自定义。
GetParent() Vs. Owner - 前者逐层向上取节点,后者则是从 GetTree().Root.Get("Main") 开始取值,指向当前节点的路径上,若有更改了SetOwner()的则取代之,类似node.SetMultiplayerAuthority(ma)传播机制。
Godot.Node - 最小执行单位,显式释放Free(),分为可见节点(Button等)和小部分不可见节点(Timer等),通过树形结构(即场景)排布,基本都能独立使用,少部分要求组合(BoxShape3D等)。
最常用节点 Node3D、Node2D 和 Control 共有属性:Position、Rotation、Scale、Transform(Control无)
Node3D 继承自 Node < Object;Transform属性即3D转换矩阵。
Node2D 和 Control 继承自 CanvasItem < Node < Object:
Control(User Interface)、Node2D( / Position, Rotation, Scale and Z-index)
Godot.Resource - 数据结构体,全局单例复用,引用计数自动GC,用于存放图片、文件、3D模型等静态内容。
PackedScene(即tscn场景类型),Texture,GDScript,CSharpScript等
场景 - 资源形式为PackedScene,ps.Instantiate<Node>()实例化后为Node对象树。
场景树:this.GetTree(); 或 无上下文时静态获取 (SceneTree)Engine.GetMainLoop();
当前:GetTree().CurrentScene; 重开:GetTree().ReloadCurrentScene();
切换:GetTree().ChangeSceneToPacked(GD.Load<PackedScene>("res://x.tscn")); // 或 ChangeSceneToFile("res://x.tscn");
序列化 - rootNode 子节点必须在r.AddChild(node)后设置 node.Owner = r,否则会跳过:
var rootNode = new Node(); var b = new Button(); rootNode.AddChild(b);
b.Owner = rootNode; // 想从根寻址则可设 b.Owner = GetTree().Root;
// b.UniqueNameInOwner = true; // 场景唯一节点 按所有者取值 b.Owner.GetNode("%"+b.Name);
GetTree().Root.FindChild(b.Name, true, false) // 参数3指是否按所有者遍历,若为true则无法寻址动态添加的控件。
var ps = new PackedScene(); ps.Pack(rootNode); ResourceSaver.Save(ps, "user://save-scene.tscn");
保存场景位置 - C:\Users\person\AppData\Roaming\Godot\app_userdata\g1\save-scene.tscn
节点 - 单行文本用TextEdit;多行文本用LineEdit
节点控制:
按Name路径定位获取节点 - GetNode("Area2D/CollisionShape2D"); “路径不存在”则返回null:
var n = GetNodeOrNull("Area2D/CollisionShape2D") as CollisionShape2D;
GodotObject.IsInstanceValid(n); // 解决 ObjectDisposedException。
按Name路径和属性名来定位属性值 - GetNodeAndResource("Area2D/CollisionShape2D:shape:extents");
节点其他:
节点可见性 -
node.Hide()指看不到但内存和树中均在,显示用node.Show();
node.QueueFree()指看不到且内存和树中也不存在;
node.RemoveChild(subNode)指看不到且树中也不存在,但内存未释放,或写为 node.GetParent().CallDeferred(Node.MethodName.RemoveChild, node);
释放信号 - 节点释放后触发 tree_exited() 信号,其Handler中this不为null,但this.GetTree()为null,故应使用Engine.GetMainLoop()取上下文;
或 先在 tree_exiting() 中 SetMeta("context",上下文); 再到 tree_exited() 信号中 GetMeta("context")。
v4.4+ 的 HasConnections(SignalName.TreeExited) 写法比 IsConnected(signal, callable) 少提供一个参数。
清空子节点 - foreach (var n in pNode.GetChildren()) { pNode.RemoveChild(n); }
判断鼠标位置是否在节点范围 - GetRect().HasPoint(ToLocal(inputEventMouseButton.Position))
选择框取值 - GetNode<CheckBox>("CheckBox").ButtonPressed; // 或不触发信号的 set_pressed_no_signal(1)
遍历所有子节点:
void childrenNodes(Node node)
{
foreach (var n in node.GetChildren())
{ GD.Print(n.GetPath()); childrenNodes(n); }
}
插件单例:
插件打包后必须存在文件 - plugin.cfg 且其脚本属性 script="export_plugin.gd" 必须相对于插件目录 res://addons/OurGameCore/,不支持D:\...路径。
插件用法:
// 若单例不存在,即使抛出 Failed to retrieve non-existent singleton 'OurGameCore',也会继续执行下去;
// 或通过判断抑制住异常:var es = Engine.HasSingleton("OurGameCore") ? Engine.GetSingleton("OurGameCore") : null;
if (Engine.HasSingleton("OurGameCore") && Engine.GetSingleton("OurGameCore") is var es && es != null)
{ appId = es.Call("appId").As<String>(); }
判null语法糖 - es?.Call("showToast", "Hi!");
Android插件:
Android v4.4+导出的apk或aab里包含了一个内置插件库 - org.godotengine.godot.plugin.AndroidRuntimePlugin
用法:Engine.GetSingleton("AndroidRuntime").Call("getActivity").AsGodotObject().Call("getPackageName").AsString();
亲测:return String和int正常;若返回long则Call时崩;
若返回Long和Integer,则启动就崩报 NoSuchMethodError? - https://github.com/godotengine/godot/issues/96046
高级:
语法:
GDScript无async关键词,但await用法与C#一样。
godotArrays.ToList().ForEach(x => GD.Print(x));
线程:
await ToSignal(GetTree().CreateTimer(2.0f), SceneTreeTimer.SignalName.Timeout); // GDS专用: await owner.ready
new System.Threading.Thread(() => { Thread.Sleep(1000); ap.CallDeferred(AnimationPlayer.MethodName.Play, "test"); }).Start();
this.AddChild(node) 时由于根视图 Ready 已触发,故 await 时会卡死,而在 Ready 后持续触发的 ProcessFrame 则只会卡一帧,但可能被暂停。
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
或者通过 GetTree().GetFrame() > 0 来判断根视图已 Ready。
自定义资源/全局类:除Node和Resource之外的基类不识别。
[GlobalClass] // 会列在 Node 创建向导中。
public partial class MyNode : Godot.Node { [Export] public MyRes res { get; set; } }
[GlobalClass] // 若继承自 Godot.Resource 则列在 [Export] 属性面板中。
public partial class MyRes : Godot.Resource { }
/* GDS全局类(无class_name则普通类):
extends Node
class_name MyNode
@export var x:float = 0
*/