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.

C++에서 커스텀 모듈

모듈

Godot는 모듈 방식으로 엔진을 확장할 수 있습니다. 새 모듈을 생성한 다음 활성화/비활성화할 수 있습니다. 이를 통해 코어를 수정하지 않고도 모든 레벨에서 새로운 엔진 기능을 추가할 수 있으며, 이를 분할하여 다른 모듈에서 사용하고 재사용할 수 있습니다.

모듈은 빌드 시스템의 modules/ 하위 디렉터리에 있습니다. 기본적으로 GDScript(기본 엔진의 일부가 아님), GridMap 지원, 정규식 모듈 등과 같은 수십 개의 모듈이 활성화됩니다. 원하는 만큼 새로운 모듈을 만들고 결합할 수 있습니다. SCons 빌드 시스템은 이를 투명하게 처리합니다.

무엇을 위해?

대부분의 게임은 스크립팅으로 작성하는 것이 권장되지만(시간이 엄청나게 절약되므로) C++를 대신 사용하는 것도 완벽하게 가능합니다. C++ 모듈을 추가하면 다음 시나리오에서 유용할 수 있습니다.

  • 외부 라이브러리를 Godot에 바인딩합니다(PhysX, FMOD 등).

  • 게임의 중요한 부분을 최적화합니다.

  • 엔진 및/또는 편집기에 새로운 기능을 추가합니다.

  • 기존 게임을 Godot로 포팅합니다.

  • C++ 없이는 살 수 없기 때문에 C++로 완전히 새로운 게임을 작성하세요.

참고

사용자 정의 게임 로직을 위한 모듈을 사용하는 것이 가능하지만 일반적으로 :ref:`GDExtension <doc_gdextension>`이 더 적합합니다. 코드가 변경될 때마다 엔진을 다시 컴파일할 필요가 없기 때문입니다.

C++ 모듈은 GDExtension이 충분하지 않고 더 깊은 엔진 통합이 필요할 때 주로 필요합니다.

새 모듈 만들기

모듈을 만들기 전에 :ref:`Godot의 소스 코드를 다운로드하고 컴파일 <toc-devel-compiling>`하세요.

새 모듈을 생성하려면 첫 번째 단계는 modules/ 내에 디렉터리를 생성하는 것입니다. 모듈을 별도로 유지 관리하려면 다른 VCS를 모듈로 체크아웃하여 사용할 수 있습니다.

예제 모듈의 이름은 "summator"(godot/modules/summator)입니다. 내부에서 summator 클래스를 생성합니다:

godot/modules/summator/summator.h
#pragma once

#include "core/object/ref_counted.h"

class Summator : public RefCounted {
    GDCLASS(Summator, RefCounted);

    int count;

protected:
    static void _bind_methods();

public:
    void add(int p_value);
    void reset();
    int get_total() const;

    Summator();
};

그리고 cpp 파일입니다.

godot/modules/summator/summator.cpp
#include "summator.h"

void Summator::add(int p_value) {
    count += p_value;
}

void Summator::reset() {
    count = 0;
}

int Summator::get_total() const {
    return count;
}

void Summator::_bind_methods() {
    ClassDB::bind_method(D_METHOD("add", "value"), &Summator::add);
    ClassDB::bind_method(D_METHOD("reset"), &Summator::reset);
    ClassDB::bind_method(D_METHOD("get_total"), &Summator::get_total);
}

Summator::Summator() {
    count = 0;
}

그런 다음 새 클래스를 어떻게든 등록해야 하므로 두 개의 파일을 더 만들어야 합니다.

register_types.h
register_types.cpp

중요

모듈이 제대로 등록되려면 이러한 파일이 모듈의 최상위 폴더(SCsubconfig.py 파일 옆)에 있어야 합니다.

적 씬은 다음 노드들을 사용할 것입니다:

godot/modules/summator/register_types.h
#include "modules/register_module_types.h"

void initialize_summator_module(ModuleInitializationLevel p_level);
void uninitialize_summator_module(ModuleInitializationLevel p_level);
/* yes, the word in the middle must be the same as the module folder name */
godot/modules/summator/register_types.cpp
#include "register_types.h"

