4 Star 33 Fork 2

BLOG/从Unity到Godot

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
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 指出的问题,现已修改 单例模式实现 章节,添加了 唯一名称 章节。

既然都用 Godot 了,当然要好好体验独特的 GDScript,本篇暂时不涉及 Godot 支持的其他语言。

Godot 支持的语言是真的多,仅我知道的就有 C#、Rust 甚至冷门的 nim

写到一半忽然感叹...上个新坑为啥要用 unity

参考环境

本文写于 2023 年 1 月 20 日,当时我用的是 Unity 2021.3.16Godot v4.0 beta 十几。祝你阅读时 Godot 依旧兼容本文内容。

U 与 G 的重要区别

要说 UG 之间的最大区别,肯定是 Godot 开源、Unity 闭源(大部分),但讲这些对于咱们初学阶段影响不大,所以直接来看 UG 两者构成游戏的方式吧:

一般情况下,Unity 构成游戏的基本元素如下:

  • 场景(Scenes)
  • 预制体、物体(GameObject)
  • 脚本、组件(Component)
  • 资源(Assets)

而 Godot 只有下面这些东西:

  • 节点(Node)
  • 脚本(Script)
  • 资源(Resource)

节点

可以把节点理解成 Unity 的组件,只不过一个游戏物体只能绑定一个组件,通过多个这种单组件物体组合成一个复合物体。

现在假设,要创建一个带有物理效果的小球,并且让它发光,那么:

img

Unity 那边就不解释了。

Godot 中的物体父子关系和 Unity 相同:父物体移动会带着子物体移动。因此,将 CollisionShape、MeshInstance、OmniLight 放到 Rigidbody 的子级中即可构成一个移动的发光小球。

Unity 中,每个物体都必须记录位置、角度、缩放,因此 Transform 是 GameObject 的必备组件。

Godot 中,每个节点也需要记录位置、角度、缩放,因此 Godot 的节点都继承了 Node3DNode2D 节点,也就是 Godot 中记录位置、角度、缩放的东西。

按照 Godot 节点 等于 Unity 仅有一个组件的物体 的逻辑,可以在 Unity 中用下面方式再做一次这个发光小球:

img

当然,把内置组件这样用太奇怪了,但假设是你在 Unity 中开发的某个超高级次世代对话系统:

img

由于系统太牛逼太复杂了,文本框、说话者头像、菜单按钮、特效层都需要单独的组件来控制,那么这样做就好像合理了一些。

脚本

Unity 的脚本也是组件,Godot 则不同。脚本可以绑定在一个节点上,同时一个节点也只能绑定一个脚本。选中任意一个节点,然后看它的属性列表最下面:

img

这个 Script 字段就是这个节点的脚本了。

如果你打开了一个脚本文件,会看到几个函数直接摆放到了文件里,没有 class 啥的,也就是说,Godot 中一个文件就是一个 class。

但是 class 里可以嵌套 class

资源

终于到了熟悉的东西,Godot 的资源类似 Unity,就是放到项目目录里就行了,只不过 Godot 的项目目录更简单。

项目根目录里有一些 . 开头的文件夹,那些地方用来存放项目设置等引擎要用的东西,就类似 Unity 的 Packages、UserSettings 等文件夹,还包含类似 Unity 中的 .meta 文件的东西。

从 C# 到 GDScript

如果你会 Python,那么恭喜你可以开启八倍速模式了,毕竟 GDScript 语法风格极像 Python。

变量、类型

Godot 在使用变量前需要先声明,使用关键字 var

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 大多数对象的基类(不是节点的基类)

想要判断一个变量的类型呢:

if typeof("HAHA") == TYPE_STRING :
	pass

类型转换

基本类型的转换同,使用 目标类型(原始数据) 的方式进行转换:

int("666")
float("123.123")

String 不是基本类型,需要使用 str(原始数据) 进行转换:

str(111)

Godot 中的非 String 数据在字符串拼接的时候不能自动转换成 String。

错误:"你有钱:" + 0

正确:"你有钱:" + str(0)

不过好在一般不会这样拼接字符串,而是用下面会介绍的字符串格式化。

强类型变量、类型注解

如果你是第一次接触弱类型语言,可能会感觉有点小爽(并不,语法补全屎一样)。

而我认为,编程界最伟大的两项发明:TypeScriptPython类型注解

如果你也是一个深陷弱类型泥潭的老将,那么太好了,Godot 支持类型注解:

var haha:int = 123
haha = "awd" # Error: String 不能转换成 int 类型。

给变量 haha 后面加了个冒号和 int,那么 haha 变量就成了一个 int 类型的了,他就只能存放整数。

