싱글톤(오토로드)(Singletons(AutoLoad))

소개

Godot의 씬 시스템은, 강력하고 유연하지만, 단점이 있습니다: 하나 이상의 씬이 필요하기 때문에 정보를 저장할 방법이 없습니다 (예: 플래이어의 점수나 인벤토리).

이러한 문제들을 해결할 방법은 있지만, 그것만의 한계점이 있습니다:

  • 다른 씬을 자식으로 불러오거나 그렇지 않을 "마스터" 씬을 사용할 수 있습니다. 하지만 그렇게 하면 바르게 작동하기 위해 더 이상 씬들을 개별적으로 실행할 수 없다는 것을 의미합니다.
  • 정보를 user:// 에 저장하고 필요할 때 불러올 수 있습니다, 하지만 자주 데이터를 저장하고 불러오는 것은 성가시고 느려질 수 있습니다.

Singleton Pattern은 씬들 사이에서 지속적인 정보를 저장하는 일반적인 사례를 해결하는 유용한 도구입니다. 우리의 경우에는 서로 다른 이름을 가진 여러가지 싱글톤을 같은 씬이나 클래스에 재사용할 수 있습니다.

이 개념을 사용해서, 다음과 같은 객체를 만드실 수 있습니다:

  • 현재 실행 중인 씬과 상관없이, 항상 불러와집니다
  • 플레이어 정보와 같은, 전역 변수를 저장할 수 있습니다
  • 씬을 바꾸는 것과 씬 전환 사이를 다룰 수 있습니다
  • GDScript가 전역 변수를 지원하지 않기 때문에 싱글톤처럼 행동합니다

Autoloading nodes and scripts can give us these characteristics.

오토로드

Node 에서 상속하는 씬이나 스크립트를 불러오기 위해 오토로드(AutoLoad)를 사용할 수 있습니다. 주석: 스크립트를 오토로드 할 떄, 노드가 만들어지고 스크립트가 거기에 붙습니다. 이 노드는 다른 씬이 불러오기 전까지 루트 뷰포트에 추가될 것입니다.

../../_images/singleton.png

씬이나 스크립트를 오토로드 하기 위해, 메뉴에서 프로젝트 -> 프로젝트 설정 을 선택하고 "오토로드" 탭으로 갑니다.

../../_images/autoload_tab.png

여기서 원하는 씬이나 스크립트를 추가하실 수 있습니다. 목록에서 각 항목에는 노드의 name 속성으로 지정하는 이름이 필요합니다. 글로벌 씬 트리에 추가된 항목의 순서는 위/아래 방향키를 사용해 조작할 수 있습니다.

../../_images/autoload_example.png

위의 경우, 어느 노드든지 "PlayerVariables" 라는 싱글톤을 액세스 할 수 있습니다:

var player_vars = get_node("/root/PlayerVariables")
player_vars.health -= 10
var playerVariables = (PlayerVariables)GetNode("/root/PlayerVariables");
playerVariables.Health -= 10; // Instance field.

"Enable" 항목이 체크 되어 있다면 (기본값은 true) 싱글톤은 쉽게 직접 액세스 될 수 있습니다:

PlayerVariables.health -= 10
// Static members can be accessed by using the class name.
PlayerVariables.Health -= 10;

씬 트리에서 다른 노드와 마찬가지로 오토로드 객체 (스크립트 혹은 씬)는 액세스됩니다. 사실, 실행하는 씬 트리를 보신다면, 오토로드 된 노드가 나타나는 것을 보실 수 있습니다:

../../_images/autoload_runtime.png

커스텀 씬 전환기

이 튜토리얼은 오토로드를 사용해 씬 전환기를 만드는 법을 설명할 것입니다. 기본 씬 전환을 위해, SceneTree.change_scene() 메서드를 사용할 수 있습니다 (자세한 점은 SceneTree(씬트리)를 참고하세요). 하지만, 씬을 전환할 때 더 복잡한 행동을 원한다면, 이 메서드가 더 많은 기능성을 제공합니다.

시작하기 위해, 여기서 템플릿을 다운로드 하시고 autoload.zip 그리고 Godot에서 여세요.

프로젝트는 두 개의 씬을 갖고 있습니다: Scene1.tscnScene2.tscn 입니다. 각 씬은 씬의 이름을 보여주는 라벨과 pressed() 시그널이 연결된 버튼이 있습니다. 프로젝트를 실행할 때, Scene1.tscn 에서 시작합니다. 하지만, 버튼을 눌러도 아무렇지 않습니다.

Global.gd