#include "core/object/class_db.h"
#include "summator.h"

void initialize_summator_module(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
        return;
    }
    ClassDB::register_class<Summator>();
}

void uninitialize_summator_module(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
        return;
    }
   // Nothing to do here in this example.
}

다음으로, 빌드 시스템이 이 모듈을 컴파일할 수 있도록 SCsub 파일을 생성해야 합니다.

godot/modules/summator/SCsub
# SCsub

Import('env')

env.add_source_files(env.modules_sources, "*.cpp") # Add all cpp files to the build

여러 소스를 사용하면 각 파일을 Python 문자열 목록에 개별적으로 추가할 수도 있습니다.

src_list = ["summator.cpp", "other.cpp", "etc.cpp"]
env.add_source_files(env.modules_sources, src_list)

이는 Python을 사용하여 루프와 논리문을 사용하여 파일 목록을 구성하는 강력한 가능성을 허용합니다. 예를 들어 기본적으로 Godot와 함께 제공되는 일부 모듈을 살펴보세요.

컴파일러가 볼 수 있는 포함 디렉터리를 추가하려면 해당 디렉터리를 환경 경로에 추가하면 됩니다.

env.Append(CPPPATH=["mylib/include"]) # this is a relative path
env.Append(CPPPATH=["#myotherlib/include"]) # this is an 'absolute' path

모듈을 빌드할 때 사용자 정의 컴파일러 플래그를 추가하려면 먼저 ``env``를 복제해야 합니다. 그러면 해당 플래그가 전체 Godot 빌드에 추가되지 않습니다(이로 인해 오류가 발생할 수 있음). 사용자 정의 플래그가 있는 ``SCsub``의 예:

godot/modules/summator/SCsub
Import('env')

module_env = env.Clone()
module_env.add_source_files(env.modules_sources, "*.cpp")
# Append CCFLAGS flags for both C and C++ code.
module_env.Append(CCFLAGS=['-O2'])
# If you need to, you can:
# - Append CFLAGS for C code only.
# - Append CXXFLAGS for C++ code only.

마지막으로 모듈의 구성 파일은 Python 스크립트이며 이름은 ``config.py``여야 합니다.

godot/modules/summator/config.py
# config.py

def can_build(env, platform):
    return True

def configure(env):
    pass

모듈은 특정 플랫폼용으로 빌드해도 괜찮은지 묻습니다(이 경우 ``True``는 모든 플랫폼용으로 빌드된다는 의미입니다).

그리고 그게 다입니다. 너무 복잡하지 않았으면 좋겠습니다! 모듈은 다음과 같아야 합니다.

godot/modules/summator/config.py
godot/modules/summator/summator.h
godot/modules/summator/summator.cpp
godot/modules/summator/register_types.h
godot/modules/summator/register_types.cpp
godot/modules/summator/SCsub

그런 다음 압축하여 다른 사람과 모듈을 공유할 수 있습니다. 모든 플랫폼에 대해 빌드할 때(이전 섹션의 지침) 모듈이 포함됩니다.

모듈 사용

이제 스크립트에서 새로 생성된 모듈을 사용할 수 있습니다.

var s = Summator.new()
s.add(10)
s.add(20)
s.add(30)
print(s.get_total())
s.reset()

출력은 ``60``입니다.

더 보기

이전 Summator 예제는 소규모 사용자 정의 모듈에 적합하지만 더 큰 외부 라이브러리를 사용하려면 어떻게 해야 합니까? 외부 라이브러리 바인딩에 대한 자세한 내용은 :ref:`doc_binding_to_external_libraries`를 참조하세요.

경고

모듈이 편집기뿐만 아니라 실행 중인 프로젝트에서 액세스되도록 되어 있는 경우 사용하려는 모든 내보내기 템플릿도 다시 컴파일한 다음 각 내보내기 사전 설정에서 사용자 정의 템플릿에 대한 경로를 지정해야 합니다. 그렇지 않으면 모듈이 내보내기 템플릿에서 컴파일되지 않아 프로젝트를 실행할 때 오류가 발생합니다. 자세한 내용은 Compiling 페이지를 참조하세요.