下面多看几个例子:

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,在字符串后使用 % 运算符:

"你好 %s,你有 %d 块钱。" % ["Rika",0]
# 结果:你好 Rika,你有 0 块钱

# 如果参数只有一个,% 后面可以不用数组:
"呵呵$s" %s "..." # 呵呵...

继续深度照搬 Python,根据占位符名称进行格式化:

"你好 {name},你有 {money} 块钱。".format({
	"name":"Rika",
	"money":0
})
# 效果同上

代码块

GDScript(和 Python)采用缩进式语法表示代码块,而不是 C# 中的 {} 花括号。

缩进相同且相邻的多行代码就是一个代码块。

假设现在有个 C# 格式的 if:

if(a >= 2){
	print("a 大于等于 2");
	a += 12;
}
print("if end");

转换成 GDScript:

if a >= 2:
	print("a 大于等于 2")
	a += 12
print("if end")

在 GDScript 中,if 语句首先省去了圆括号,然后在条件后面使用一个冒号 : 表示后面是一个语句块,然后下面的 print+= 运算都比 if 语句多缩进了一个 tab,因此他俩是 if 语句块的内容,而最后一个 print 不是。

注意缩进使用的符号,tab 和空格是不一样的,至于使用哪个可以凭喜好。

代码块中的空行和注释可以没有缩进或缩进不同,不影响代码行。

控制语句

控制语句就还是那些,直接来看看各种控制语句的写法吧(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 "寄了")``

代码块占位符

有时候不知为何要写一些奇怪的东西,例如下面例子:

if hp <= 0:
	# 寄了,怎么办?TODO: 以后再说吧。
else:
	我还活着()

看似没问题,但却迎来了一个错误,因为 if 后面找不到代码块,毕竟注释不算代码。

这个时候就可以先用一个关键字 pass 顶着,表示这是个空代码块:

if hp <= 0:
	# 虽然寄了然后什么也没干,但是不报错了
	pass

当然这个写法不仅用于 if,在循环、方法定义、class 定义等地方都可以用。

方法

之前说过一个脚本就是一个 class,所以可以直接在脚本里定义方法:

# Godot4.0 beta15 版本开始支持中文标识符
func 我这个方法有两个参数(这个是参数, 这个也是参数):
	pass

嗯,学会了吧,现在加上类型注解?:

func 我帮你计算字符串长度(字符串:String) -> int:
	return len(字符串)

嗯,又学会了吧,现在加上可选参数?:

# 可以算个寂寞
func 我帮你计算字符串长度(字符串:String = "") -> int:
	return len(字符串)

嗯,又又学会了吧,暂时想不到还有啥了。

class

定义方式不想解释,直接看代码:

# 使用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() 来实例化:

var myNode:MyNode = MyNode.new() # 诞生了一个 MyNode 示例
myNode.value = 123
print(myNode) # MyNode[value = 123]
var innerClass:MyNode.InnerClass = MyNode.InnerClass.new()

脚本也是class

之前说过脚本也是 class,那么这个 class 叫啥,继承啥?

这些需要在脚本中声明:

extends Node # 继承自 Node
class_name Abc # 叫做 abc

这样操作之后,在其他的脚本里面就可以用 Abc 这个名字指代上面那个脚本了。

Godot 内置编辑器有时会缺少代码补全,遇到没补全的情况,不一定是代码错了,建议运行一下看看是否正常。

脚本

创建脚本 & 应用

Godot 的脚本有两种存储方式,一种和 Unity 相同,作为代码文件存在资源目录里,另一种可以将文本代码直接存储到节点上,省去了在资源中管理脚本的工作,缺点是这样的脚本就不能复用了。

对着一个节点右键,点击添加脚本即可看见创建脚本的窗口:

img

上图是我对着一个 Node2D 节点创建脚本时的弹窗,注意继承选项,也是 Node2D,然后我们点击创建后,就会得到一个继承自 Node2D 的脚本。

为啥要重点说继承自 Node2D 呢,因为此时,这个 Node2D 节点其实就已经不是 Node2D 节点了(我在说什么?),这个节点其实已经变成了咱们这个脚本的实例,就好像在 Unity 中继承自一个组件去写了一个新组件一样。

Godot 的脚本正是采用这种继承方式去访问节点属性的,因此,这里的父类不能乱选,例如不能给一个 Node2D 节点加一个继承自 Rigidbody3D 的脚本。

一般这个父类就选节点本身的类型即可,但如果为了让脚本可以复用在更多节点上,也可以让脚本继承自这些节点的共同父类,当然,这会导致脚本中不能直接访问那些节点子类独有的成员。