"Script" 탭으로 전환하고 Global.gd 라는 새 스크립트를 만듭니다. Node 에서 상속하도록 하십시오:

../../_images/autoload_script.png

다음은 이 스크립트를 오토로드 목록으로 추가하는 것입니다. 메뉴에서 프로젝트 > 프로젝트 설정 을 열고, "오토로드" 탭으로 가서 .. 를 클릭하거나 경로 res://Global.gd 를 입력해서 스크립트를 선택합니다. "추가"를 눌러 오토로드 목록에 추가하십시오:

../../_images/autoload_tutorial1.png

이제 언제든지 우리는 어떤 씬이 프로젝트에서 실행하든지, 이 스크립트가 항상 불러와질 것입니다.

스크립트로 돌아가서, _ready() 함수에서 현재 씬을 가져와야 합니다. (버튼이 있는) 현재 씬과 global.gd 는 둘 다 루트의 자식이지만, 오토로드 된 노드는 항상 첫 번째 입니다. 즉, 루트의 마지막 자식은 항상 불러온 씬이라는 것입니다.

extends Node

var current_scene = null

func _ready():
    var root = get_tree().get_root()
    current_scene = root.get_child(root.get_child_count() - 1)
using Godot;
using System;

public class Global : Godot.Node
{
    public Node CurrentScene { get; set; }

    public override void _Ready()
    {
        Viewport root = GetTree().GetRoot();
        CurrentScene = root.GetChild(root.GetChildCount() - 1);
    }
}

이제, 우리는 씬을 바꾸기 위한 함수가 필요합니다. 이 함수는 현재 씬을 해제하고 요청하는 것으로 바꾸어야 합니다.

func goto_scene(path):
    # This function will usually be called from a signal callback,
    # or some other function in the current scene.
    # Deleting the current scene at this point is
    # a bad idea, because it may still be executing code.
    # This will result in a crash or unexpected behavior.

    # The solution is to defer the load to a later time, when
    # we can be sure that no code from the current scene is running:

    call_deferred("_deferred_goto_scene", path)


func _deferred_goto_scene(path):
    # It is now safe to remove the current scene
    current_scene.free()

    # Load the new scene.
    var s = ResourceLoader.load(path)

    # Instance the new scene.
    current_scene = s.instance()

    # Add it to the active scene, as child of root.
    get_tree().get_root().add_child(current_scene)

    # Optionally, to make it compatible with the SceneTree.change_scene() API.
    get_tree().set_current_scene(current_scene)
public void GotoScene(string path)
{
    // This function will usually be called from a signal callback,
    // or some other function from the current scene.
    // Deleting the current scene at this point is
    // a bad idea, because it may still be executing code.
    // This will result in a crash or unexpected behavior.

    // The solution is to defer the load to a later time, when
    // we can be sure that no code from the current scene is running:

    CallDeferred(nameof(DeferredGotoScene), path);
}

public void DeferredGotoScene(string path)
{
    // It is now safe to remove the current scene
    CurrentScene.Free();

    // Load a new scene.
    var nextScene = (PackedScene)GD.Load(path);

    // Instance the new scene.
    CurrentScene = nextScene.Instance();

    // Add it to the active scene, as child of root.
    GetTree().GetRoot().AddChild(CurrentScene);

    // Optionally, to make it compatible with the SceneTree.change_scene() API.
    GetTree().SetCurrentScene(CurrentScene);
}

Using Object.call_deferred(), the second function will only run once all code from the current scene has completed. Thus, the current scene will not be removed while it is still being used (i.e. its code is still running).

마지막으로, 두 씬에 비어있는 콜백 함수를 채워야 합니다:

# Add to 'Scene1.gd'.

func _on_Button_pressed():
    Global.goto_scene("res://Scene2.tscn")
// Add to 'Scene1.cs'.

public void OnButtonPressed()
{
    var global = (Global)GetNode("/root/Global");
    global.GotoScene("res://Scene2.tscn");
}

그리고

# Add to 'Scene2.gd'.

func _on_Button_pressed():
    Global.goto_scene("res://Scene1.tscn")
// Add to 'Scene2.cs'.

public void OnButtonPressed()
{
    var global = (Global)GetNode("/root/Global");
    global.GotoScene("res://Scene1.tscn");
}

Run the project and test that you can switch between scenes by pressing the button.

주석: 씬이 작을 때, 전환은 동시에 일어납니다. 하지만 씬이 더 복잡하다면, 눈에 띄는 시간 차가 발생합니다. 어떻게 이를 다루는 지 배우기 위해, 다음 튜토리얼을 확인하세요: Background loading