씬 조직

이 문서는 효과적인 씬 내용 조직에 관련된 주제를 다룹니다. 어떤 노드가 사용되어야 할까? 어디에 그들을 배치해야할까? 어떻게 그들을 상호작용할까?

효과적으로 관계를 짓는 방법

Godot 사용자는 그들만의 씬을 제작하려 했을 때, 종종 다음과 같은 문제점에 봉착했습니다:

사용자들은 먼저 첫 씬을 만들고 거기에 내용물을 넣습니다, 이후 씬을 재 사용할 수 있는 조각들로 나눠야 하는 섬뜩한 기운이 그들에게 엄습할 줄은 몰랐을 것입니다. 사용자들은 씬 분기들을 그들의 씬에 저장합니다. 하지만 그 뒤 그들은 이제까지 의지해온 어려운 참고가 더 이상 효과가 없다는 것을 알게됩니다. 씬을 여러 방면에서 재 사용할 수 있는 것이 문제를 일으켰는데, 노드 경로가 대상을 찾지 못하기 때문입니다. 편집기에서 설정된 시그널 연결이 끊어집니다.

이 문제를 해결하기 위해, 환경에 대한 세부 정보가 필요없는 하위 씬을 인스턴스화해야 합니다. 하위 씬은 쓰이는 방식에 따라 까다롭게 만들어지진 않는다는 신뢰를 가져야 합니다.

객체 기반 프로그래밍에서 고려해야할 가장 큰 점은 집중된 단일 목적의 클래스를 코드베이스의 다른 부분과의 느슨한 연결(loose coupling)을 유지하는 것입니다. 이것으로 객체의 크기를 (유지 보수성을 위해) 작게 유지하며 재사용성을 높여줍니다, 완전한 논리를 다시 짤 필요가 없어집니다.

이런 객체 지향 프로그래밍 모범 사례는 씬 구조와 스크립트 사용의 모범 사례에 있어 여러 가지 파급 효과가 있습니다.

가능하다면 의존성이 없는 씬을 설계해야합니다. 이는 즉, 외부로 의존하지 않아 모든 것을 유지하는 씬을 만들어야합니다.

씬이 외부 컨텍스트와 상호작용해야 한다면, 경험있는 개발자들은 의존성 주입사용을 권장합니다. 이 기술은 하이 레벨 API를 사용해 로우 레벨 API의 의존성을 제공하는 것과 관련이 있습니다. 왜 이럴까요? 외부 환경에 의존하는 클래스가 우연히 버그를 일으키거나 예측하지 못한 행동을 할 수 있기 때문입니다.

이러기 위해, 데이터를 드러낸 다음 초기화하기 위해 부모 컨텍스트에 의존하게 해야합니다:

  1. 시그널에 연결합니다. 매우 안전하지만 행동에 "응답"하는 데에만 사용해야하고, 시작에는 쓰면 안됩니다. 시그널 이름은 보통 과거 시제 동사를 사용합니다, "entered", "skill_activated", 혹은 "item_collected"처럼 말이죠.

    # Parent
    $Child.connect("signal_name", object_with_method, "method_on_the_object")
    
    # Child
    emit_signal("signal_name") # Triggers parent-defined behavior.
    
    // Parent
    GetNode("Child").Connect("SignalName", ObjectWithMethod, "MethodOnTheObject");
    
    // Child
    EmitSignal("SignalName"); // Triggers parent-defined behavior.
    
  2. 메서드를 호출합니다. 시작 동작에 사용됩니다.

    # Parent
    $Child.method_name = "do"
    
    # Child, assuming it has String property 'method_name' and method 'do'.
    call(method_name) # Call parent-defined method (which child must own).
    
    // Parent
    GetNode("Child").Set("MethodName", "Do");
    
    // Child
    Call(MethodName); // Call parent-defined method (which child must own).
    
  3. FuncRef 속성을 초기화합니다. 소유권이 불필요한 메서드보다 안전합니다. 시작 동작에 사용됩니다.

    # Parent
    $Child.func_property = funcref(object_with_method, "method_on_the_object")
    
    # Child
    func_property.call_func() # Call parent-defined method (can come from anywhere).
    
    // Parent
    GetNode("Child").Set("FuncProperty", GD.FuncRef(ObjectWithMethod, "MethodOnTheObject"));
    
    // Child
    FuncProperty.CallFunc(); // Call parent-defined method (can come from anywhere).
    
  4. 노드나 다른 객체 참조를 초기화합니다.

    # Parent
    $Child.target = self
    
    # Child
    print(target) # Use parent-defined node.
    
    // Parent
    GetNode("Child").Set("Target", this);
    
    // Child
    GD.Print(Target); // Use parent-defined node.
    
  5. 노드 경로를 초기화합니다.

    # Parent
    $Child.target_path = ".."
    
    # Child
    get_node(target_path) # Use parent-defined NodePath.
    
    // Parent
    GetNode("Child").Set("TargetPath", NodePath(".."));
    
    // Child
    GetNode(TargetPath); // Use parent-defined NodePath.
    