继承组件的父类不会导致组件子类丧失效果,感觉他们的关系有点像这样:

img

title: 内置脚本转换成独立脚本 找到节点的 Script 属性,对着后面的脚本右键,选择保存即可。

暴露属性

当初刚会编程去玩 Unity 的时候,最震惊我的事情竟然是......public 的变量能在 Unity 界面上显示出来!

作为同行,咱 Godot 也能,只要在成员定义的 var 关键字前面加上 @export

@export var Name:String = ""
# 开头的 @ 符号是我这个版本的 Godot4 加上的,如果你是 Godot3 或未来的 Godot4,可能需要去掉 @ 符号。

然后在节点属性面板的最上面就能看到:

img

生命周期

关于生命周期应该不用过多解释了,三个常用周期对照表:

Unity 生命周期 Godot 生命周期
Start _ready()
Update _process(delta:float)
FixedUpdate _physics_process(delta:float)

详细的周期可以看官方文档:Node节点 Methods

这些生命周期方法直接写在脚本里就行了,其中两个 process 方法的参数就是两帧间隔时间,也就是 Unity 中的 Time.DeltaTime

信号

Event?!

信号这个名字可能一听就蒙蒙的,其实就是 C# 中的 event,如果做过 WinForm 开发一定非常熟悉。(Godot 做 UI 的时候真的感觉就像在用 WinForm)

Unity 的 UGUIInputSystem 也用信号这个东西,只不过 Unity 里叫 Event,就是这个东西: img

默认布局下,Godot 的信号面板和节点属性面板在同一个位置:

img

点进去就会看到各种密密麻麻的信号,双击一个信号就可以指定连接对象了,和 Unity 基本相同,这里就不继续讲解了。

信号面板旁边还有个分组面板,感觉有点类似 Unity 的 Tag

从代码连接信号

直接代码加注释解释吧:

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 关键字定义信号,可选添加小括号与参数类型注解:

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

emit 方法不做类型检查,但是若接收端参数不一致会报错。

再来认识个函数:emit(name, args...),通过字符串触发信号,而且后续参数是变长的。

节点

场景呢?预制体呢?

不知读者是否熟悉 html,如今前端框架发达,浏览器切换 url 往往不用刷新页面了,而是只刷新部分 html 标签,我感觉 Godot 就有这种前端的味道。

Godot 中的节点可以保存成资源,和 Unity 的预制体起到同样的作用,对着节点列表中的某个节点右键,即可把节点保存:

img

也可以和 Unity 保存预制体一样,从大纲视图直接拖到左下角的资源视图即可。

注意了,这个东西叫做 把分支保存为场景,也就是说,这个"预制体"可以当作场景来用。(其实这里我觉得没必要这么翻译,直接说 保存节点树 就挺好的)

如果咱把之前的发光小球保存成场景,那么发光小球场景其实就是 Unity 的预制体,创建出来就是生成了一个发光小球。

如果把发光小球滚来滚去的场景保存下来,那么就成了真正的场景,创建出来并把之前的场景删了,那就是切换场景了。

Unity 中也可以在一个场景下,通过实例化和删除物体的方式做到切换地图的效果。

到底谁做根节点

本节标题正是我刚接触 Godot 时的最大疑惑,当要创建一个游戏物体时,我的个人习惯如下:

  • 如果要创建的物体具有物理效果:Rigidbody 做根节点。
  • 如果没有物理效果:随缘。

父级其实也就影响子级移动旋转,因此产生运动的 Rigidbody 做根节点最合适。

不过有一点需要注意,Godot 中某些节点有明确的父子级关系要求,例如 Area3D 必须要求子级有 Collision 类节点,因此创建物体时要熟悉所用到的各种节点,根据他们的依赖关系选择父子级。

获取节点

使用 get_node(节点路径:String) 获取节点,参数是节点路径,这个路径可以是于当前节点的相对路径,也可以是用 /root/ 开头表示绝对路径,现假设有一个这样的场景:

img

在 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

之后就能看到整个节点多了一个 % 小标记:

img

在代码中通过 %【节点名称】 语法糖来获取这种拥有唯一名称的节点:

var 我是那个Button = %Button

当然这样的节点不能重名,毕竟他叫唯一名称节点。

感觉类似 html 中的 id 属性。

get_node 方法也支持获取唯一名称节点:get_node("%Button")

godot 3 不支持 % 语法糖,只能用 get_node("%Button")$"%Button"

实例化节点、删除节点

如果只是操作一个简单的节点,可以直接:

# 创建节点
var n = Node2D.new()
# 添加到场景中(作为当前节点的子级)
add_child(n)
# 删除节点
# n.free()

