Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
资源
节点和资源
截止到本教程,我们重点研究的都是 Godot 中的 Node 类,因为它是你用来编码行为的类,引擎的大多数功能也都依赖于这个类。还有另一个同样重要的数据类型: Resource。
节点提供的是功能:绘制精灵、绘制 3D 模型、模拟物理、排列用户界面等。资源则是数据的容器,它们本身并不做任何事情:相反,节点会使用资源中包含的数据。
Godot 保存到磁盘、从磁盘读取的都是资源。资源可以是场景(.tscn 或 .scn 文件)、图像、脚本……以下是一些 Resource 的示例:
当引擎从磁盘加载某个资源时只会加载一次。如果内存中已经存在该资源的副本,那么尝试再多次加载该资源返回的都是同一个副本。资源只包含数据,因此无需制作副本。
无论是 Node 还是 Resource,只要是对象就都可以导出属性。属性的类型很多,比如 String、整数、Vector2 等,这些类型都可以成为资源。也就是说,节点和资源都可以将资源作为自身的属性:
外部资源与内置资源
保存资源的方法有两种,即:
在场景外部保存,作为单独的文件保存在磁盘上。
内置,保存在资源所附加的
.tscn或.scn文件内。
具体来说,这是在 Sprite2D 节点中的一个 Texture2D:
点击资源预览可以查看这个资源的属性。
路径属性告诉我们资源来自何处。这里就是来自一个名为 robi.png 的 PNG 图像。当资源来自这样的文件时就属于外部资源。如果你去掉这个路径,或者这个路径本来就是空的,那么它就是内置资源了。
内置资源和外部资源的转换发生在保存场景时。在上面的例子中,如果去掉路径 "res://robi.png" 并保存,那么 Godot 就会将图像保存在 .tscn 场景文件中。
备注
即便以内置资源的形式保存,多次实例化场景时,引擎也只会加载该资源的一个副本。
通过代码加载资源
使用代码加载资源的方法有两种。第一种是随时都可以使用的 load() 函数:
func _ready():
# Godot loads the Resource when it reads this very line.
var imported_resource = load("res://robi.png")
$sprite.texture = imported_resource
public override void _Ready()
{
// Godot loads the Resource when it executes this line.
var texture = GD.Load<Texture>("res://Robi.png");
var sprite = GetNode<Sprite2D>("sprite");
sprite.Texture = texture;
}
你还可以用 preload 预加载资源。与 load 不同,这个函数会在编译时读取磁盘中的文件并加载。因此,调用 preload 时无法使用可变路径:需要常量字符串。
func _ready():
# Godot loads the resource at compile-time
var imported_resource = preload("res://robi.png")
get_node("sprite").texture = imported_resource
// 'preload()' is unavailable in C Sharp.
加载场景
场景也是资源,不过有一点需要注意。保存到磁盘的场景是 PackedScene 类型的资源。场景是被打包进了一个 Resource。
必须使用 PackedScene.instantiate() 方法来获取场景的实例。
func _on_shoot():
var bullet = preload("res://bullet.tscn").instantiate()
add_child(bullet)
private PackedScene _bulletScene = GD.Load<PackedScene>("res://Bullet.tscn");
private void OnShoot()
{
Node bullet = _bulletScene.Instantiate();
AddChild(bullet);
}
这个方法会创建场景结构中的节点、对这些节点进行配置、然后返回场景的根节点。然后你就可以将它添加为其他节点的子节点了。
这种方法的好处有很多。由于 PackedScene.instantiate() 函数很快,每次新建敌人、子弹、特效等场景时就不必再从磁盘上加载了。请始终牢记,图像、网格等资源是在场景实例之间共享的。
释放资源
一个 Resource 不再被使用时就会自动释放。由于在大多数情况下 Resource 都包含在 Node 中,释放节点时,该节点所拥有的资源如果没有其他节点仍在使用,引擎就会释放这些资源。
创建自己的资源
与 Godot 中的其他 Object 一样,用户也可以为 Resource 编写脚本。资源的脚本会继承其在对象属性与序列化文本或二进制数据(*.tres、*.res)之间资源转换的能力。RefCounted 类型中的引用计数内存管理也会得到继承。
与 JSON、CSV 和自定义 TXT 文件等其他数据结构相比,资源具有许多明显的优势。用户只能将前面这些资产导入为 Dictionary(JSON)或者使用 FileAccess 解析。而资源则不同,它继承了 Object、RefCounted 和 Resource 的如下特性:
它们可以定义常量,因此无需其他数据字段或对象中的常量。
它们可以定义方法,包括属性的 setter/getter 方法。这有助于实现对底层数据的抽象和封装。如果资源脚本的结构需要更改,使用该资源的游戏则无需随之改变。
它们可以定义信号,因此资源可以触发对所管理数据更改的响应。
它们具有已定义的属性,因此用户可以 100% 确定其数据将存在。
资源的自动序列化和反序列化是 Godot 引擎的内置功能。用户无需实现自定义逻辑即可导入/导出资源文件的数据。
资源甚至可以递归地序列化子资源,这意味着用户可以设计更复杂的数据结构。
用户可以将资源保存为对版本控制友好的文本文件(*.tres)。在导出游戏时,Godot 会将资源文件序列化为二进制文件(*.res),以提高速度和压缩率。
Godot 引擎的检查器可以直接渲染和编辑资源文件。因此,用户通常无需实现自定义逻辑就可以可视化或编辑数据。要执行此操作,只需在文件系统停靠面板中双击资源文件,或在检查器中单击文件夹图标并在对话框中选择打开该文件即可。
除了基础资源类型外,它们还可以扩展其他资源类型。
Godot 让在检视器(Inspector)中创建自定义资源(Resources)变得非常简单。
在“检查器”中新建 Resource 对象,甚至可以是 Resource 派生的类型,只要脚本扩展了该类型。
将“检查器”中的
script属性设置为你的脚本。
现在检查器就会显示你所编写的 Resource 脚本中的自定义属性。编辑这些值并保存资源,检查器也会序列化这些自定义的属性!要在检查器中保存资源,请点击检查器顶部的保存图标,然后选择“保存”或“另存为...”。
如果脚本的语言支持脚本类,则可以简化该过程。仅为脚本定义名称会将其添加到“检查器”的创建对话框。这会将脚本自动添加到你创建的 Resource 对象中。
让我们来看一个例子。创建一个 Resource 然后把它命名为 bot_stats。此时文件面板中会显示全名 bot_stats.tres。不带脚本的话这个文件没什么意义,那我们就来添加一些数据和逻辑吧!给它附加一个名叫 bot_stats.gd 的脚本(或者新建一个脚本之后把它拖上来)。
备注
要让新的资源类出现在“创建资源”界面,你需要在 GDScript 中为其提供类名,或在 C# 中使用 [GlobalClass] 特性。
class_name BotStats
extends Resource
@export var health: int
@export var sub_resource: Resource
@export var strings: PackedStringArray
# Make sure that every parameter has a default value.
# Otherwise, there will be problems with creating and editing
# your resource via the inspector.
func _init(p_health = 0, p_sub_resource = null, p_strings = []):
health = p_health
sub_resource = p_sub_resource
strings = p_strings
// BotStats.cs
using Godot;
namespace ExampleProject
{
[GlobalClass]
public partial class BotStats : Resource
{
[Export]
public int Health { get; set; }
[Export]
public Resource SubResource { get; set; }
[Export]
public string[] Strings { get; set; }
// Make sure you provide a parameterless constructor.
// In C#, a parameterless constructor is different from a
// constructor with all default values.
// Without a parameterless constructor, Godot will have problems
// creating and editing your resource via the inspector.
public BotStats() : this(0, null, null) {}
public BotStats(int health, Resource subResource, string[] strings)
{
Health = health;
SubResource = subResource;
Strings = strings ?? System.Array.Empty<string>();
}
}
}
然后创建一个 CharacterBody3D,命名为 Bot,再加一个脚本,内容如下:
extends CharacterBody3D
@export var stats: Resource
func _ready():
# Uses an implicit, duck-typed interface for any 'health'-compatible resources.
if stats:
stats.health = 10
print(stats.health)
# Prints "10"
// Bot.cs
using Godot;
namespace ExampleProject
{
public partial class Bot : CharacterBody3D
{
[Export]
public Resource Stats;
public override void _Ready()
{
if (Stats is BotStats botStats)
{
GD.Print(botStats.Health); // Prints '10'.
}
}
}
}
现在选中这个名为 bot 的 CharacterBody3D 节点,将 bot_stats.tres 资源拖到检查器中。这样就会输出 10 了!很显然,这种做法能够实现比这更加高级的功能,只要你能够理解其中的原理,那么 Resource 相关的东西就是一通百通的了。
备注
资源脚本类似于 Unity 的 ScriptableObject。检查器为自定义资源提供内置支持。如果需要的话,用户甚至可以自己设计基于 Control 控件的工具脚本,将其与一个 EditorPlugin 结合,为数据创建自定义的呈现方式和编辑器。
用 Resource 脚本来模拟虚幻引擎的 DataTable 和 CurveTable 也很容易。DataTable 是将字符串映射到了自定义结构体,类似于用字典将字符串映射到次级自定义 Resource 脚本。
# bot_stats_table.gd
extends Resource
const BotStats = preload("bot_stats.gd")
var data = {
"GodotBot": BotStats.new(10), # Creates instance with 10 health.
"DifferentBot": BotStats.new(20) # A different one with 20 health.
}
func _init():
print(data)
using Godot;
[GlobalClass]
public partial class BotStatsTable : Resource
{
private Godot.Collections.Dictionary<string, BotStats> _stats = new Godot.Collections.Dictionary<string, BotStats>();
public BotStatsTable()
{
_stats["GodotBot"] = new BotStats(10); // Creates instance with 10 health.
_stats["DifferentBot"] = new BotStats(20); // A different one with 20 health.
GD.Print(_stats);
}
}
除了内联 Dictionary 值之外,还可以选择:
从电子表格导入值表并生成这些键值对。
在编辑器中设计可视化方法,创建一个简单的插件,在你打开这些类型的 Resource 时,将其添加到检查器中。
警告
请注意,资源文件(*.tres/*.res)将在文件中存储它们所使用的脚本的路径。加载时会获取并加载该脚本作为其类型的扩展。这意味着尝试指定脚本的内部类(例如在 GDScript 中使用 class 关键字)将不起作用。Godot 无法正确序列化脚本子类上的自定义属性。
在下面的示例中,Godot 将加载 Node 脚本,可以看到它扩展的并不是 Resource,然后就会发现脚本由于类型不兼容而无法加载出 Resource 对象。
extends Node
class MyResource:
extends Resource
@export var value = 5
func _ready():
var my_res = MyResource.new()
# This will NOT serialize the 'value' property.
ResourceSaver.save(my_res, "res://my_res.tres")
using Godot;
public partial class MyNode : Node
{
[GlobalClass]
public partial class MyResource : Resource
{
[Export]
public int Value { get; set; } = 5;
}
public override void _Ready()
{
var res = new MyResource();
// This will NOT serialize the 'Value' property.
ResourceSaver.Save(res, "res://MyRes.tres");
}
}