从此
📄文章 #️⃣专题 🌐酷站 👨‍💻技术 📺 📱

Godot语法、核心概念、特殊用法

Godot C#/Mono | Godot官方文档 | Godot源码 | Godot Shaders 着色器 | 产品发布检查清单 | 游戏开发专题

核心:

新: 设置相对于该控件的鼠标坐标位置 - Control.WarpMouse(Vector2.Zero) 综合 死记: Godot 倡导游戏即使报错也不应该崩溃,故在记录错误后会继续执行,但C#层异常则直接崩,不再向下执行。 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); 主题默认字体: 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#脚本默认模板:
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();"); } // 即实例化后触发的 _init() public MyNode(int x) { GD.Print("GD.Load<GDScript>("res://new_script.gd").New(123);"); } // 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 层次结构: 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。 调用链入口:
  1. project.godot -> run/main_scene="res://node.tscn"
  2. 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" 决定,根节点无此属性。
  3. 解析外部资源 -> [ext_resource type="PackedScene" uid="uid://ceqmi2m1ad1kr" path="res://ext.tscn" id="4"]
  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(code),设置后即触发其_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 - GetNodeOrNull("Area2D/CollisionShape2D"); 按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 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[]。 包装为Variant - Variant.From<int>(123); 从Variant转回 - variant.As<MyEnum>(); 注解导出 Test[] 必须换用 Variant-compatible 的 [Export] Godot.Collections.Array<Test> enums;

高级:

语法: 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); GD.Print("After Sleep"); 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 */
信号: IsConnected无法判断“+=”方式连接的信号,但Connect(...)方式的则判定正确。 C# nameof(method)效果等同Godot MethodName.method 信号增加尾参:注意 - 只有GDScript才有bind方法! var t:Timer=get_node("Timer"); #t.one_shot=true; t.timeout.connect(Callable(self,"m").bind(123)); t.start() 解决C#的Lambda中无法使用-=取消连接信号的语法限制: 首选 IsConnected判断方式避免重复连接; 或 ap.AnimationFinished += (sn) => { var list = ap.GetSignalConnectionList(AnimationPlayer.SignalName.AnimationFinished); foreach (var dict in list) // signal、callable、flags { ap.Disconnect(AnimationPlayer.SignalName.AnimationFinished, (Callable)dict["callable"]); } }; ap.Play("Idle"); 或 一次性连接信号 - ap.Connect(AnimationPlayer.SignalName.AnimationFinished, new Callable(this, MethodName.xxx), (uint)ConnectFlags.OneShot);

其他


树形场景:
  节点可以直接new出来并add;或通过持有id来获取 GodotObject.InstanceFromId(id)
    首选GD.Load方法;次选比GD.Load多一个CacheMode.Reuse参数的ResourceLoader.Load; 
    异步载入用ResourceLoader.LoadThreadedRequest("res://s.tscn"); 
      轮询后取用 if (ResourceLoader.LoadThreadedGetStatus("res://s.tscn") == ThreadLoadStatus.Loaded){ ResourceLoader.LoadThreadedGet("res://s.tscn"); }
    调用Load返回预载入(首次缓存)PackedScene,ps.Dispose()或ps.Free()释放缓存;执行ps.Instantiate()后则返回可使用的node了。


节点分类:
  单独使用 - Button、MeshInstance3D等
  组合使用 - CharacterBody3D需要子节点CollisionShape3D来提供碰撞,需要MeshInstance3D等提供可见性,后两者为同级;
    因MeshInstance3D无IsOnFloor()碰撞判断,且只提供可见性,故无法单独与CollisionShape3D搭配使用,可换用RigidBody3D。

JSON:
        var v = Json.ParseString(rs); //var j = new Json(); j.Parse("{}"); GD.Print(j.Data);
        // 不支持C#的as转换 - v as Godot.Collections.Dictionary;
        var dict = v.As(); GD.Print(dict);
        if (dict != null && dict.ContainsKey("msg"))
        { GD.Print(dict["msg"].As()); }