# 从Unity到Godot **Repository Path**: blog_rika/from-unity-to-godot ## Basic Information - **Project Name**: 从Unity到Godot - **Description**: 如果你熟悉Unity,通过这篇文章可以快速学习Godot。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 33 - **Forks**: 2 - **Created**: 2023-01-20 - **Last Updated**: 2024-05-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 从 Unity 到 Godot 最后编写: `2023-1-20` `迈向黎明` 。 本文使用 Obsidian 加了一堆插件编写,Git 仓库的显示效果经过代码转换,与 PDF 略有差异(PDF 更好看一些) ## 你好 想学 Godot 吗,是不是厌倦从 if 讲起的初学者教程? 既然你来看了这篇文章,那么你一定是个 Unity 大佬吧。 那么好,咱们来以 Unity 的角度快速入门 Godot,省去繁琐的基础教程吧。 ### 本文适用对象 1. 想学 Godot (废话)。 2. 对 Unity 有一定了解。 3. 会 C# 基础语法,理解封装、继承、多态即可(或许不理解也行?)。 4. 启动过 Godot,琢磨时长大于 20 分钟。 > Unity 可以不会,但不建议 0 编程基础的看,文中不会着重讲解代码语法和逻辑。 ### 关于 这篇文章看似是教程,其实是我的个人学习笔记,长得比较像教程而已 233。 我对 Godot 了解不深,文章是边学边写的,出现错误感谢指正。我的联系 QQ `2293840045` > 感谢来自 Godot中文社区 的 [Life](https://godoter.cn/u/life) 指出的问题,现已修改 **单例模式实现** 章节,添加了 **唯一名称** 章节。 既然都用 Godot 了,当然要好好体验独特的 GDScript,本篇暂时不涉及 Godot 支持的其他语言。 > Godot 支持的语言是真的多,仅我知道的就有 C#、Rust 甚至冷门的 nim > 写到一半忽然感叹...上个新坑为啥要用 unity ### 参考环境 本文写于 2023 年 1 月 20 日,当时我用的是 `Unity 2021.3.16` 和 `Godot v4.0 beta 十几`。祝你阅读时 Godot 依旧兼容本文内容。 ## U 与 G 的重要区别 要说 UG 之间的最大区别,肯定是 Godot 开源、Unity 闭源(大部分),但讲这些对于咱们初学阶段影响不大,所以直接来看 UG 两者构成游戏的方式吧: 一般情况下,Unity 构成游戏的基本元素如下: - 场景(Scenes) - 预制体、物体(GameObject) - 脚本、组件(Component) - 资源(Assets) 而 Godot 只有下面这些东西: - 节点(Node) - 脚本(Script) - 资源(Resource) ### 节点 可以把节点理解成 Unity 的组件,只不过**一个游戏物体只能绑定一个组件**,通过多个这种单组件物体组合成一个复合物体。 现在假设,要创建一个带有物理效果的小球,并且让它发光,那么: ![img](images/Pasted%20image%2020230119235906.png) Unity 那边就不解释了。 Godot 中的物体父子关系和 Unity 相同:父物体移动会带着子物体移动。因此,将 **CollisionShape、MeshInstance、OmniLight** 放到 **Rigidbody** 的子级中即可构成一个移动的发光小球。 > Unity 中,每个物体都必须记录**位置、角度、缩放**,因此 **Transform** 是 GameObject 的必备组件。 > > Godot 中,每个节点也需要记录**位置、角度、缩放**,因此 Godot 的节点都继承了 **Node3D** 或 **Node2D** 节点,也就是 Godot 中记录位置、角度、缩放的东西。 按照 `Godot 节点` 等于 `Unity 仅有一个组件的物体` 的逻辑,可以在 Unity 中用下面方式再做一次这个发光小球: ![img](images/Pasted%20image%2020230120001326.png) 当然,把内置组件这样用太奇怪了,但假设是你在 Unity 中开发的某个超高级次世代对话系统: ![img](images/Pasted%20image%2020230120002112.png) 由于系统太牛逼太复杂了,文本框、说话者头像、菜单按钮、特效层都需要单独的组件来控制,那么这样做就好像合理了一些。 ### 脚本 Unity 的脚本也是组件,Godot 则不同。脚本可以绑定在一个节点上,同时一个节点也只能绑定一个脚本。选中任意一个节点,然后看它的属性列表最下面: ![img](images/Pasted%20image%2020230120001518.png) 这个 Script 字段就是这个节点的脚本了。 如果你打开了一个脚本文件,会看到几个函数直接摆放到了文件里,没有 class 啥的,也就是说,Godot 中一个文件就是一个 class。 > 但是 class 里可以嵌套 class ### 资源 终于到了熟悉的东西,Godot 的资源类似 Unity,就是放到项目目录里就行了,只不过 Godot 的项目目录更简单。 项目根目录里有一些 `.` 开头的文件夹,那些地方用来存放项目设置等引擎要用的东西,就类似 Unity 的 Packages、UserSettings 等文件夹,还包含类似 Unity 中的 `.meta` 文件的东西。 ## 从 C# 到 GDScript > 如果你会 Python,那么恭喜你可以开启八倍速模式了,毕竟 GDScript 语法风格极像 Python。 ### 变量、类型 Godot 在使用变量前需要先声明,使用关键字 `var`: ```python var playerName = "Rika" var girlfriend = null print(playerName) # print 就是 godot 的输出了。 # 不是说好像 python 么,怎么成 c# 了 ? ``` **playerName** 变量明显是个字符串类型,在 Godot 中用 `String` 表示,而 **girlfriend** 是个空变量。 那么下面来列举一下 Godot 常用变量类型: | 类型 | 解释 | | -----------------------:| ------------------------------------------- | | String | | | bool、int、float | | | Vector2、Vector3、Color | | | Variant | 任意类型 | | Array | 等于 C# 的 `List` 和 Python 的 `[]` | | Dictionary | 等于 C# 的 `Dictionary` 和 Python 的 `{ ||` | | Object | 大多数对象的基类(不是节点的基类) | 想要判断一个变量的类型呢: ```python if typeof("HAHA") == TYPE_STRING : pass ``` ### 类型转换 基本类型的转换同,使用 `目标类型(原始数据)` 的方式进行转换: ```python int("666") float("123.123") ``` String 不是基本类型,需要使用 `str(原始数据)` 进行转换: ```python str(111) ``` > Godot 中的非 String 数据在字符串拼接的时候不能自动转换成 String。 > > 错误:`"你有钱:" + 0` > > 正确:`"你有钱:" + str(0)` > > 不过好在一般不会这样拼接字符串,而是用下面会介绍的字符串格式化。 ### 强类型变量、类型注解 如果你是第一次接触弱类型语言,可能会感觉有点小爽(并不,语法补全屎一样)。 而我认为,编程界最伟大的两项发明:`TypeScript` 和 `Python类型注解`。 如果你也是一个深陷弱类型泥潭的老将,那么太好了,Godot 支持类型注解: ```python var haha:int = 123 haha = "awd" # Error: String 不能转换成 int 类型。 ``` 给变量 **haha** 后面加了个冒号和 **int**,那么 **haha** 变量就成了一个 **int** 类型的了,他就只能存放整数。 下面多看几个例子: ```python var arg:int = 22 var items:Array = [1,2,"3",false] var friends:Array[String] = ["You","Self"] var KV:Dictionary = { "A":1,"B":2 } ``` 其中 **friends** 变量的类型 **Array** 后面加了一个 `[String]`,者可以看作是 C# 中的泛型,就是说 **friends** 必须是字符串组成的数组。 > 我就奇了怪了,**Dictionary** 怎么就不支持泛型。 ### 字符串格式化 照搬 Python,在字符串后使用 `%` 运算符: ```python "你好 %s,你有 %d 块钱。" % ["Rika",0] # 结果:你好 Rika,你有 0 块钱 # 如果参数只有一个,% 后面可以不用数组: "呵呵$s" %s "..." # 呵呵... ``` 继续深度照搬 Python,根据占位符名称进行格式化: ```python "你好 {name},你有 {money} 块钱。".format({ "name":"Rika", "money":0 }) # 效果同上 ``` ### 代码块 GDScript(和 Python)采用缩进式语法表示代码块,而不是 C# 中的 `{` 和 `}` 花括号。 缩进相同且相邻的多行代码就是一个代码块。 假设现在有个 C# 格式的 if: ```csharp if(a >= 2){ print("a 大于等于 2"); a += 12; } print("if end"); ``` 转换成 GDScript: ```python if a >= 2: print("a 大于等于 2") a += 12 print("if end") ``` 在 GDScript 中,`if` 语句首先省去了圆括号,然后在条件后面使用一个冒号 `:` 表示后面是一个语句块,然后下面的 **print** 和 **+=** 运算都比 `if` 语句多缩进了一个 tab,因此他俩是 `if` 语句块的内容,而最后一个 **print** 不是。 > 注意缩进使用的符号,tab 和空格是不一样的,至于使用哪个可以凭喜好。 > > 代码块中的空行和注释可以没有缩进或缩进不同,不影响代码行。 ### 控制语句 控制语句就还是那些,直接来看看各种控制语句的写法吧(python 同学可以直接跳过): ```python # if .. elif .. else if a >= b: pass elif a == 0: pass else: pass # while while 5 != 6: pass # for var arr = ["A","B","C"] for i in arr: print(i) # 输出 A B C # range 方法返回一个迭代器 for i in range(3): print(i) # 输出 0 1 2 # match ,这就是个不用写 break 的 switch var a = 2 match(a): 1: print("a 是个 1") 2: print("a 是个 2") _: print("a 不是 1 也不是 2") ``` 嗯,挺好理解的吧,等等,为什么随便打的占位符 `pass` 被语法高亮了? > 给你看一下屎一样的三元运算符:`print("活着" if hp>0 else "寄了")`` ### 代码块占位符 有时候不知为何要写一些奇怪的东西,例如下面例子: ```python if hp <= 0: # 寄了,怎么办?TODO: 以后再说吧。 else: 我还活着() ``` 看似没问题,但却迎来了一个错误,因为 `if` 后面找不到代码块,毕竟注释不算代码。 这个时候就可以先用一个关键字 `pass` 顶着,表示这是个空代码块: ```python if hp <= 0: # 虽然寄了然后什么也没干,但是不报错了 pass ``` 当然这个写法不仅用于 `if`,在循环、方法定义、`class` 定义等地方都可以用。 ### 方法 之前说过一个脚本就是一个 `class`,所以可以直接在脚本里定义方法: ```python # Godot4.0 beta15 版本开始支持中文标识符 func 我这个方法有两个参数(这个是参数, 这个也是参数): pass ``` 嗯,学会了吧,现在加上类型注解?: ```python func 我帮你计算字符串长度(字符串:String) -> int: return len(字符串) ``` 嗯,又学会了吧,现在加上可选参数?: ```python # 可以算个寂寞 func 我帮你计算字符串长度(字符串:String = "") -> int: return len(字符串) ``` 嗯,又又学会了吧,暂时想不到还有啥了。 ### class 定义方式不想解释,直接看代码: ```python # 使用extends关键字表示继承 class MyNode extends Node: class InnerClass: func testFunc(): print('我在里头') # 比Python高级,在class里面用var定义成员变量 var value:int func testFunc(): print("我是 MyNode") func _init(): # 我是构造方法 print("诞生了一个 MyNode 示例") func _to_string() -> String: # 我是 ToString return "MyNode[value = %d]" % value ``` 采用 `类.new()` 来实例化: ```python var myNode:MyNode = MyNode.new() # 诞生了一个 MyNode 示例 myNode.value = 123 print(myNode) # MyNode[value = 123] var innerClass:MyNode.InnerClass = MyNode.InnerClass.new() ``` ### 脚本也是class 之前说过脚本也是 `class`,那么这个 `class` 叫啥,继承啥? 这些需要在脚本中声明: ```python extends Node # 继承自 Node class_name Abc # 叫做 abc ``` 这样操作之后,在其他的脚本里面就可以用 **Abc** 这个名字指代上面那个脚本了。 > Godot 内置编辑器有时会缺少代码补全,遇到没补全的情况,不一定是代码错了,建议运行一下看看是否正常。 ## 脚本 ### 创建脚本 & 应用 Godot 的脚本有两种存储方式,一种和 Unity 相同,作为代码文件存在资源目录里,另一种可以将文本代码直接存储到节点上,省去了在资源中管理脚本的工作,缺点是这样的脚本就不能复用了。 对着一个节点右键,点击添加脚本即可看见创建脚本的窗口: ![img](images/Pasted%20image%2020230120054255.png) 上图是我对着一个 **Node2D** 节点创建脚本时的弹窗,注意继承选项,也是 **Node2D**,然后我们点击创建后,就会得到一个继承自 **Node2D** 的脚本。 为啥要重点说继承自 **Node2D** 呢,因为此时,这个 **Node2D** 节点其实就已经不是 **Node2D** 节点了(我在说什么?),这个节点其实已经变成了咱们这个脚本的实例,就好像在 Unity 中继承自一个组件去写了一个新组件一样。 Godot 的脚本正是采用这种继承方式去访问节点属性的,因此,这里的父类不能乱选,例如不能给一个 **Node2D** 节点加一个继承自 **Rigidbody3D** 的脚本。 一般这个父类就选节点本身的类型即可,但如果为了让脚本可以复用在更多节点上,也可以让脚本继承自这些节点的共同父类,当然,这会导致脚本中不能直接访问那些节点子类独有的成员。 继承组件的父类不会导致组件子类丧失效果,感觉他们的关系有点像这样: ![img](images/Pasted%20image%2020230120062549.png) > title: 内置脚本转换成独立脚本 > 找到节点的 Script 属性,对着后面的脚本右键,选择**保存**即可。 ### 暴露属性 当初刚会编程去玩 Unity 的时候,最震惊我的事情竟然是......`public` 的变量能在 Unity 界面上显示出来! 作为同行,咱 Godot 也能,只要在成员定义的 `var` 关键字前面加上 `@export`: ```python @export var Name:String = "" # 开头的 @ 符号是我这个版本的 Godot4 加上的,如果你是 Godot3 或未来的 Godot4,可能需要去掉 @ 符号。 ``` 然后在节点属性面板的最上面就能看到: ![img](images/Pasted%20image%2020230120020611.png) ### 生命周期 关于生命周期应该不用过多解释了,三个常用周期对照表: | Unity 生命周期 | Godot 生命周期 | | -------------- | -------------- | | Start | `_ready()` | | Update | `_process(delta:float)` | | FixedUpdate | `_physics_process(delta:float)` | 详细的周期可以看官方文档:[Node节点 Methods](https://docs.godotengine.org/en/stable/classes/class_node.html?highlight=Node#methods) 这些生命周期方法直接写在脚本里就行了,其中两个 `process` 方法的参数就是两帧间隔时间,也就是 Unity 中的 `Time.DeltaTime`。 ## 信号 ### Event?! 信号这个名字可能一听就蒙蒙的,其实就是 C# 中的 `event`,如果做过 WinForm 开发一定非常熟悉。(Godot 做 UI 的时候真的感觉就像在用 WinForm) Unity 的 **UGUI** 和 **InputSystem** 也用信号这个东西,只不过 Unity 里叫 **Event**,就是这个东西: ![img](images/Pasted%20image%2020230120063915.png) 默认布局下,Godot 的信号面板和节点属性面板在同一个位置: ![img](images/Pasted%20image%2020230120064051.png) 点进去就会看到各种密密麻麻的信号,双击一个信号就可以指定连接对象了,和 Unity 基本相同,这里就不继续讲解了。 > 信号面板旁边还有个分组面板,感觉有点类似 Unity 的 Tag ### 从代码连接信号 直接代码加注释解释吧: ```python extends Button func _ready(): # 使用 connect 连接信号 # pressed 信号会在按钮点击时触发 pressed.connect(self.OnPressed) # GDScript 的 self 就是 C# 的 this 关键字 func OnPressed(): print("Hello") pressed.disconnect(self.OnPressed) ``` > pressed 是 Button 的属性,如果有需求,还可以用字符串指定信号名,使用 `connect(name, fun)` 和 `disconnect(name, fun)`,同时去掉前面的信号属性。 > 如果脚本就是简单的监听按钮点击,其实可以直接用 `_pressed` 生命周期方法。 ### 自定义信号 在脚本中使用 `signal` 关键字定义信号,可选添加小括号与参数类型注解: ```python extends Button signal testSignal(a:float) signal testSignalWithOutParams func _ready(): testSignal.connect(OnPressed) func _process(delta): # 触发信号 testSignal.emit(delta) func OnPressed(v): print("Hello %f" % v) ``` 此时再看这个按钮的信号面板: ![img](images/Pasted%20image%2020230120071030.png) ```ad-attention emit 方法不做类型检查,但是若接收端参数不一致会报错。 ``` > 再来认识个函数:`emit(name, args...)`,通过字符串触发信号,而且后续参数是变长的。 ## 节点 ### 场景呢?预制体呢? > 不知读者是否熟悉 html,如今前端框架发达,浏览器切换 url 往往不用刷新页面了,而是只刷新部分 html 标签,我感觉 Godot 就有这种前端的味道。 Godot 中的节点可以保存成资源,和 Unity 的预制体起到同样的作用,对着节点列表中的某个节点右键,即可把节点保存: ![img](images/Pasted%20image%2020230120021323.png) > 也可以和 Unity 保存预制体一样,从大纲视图直接拖到左下角的资源视图即可。 注意了,这个东西叫做 `把分支保存为场景`,也就是说,这个"**预制体**"可以当作场景来用。(其实这里我觉得没必要这么翻译,直接说 `保存节点树` 就挺好的) 如果咱把之前的发光小球保存成场景,那么发光小球场景其实就是 Unity 的预制体,创建出来就是生成了一个发光小球。 如果把发光小球滚来滚去的场景保存下来,那么就成了真正的场景,创建出来并把之前的场景删了,那就是切换场景了。 > Unity 中也可以在一个场景下,通过实例化和删除物体的方式做到切换地图的效果。 ### 到底谁做根节点 本节标题正是我刚接触 Godot 时的最大疑惑,当要创建一个游戏物体时,我的个人习惯如下: - 如果要创建的物体具有物理效果:**Rigidbody** 做根节点。 - 如果没有物理效果:随缘。 父级其实也就影响子级移动旋转,因此产生运动的 **Rigidbody** 做根节点最合适。 不过有一点需要注意,Godot 中某些节点有明确的父子级关系要求,例如 **Area3D** 必须要求子级有 **Collision** 类节点,因此创建物体时要熟悉所用到的各种节点,根据他们的依赖关系选择父子级。 ### 获取节点 使用 `get_node(节点路径:String)` 获取节点,参数是节点路径,这个路径可以是于当前节点的相对路径,也可以是用 `/root/` 开头表示绝对路径,现假设有一个这样的场景: ![img](images/Pasted%20image%2020230120071843.png) 在 Player 节点上执行 `get_node`,下面列举几个节点路径的获取结果: | 路径 | Node | | ----------------------- | -------------------------------------- | | . | Player 自己 | | Image | Player 下面的 Image 节点 | | ./Image | 同上 | | ./Area/CollisionShape2D | 最里面那个方块图标的节点 | | /root/Node2D | 第一个节点 Node2D | | .. | Player 的父级,也就是第一个节点 Node2D | | ../Control | 下面那个图标是绿色圈圈的节点 | > 路径不支持任何的模糊匹配。 > > `/root` 会得到一个 Window 对象,而不是最顶层的节点。 由于获取节点这个操作太常用了,Godot 就设置了个语法糖,使用美元符号 `$` 即可直接代替 `get_node` 方法调用: `$FirePosition` 等于 `get_node("FirePosition")` `$/root/Node2D/Control` 等于 `get_node("/root/Node2D/Control")` 如果路径包含特殊符号导致语法出错,也可以把 `$` 后面的东西用字符串表示:`$"../Control"` 等于 `get_node("../Control")` > 可以调用其他节点的 get_node,实现从其他位置作为起点获取节点。 ### 唯一名称 可以将场景中名称不重复的节点标记为**唯一名称**,方便在代码中获取它: ![img](images/Pasted%20image%2020230121195105.png) 之后就能看到整个节点多了一个 % 小标记: ![img](images/Pasted%20image%2020230121195124.png) 在代码中通过 `%【节点名称】` 语法糖来获取这种拥有**唯一名称**的节点: ```python var 我是那个Button = %Button ``` 当然这样的节点不能重名,毕竟他叫**唯一**名称节点。 > 感觉类似 html 中的 id 属性。 > `get_node` 方法也支持获取唯一名称节点:`get_node("%Button")`。 > > godot 3 不支持 % 语法糖,只能用 `get_node("%Button")` 或 `$"%Button"` ### 实例化节点、删除节点 如果只是操作一个简单的节点,可以直接: ```python # 创建节点 var n = Node2D.new() # 添加到场景中(作为当前节点的子级) add_child(n) # 删除节点 # n.free() ``` > 还有个 `remove_child` 方法也能实现删除节点的效果,但它并不释放内存,还能把节点重新 add 到场景中。 当然我觉得大家更想要 Unity 中实例化预制体那种效果,于是咱么可以这样写: ```python @export var NewNodes:PackedScene func Get(): var n:Sprite2D = NewNodes.instantiate(); add_child(n) # n.free() ``` `PackedScene` 类型表示保存在资源里面的节点,它的 `instantiate` 方法可以把这些节点实例化出来,这个方法的返回值是那堆节点的根节点,上面例子中根节点是个 Sprite2D。 ## API ### 获取输入 Unity 最近引入了 **InputSystem**,不过 Godot 的输入系统更类似 Unity 的传统输入。 在 Godot 界面菜单栏中点击【项目】, 点击【项目设置】,进入【键位映射】选项卡,就能看到 Godot 的键位管理界面了。 ![img](images/Pasted%20image%2020230120080400.png) > 和 Unity 一样,Godot也内置了很多键位映射,不过需要点开右上角的 【Show Built-in Actions】 才能看到。 当你在这里创建好需要的键位后,在代码中使用 `Input.get_action_strength("映射名称")` 即可获取一个 0 到 1 的 `float`,也就是按键的状态。 如果想要在 `_process` 中检测按键刚刚按下或刚刚抬起,也就是 Unity 中的 `Input.GetKeyDown` 和 `GetKeyUp`,则使用 `Input.is_action_just_pressed("XXX")` 和 ` Input.is_action_just_released("XXX") ` `Input` 里包含很多“人如其名”的方法,这里不一一介绍,比较有意思的是有俩 `get_vector` 和 `get_axis` 方法可以快速获取成对的输入,例如方向移动等。 > 如果你想完全掌控输入,可以尝试下 `_input` 生命周期,这可以绕过键位映射这些东西。 ### 持久化数据 使用 `FileAccess` 类进行文件操作。 > 在Godot3中可能是`File`类 `FileAccess` 类的静态方法 `open` 可用于打开文件: ```python var f = FileAccess.open("user://test.txt",FileAccess.WRITE) ``` > title: Godot 的文件路径 > > Godot 采用文件沙箱机制,FileAccess操作的文件路径需要使用 `user://` 或 `res://` 开头来表示用户目录和资源目录,例如上文的 `user://test.txt` 就是读取用户目录的数据。 > > `res://`就是指Godot的项目目录了,这个目录的文件在游戏打包后是只读的。 > > `user://` 在 Windows 下默认是 `%APPDATA%\Godot\app_userdata\项目名称` 这个目录,可以在项目设置中修改(需要开启项目设置的高级选项): > > ![img](images/Pasted%20image%2020230120224245.png) > > 这样的话文件就会存到 `%APPDATA%\Hello` 目录。 FileAccess 实例有 `store_string` 方法用来向文件追加字符串,`flush` 方法将内容写入到磁盘,`get_as_text` 方法返回整个文件内容,接下来就可以自行发挥存储数据的格式了。 顺便再看一下 Godot 的 JSON 操作吧: ```python var a = JSON.stringify({ "haha":false }) print(a) # {"haha":false} var t = JSON.parse_string('{"test":123}') print(t.test) # 123 ``` > 当然要是觉得纯文本存数据不好用,也可以尝试带有类型的存储方式,FileAccess 有很多 `store_` 开头的方法用来存储各种类型,以及 `get_` 开头的方法读取各种类型数据。 > > 不过感觉用起来不够灵活,我就没详细研究,因此就先不介绍了。 ### 游戏设置 在左上角菜单中点击【项目】【项目设置】即可看到项目设置面板,当然这个界面没什么好讲的,咱来看看怎么用代码访问这里面的选项: ```python # 这是窗口高度字段的路径 var path = "display/window/size/viewport_height"; # 获取窗口高度 var height:int = ProjectSettings.get_setting(path) # 设置窗口高度 ProjectSettings.set_setting(path,height + 10) # 保存设置 ProjectSettings.save() ``` 那么哪个窗口高度字段怎么得到呢?看图: ![img](images/Pasted%20image%2020230120230245.png) > 不过,运行之后你会发现窗口并不能变大,这是因为 `ProjectSettings` 只是存放设置的地方,而不负责这些设置产生的效果。 > > 不过咱已经 `save` 了修改后的窗口高度,重启游戏后就能看到窗口变大了。 > 应该你也看到了,项目设置窗口上面有`添加`和`删除`两个按钮,也就是说你可以在这里添加自己的项目设置。 ### 单例模式实现 来到 GDScript 后某些习惯的东西就不太会写了,其中对于游戏开发最常用的应该就是单例设计模式。 Godot 提供了一种实现单例的方法,点击菜单栏中的【项目】【项目设置】进入【Autoload】选项卡,这里可以添加一些脚本,他们会在游戏开始时自动实例化,并作为 `/root` 的子节点添加到场景中。 > 正因为这个脚本要作为子节点添加到场景中,所以他必须要继承自 `Node` 类 现在咱来写个单例脚本: ```python extends Node class_name TestClass var myname:String = "Rika" func hello(): print("Hello " + myname) ``` 然后添加到 Autoload: ![img](images/Pasted%20image%2020230120232122.png) 现在就可以在代码中使用**Autoload 中的名称**,也就是**TC**直接引用这个实例: ```python print(TC) # TC: TC.hello() # Hello Rika ``` > 要开启**Autoload**中的全局变量选项才能在代码中直接获取这个实例。 > > 因为这个单例是根节点的子节点,所以也可以通过 `get_node("/root/TC")` 找到它。 ## 本文后续 文章顶部说过,本文其实不是个真正的教程,只是我的个人学习笔记长得想教程。 因此,本文的后续更新取决于我的学习进度。 如果认为某些内容需要修改,或想给我提出任何建议,欢迎联系我或使用本仓库的 Issue 功能! > 目前可能的后续内容: > - GDScript 反射 > - 插件开发? > - 试试 Godot C#