Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

씬 구성

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

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

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

사용자들은 먼저 첫 씬을 만들고 거기에 내용물을 넣다가 왠지 씬을 좀 나눠야 할 것 같은 불길한 기분을 느낄 쯤 씬의 가지를 개별의 씬으로 저장합니다. 그리고는 더 이상 이전처럼 직접적인 참조를 할 수가 없다는 것을 깨닫는거죠. 노드 경로가 목표를 찾을 수 없고 에디터의 시그널 연결이 무너지기 때문에 씬을 여러 곳에서 재사용하는 것은 문제를 일으킵니다.

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

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

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. By convention, signal names are usually past-tense verbs like "entered", "skill_activated", or "item_collected".

    # Parent
    $Child.signal_name.connect(method_on_the_object)
    
    # Child
    signal_name.emit() # 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).
    
  3. Initialize a Callable property. Safer than a method as ownership of the method is unnecessary. Used to start behavior.

    # Parent
    $Child.func_property = object_with_method.method_on_the_object
    
    # Child
    func_property.call() # Call parent-defined method (can come from anywhere).
    
  4. 노드나 다른 오브젝트 참조를 초기화합니다.

    # Parent
    $Child.target = self
    
    # Child
    print(target) # Use parent-defined node.
    
  5. 노드 경로를 초기화합니다.

    # Parent
    $Child.target_path = ".."
    
    # Child
    get_node(target_path) # 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 reuse 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)

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

경고

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

To avoid creating and maintaining such documentation, one converts the dependent node ("child" above) into a tool script that implements _get_configuration_warnings(). Returning a non-empty PackedStringArray from it will make the Scene dock generate a warning icon with the string(s) as a tooltip by the node. This is the same icon that appears for nodes such as the Area2D node when it has no child CollisionShape2D nodes defined. The editor then self-documents the scene through the script code. No content duplication via documentation is necessary.

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

So, why does 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 dependent on it.

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

노드 트리 구조 선택하기

So, a developer starts work on a game only to stop at the vast possibilities before them. They might know what they want to do, what systems they want to have, but where to put them all? Well, how one goes about making their game is always up to them. One can construct node trees in countless ways. But, for those who are unsure, this helpful guide can give them a sample of a decent structure to start with.

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/Node3D "World" (game_world.gd)

    • Control "GUI" (gui.gd)

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

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

  1. 모든 데이터를 내부적으로 추적하고

  2. 전역으로 접근해야 하고

  3. 독립적으로 존재해야 하는 것

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

참고

For smaller games, a simpler alternative with less control would be to have a "Game" singleton that simply calls the SceneTree.change_scene_to_file() method to swap out the main scene's content. This structure more or less keeps the "World" as the main game node.

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.

If one has systems that modify other systems' data, one should define those as their own scripts or scenes rather than autoloads. For more information on the reasons, please see the Autoloads versus regular nodes documentation.

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

참고

In some cases, one needs these separated nodes to also position themselves relative to each other. One can use the RemoteTransform / RemoteTransform2D nodes for this purpose. They will allow a target node to conditionally inherit selected transform elements from the Remote* node. To assign the target NodePath, use one of the following:

  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. The imperative solution: Use the top_level property for the CanvasItem or Node3D node. This will make the node ignore its inherited transform.

참고

네트워크 게임을 만든다면, 어떤 노드와 게임 플레이 시스템이 모든 플레이어와 관련 있는지, 단지 권위 있는 서버와 관련 있는지 알아야 합니다. 예를 들어, 사용자는 모든 플레이어의 "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 existence? 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의 노드 트리는 집합 관계를 형성합니다, 구성의 일부가 아닙니다. 하지만 노드가 움직이는 유동성을 지니고 있더라도, 그것이 기본적으로 불필요한 것이 가장 좋습니다.