이런 설정은 자식 노드로부터의 접근 소스를 숨깁니다. 이것은 차례대로 자식을 환경에 느슨하게 결합합니다. API에 다른 추가 변경사항 없이 다른 컨텍스트에 재 사용할 수 있습니다.

주석

위의 예제는 부모 자식 관계를 설명하고 있지만, 모든 객체 관계에 이 원리를 적용할 수 있습니다. 형제 노드는 그들의 계층 구조만 알아야 하며 부모 노드가 그들의 상호작용과 참조를 조정해야 합니다.

# Parent
$Left.target = $Right.get_node("Receiver")

# Left
var target: Node
func execute():
    # Do something with 'target'.

# Right
func _init():
    var receiver = Receiver.new()
    add_child(receiver)
// Parent
GetNode<Left>("Left").Target = GetNode("Right/Receiver");

public class Left : Node
{
    public Node Target = null;

    public void Execute()
    {
        // Do something with 'Target'.
    }
}

public class Right : Node
{
    public Node Receiver = null;

    public Right()
    {
        Receiver = ResourceLoader.Load<Script>("Receiver.cs").New();
        AddChild(Receiver);
    }
}

다른 객체에 종속하는 노드가 아닌 객체도 같은 원리가 적용됩니다. 어떤 객체든지 사실 객체들을 소유하며 그들 간의 관계를 관리해야 합니다.

경고

외부 컨텍스트에 종속성을 배치하듯이 데이터를, 심지어 느슨하게 결합된 것도 작업 내 (씬 내부)에 유지하는 것이 좋지만, 여전히 노드가 환경의 무언가가 참이라는 것을 생각하는 것입니다. 프로젝트의 디자인 철학은 문제가 발생하기 전에 이를 예방하는 것입니다. 그렇지 않으면, 코드의 고유 책임이 개발자에게 넘어가 아주 작은 규모에서 객체 관계를 추적하기 위해 문서를 사용하게 될 것입니다; 달리 말해 개발 지옥으로 알려져 있습니다. 외부 문서를 사용하여 코드를 안전하게 사용하는 코드 작성은 기본적으로 오류가 발생하기 쉽습니다.

그러한 문서를 작성하고 유지하는 것을 피하려면, 종속 노드 (위의 "child")를 tool 스크립트로 변환하여 _get_configuration_warning()을 구현해야 합니다. 여기서 비어있지 않은 문자열을 반환하는 것으로 씬 독은 노드에 의해 툴팁 문자열과 함께 경고 아이콘을 생성합니다. Area2D에서 자식 CollisionShape2D 노드가 없을 때 나타나는 그런 아이콘과 같습니다. 그런 다음 편집기는 씬을 스크립트 코드를 통해 자체 문서화합니다. 문서를 통한 콘텐츠 중복은 없습니다.

이런 GUI는 프로젝트 사용자에게 노드에 대한 중요한 정보를 더 잘 전달할 수 있습니다. 외부 종속성을 갖고 있나요? 그러한 종속성을 만족시켰나요? 다른 프로그래머들, 특히 디자이너와 작가들에게 노드에 무엇을 구성해야 하는 지를 메시지로 명확하게 설명해야 할 것입니다.

그럼 이런 밑장빼기 식의 작업을 왜 할까요? 음, 씬은 홀로 작동할 때 가장 잘 작동하기 때문입니다. 홀로 작동하지 않는 경우, 익명으로 다른 것들과 작동합니다 (최소한의 하드 종속성, 예: 느슨한 결합). 뜻밖의 방법으로 씬과 상호작용하기 위해 클래스가 씬에게 피할 수 없는 변경을 만든다면, 작업은 무너집니다. 한 클래스로의 변경은 다른 클래스에 영향을 끼칠 수 있습니다.

