资源

节点和资源

在本教程之前,我们重点研究Godot中的 Node 类,因为它是您用来编码行为的类,并且引擎的大多数功能都依赖于该类。还有另一个同样重要的数据类型: Resource

节点 为您提供功能:它们绘制精灵、3D模型、模拟物理、排列用户界面等。资源数据容器。它们自己不能做任何事情:而是,节点使用资源中包含的数据。

Anything Godot saves or loads from disk is a resource. Be it a scene (a .tscn or an .scn file), an image, a script… Here are some Resource examples: Texture, Script, Mesh, Animation, AudioStream, Font, Translation.

当引擎从磁盘加载资源时,它只加载一次。如果该资源的副本已在内存中,则每次尝试再次加载该资源将返回相同的副本。由于资源只包含数据,因此无需复制它们。

每个对象(无论是节点还是资源)都可以导出属性。属性有很多类型,例如String、integer、Vector2等,并且任何这些类型都可以成为资源。这意味着节点和资源都可以包含资源以作为属性:

../../_images/nodes_resources.png

外部与内置

有两种保存资源的方法。它们是:

  1. 外部,对于场景,作为单独文件保存在磁盘上。
  2. Built-in, saved inside the .tscn or the .scn file they’re attached to.

更具体地说,这是一个 Sprite 节点中的一个 Texture

../../_images/spriteprop.png

点击资源预览可以使我们查看和编辑资源的属性。

../../_images/resourcerobi.png

Path 属性告诉我们资源来自何处。在这里,它来自一个叫 robi.png 的PNG图像。当资源来自这样的文件时,它属于外部资源。如果您去掉这个路径或此路径为空,则它将成为内置资源。

The switch between built-in and external resources happens when you save the scene. In the example above, if you erase the path "res://robi.png" and save, Godot will save the image inside the .tscn scene file.

注解

即使您保存一个内置资源,当多次实例化一个场景时,引擎也只会加载该场景的一个副本。

从代码中加载资源

有两种方法可以从代码加载资源。首先,您可以随时使用 load() 函数:

func _ready():
        var res = load("res://robi.png") # Godot loads the Resource when it reads the line.
        get_node("sprite").texture = res
public override void _Ready()
{
    var texture = (Texture)GD.Load("res://robi.png"); // Godot loads the Resource when it reads the line.
    var sprite = (Sprite)GetNode("sprite");
    sprite.Texture = texture;
}

您也可以 预加载(preload) 资源。与 load 不同, preload 会从硬盘中读取文件,并在编译时加载它。因此,您不能使用一个变量化的路径调用预加载:您需要使用常量字符串。

func _ready():
        var res = preload("res://robi.png") # Godot loads the resource at compile-time
        get_node("sprite").texture = res
// 'preload()' is unavailable in C Sharp.

加载场景

场景也是资源, 但也有一个陷阱。保存到磁盘的场景是 PackedScene 类型的资源。该场景被打包在资源内部。

要获取场景的实例,您必须使用 PackedScene.instance() 方法。

func _on_shoot():
        var bullet = preload("res://bullet.tscn").instance()
        add_child(bullet)
private PackedScene _bulletScene = (PackedScene)GD.Load("res://bullet.tscn");

public void OnShoot()
{
    Node bullet = _bulletScene.Instance();
    AddChild(bullet);
}

此方法在场景的层次结构中创建节点,对其进行配置,然后返回场景的根节点。然后,您可以将其添加为任何其他节点的子级。

该方法有几个优点。由于 PackedScene.instance() 函数速度相当快,您可以创建新的敌人、子弹、效果等,而无需每次都从磁盘再次加载它们。请记住,像往常一样,图像、网格等都是在场景实例之间共享的。

释放资源

资源(Resource) 不再使用时,它将自动释放自己。由于在大多数情况下,资源包含在节点中,因此当您释放节点时,如果没有其他节点使用该节点拥有的所有资源,则引擎也会释放它们。

创建自己的资源

像Godot中的任何Object一样,用户也可以编写资源脚本。资源脚本继承了在对象属性和序列化文本或二进制数据( *.tres*.res)之间自由转换的能力。它们还从 Reference 类型继承引用计数内存管理。

与其他替代数据结构(如JSON、CSV或自定义TXT文件)相比,它具有许多明显的优势。用户只能将这些资源导入为 Dictionary (JSON)或要解析的 File。将资源区分开来的是它们对 ObjectReference、和 Resource 功能的继承:

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

警告

