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

소개

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

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

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

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

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

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

노드와 스크립트를 Autoload하는 것은 위와 같은 기능을 제공합니다.

오토로드

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.

If the "Enable" column is checked (default true) then the singleton can simply be accessed directly:

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

The next step is to add this script to the autoLoad list. Open Project > Project Settings from the menu, switch to the "AutoLoad" tab and select the script by clicking the browse button or typing its path: res://Global.gd. Press "Add" to add it to the autoload list:

../../_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);
}

Object.call_deferred() 를 사용한다면 두 번째 기능은 현재 씬의 코드가 모두 종료되었을 때 실행된다. 따라서, 현재 씬은 그것이 사용되고 있을 때 까지 제거되지 않을 것이다.

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

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

프로젝트를 실행하고 버튼을 누를 때 씬이 바뀌는지 확인해보세요.

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