외부에서 모듈 컴파일하기

모듈을 컴파일하려면 모듈의 소스를 엔진의 modules/ 디렉터리 바로 아래로 이동해야 합니다. 이것이 모듈을 컴파일하는 가장 간단한 방법이지만, 왜 이것이 실제적인 일이 아닐 수 있는지에 대한 몇 가지 이유가 있습니다:

  1. 모듈이 있든 없든 엔진을 컴파일할 때마다 모듈 소스를 수동으로 복사해야 하거나, ``module_summator_enabled=no``와 유사한 빌드 옵션을 사용하여 컴파일 중에 모듈을 수동으로 비활성화하는 데 필요한 추가 단계를 수행해야 합니다. 기호 링크를 생성하는 것도 해결책이 될 수 있지만 스크립트를 통해 이 작업을 수행하는 경우 기호 링크 권한이 필요한 것과 같은 OS 제한을 추가로 극복해야 할 수도 있습니다.

  2. 엔진의 소스 코드로 작업해야 하는지 여부에 따라 modules/``에 직접 추가된 모듈 파일은 VCS(예: ``git)를 사용하는 것이 번거로운 지점으로 작업 트리를 변경합니다. 변경 사항을 필터링하여 엔진 관련 코드만 커밋되도록 해야 하기 때문입니다.

따라서 사용자 정의 모듈의 독립적인 구조가 필요하다고 생각되면 "summator" 모듈을 가져와 엔진의 상위 디렉터리로 이동해 보겠습니다.

mkdir ../modules
mv modules/summator ../modules

다음과 유사하게 사용자 정의 C++ 모듈이 포함된 쉼표로 구분된 디렉터리 경로 목록을 허용하는 custom_modules 빌드 옵션을 제공하여 모듈로 엔진을 컴파일합니다.

scons custom_modules=../modules

빌드 시스템은 ../modules 디렉터리 아래의 모든 모듈을 감지하고 "summator" 모듈을 포함하여 그에 따라 컴파일합니다.

경고

``custom_modules``에 전달된 모든 경로는 사용자 정의 모듈과 내장 모듈을 구별하는 방법으로 내부적으로 절대 경로로 변환됩니다. 이는 모듈 문서 생성과 같은 작업이 컴퓨터의 특정 경로 구조에 의존할 수 있음을 의미합니다.

모듈 유형 초기화 사용자 정의

모듈은 런타임 중에 다른 내장 엔진 클래스와 상호 작용할 수 있으며 핵심 유형이 초기화되는 방식에도 영향을 미칠 수 있습니다. 지금까지 우리는 엔진 내에서 사용할 수 있는 모듈 클래스를 가져오는 방법으로 ``register_summator_types``를 사용해 왔습니다.

엔진 설정의 대략적인 순서는 다음 유형 등록 방법 목록으로 요약될 수 있습니다.

preregister_module_types();
preregister_server_types();
register_core_singletons();
register_server_types();
register_scene_types();
EditorNode::register_editor_types();
register_platform_apis();
register_module_types();
initialize_physics();
initialize_navigation_server();
register_server_singletons();
register_driver_types();
ScriptServer::init_languages();

Summator 클래스는 register_module_types() 호출 중에 초기화됩니다. Imagine that we need to satisfy some common module runtime dependency (like 싱글톤), or allow us to override existing engine method callbacks before they can be assigned by the engine itself. 이 경우, 우리는 모듈 클래스가 다른 내장 유형 전에 등록되었는지 확인하고 싶습니다.

여기서는 preregister_module_types() 엔진 설정 단계에서 다른 것보다 먼저 호출되는 선택적 preregister_summator_types() 메서드를 정의할 수 있습니다.

이제 이 메서드를 register_types 헤더 및 소스 파일에 추가해야 합니다.

godot/modules/summator/register_types.h
#define MODULE_SUMMATOR_HAS_PREREGISTER
void preregister_summator_types();

void register_summator_types();
void unregister_summator_types();

참고

다른 등록 방법과 달리, 컴파일 타임에 포함할 관련 메서드 호출이 무엇인지 빌드 시스템이 알 수 있도록 ``MODULE_SUMMATOR_HAS_PREREGISTER``를 명시적으로 정의해야 합니다. 모듈 이름도 대문자로 변환해야 합니다.