资源(Resources)字典(Dictionaries) 都是通过引用传递的,但是只有 资源(Resources) 是引用计数的。这意味着,如果在对象之间传递一个 字典(Dictionary),并且删除第一个对象,则所有其他对象对该 字典(Dictionary) 的引用都将无效。相反,直到删除 所有 对象之前,不会从内存中释放 资源(Resources)

extends Node

class MyObject:
    extends Object
    var dict = {}

func _ready():
    var obj1 = MyObject.new()
    var obj2 = MyObject.new()
    obj1.dict.greeting = "hello"
    obj2.dict = obj1.dict             # 'obj2.dict' now references 'obj1's Dictionary.
    obj1.free()                       # 'obj1' is freed and the Dictionary too!
    print(obj2.dict.greeting)         # Error! 'greeting' index accessed on null instance!

    # To avoid this, we must manually duplicate the Dictionary.
    obj1 = MyObject.new()
    obj1.dict.greeting = "hello"
    obj2.dict = obj1.dict.duplicate() # Now we are passing a copy, not a reference.
    obj1.free()                       # obj2's Dictionary still exists.
    print(obj2.dict.greeting)         # Prints 'hello'.

Godot 使在属性检查器面板中创建自定义 资源(Resources) 变得很容易。

  1. 在属性检查器面板中创建一个普通的 资源(Resources) 对象。只要您的脚本扩展了该类型,它甚至可以是派生 资源(Resource) 的类型。
  2. 将属性检查器中的 script 属性设置为您的脚本。

现在,属性检查器将显示 资源(Resource) 脚本的自定义属性。如果编辑这些值并保存资源,则属性检查器也会序列化自定义属性!要从属性检查器中保存资源,请点击属性检查器的工具菜单(右上角),然后选择 保存另存为...

如果脚本的语言支持 脚本类型,则可以简化该过程。仅为脚本定义名称会将其添加到属性检查器的创建对话框。这会将脚本自动添加到您创建的 资源(Resource) 对象中。

让我们看一些示例。

# bot_stats.gd
extends Resource
export(int) var health
export(Resource) var sub_resource
export(Array, String) var strings

func _init(p_health = 0, p_sub_resource = null, p_strings = []):
    health = p_health
    sub_resource = p_sub_resource
    strings = p_strings

# bot.gd
extends KinematicBody

export(Resource) var stats

func _ready():
    # Uses an implicit, duck-typed interface for any 'health'-compatible resources.
    if stats:
        print(stats.health) # Prints '10'.
// BotStats.cs
using System;
using Godot;

namespace ExampleProject {
    public class BotStats : Resource
    {
        [Export]
        public int Health { get; set; }

        [Export]
        public Resource SubResource { get; set; }

        [Export]
        public String[] Strings { get; set; }

        public BotStats(int health = 0, Resource subResource = null, String[] strings = null)
        {
            Health = health;
            SubResource = subResource;
            Strings = strings ?? new String[0];
        }
    }
}

// Bot.cs
using System;
using Godot;

namespace ExampleProject {
    public class Bot : KinematicBody
    {
        [Export]
        public Resource Stats;

        public override void _Ready()
        {
            if (Stats != null && Stats is BotStats botStats) {
                GD.Print(botStats.Health); // Prints '10'.
            }
        }
    }
}

注解

资源(Resource) 脚本类似于Unity的 ScriptableObjects。属性检查器为自定义资源提供内置支持。如果需要的话,用户甚至可以设计自己的基于 控制(Control) 的工具脚本,并将它们与一个 EditorPlugin 结合起来,以为他们的数据创建自定义的可视化和编辑器。

虚幻4引擎的 数据表(DataTables)CurveTables 也很容易使用 资源(Resource) 脚本重新创建。数据表(DataTables) 是映射到自定义结构的字符串,类似于将字符串映射到辅助自定义 资源(Resource) 脚本的 字典(Dictionary)

# 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 System;
using Godot;

public class BotStatsTable : Resource
{
    private Godot.Dictionary<String, BotStats> _stats = new Godot.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) 值,还可以选择…

  1. 从电子表格导入值表并生成这些键值对,或者…
  2. 在编辑器中设计可视化,并创建一个简单的插件,可在当您打开这些类型的 资源(Resources) 时,将其添加到属性检查器中。

CurveTables 是相同的东西,除了映射到一个浮点值数组或一个 Curve/Curve2D 资源对象之外。

警告

请注意,资源文件(*.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("res://my_res.tres", my_res)
using Godot;

public class MyNode : Node
{
    public 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://MyRes.tres", res);
    }
}