리소스

노드와 리소스

지금까지, Node 클래스는 Godot에서 가장 중요한 데이터 형식으로 엔진 내 대부분의 기능과 작동이 이것으로 이루어 졌다는 것에 초점을 두었습니다. 이와 똑같이 중요한 데이터 형식이 있습니다: Resource 입니다.

노드는 다음과 같은 기능을 제공합니다: 스프라이트를 그려주고 3D 모델링을 해주며 물리 시뮬레이션을 해주고, 유저 인터페이스 정리 등을 해줍니다. 리소스데이터 컨테이너입니다. 리소스 그 자체로는 아무 일도 하지 않습니다: 대신에 노드가 리소스에 포함되어 있는 데이터를 사용합니다.

Godot에서 디스크에 저장하고 불러오는 모든 것이 리소스입니다. 씬이나 (.tscn 이나 .scn 파일), 이미지, 스크립트... 리소스 예제들입니다: Texture, Script, Mesh, Animation, AudioStream, Font, Translation.

리소스가 디스크에서 불러올 때, 항상 한번만 불러옵니다. 즉, 메모리에서 이미 불러온 리소스의 복사본이 있다면, 다시 리소스를 불러 오려고 해도 같은 복사본을 계속해서 반환될 것입니다. 리소스는 오직 데이터 컨테이너로 복사할 필요가 없습니다.

노드가 될 수 있는 모든 대상은 속성을 내보낼 수 있습니다. 속성은 문자열, 정수, Vector2, 등과 같은 많은 타입이 될 수 있고, 그 유형들 중 하나가 리소스가 될 수 있습니다. 이는 노드와 리소스가 모두 리소스를 속성으로 가질 수 있다는 것을 의미합니다:

../../_images/nodes_resources.png

외부(External) vs 내장(built-in)

리소스를 저장하는 두 가지 방법이 있습니다. 이렇게 될 수 있습니다:

  1. 씬의 **외부**에 개별 파일로 저장.
  2. *.tscn 이나 *.scn 파일 내부에 첨부하여 씬에 **내장**하여 저장.

보다 구체적으로는, Sprite 노드의 Texture 가 있습니다:

../../_images/spriteprop.png

리소스 미리보기를 클릭해서 리소스를 보고 속성을 편집합니다.

../../_images/resourcerobi.png

경로 속성은 리소스가 어디에서 오는 지를 알려줍니다. 이 경우에 리소스는 robi.png라 부르는 PNG 이미지에서 왔습니다. 리소스가 이와 같은 파일에서 올 때, 그것은 외부 리소스 입니다. 경로를 지우거나 경로가 비어있다면, 그것은 내장 리소스가 됩니다.

내장과 외부 리소스 간의 전환은 씬을 저장할 때 발생합니다. 위의 예시에서, 경로 `"res://robi.png"`를 지우고 저장한다면 Godot는 .tscn 씬 파일 안에 이미지를 저장합니다.

주석

씬을 여러 번 인스턴스 할 때, 내장 리소스를 저장하더라도 엔진은 오직 하나의 사본으로만 불러옵니다.

코드에서 리소스를 불러오기

코드에서 리소스를 불러오는 두 가지 방법이 있습니다. 첫 번째로, 언제든지 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 와는 다르게, 이 함수는 디스크에서 파일을 읽고 컴파일 시간에 파일을 불러옵니다. 그 결과, 변수 경로로 미리 불러오기를 호출할 수 없습니다: 상수 문자열을 사용해야 합니다.

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);
}

이 메서드는 씬의 계층 구조에 노드들을 만들고, 그들을 구성하고, 씬의 루트 노드로 반환합니다. 그런 뒤 이것을 다른 노드의 자식으로 추가할 수 있습니다.

The approach has several advantages. As the PackedScene.instance() function is fast, you can create new enemies, bullets, effects, etc. without having to load them again from disk each time. Remember that, as always, images, meshes, etc. are all shared between the scene instances.

리소스 해제(Free)하기

리소스가 더 이상 사용되지 않을 때, 그것은 자동으로 스스로를 해제(Free)합니다. 대부분의 경우, 리소스는 노드, 스크립트 혹은 다른 리소스에 포함되어 있기 때문에, 노드를 해제할 때, 엔진은 이 노드 외에 다른 노드가 더 이상 쓰지 않는 리소스도 해제합니다.

자신의 리소스 만들기

Godot에서 어느 객체와 마찬가지로, 사용자는 리소스를 작성할 수 있습니다. 리소스 스크립트는 객체 속성간의 자유로운 변환과 텍스트 또는 이진 데이터 (/.tres, /.res)를 직렬화하는 기능을 상속합니다. 그리고 참조 타입으로부터 참조 계산 메모리 관리를 상속합니다.