godot/modules/summator/register_types.cpp
#include "register_types.h"

#include "core/object/class_db.h"
#include "summator.h"

void preregister_summator_types() {
    // Called before any other core types are registered.
    // Nothing to do here in this example.
}

void register_summator_types() {
    ClassDB::register_class<Summator>();
}

void unregister_summator_types() {
   // Nothing to do here in this example.
}

사용자 정의 문서 작성

Writing documentation may seem like a boring task, but it is highly recommended to document your newly created module to make it easier for users to benefit from it. 1년 전에 작성한 코드가 다른 사람이 작성한 코드와 구별할 수 없게 될 수도 있다는 점은 말할 것도 없고, 미래의 자신에게 친절하게 대해주세요!

모듈에 대한 사용자 정의 문서를 설정하려면 몇 가지 단계가 있습니다:

  1. 모듈의 루트에 새 디렉터리를 만듭니다. 디렉터리 이름은 무엇이든 가능하지만 이 섹션에서는 doc_classes 이름을 사용합니다.

  2. 이제 ``config.py``를 편집하고 다음 스니펫을 추가해야 합니다.

    def get_doc_path():
        return "doc_classes"
    
    def get_doc_classes():
        return [
            "Summator",
        ]
    

get_doc_path() 함수는 빌드 시스템에서 문서의 위치를 결정하는 데 사용됩니다. 이 경우 modules/summator/doc_classes 디렉터리에 위치합니다. 이를 정의하지 않으면 모듈의 문서 경로가 기본 doc/classes 디렉터리로 대체됩니다.

get_doc_classes() 메서드는 빌드 시스템이 어떤 등록된 클래스가 모듈에 속하는지 파악하는 데 필요합니다. 여기에 모든 수업을 나열해야 합니다. 나열하지 않은 클래스는 기본 doc/classes 디렉토리에 위치하게 됩니다.

Git을 사용하면 ``git status``로 추적되지 않은 파일을 확인하여 일부 수업을 놓쳤는지 확인할 수 있습니다. 예를 들면:

git status

출력 예시:

Untracked files:
    (use "git add <file>..." to include in what will be committed)

    doc/classes/MyClass2D.xml
    doc/classes/MyClass4D.xml
    doc/classes/MyClass5D.xml
    doc/classes/MyClass6D.xml
    ...
  1. 이제 우리는 문서를 생성할 수 있습니다:

우리는 Godot의 doctool, 즉 ``godot --doctool <path>``를 실행하여 이 작업을 수행할 수 있습니다. 이는 XML 형식으로 주어진 ``<path>``에 대한 엔진 API 참조를 덤프합니다.

우리의 경우에는 복제된 저장소의 루트를 가리킬 것입니다. 다른 폴더를 가리키고 필요한 파일을 복사하면 됩니다.

실행 명령:

bin/<godot_binary> --doctool .

이제 godot/modules/summator/doc_classes 폴더로 이동하면 Summator.xml 파일 또는 get_doc_classes 함수에서 참조한 다른 클래스가 포함되어 있음을 알 수 있습니다.

Edit the file(s) following the class reference primer and recompile the engine.

컴파일 프로세스가 완료되면 엔진에 내장된 문서 시스템 내에서 해당 문서에 접근할 수 있게 됩니다.

문서를 최신 상태로 유지하려면 지금부터 XML 파일 중 하나를 수정하고 엔진을 다시 컴파일하기만 하면 됩니다.

모듈의 API를 변경하면 문서를 다시 추출할 수도 있습니다. 문서에는 이전에 추가한 내용이 포함됩니다. 물론, Godot 폴더를 가리키는 경우, 최신 엔진 빌드 위에 오래된 엔진 빌드에서 오래된 문서를 추출하여 작업을 잃지 않도록 하세요.

제공된 ``<path>``에 대한 쓰기 액세스 권한이 없으면 다음과 유사한 오류가 발생할 수 있습니다.

ERROR: Can't write doc file: docs/doc/classes/@GDScript.xml
   At: editor/doc/doc_data.cpp:956

