资源
节点和资源
截止到本教程,我们重点研究的都是 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方法. 这允许对基础数据进行抽象和封装. 如果资源脚本的结构需要更改, 则使用资源的游戏则不必更改.它们可以定义信号, 因此
Resources可以触发对所管理数据更改的响应.它们具有已定义的属性, 因此用户知道其数据将100%存在.
资源自动序列化和反序列化是一个Godot引擎的内置功能. 用户无需实现自定义逻辑即可导入/导出资源文件的数据.
资源甚至可以递归地序列化子资源, 这意味着用户可以设计更复杂的数据结构.
用户可以将资源保存为版本控制友好的文本文件(
*.tres). 导出游戏后,Godot将资源文件序列化为二进制文件(*.res), 以提高速度和压缩率.Godot 引擎的检查器开箱即用地渲染和编辑资源文件。这样,用户通常不需要实现自定义逻辑即可可视化或编辑其数据。为此,请在文件系统面板中双击资源文件,或在检查器中点击文件夹图标,然后在对话框中打开该文件。
它们可以扩展除基本 Resource 之外的其他资源类型。
Godot 可以轻松地在“检查器”中创建自定义资源。
在“检查器”中新建 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");
}
}