JSON, CSV, 혹은 커스텀 TXT 파일로 이루어진 이것은 대체 데이터 구조를 넘어서 많은 뚜렷한 이점을 제공합니다. 사용자는 이 에셋을 문법 분석하기 위해 Dictionary (JSON) 혹은 File로만 가져올 수 있습니다. 리소스는 Object, Reference, 그리고 Resource 기능의 상속을 통해 구분합니다:

  • 그들은 상수를 정의할 수 있기 때문에, 다른 데이터 필드나 객체의 상수는 필요하지 않습니다.
  • 그들은 속성을 위한 setter/getter 메서드를 포함한 메서드를 정의할 수 있습니다. 이것으로 기본 데이터의 추출과 캡슐화를 할 수 있습니다. 리소스 스크립트의 구조가 변경되어야 하더라도, 리소스를 사용하는 게임이 변경되지 않아도 됩니다.
  • 그들은 시그널을 정의할 수 있기 때문에, 리소스는 관리한 데이터의 변경에 대한 응답을 트리거 할 수 있습니다.
  • 그들은 속성을 정의하기 때문에, 사용자는 데이터가 존재한다는 것을 100% 압니다.
  • 리소스 자동 직렬화와 비 직렬화는 Godot 엔진 내장 기능입니다. 사용자는 리소스 파일의 데이터를 가져오기/내보내기 위해 커스텀 로직을 구현할 필요가 없습니다.
  • 리소스는 심지어 하위 리소스를 재귀적으로 직렬화 할 수 있습니다, 즉 사용자는 훨씬 더 정교한 데이터 구조를 설계할 수 있습니다.
  • 사용자는 리소스를 버전 제어 친화적 텍스트 파일 (*.tres)로 저장할 수 있습니다. 게임을 내보낼 때, Godot는 리소스 파일을 이진 파일 (*.res)로 직렬화 하여 속도와 압축을 증가시킵니다.
  • Godot 엔진의 인스펙터는 리소스 파일을 즉시 렌더링하고 편집합니다. 이와 같이, 사용자는 종종 데이터를 시각화 하거나 편집하기 위한 커스텀 로직을 구현할 필요가 없습니다. 그러기 위해서, 파일 시스템 독에서 리소스를 더블 클릭하거나 인스펙터에서 폴더 아이콘을 클릭하고 대화 상자에서 파일을 엽니다.
  • 그들은 기본 리소스 외에도 다른 리소스 타입도 확장할 수 있습니다.

경고

Resources and Dictionaries are both passed by reference, but only Resources are reference-counted. This means that if a Dictionary is passed between objects and the first object is deleted, all other objects' references to the Dictinoary will be invalidated. Conversely, Resources will not be freed from memory until all the objects are deleted.

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는 인스펙터에서 커스텀 리소스를 만들기 쉽게 합니다.

  1. 인스펙터에서 순수한 리소스를 만듭니다. 스크립트가 해당 타입을 확장하는 동안, 이것은 심지어 리소스를 파생하는 타입일 수 있습니다.
  2. 인스펙터에서 script 속성을 당신의 스크립트로 설정합니다.

인스펙터는 이제 리소스 스크립트의 커스텀 속성을 보여줍니다. 이 값을 편집하고 리소스를 저장하면, 인스펙터는 커스텀 속성 역시 직렬화 합니다! 인스펙터에서 리소스를 저장하기 위해, 인스펙터의 도구 메뉴 (우측 상단)를 클릭하고, "저장하기"나 "다른 이름으로 저장..."을 선택합니다.

스크립트의 언어가 스크립트 클래스를 지원한다면, 프로세스가 간소화됩니다. 스크립트에 이름만 정의하는 것으로 인스펙터의 생성 대화 상자에 리소스를 추가할 것입니다. 이렇게 하면 생성된 리소스 객체에 스크립트가 자동으로 추가됩니다.

몇 가지 예를 살펴보겠습니다.

# 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'.
            }
        }
    }
}

주석

리소스 스크립트는 유니티의 ScriptableObjects(스크립트 가능한 객체)와 유사합니다. 인스펙터는 커스텀 리소스를 위한 내장 지원을 제공합니다. 그래도 원한다면, 사용자는 자신만의 제어 기반 도구 스크립트를 설계하고 그들을 EditorPlugin으로 결합하여 커스텀 시각화와 데이터를 위한 편집기를 만들 수 있습니다.

언리얼 엔진 4의 DataTables와 CurveTables 또한 리소스 스크립트로 쉽게 재생성 할 수 있습니다. DataTables은 커스텀 구조체에 매핑된 문자열로, 이차적인 커스텀 리소스 스크립트에 문자열을 매핑하는 딕셔너리와 유사합니다.

# 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);
    }
}

딕셔너리 값을 inlining(인라이닝)하는 대신, 또 다른 할 수 있는 대안으로...

  1. 스프레드 시트에서 테이블 값을 가져오고 키 값 쌍을 생성합니다, 혹은...
  2. 에디터 내에서 시각화를 설계하고 해당 타입 리소스를 열 때 인스펙터에 리소스를 추가하는 간단한 플러그인을 제작합니다.

두 가지 방법이 항상 호환되는 것은 아닙니다. 컨테이너는 자식을 제어하기 때문에, 레이아웃 메뉴를 그것들에게 사용할 수 없습니다. 각 컨테이너는 특정한 효과를 갖고 있으므로 당신은 제대로 동작하는 인터페이스를 얻기 위해 그들을 중첩(nest)할지도 모릅니다. 레이아웃 접근을 사용하는 경우 자식들을 아래계층에서 위계층 방향으로 작업합니다. 씬에 추가적이 컨테이너를 사용하지 않으므로 더 깨끗한 계층 구조를 만들 수 있지만, 항목을 행이나 열, 격자 등으로 배열하기는 더 어렵습니다.

경고

리소스 파일 (*.tres/*.res)이 파일에서 사용하는 스크립트의 경로를 저장한다는 점에 주의하세요. 스크립트를 불러올 때, 이 스크립트를 가져와서 해당 타입의 확장으로 불러옵니다. 다시 말해 하위 클래스, 즉 스크립트의 내부 클래스 (GDScript에서 class 키워드를 사용하는 것)를 할당할 수 없습니다. Godot는 스크립트 하위 클래스에 커스텀 속성을 정확하게 직렬화 하지 않을 것입니다.

아래 예시에서, Godot는 Node 스크립트를 불러오고, Resource를 확장(extend)하지 않음을 확인합니다, 그런 다음 타입이 맞지 않으면 스크립트가 리소스 객체에 대해 불러오지 못했음을 확인합니다.

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);
    }
}