씬 구성

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

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

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

They create their first scene and fill it with content only to eventually end up saving branches of their scene into separate scenes as the nagging feeling that they should split things up starts to accumulate. However, they then notice that the hard references they were able to rely on before are no longer possible. Re-using the scene in multiple places creates issues because the node paths do not find their targets and signal connections established in the editor break.

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

One of the biggest things to consider in OOP is maintaining focused, singular-purpose classes with loose coupling to other parts of the codebase. This keeps the size of objects small (for maintainability) and improves their reusability.

These OOP best practices have several implications for best practices in scene structure and script usage.

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

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

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

  1. Connect to a signal. Extremely safe, but should be used only to "respond" to behavior, not start it. Note that signal names are usually past-tense verbs like "entered", "skill_activated", or "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.
    

These options hide the points of access from the child node. This in turn keeps the child loosely coupled to its environment. One can re-use it in another context without any extra changes to its 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는 프로젝트 사용자에게 노드에 대한 중요한 정보를 더 잘 전달할 수 있습니다. 외부 종속성을 갖고 있나요? 그러한 종속성을 만족시켰나요? 다른 프로그래머들, 특히 디자이너와 작가들에게 노드에 무엇을 구성해야 하는 지를 메시지로 명확하게 설명해야 할 것입니다.

So, why do all this complex switcharoo work? Well, because scenes operate best when they operate alone. If unable to work alone, then working with others anonymously (with minimal hard dependencies, i.e. loose coupling) is the next best thing. Inevitably, changes may need to be made to a class and if these changes cause it to interact with other scenes in unforeseen ways, then things will start to break down. The whole point of all this indirection is to avoid ending up in a situation where changing one class results in adversely effecting other classes.

Scripts and scenes, as extensions of engine classes, should abide by all OOP principles. Examples include...

노드 트리 구조 선택하기

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

A game should always have a sort of "entry point"; somewhere the developer can definitively track where things begin so that they can follow the logic as it continues elsewhere. This place also serves as a bird's eye view of all of the other data and logic in the program. For traditional applications, this would be the "main" function. In this case, it would be a Main node.

  • 노드 "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"를 메인 게임 노드로 어느정도는 유지해줍니다.

Any GUI would need to also be a singleton; be a transitory part of the "World"; or be manually added as a direct child of the root. Otherwise, the GUI nodes would also delete themselves during scene transitions.

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

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

주석

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

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

When should one do this? Well, this is subjective. The dilemma arises when one must micro-manage when a node must move around the SceneTree to preserve itself. For example...

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

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

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

    메모리가 걱정인가?

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

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

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

The issue is that the player here is a "special case"; one where the developers must know that they need to handle the player this way for the project. As such, the only way to reliably share this information as a team is to document it. Keeping implementation details in documentation however is dangerous. It's a maintenance burden, strains code readability, and bloats the intellectual content of a project unnecessarily.

In a more complex game with larger assets, it can be a better idea to simply keep the player somewhere else in the SceneTree entirely. This results in:

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

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

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

주석

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

The key to scene organization is to consider the SceneTree in relational terms rather than spatial terms. Are the nodes dependent on their parent's existance? If not, then they can thrive all by themselves somewhere else. If they are dependent, then it stands to reason that they should be children of that parent (and likely part of that parent's scene if they aren't already).

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