Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
씬 조직
이 문서는 효과적인 씬 컨텐츠 구성 방법을 다룹니다. 어떤 노드를 사용해야 할까요? 어디에 그들을 배치해야 할까요? 어떻게 상호작용하도록 해야 할까요?
효과적으로 관계를 구축하는 방법
Godot 사용자들은 씬을 만들 때, 종종 다음과 같은 문제점을 마주치게 됩니다:
사용자들은 첫 번째 씬을 만들고 거기에 내용을 채우지만 왠지 씬을 나눠야 한다는 불길한 기분을 느낄 즈음 결국 씬의 여러 가지를 여러 개의 씬으로 나누어 저장하게 됩니다. 하지만 이전에는 의존할 수 있었던 직접적인 참조를 더 이상 가능하지 않다는 것을 깨닫게 됩니다. 노드 경로가 대상을 찾지 못하고 편집기에서 확립된 시그널 연결이 끊어지기 때문에 씬을 여러 곳에서 재사용하면 문제가 발생합니다.
이 문제를 해결하기 위해, 환경에 대한 세부 정보가 필요없는 하위 씬을 인스턴스화해야 합니다. 하위 씬이 사용되는 방식에 관계없이 잘 동작한다는 것을 믿을 수 있어야 합니다.
One of the biggest things to consider in Object-Oriented Programming (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.
이런 객체 지향 프로그래밍 모범 사례는 씬 구조와 스크립트 사용의 모범 사례에 있어 여러 가지 파급 효과가 있습니다.
가능하다면 의존성이 없는 씬을 설계해야합니다. 즉, 필요한 모든 것들을 스스로 가지고 있는 씬을 만들어야 합니다.
씬이 외부 컨텍스트와 상호작용해야 한다면, 경험있는 개발자들은 의존성 주입사용을 권장합니다. 이 기술은 고수준 API를 사용해 저수준 API의 의존성을 제공하는 것과 관련이 있습니다. 왜 이럴까요? 외부 환경에 의존하는 클래스에서는 버그나 예측하지 못한 동작이 발생하기 쉽기 때문입니다.
따라서 데이터를 노출하고, 초기화는 부모 컨텍스트 담당하도록 해야 합니다:
시그널에 연결합니다. 매우 안전하지만 동작에 "응답"하기 위해 사용해야하고, 동작을 시작하기 위해 사용하면 안됩니다. 시그널 이름은 보통 과거 시제 동사를 사용합니다, "entered", "skill_activated", 혹은 "item_collected"처럼 말이죠.
# Parent $Child.signal_name.connect(method_on_the_object) # Child signal_name.emit() # Triggers parent-specified behavior.
// Parent GetNode("Child").Connect("SignalName", Callable.From(ObjectWithMethod.MethodOnTheObject)); // Child EmitSignal("SignalName"); // Triggers parent-specified behavior.
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { // Note that get_node may return a nullptr, which would make calling the connect method crash the engine if "Child" does not exist! // So unless you are 1000% sure get_node will never return a nullptr, it's a good idea to always do a nullptr check. node->connect("signal_name", callable_mp(this, &ObjectWithMethod::method_on_the_object)); } // Child emit_signal("signal_name"); // Triggers parent-specified behavior.
메서드를 호출합니다. 동작을 시작하기 위해 사용됩니다.
# Parent $Child.method_name = "do" # Child, assuming it has String property 'method_name' and method 'do'. call(method_name) # Call parent-specified method (which child must own).
// Parent GetNode("Child").Set("MethodName", "Do"); // Child Call(MethodName); // Call parent-specified method (which child must own).
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { node->set("method_name", "do"); } // Child call(method_name); // Call parent-specified method (which child must own).
Callable 속성을 초기화합니다. 메서드의 소유권이 불필요하므로 메서드 호출보다 안전합니다. 동작을 시작하는 데 사용됩니다.
# Parent $Child.func_property = object_with_method.method_on_the_object # Child func_property.call() # Call parent-specified method (can come from anywhere).
// Parent GetNode("Child").Set("FuncProperty", Callable.From(ObjectWithMethod.MethodOnTheObject)); // Child FuncProperty.Call(); // Call parent-specified method (can come from anywhere).
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { node->set("func_property", Callable(&ObjectWithMethod::method_on_the_object)); } // Child func_property.call(); // Call parent-specified method (can come from anywhere).
노드나 다른 오브젝트 참조를 초기화합니다.
# Parent $Child.target = self # Child print(target) # Use parent-specified node.
// Parent GetNode("Child").Set("Target", this); // Child GD.Print(Target); // Use parent-specified node.
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { node->set("target", this); } // Child UtilityFunctions::print(target);
노드 경로를 초기화합니다.
# Parent $Child.target_path = ".." # Child get_node(target_path) # Use parent-specified NodePath.
// Parent GetNode("Child").Set("TargetPath", NodePath("..")); // Child GetNode(TargetPath); // Use parent-specified NodePath.
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { node->set("target_path", NodePath("..")); } // Child get_node<Node>(target_path); // Use parent-specified 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 partial class Left : Node
{
public Node Target = null;
public void Execute()
{
// Do something with 'Target'.
}
}
public partial class Right : Node
{
public Node Receiver = null;
public Right()
{
Receiver = ResourceLoader.Load<Script>("Receiver.cs").New();
AddChild(Receiver);
}
}
// Parent
get_node<Left>("Left")->target = get_node<Node>("Right/Receiver");
class Left : public Node {
GDCLASS(Left, Node)
protected:
static void _bind_methods() {}
public:
Node *target = nullptr;
Left() {}
void execute() {
// Do something with 'target'.
}
};
class Right : public Node {
GDCLASS(Right, Node)
protected:
static void _bind_methods() {}
public:
Node *receiver = nullptr;
Right() {
receiver = memnew(Node);
add_child(receiver);
}
};
노드가 아닌 오브젝트라도 다른 오브젝트와 종속성이 있는 경우 같은 원리가 적용됩니다. 어떤 오브젝트든지 실제로 그들을 소유한 오브젝트가 그들간의 관계를 관리해야 합니다.
경고
데이터를 스스로(씬 내부에) 가지고 있는 것을 우선시해야 합니다. 느슨한 연결로 구현했다 할지라도 외부 컨텍스트에 종속성을 두는 것은 여전히 노드가 외부 환경에 의존적이 되는 것이기 때문입니다. 프로젝트의 디자인 철학에서 이러한 점을 미리 예방해야 합니다. 그렇지 않으면, 원래 코드 자체가 책임져야할 것들이 개발자가 오브젝트간 세세한 관계까지 문서를 통해 확인해야만 하는 상황이 됩니다. 이를 개발 지옥이라고 합니다. 안전한 사용을 위해 외부 문서를 확인해야만 하는 코드를 작성한다면, 기본적으로 오류가 발생하기 쉽습니다.
그러한 문서를 작성하고 유지하는 것을 피하려면, 종속 노드 (위의 "child")를 _get_configuration_warnings()를 구현하는 tool 스크립트로 변환해야 합니다. 여기서 비어있지 않은 PackedStringArray을 반환하면 씬 독은 노드의 툴팁으로 문자열과 함께 경고 아이콘을 생성합니다. Area2D에서 자식 CollisionShape2D 노드가 없을 때 나타나는 그런 아이콘입니다. 이렇게 하는 것은 편집기가 스크립트 코드를 통해 씬을 자체적으로 문서화한 것과 같습니다. 문서를 통해 중복된 내용을 다시 기재할 필요가 없습니다.
A Graphical User Interface (GUI) like this can better inform project users of critical information about a Node. Does it have external dependencies? Have those dependencies been satisfied? Other programmers, and especially designers and writers, will need clear instructions in the messages telling them what to do to configure it.
그럼 이런 복잡한 변환은 왜 하는걸까요? 씬은 홀로 작동할 때 가장 잘 작동하기 때문입니다. 홀로 작동할 수 없는 경우엔 익명으로 다른 것들과 (느슨한 결합, 즉 최소한의 종속성만으로) 작동하는 것이 차선입니다. 만약 클래스를 피치 못하게 변경해야 하는 상황이 생기고 이 변경으로 인해 다른 씬과 예상하지 못한 방법으로 상호작용이 일어난다면 문제가 생기기 시작합니다. 한 클래스의 결과의 변화가 다른 클래스에 영향을 끼치는 일을 피할 수 있도록 하는게 이런 간접적인 접근을 지향하는 이유입니다.
스크립트와 씬은, 엔진 클래스의 확장 프로그램으로 모든 객체 지향 프로그래밍 원칙을 준수해야 합니다. 예시는 다음을 포함합니다...
노드 트리 구조 선택
개발자가 게임을 만들기다 보면 막대한 가능성 때문에 멈춰서게 됩니다. 작업하고 싶은 것이 무엇이고, 필요한 시스템이 뭔지는 알지만, 어디에 그것들을 넣어야 할까요? 음, 게임을 만드는 것은 항상 스스로에게 달려있습니다. 노드 트리는 무수히 많은 방법으로 만들 수 있습니다. 하지만 잘 모르겠는 분들을 위해 이 가이드에서 시작하기에 알맞은 구조 예시들을 보여드리겠습니다.
게임은 항상 일종은 "진입점"을 갖고, 이 진입점은 개발자들이 어디서부터 게임이 시작되는지 확실히 파악할 수 있어서 이후 로직을 명확하게 추적할 수 있게 해줍니다. 이 지점은 프로그램의 모든 데이터와 로직을 확인하기 위한 큰 그림으로 활용되기도 합니다. 전통적인 애플리케이션의 경우, 이것은 "main" 함수가 될 것입니다. 우리의 경우 이는 Main 노드입니다.
노드 "Main" (main.gd)
main.gd 스크립트는 게임의 주된 컨트롤러 역할을 할 것입니다.
그런 다음 실제 게임 내 "World" (2D나 3D)를 갖습니다. Main의 자식이 될 수 있습니다. 그리고 프로젝트에 필요한 다양한 메뉴와 위젯을 관리하는 게임용 기본 GUI가 필요합니다.
- 노드 "Main" (main.gd)
Node2D/Node3D "World" (game_world.gd)
Control "GUI" (gui.gd)
레벨을 변경할 때, "World" 노드의 자식을 바꾸면 됩니다. 씬을 수동으로 바꾸는 것으로 사용자는 게임 세계 전환 방식을 완전히 제어할 수 있습니다.
다음 단계는 이 프로젝트에는 무슨 게임 플레이 시스템이 필요한 지 고려해야 합니다. 시스템은 다음을 갖고 있습니다...
모든 데이터를 내부적으로 추적하고
전역으로 접근해야 하고
독립적으로 존재해야 하는 것
... 그러려면 오토로드 '싱글톤' 노드를 만들어야 합니다.
참고
더 작은 게임일수록, 더 적은 제어로 "Game" 싱글톤을 갖는 간단한 대안이 필요한데, SceneTree.change_scene_to_file()이라고 하는 이 메서드는 메인 씬의 내용을 바꿔 넣습니다. 이 구조는 "World"를 메인 게임 노드로 어느 정도는 유지해줍니다.
모든 GUI는 "World"의 일시적인 일부로, 싱글톤이 되어야 합니다, 아니면 수동으로 루트의 자식으로 추가해야 합니다. 그렇지 않으면, GUI 노드는 씬을 전환하는 동안 자신 또한 삭제됩니다.
If you have systems that modify other systems' data, you should define those as their own scripts or scenes, rather than autoloads. For more information, see Autoloads versus regular nodes.
게임 내 각 하위 시스템은 SceneTree 안에 자체 섹션을 가져야 합니다. 노드가 부모의 실질적인 요소가 되는 경우에만 노드 자식 관계를 사용해야 합니다. 부모를 제거하는 경우 자식이 반드시 같이 제거되어야 하나요? 그렇지 않다면 노드는 형제나 다른 관계로서 계층 구조 내에 자리를 잡아야 합니다.
참고
어떤 경우에는, 이렇게 분리된 노드 간에 상대적인 위치가 물론 필요합니다. 이를 위해 RemoteTransform / RemoteTransform2D 노드를 사용할 수 있습니다. 이를 사용하면 대상 노드가 Remote* 노드의 선택된 변형 요소를 조건부로 상속할 수 있습니다. 대상 NodePath을 할당하려면 다음 방식 중 하나를 사용합니다:
지정하는 것을 조정하기 위한, 부모 노드와 같은, 신뢰할 수 있는 서드 파티.
원하는 노드에 쉽게 참조를 끌어올 수 있는 그룹 (대상 하나만 속한다고 가정).
언제 이러한 방법을 사용해야 할까요? 여러분에게 달렸습니다. SceneTree 내에서 노드가 여기저기 바뀌어야 할 때 마이크로매니지를 해야하는 딜레마가 발생합니다. 예를 들어...
"플레이어" 노드를 "방"에 추가합니다.
방을 바꿔야 해서, 현재 방을 삭제해야 합니다.
방을 삭제할 수 있기 전, 플레이어를 보존하거나/혹은 움직여야 합니다.
만약 메모리가 걱정되지 않는다면, 당신은...
방을 삭제합니다.
플레이어를 트리 어딘가로 이동시킵니다.
방을 삭제합니다.
걱정된다면, 아래와 같이 해야 합니다...
플레이어를 트리 어딘가로 이동시킵니다.
방을 삭제합니다.
새로운 방을 인스턴스화하고 추가합니다.
플레이어를 다시 추가합니다.
이 문제에서 플레이어는 "특수한 상황"에 있습니다, 개발자는 프로젝트를 위해 플레이어를 이런 방식으로 다룰 줄 알아야 하죠. 즉, 팀원으로서 이 정보를 확실히 공유하는 유일한 방법은 문서화하는 것입니다. 하지만 문서에 구현 방법의 세부 사항을 유지하는 것은 위험합니다. 유지 관리의 부담과, 코드의 가독성을 해치고, 불필요하게 프로젝트의 지적 내용을 부풀리기 때문입니다.
더 많은 자원을 사용하는 더 복잡한 게임이라면, 차라리 플레이어를 SceneTree 어딘가에 통째로 유지하는 것이 더 좋은 생각일 수 있습니다. 이것이 의미하는 것은...
더 일관성 있음.
문서화하거나 어딘가에 남겨둬야 하는 "특수한 상황"이 없음.
이러한 세부 사항이 고려되지 않았기 때문에 오류가 발생하지 않음.
반대로 부모의 변형을 상속받지 않는 자식 노드가 필요하다면, 다음 옵션이 있습니다:
선언형 해결: Node를 둘의 계층 구조 사이에 놓습니다. 변형이 없는 노드이므로 노드는 자식에게 이 정보를 전달하지 않습니다.
명령형 해결: CanvasItem 또는 Node3D 노드의
top_level속성을 사용합니다. 이렇게 하면 노드는 상속된 변형을 무시합니다.
참고
네트워크 게임을 만든다면, 어떤 노드와 게임 플레이 시스템이 모든 플레이어와 관련 있는지, 단지 권위 있는 서버와 관련 있는지 알아야 합니다. 예를 들어, 사용자는 모든 플레이어의 "PlayerController" 로직의 사본을 가질 필요가 없습니다. 대신 자신의 것만 있으면 됩니다. 따라서 "World"에서 그들을 분리하여 게임 연결의 관리를 단순화할 수 있습니다.
씬 조직의 핵심은 SceneTree를 공간적 용어보다는 관계적 용어로 생각하는 것입니다. 노드가 부모의 존재에 의존적인가요? 그렇지 않다면, 그들은 다른 어딘가에서 스스로 잘 있을 수 있을 겁니다. 의존적이라면, 그들은 부모의 자식으로 (그리고 부모의 씬의 일부로) 존재해야 할 것입니다.
이것이 노드가 컴포넌트라는 뜻일까요? 전혀 아닙니다. Godot의 노드 트리는 집합 관계를 형성하는 것이지 구성 관계를 형성하는 것이 아닙니다. 그러나 노드를 유연하게 이동할 수 있긴 하지만, 기본적으로 노드에는 그러한 이동이 필요하지 않은 것이 가장 좋습니다.