커스텀 단위 테스트 작성하기

C++ 모듈의 일부로 자체 포함된 단위 테스트를 작성하는 것이 가능합니다. 아직 Godot의 단위 테스트 프로세스에 익숙하지 않다면 :ref:`doc_unit_testing`를 참조하세요.

다음과 같은 주의 사항이 있습니다:

  1. 만들기 모듈 루트 아래에 ``tests/``라는 새 디렉터리를 만듭니다.

cd modules/summator
mkdir tests
cd tests
  1. 만들기 새로운 테스트 스위트: test_summator.h. 빌드 시스템이 헤더를 수집하고 테스트가 실행되는 tests/test_main.cpp``의 일부로 포함할 있도록 헤더에는 ``test_ 접두사가 붙어야 합니다.

  2. 몇 가지 테스트 케이스를 작성해 보세요. 예는 다음과 같습니다.

godot/modules/summator/tests/test_summator.h
#pragma once

#include "tests/test_macros.h"

#include "modules/summator/summator.h"

namespace TestSummator {

TEST_CASE("[Modules][Summator] Adding numbers") {
    Ref<Summator> s = memnew(Summator);
    CHECK(s->get_total() == 0);

    s->add(10);
    CHECK(s->get_total() == 10);

    s->add(20);
    CHECK(s->get_total() == 30);

    s->add(30);
    CHECK(s->get_total() == 60);

    s->reset();
    CHECK(s->get_total() == 0);
}

} // namespace TestSummator
  1. ``scons tests=yes``로 엔진을 컴파일하고 다음 명령을 사용하여 테스트를 실행합니다.

./bin/<godot_binary> --test --source-file="*test_summator*" --success

이제 통과 어설션이 표시되어야 합니다.

빛 추가하기

모듈 내에서 자체 포함된 문서를 작성하는 방법과 마찬가지로 클래스가 편집기에 표시될 사용자 정의 아이콘을 직접 만들 수도 있습니다.

엔진에 통합될 에디터 아이콘을 생성하는 실제 과정은 먼저 :ref:`doc_editor_icons`를 참고하시기 바랍니다.

아이콘을 만든 후 다음 단계를 진행하세요.

  1. ``icons``라는 모듈의 루트에 새 디렉터리를 만듭니다. 이는 엔진이 모듈의 편집기 아이콘을 찾는 기본 경로입니다.

  2. 새로 생성된 svg 아이콘(최적화 여부)을 해당 폴더로 이동합니다.

  3. 엔진을 다시 컴파일하고 편집기를 실행합니다. 이제 해당 아이콘이 편집기 인터페이스에 나타납니다.

모듈 내 다른 곳에 아이콘을 저장하려면 다음 코드 조각을 ``config.py``에 추가하여 기본 경로를 재정의하세요.

def get_icons_path():
    return "path/to/icons"

요약

다음을 기억하세요:

  • 상속을 위해 GDCLASS 매크로를 사용하면 Godot가 이를 래핑할 수 있습니다.

  • ``_bind_methods``를 사용하여 함수를 스크립팅에 바인딩하고 함수가 시그널에 대한 콜백으로 작동하도록 허용합니다.

  • Godot에 노출된 클래스에 대한 다중 상속을 피하세요, ``GDCLASS``는 이를 지원하지 않습니다. Godot의 스크립팅 API에 노출되지 않는 한 자신의 클래스에서 다중 상속을 계속 사용할 수 있습니다.

그러나 이것이 전부는 아닙니다. 무엇을 하느냐에 따라 (희망적으로 긍정적인) 놀라움을 맞이하게 될 것입니다.

  • :ref:`class_Node`(또는 Sprite2D와 같은 파생된 노드 유형)에서 상속하는 경우 새 클래스가 편집기의 "노드 추가" 대화 상자에 있는 상속 트리에 나타납니다.

  • :ref:`class_Resource`에서 상속하면 리소스 목록에 나타나며 노출된 모든 속성은 저장/로드 시 직렬화될 수 있습니다.

  • 이와 동일한 논리로 편집기와 엔진의 거의 모든 영역을 확장할 수 있습니다.