还有个 remove_child 方法也能实现删除节点的效果,但它并不释放内存,还能把节点重新 add 到场景中。

当然我觉得大家更想要 Unity 中实例化预制体那种效果,于是咱么可以这样写:

@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

和 Unity 一样,Godot也内置了很多键位映射,不过需要点开右上角的 【Show Built-in Actions】 才能看到。

当你在这里创建好需要的键位后,在代码中使用 Input.get_action_strength("映射名称") 即可获取一个 0 到 1 的 float,也就是按键的状态。

如果想要在 _process 中检测按键刚刚按下或刚刚抬起,也就是 Unity 中的 Input.GetKeyDownGetKeyUp,则使用 Input.is_action_just_pressed("XXX")Input.is_action_just_released("XXX")

Input 里包含很多“人如其名”的方法,这里不一一介绍,比较有意思的是有俩 get_vectorget_axis 方法可以快速获取成对的输入,例如方向移动等。

如果你想完全掌控输入,可以尝试下 _input 生命周期,这可以绕过键位映射这些东西。

持久化数据

使用 FileAccess 类进行文件操作。

在Godot3中可能是File

FileAccess 类的静态方法 open 可用于打开文件:

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

这样的话文件就会存到 %APPDATA%\Hello 目录。

FileAccess 实例有 store_string 方法用来向文件追加字符串,flush 方法将内容写入到磁盘,get_as_text 方法返回整个文件内容,接下来就可以自行发挥存储数据的格式了。

顺便再看一下 Godot 的 JSON 操作吧:

var a = JSON.stringify({
	"haha":false
})
print(a) # {"haha":false}
var t = JSON.parse_string('{"test":123}')
print(t.test) # 123

当然要是觉得纯文本存数据不好用,也可以尝试带有类型的存储方式,FileAccess 有很多 store_ 开头的方法用来存储各种类型,以及 get_ 开头的方法读取各种类型数据。

不过感觉用起来不够灵活,我就没详细研究,因此就先不介绍了。

游戏设置

在左上角菜单中点击【项目】【项目设置】即可看到项目设置面板,当然这个界面没什么好讲的,咱来看看怎么用代码访问这里面的选项:

# 这是窗口高度字段的路径
var path = "display/window/size/viewport_height";
# 获取窗口高度
var height:int = ProjectSettings.get_setting(path)
# 设置窗口高度
ProjectSettings.set_setting(path,height + 10)
# 保存设置
ProjectSettings.save()

那么哪个窗口高度字段怎么得到呢?看图:

img

不过,运行之后你会发现窗口并不能变大,这是因为 ProjectSettings 只是存放设置的地方,而不负责这些设置产生的效果。

不过咱已经 save 了修改后的窗口高度,重启游戏后就能看到窗口变大了。

应该你也看到了,项目设置窗口上面有添加删除两个按钮,也就是说你可以在这里添加自己的项目设置。

单例模式实现

来到 GDScript 后某些习惯的东西就不太会写了,其中对于游戏开发最常用的应该就是单例设计模式。

Godot 提供了一种实现单例的方法,点击菜单栏中的【项目】【项目设置】进入【Autoload】选项卡,这里可以添加一些脚本,他们会在游戏开始时自动实例化,并作为 /root 的子节点添加到场景中。

正因为这个脚本要作为子节点添加到场景中,所以他必须要继承自 Node

现在咱来写个单例脚本:

extends Node
class_name TestClass

var myname:String = "Rika"
func hello():
	print("Hello " + myname)

然后添加到 Autoload:

img

现在就可以在代码中使用Autoload 中的名称,也就是TC直接引用这个实例:

print(TC) # TC:<Node#26289897900>
TC.hello() # Hello Rika

要开启Autoload中的全局变量选项才能在代码中直接获取这个实例。

因为这个单例是根节点的子节点,所以也可以通过 get_node("/root/TC") 找到它。

本文后续

文章顶部说过,本文其实不是个真正的教程,只是我的个人学习笔记长得想教程。

因此,本文的后续更新取决于我的学习进度。

如果认为某些内容需要修改,或想给我提出任何建议,欢迎联系我或使用本仓库的 Issue 功能!

目前可能的后续内容:

  • GDScript 反射
  • 插件开发?
  • 试试 Godot C#

空文件

简介

如果你熟悉Unity,通过这篇文章可以快速学习Godot。 展开 收起
取消

发行版

暂无发行版

贡献者

全部

近期动态

不能加载更多了
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/blog_rika/from-unity-to-godot.git
git@gitee.com:blog_rika/from-unity-to-godot.git
blog_rika
from-unity-to-godot
从Unity到Godot
master

搜索帮助