스크립트와 씬은, 엔진 클래스의 확장 프로그램으로 모든 객체 지향 프로그래밍 원칙을 준수해야 합니다. 예시는 다음을 포함합니다...

노드 트리 구조 선택하기

따라서 개발자는 게임을 작업하기 전에 게임에 있어서의 막대한 가능성을 막을 수 있습니다. 그들은 그들이 작업하고 싶은 것, 그들이 갖고 싶은 시스템을 알고 있을 것입니다, 하지만 어디에 그것들을 넣을 수 있을까요? 음, 게임을 만드는 것은 항상 그들에게 달려있습니다. 노드 트리는 무수한 방법으로 만들 수 있습니다. 하지만 확실하지 않은 사람들을 위해, 이 유용한 가이드가 시작하기에 알맞은 구조 샘플을 보여드리겠습니다.

게임은 항상 일종은 "진입 지점"을 갖습니다; 때로는 개발자들이 어디서 진입 지점이 시작하는지 명확하게 추적하고 그렇게 해서 다른 곳에서도 그 논리를 따르게 할 수 있습니다. 이 지점은 프로그램에서 조감 시점으로 모든 다른 데이터와 논리를 전달하는 곳이기도 합니다. 전통적인 애플리케이션의 경우, 이것은 "main" 함수가 될 것입니다. 이 경우에는, Main 노드가 될 것입니다.

  • 노드 "Main" (main.gd)

main.gd 스크립트는 게임의 주된 컨트롤러 역할을 할 것입니다.

그런 다음 실제 게임 내 "World" (2D나 3D)를 갖습니다. Main의 자식이 될 수 있습니다. 그리고 프로젝트에 필요한 다양한 메뉴와 위젯을 관리하는 게임용 기본 GUI가 필요합니다.

  • 노드 "Main" (main.gd)
    • Node2D/Spatial "World" (game_world.gd)
    • Control "GUI" (gui.gd)

레벨을 변경할 때, "World" 노드의 자식을 바꾸면 됩니다. 씬을 수동으로 바꾸는 것으로 사용자는 게임 월드 전환 방식을 완전히 제어할 수 있습니다.

다음 단계는 이 프로젝트에는 무슨 게임 플레이 시스템이 필요한 지 고려해야 합니다. 시스템은 다음을 갖고 있습니다...

  1. 모든 데이터를 내부적으로 추적하고
  2. 전역으로 접근해야 하고
  3. 독립적으로 존재해야 하는 것

... 그러려면 오토로드 '싱글톤' 노드를 만들어야 합니다.

주석

더 작은 게임일수록, 더 적은 제어로 "Game" 싱글톤을 갖는 간단한 대안이 필요한데, SceneTree.change_scene() 이라고 하는 이 메서드는 메인 씬의 내용물을 바꿔 넣습니다. 이 구조는 "World"를 메인 게임 노드로 어느정도는 유지해줍니다.

모든 GUI는 "World"의 일시적인 일부로, 싱글톤이 되어야 합니다, 아니면 수동으로 루트의 자식으로 추가해야 합니다. 그렇지 않으면, GUI 노드는 씬을 전환하는 동안 자신 또한 삭제됩니다.

다은 시스템의 데이터를 수정하는 시스템을 갖고 있다면, 오토로드보다는 자체 스크립트나 씬으로 데이터를 정의해야 합니다. 그 이유에 대한 더 자세한 정보는 ref:'오토로드 vs. 내장 노드' <doc_autoloads_versus_internal_nodes> 문서를 참고하세요.

게임 내 각 하위 시스템은 SceneTree 안에 자체 섹션을 가져야 합니다. 노드가 부모의 실질적인 요소가 되는 경우에만 노드 자식 관계를 사용해야 합니다. 자식을 제거해야 부모를 합리적으로 제거하는 것일까요? 그렇지 않으면 노드는 형제나 다른 관계로서 계층 구조 내에 자리를 잡아야 합니다.

주석

몇몇 경우에는, 분리된 노드 간의 상대적인 위치가 필요합니다. 이를 위해 RemoteTransform / RemoteTransform2D <class_RemoteTransform2D>`노드를 사용할 수 있습니다. 이것으로 대상 노드는 조건부로 선택된 Remote* 노드의 변형 요소를 상속할 수 있습니다. ``대상` NodePath에 접근하려면 다음 방식 중 하나를 사용하세요:

  1. 지정하는 것을 조정하기 위한, 부모 노드와 같은, 신뢰할 수 있는 제 3자.
  2. 원하는 노드에 쉽게 참조를 끌어올 수 있는 그룹 (대상 하나만 속한다고 가정).

언제 해봐야 할까요? 흠, 결정하는 것은 여러분에게 달렸습니다. 마이크로 급 관리를 해야 할 때나, 노드 자체를 보존하기 위해 SceneTree의 여기 저기로 움직여야 할 때 이 딜레마가 발생합니다. 예를 들어...

  • "플레이어" 노드를 "방"에 추가하기.

  • 방을 바꿔야 해서, 현재 방을 삭제해야 합니다.

  • 방을 삭제할 수 있기 전, 플레이어를 보존하거나/혹은 움직여야 합니다.

    메모리가 걱정인가?

    • 걱정되지 않다면, 그냥 두 개의 방을 만들고, 플레이어를 이동시키고, 이전 것을 삭제합니다. 문제없습니다.

    걱정된다면, 이렇게 해보죠...

    • 플레이어를 트리 어딘가로 이동시킵니다.
    • 방을 삭제합니다.
    • 새로운 방을 인스턴스화하고 추가합니다.
    • 플레이어를 다시 추가합니다.

이 문제에서 플레이어는 "특수한 상황"에 있습니다, 개발자는 프로젝트를 위해 플레이어를 이런 방식으로 다룰 줄 알아야 하죠. 즉, 팀원으로서 이 정보를 확실히 공유하는 유일한 방법은 문서화하는 것입니다. 하지만 문서에 구현 방법의 세부 사항을 유지하는 것은 위험합니다. 유지 관리의 부담과, 코드의 가독성을 해치고, 불필요하게 프로젝트의 지적 내용을 부풀리기 때문입니다.

더 많은 자원을 사용하는 더 복잡한 게임이라면, 차라리 플레이어를 SceneTree 어딘가에 통째로 유지하는 것이 더 좋은 생각일 수 있습니다. 이것이 의미하는 것은...

  1. 더 일관성 있음.
  2. 문서화하거나 어딘가에 남겨둬야 하는 "특수한 상황"이 없음.
  3. 이러한 세부 사항이 고려되지 않았기 때문에 오류가 발생하지 않음.

반대로 부모의 변형을 상속받지 않는 자식 노드가 필요하다면, 다음 옵션이 있습니다:

  1. 선언형 해결: Node를 둘의 계층 구조 사이에 놓습니다. 변형이 없는 노드이므로 노드는 자식들에게 부모의 정보를 전달하지 않을 것입니다.
  2. 명령형 해결: CanvasItem 노드의 set_as_toplevel Setter를 사용합니다. 이것으로 노드는 상속된 변형의 영향을 무시합니다.

주석

네트워크 게임을 만든다면, 어떤 노드와 게임 플레이 시스템이 모든 플레이어와 관련 있는지, 단지 권위 있는 서버와 관련 있는지 알아야 합니다. 예를 들어, 사용자는 모든 플레이어의 "PlayerController" 논리 사본을 가질 필요가 없습니다. 대신 그들의 것만이 필요합니다. 따라서 "World"에서 그들을 분리하여 게임 연결의 관리를 단순화할 수 있습니다.

씬 조직의 핵심은 SceneTree를 공간적 용어보다는 관계적 용어로 생각하는 것입니다. 노드가 부모의 존재에 의존해야 할까요? 그렇지 않아도 된다면, 그들은 다른 어딘가에서 스스로 잘 있을 수 있을 겁니다. 의존해야 한다면, 그들은 부모의 자식으로 (그리고 준비가 되지 않았다면 부모의 씬의 일부로) 존재해야 할 것입니다.

이것이 노드 자체가 구성 요소를 뜻하는 걸까요? 전혀요. Godot의 노드 트리는 집합 관계를 형성합니다, 구성의 일부가 아닙니다. 하지만 노드가 움직이는 유동성을 지니고 있더라도, 그것이 기본적으로 불필요한 것이 가장 좋습니다.