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.

GDExtension C 예제

소개

이것은 C 코드로 직접 GDExtension을 사용하는 방법에 대한 간단한 예입니다. API는 직접 사용하기 위한 것이 아니기 때문에 아주 장황하고 작은 예에도 많은 단계가 필요합니다. 그러나 이는 다른 언어에 대한 바인딩을 생성하기 위한 참조 역할을 합니다. 원하는 경우 API를 직접 사용할 수도 있으며 이는 타사 라이브러리만 바인딩할 때 편리할 수 있습니다.

이 예에서는 사용자 매개변수에 따라 화면에서 스프라이트를 이동하는 사용자 정의 노드를 생성합니다. 매우 간단하지만 메소드, 속성 및 시그널를 사용하여 사용자 정의 클래스를 등록하는 등 GDExtension을 사용하여 일부 작업을 수행하는 방법을 보여줍니다. GDExtension API에 대한 통찰력을 제공합니다.

프로젝트 설정하기

필요한 몇 가지 전제 조건은 다음과 같습니다.

  • Godot 4.2(또는 그 이상) 실행 파일,

  • 발사 예제

  • 빌드 도구로서의 SCons.

godot-cpp 저장소 의 사본.

파일 구조

파일을 정리하기 위해 주로 두 개의 폴더로 나눕니다.

gdextension_c_example/
|
+--project/                  # game example/demo to test the extension
|
+--src/                   # source code of the extension we are building

우리는 또한 다음 명령을 실행하여 Godot 실행 파일에서 직접 얻을 수 있는 Godot 소스 코드의 gdextension_interface.h 헤더 사본이 필요합니다:

godot --dump-gdextension-interface

그러면 현재 폴더에 헤더가 생성되므로 예제 프로젝트의 src 폴더에 복사하면 됩니다.

마지막으로, 우리가 참조해야 할 또 다른 정보 소스가 있는데, 그것은 Godot API 참조가 포함된 JSON 파일입니다. 이 파일은 코드에서 직접 사용되지 않으며 일부 정보를 수동으로 추출하는 데만 사용됩니다.

이 JSON 파일을 얻으려면 Godot 실행 파일을 호출하세요:

godot --dump-extension-api

결과 extension_api.json 파일이 현재 폴더에 생성됩니다. 이 파일을 예제 폴더에 복사하여 편리하게 사용할 수 있습니다.

참고

이 확장은 Godot 4.2를 대상으로 하지만 이후 버전에서도 작동합니다. 다른 최소 버전을 대상으로 삼고 싶다면 대상으로 삼고 있는 Godot 버전에서 헤더와 JSON을 가져와야 합니다.

빌드시스템

빌드 시스템을 사용하면 C 코드를 다룰 때 우리 삶이 훨씬 쉬워집니다. 편의상 Godot 자체가 사용하는 것과 동일하므로 SCons를 사용하겠습니다.

다음 SConstruct 파일은 Linux, macOS 또는 Windows 등 현재 사용 중인 플랫폼에 대한 확장을 빌드하는 간단한 파일입니다. 이는 디버깅 목적으로 최적화되지 않은 빌드입니다. 또한 예제 코드의 일부 부분과 관련된 64비트 빌드를 가정합니다. 다른 빌드 유형을 만들고 크로스 컴파일하는 것은 이 튜토리얼의 범위를 벗어납니다. 이 파일을 루트 폴더에 저장합니다.

#!/bin/env python
from SCons.Script import Environment
from os import path
import sys

env = Environment()

# Set the target path and name.
target_path = "project/bin/"
target_name = "libgdexample"

# Set the compiler and flags.
env.Append(CPPPATH=["src"])  # Add the src folder to the include path.
env.Append(CFLAGS=["-O0", "-g"])  # Make it a debug build.

# Use Clang on macOS.
if sys.platform == "darwin":
    env["CC"] = "clang"

# Add all C files in "src" folder as sources.
sources = env.Glob("src/*.c")

# Create a shared library.
library = env.SharedLibrary(
    target=path.join(target_path, target_name),
    source=sources,
)

# Set the library as the default target.
env.Default(library)

여기에는 src 폴더의 모든 C 파일이 포함되므로 새 소스 파일을 추가할 때 이 파일을 변경할 필요가 없습니다.

패키지 설치하기

코드의 첫 번째 비트는 확장을 초기화하는 역할을 합니다. 이것이 Godot가 클래스와 플러그인과 같은 GDExtension이 제공하는 것을 인식하게 만드는 것입니다.

만들기 src 폴더에 다음 내용이 포함된 init.h 파일을 추가합니다.

#pragma once

#include "defs.h"

#include "gdextension_interface.h"

void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level);
void deinitialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level);
GDExtensionBool GDE_EXPORT gdexample_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization);

여기에 선언된 함수에는 GDExtension API에서 예상하는 서명이 있습니다.

defs.h 파일이 포함되어 있습니다. 이는 확장 코드 작성을 단순화하는 도우미 중 하나입니다. 지금은 Godot가 적절하게 호출할 수 있도록 공유 라이브러리에서 함수를 공개하는 매크로인 ``GDE_EXPORT``의 정의만 포함합니다. 이 매크로는 각 컴파일러가 기대하는 것을 추상화하는 데 도움이 됩니다.

만들기 다음 내용이 포함된 src 폴더의 defs.h 파일:

#pragma once

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

#if !defined(GDE_EXPORT)
#if defined(_WIN32)
#define GDE_EXPORT __declspec(dllexport)
#elif defined(__GNUC__)
#define GDE_EXPORT __attribute__((visibility("default")))
#else
#define GDE_EXPORT
#endif
#endif // ! GDE_EXPORT

또한 작업을 더 쉽게 하기 위해 몇 가지 표준 헤더도 포함합니다. 이제 ``defs.h``만 포함하면 되며 이는 보너스로 제공됩니다.

이제 방금 선언한 함수를 구현해 보겠습니다. 만들기 src 폴더에 ``init.c``라는 파일을 만들고 다음 코드를 추가합니다.

#include "init.h"

void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
}

void deinitialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
}

GDExtensionBool GDE_EXPORT gdexample_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization)
{
    r_initialization->initialize = initialize_gdexample_module;
    r_initialization->deinitialize = deinitialize_gdexample_module;
    r_initialization->userdata = NULL;
    r_initialization->minimum_initialization_level = GDEXTENSION_INITIALIZATION_SCENE;

    return true;
}

이것이 하는 일은 Godot가 기대하는 초기화 데이터를 설정하는 것입니다. 초기화 및 초기화 해제 기능이 설정되어 Godot가 필요할 때 호출합니다. 또한 확장별로 달라지는 초기화 수준을 설정합니다. 맞춤형 노드를 추가할 계획이므로 SCENE 수준이면 충분합니다.

나중에 사용자 정의 클래스를 등록하기 위해 initialize_gdexample_module() 함수를 채울 것입니다.

전역 클래스(Global class).

실제 노드를 만들기 위해 먼저 메서드 역할을 할 데이터와 함수를 보유하는 C 구조체를 만듭니다. 계획은 이를 :ref:`Sprite2D <class_Sprite2D>`에서 상속하는 사용자 지정 노드로 만드는 것입니다.

만들기 다음 내용이 포함된 src 폴더에 ``gdexample.h``라는 파일이 있습니다.

#pragma once

#include "gdextension_interface.h"

#include "defs.h"

// Struct to hold the node data.
typedef struct
{
    // Metadata.
    GDExtensionObjectPtr object; // Stores the underlying Godot object.
} GDExample;

// Constructor for the node.
void gdexample_class_constructor(GDExample *self);

// Destructor for the node.
void gdexample_class_destructor(GDExample *self);

// Bindings.
void gdexample_class_bind_methods();

여기서 주목할만한 것은 Godot 객체에 대한 포인터를 보유하는 object 필드와 사용자 정의 클래스(속성, 메소드 및 시그널)의 메타데이터를 등록하는 gdexample_class_bind_methods() 함수입니다. 클래스를 등록할 때 할 수 있으므로 후자는 완전히 필요한 것은 아니지만 문제를 분리하고 클래스가 자체 메타데이터를 등록하도록 하는 것이 더 명확합니다.

우리 클래스가 Godot 클래스를 상속할 것이기 때문에 object 필드가 필요합니다. 소스 코드와 상호 작용하지 않고(그리고 C에는 클래스도 없기 때문에) 이를 직접 상속할 수 없기 때문에 대신 Godot에게 알고 있는 유형의 객체를 생성하고 확장 기능을 첨부하라고 지시합니다. 예를 들어 부모 클래스에서 메서드를 호출할 때 그러한 개체에 대한 참조가 필요합니다.

이 헤더의 소스 대응물을 생성해 보겠습니다. src 폴더에 gdexample.c 파일을 만들기 만들고 다음 코드를 추가합니다.

#include "gdexample.h"

void gdexample_class_constructor(GDExample *self)
{
}

void gdexample_class_destructor(GDExample *self)
{
}

void gdexample_class_bind_methods()
{
}

아직 해당 기능과 관련이 없으므로 한동안 비어 있을 것입니다.

다음 단계는 수업을 등록하는 것입니다. 그러나 그렇게 하려면 :ref:`StringName <class_StringName>`을 생성해야 하며 이를 위해 GDExtension API에서 함수를 가져와야 합니다. 이 작업이 여러 번 필요하고 다른 것도 필요하므로 이러한 종류의 작업을 용이하게 하기 위해 래퍼 API를 만들어 보겠습니다.

래퍼 API

src 폴더에 api.h 파일을 생성하는 것부터 시작하겠습니다.

#pragma once

/*
This file works as a collection of helpers to call the GDExtension API
in a less verbose way, as well as a cache for methods from the discovery API,
just so we don't have to keep loading the same methods again.
*/

#include "gdextension_interface.h"

#include "defs.h"

extern GDExtensionClassLibraryPtr class_library;

// API methods.

extern struct Constructors
{
    GDExtensionInterfaceStringNameNewWithLatin1Chars string_name_new_with_latin1_chars;
} constructors;

extern struct Destructors
{
    GDExtensionPtrDestructor string_name_destructor;
} destructors;

extern struct API
{
    GDExtensionInterfaceClassdbRegisterExtensionClass2 classdb_register_extension_class2;
} api;

void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address);

확장 기능을 유용한 것으로 채울 때 이 파일에는 다른 많은 도우미가 포함됩니다. 지금은 C 문자열(Latin-1 인코딩)에서 StringName을 생성하는 함수에 대한 포인터와 메모리 누수를 방지하기 위해 사용해야 하는 StringName을 파괴하는 함수에 대한 포인터와 초기 목표인 클래스를 등록하는 함수만 있습니다.

또한 여기서는 ``class_library``에 대한 참조를 유지합니다. 이것은 확장을 초기화할 때 Godot가 우리에게 제공하는 것이며 우리가 생성한 것을 등록할 때 그것을 사용해야 Godot가 어떤 확장이 호출하는지 알 수 있습니다.

GDExtension API에서 해당 함수 포인터를 로드하는 함수도 있습니다.

이 헤더의 소스 대응 부분에 대해 작업해 보겠습니다. 만들기 src 폴더의 api.c 파일에 다음 코드를 추가합니다.

#include "api.h"

GDExtensionClassLibraryPtr class_library = NULL;

struct Constructors constructors;
struct Destructors destructors;
struct API api;

void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
    // Get helper functions first.
    GDExtensionInterfaceVariantGetPtrDestructor variant_get_ptr_destructor = (GDExtensionInterfaceVariantGetPtrDestructor)p_get_proc_address("variant_get_ptr_destructor");

    // API.
    api.classdb_register_extension_class2 = (GDExtensionInterfaceClassdbRegisterExtensionClass2)p_get_proc_address("classdb_register_extension_class2");

    // Constructors.
    constructors.string_name_new_with_latin1_chars = (GDExtensionInterfaceStringNameNewWithLatin1Chars)p_get_proc_address("string_name_new_with_latin1_chars");

    // Destructors.
    destructors.string_name_destructor = variant_get_ptr_destructor(GDEXTENSION_VARIANT_TYPE_STRING_NAME);
}

여기서 가장 먼저 중요한 것은 ``p_get_proc_address``입니다. 이는 초기화 중에 전달되는 GDExtension API의 함수입니다. 이 함수를 사용하면 API에서 이름으로 특정 함수를 요청할 수 있습니다. 여기에서는 결과를 캐싱하므로 ``p_get_proc_address``에 대한 참조를 모든 곳에 유지하고 대신 래퍼를 사용할 필요가 없습니다.

처음에는 variant_get_ptr_destructor() 기능을 요청합니다. 이는 이 함수 외부에서 사용되지 않으므로 래퍼에 추가하지 않고 로컬에서만 캐시합니다. 컴파일러 경고를 침묵시키려면 캐스트가 필요합니다.

그런 다음 C 문자열에서 StringName을 생성하는 함수를 얻습니다. 이는 이전에 필요한 함수로 언급한 것과 정확히 같습니다. 우리는 이를 constructors 구조체에 저장합니다.

다음으로, 방금 얻은 variant_get_ptr_destructor() 함수를 사용하여 gdextension_interface.h API의 열거형 값을 매개변수로 사용하여 StringName에 대한 소멸자를 쿼리합니다. 비슷한 방식으로 다른 유형에 대한 소멸자를 얻을 수 있지만 예제에 필요한 것만으로 제한하겠습니다.

마지막으로 사용자 정의 클래스를 등록하는 데 필요한 classdb_register_extension_class2() 함수를 얻습니다.

참고

함수 이름에 왜 ``2``가 있는지 궁금할 것입니다. 이는 이 함수의 두 번째 버전임을 의미합니다. 이전 버전은 이전 확장과의 호환성을 보장하기 위해 유지되지만, 두 번째 버전을 사용할 수 있으므로 새 버전을 사용하는 것이 가장 좋습니다. 이 예에서는 이전 Godot 버전을 지원할 의도가 없기 때문입니다.

gdextension_interface.h 헤더 문서에는 Godot 버전의 각 기능이 도입되었습니다.

또한 여기에서는 초기화 중에 설정될 class_library 변수를 정의합니다.

초기화에 관해 말하자면, 이제 방금 추가한 내용을 채우기 위해 init.c 파일을 변경해야 합니다.

GDExtensionBool GDE_EXPORT gdexample_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization)
{
    class_library = p_library;
    load_api(p_get_proc_address);

    ...

여기서는 필요에 따라 class_library``를 설정하고 새로운 ``load_api() 함수를 호출합니다. 이 파일 상단에 새 헤더도 포함하는 것을 잊지 마세요.

#include "init.h"

#include "api.h"
#include "gdexample.h"
...

여기 있으므로 새로운 사용자 정의 클래스를 등록할 수 있습니다. initialize_gdexample_module() 함수를 채워보겠습니다.

void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
    if (p_level != GDEXTENSION_INITIALIZATION_SCENE)
    {
        return;
    }

    // Register class.
    StringName class_name;
    constructors.string_name_new_with_latin1_chars(&class_name, "GDExample", false);
    StringName parent_class_name;
    constructors.string_name_new_with_latin1_chars(&parent_class_name, "Sprite2D", false);

    GDExtensionClassCreationInfo2 class_info = {
        .is_virtual = false,
        .is_abstract = false,
        .is_exposed = true,
        .set_func = NULL,
        .get_func = NULL,
        .get_property_list_func = NULL,
        .free_property_list_func = NULL,
        .property_can_revert_func = NULL,
        .property_get_revert_func = NULL,
        .validate_property_func = NULL,
        .notification_func = NULL,
        .to_string_func = NULL,
        .reference_func = NULL,
        .unreference_func = NULL,
        .create_instance_func = gdexample_class_create_instance,
        .free_instance_func = gdexample_class_free_instance,
        .recreate_instance_func = NULL,
        .get_virtual_func = NULL,
        .get_virtual_call_data_func = NULL,
        .call_virtual_with_data_func = NULL,
        .get_rid_func = NULL,
        .class_userdata = NULL,
    };

    api.classdb_register_extension_class2(class_library, &class_name, &parent_class_name, &class_info);

    // Bind methods.
    gdexample_class_bind_methods();

    // Destruct things.
    destructors.string_name_destructor(&class_name);
    destructors.string_name_destructor(&parent_class_name);
}

클래스 정보가 포함된 구조체가 여기서 가장 큰 것입니다. create_instance_funcfree_instance_func``를 제외하고는 해당 필드가 필요하지 않습니다. 우리는 아직 그러한 기능을 만들지 않았으므로 작업해야 것입니다. ``SCENE 수준이 아닌 경우 초기화를 건너뜁니다. 이 함수는 각 레벨마다 한 번씩 여러 번 호출할 수 있지만 우리는 클래스를 한 번만 등록하려고 합니다.

여기서 정의되지 않은 또 다른 것은 StringName``입니다. 이것은 우리 확장에서 Godot StringName의 데이터를 담기 위한 불투명 구조체가 것입니다. 적절한 이름의 ``defs.h 파일에서 이를 정의하겠습니다.

...
// The sizes can be obtained from the extension_api.json file.
#ifdef BUILD_32
#define STRING_NAME_SIZE 4
#else
#define STRING_NAME_SIZE 8
#endif

// Types.

typedef struct
{
    uint8_t data[STRING_NAME_SIZE];
} StringName;

주석에서 언급했듯이 크기는 이전에 생성한 extension_api.json 파일의 builtin_class_sizes 속성 아래에서 찾을 수 있습니다. 여기서는 64비트 Godot 빌드로 작업한다고 가정하므로 BUILD_32``는 정의되지 않습니다. 하지만 필요한 경우 ``env.Append(CPPDEFINES=["BUILD_32"])``를 ``SConstruct 파일에 추가할 수 있습니다.

// Types. 주석은 우리가 이 파일에 더 많은 유형을 추가할 것임을 암시합니다. 그건 나중을 위해 남겨두자.

여기서 StringName 구조체는 단지 Godot 데이터를 보관하기 위한 것이므로 그 안에 무엇이 있는지는 별로 신경쓰지 않습니다. 하지만 이 경우에는 힙의 데이터에 대한 포인터일 뿐입니다. 클래스를 등록할 때와 마찬가지로 StringName에 대한 데이터를 직접 할당해야 할 때 이 구조체를 사용합니다.

등록으로 돌아가서 생성 및 무료 기능을 작업해야 합니다. 이는 사용자 정의 클래스에만 해당되므로 ``gdexample.h``에 포함시키겠습니다.

...
// Bindings.
void gdexample_class_bind_methods();
GDExtensionObjectPtr gdexample_class_create_instance(void *p_class_userdata);
void gdexample_class_free_instance(void *p_class_userdata, GDExtensionClassInstancePtr p_instance);
...

해당 기능을 구현하기 전에 API에 몇 가지 사항이 더 필요합니다. 메모리를 할당하고 해제하는 방법이 필요합니다. 좋은 ``malloc()``로 이 작업을 수행할 수 있지만 대신 Godot의 메모리 관리 기능을 사용할 수 있습니다. 또한 Godot 객체를 생성하고 이를 사용자 정의 인스턴스로 설정하는 방법이 필요합니다.

따라서 다음과 같은 새로운 기능을 포함하도록 ``api.h``를 변경해 보겠습니다.

...
extern struct API
{
    GDExtensionInterfaceClassdbRegisterExtensionClass2 classdb_register_extension_class2;
    GDExtensionInterfaceClassdbConstructObject classdb_construct_object;
    GDExtensionInterfaceObjectSetInstance object_set_instance;
    GDExtensionInterfaceObjectSetInstanceBinding object_set_instance_binding;
    GDExtensionInterfaceMemAlloc mem_alloc;
    GDExtensionInterfaceMemFree mem_free;
} api;

그런 다음 api.c``의 ``load_api() 기능을 변경하여 다음과 같은 새로운 기능을 가져옵니다.

...
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
    ...
    // API.
    api.classdb_register_extension_class2 = p_get_proc_address("classdb_register_extension_class2");
    api.classdb_construct_object = (GDExtensionInterfaceClassdbConstructObject)p_get_proc_address("classdb_construct_object");
    api.object_set_instance = (GDExtensionInterfaceObjectSetInstance)p_get_proc_address("object_set_instance");
    api.object_set_instance_binding = (GDExtensionInterfaceObjectSetInstanceBinding)p_get_proc_address("object_set_instance_binding");
    api.mem_alloc = (GDExtensionInterfaceMemAlloc)p_get_proc_address("mem_alloc");
    api.mem_free = (GDExtensionInterfaceMemFree)p_get_proc_address("mem_free");
}

이제 gdexample.c 헤더를 포함하는 것을 잊지 않고 ``gdexample.c``로 돌아가서 새 기능을 정의할 수 있습니다.

#include "gdexample.h"

#include "api.h"

...

const GDExtensionInstanceBindingCallbacks gdexample_class_binding_callbacks = {
    .create_callback = NULL,
    .free_callback = NULL,
    .reference_callback = NULL,
};

GDExtensionObjectPtr gdexample_class_create_instance(void *p_class_userdata)
{
    // Create native Godot object;
    StringName class_name;
    constructors.string_name_new_with_latin1_chars(&class_name, "Sprite2D", false);
    GDExtensionObjectPtr object = api.classdb_construct_object(&class_name);
    destructors.string_name_destructor(&class_name);

    // Create extension object.
    GDExample *self = (GDExample *)api.mem_alloc(sizeof(GDExample));
    gdexample_class_constructor(self);
    self->object = object;

    // Set the extension instance in the native Godot object.
    constructors.string_name_new_with_latin1_chars(&class_name, "GDExample", false);
    api.object_set_instance(object, &class_name, self);
    api.object_set_instance_binding(object, class_library, self, &gdexample_class_binding_callbacks);
    destructors.string_name_destructor(&class_name);

    return object;
}

void gdexample_class_free_instance(void *p_class_userdata, GDExtensionClassInstancePtr p_instance)
{
    if (p_instance == NULL)
    {
        return;
    }
    GDExample *self = (GDExample *)p_instance;
    gdexample_class_destructor(self);
    api.mem_free(self);
}

객체를 인스턴스화할 때 먼저 새 Sprite2D 객체를 만듭니다. 왜냐하면 그것이 우리 클래스의 부모이기 때문입니다. 그런 다음 사용자 정의 구조체에 메모리를 할당하고 해당 생성자를 호출합니다. 앞서 언급한 것처럼 구조체의 Godot 객체에 대한 포인터를 저장합니다.

그런 다음 사용자 정의 구조체를 인스턴스 데이터로 설정합니다. 이렇게 하면 Godot는 객체가 우리 사용자 정의 클래스의 인스턴스임을 알게 되고 예를 들어 사용자 정의 메소드를 적절하게 호출할 뿐만 아니라 이 데이터를 다시 전달할 것입니다.

우리는 사용자 정의 구조체가 아닌 우리가 만든 Godot 객체를 반환한다는 점에 유의하세요.

gdextension_free_instance() 함수의 경우 소멸자를 호출하고 사용자 지정 데이터에 할당한 메모리만 해제합니다. 엔진 자체에서 처리되므로 Godot 객체를 파괴할 필요는 없습니다.

내적

이제 사용자 정의 개체를 만들고 해제할 수 있으므로 실제 프로젝트에서 시험해 볼 수 있습니다. 이를 위해서는 Godot를 열고 project 폴더에 새 프로젝트를 만들어야 합니다. 이전에 확장을 컴파일한 경우 프로젝트 관리자가 폴더가 비어 있지 않다고 경고할 수 있습니다. 이번에는 이 경고를 무시해도 됩니다.

아직 확장을 컴파일하지 않았다면 지금 컴파일할 때입니다. 그렇게 하려면 터미널이나 명령 프롬프트를 열고 확장 프로그램의 루트 폴더로 이동한 후 ``scons``를 실행하세요. 확장은 매우 간단하므로 빠르게 컴파일되어야 합니다.

그런 다음 project 폴더 안에 ``gdexample.gdextension``라는 파일을 만듭니다. 이것은 엔진이 확장을 적절하게 로드할 수 있도록 확장을 설명하는 Godot 리소스입니다. 이 파일에 다음 내용을 넣으세요:

[configuration]

entry_symbol = "gdexample_library_init"
compatibility_minimum = "4.2"

[libraries]
macos.debug = "res://bin/libgdexample.dylib"
linux.debug = "res://bin/libgdexample.so"
windows.debug = "res://bin/libgdexample.dll"

보시다시피, gdexample_library_init()``는 ``init.c 파일에 정의한 함수와 동일한 이름입니다. Godot가 확장의 진입점을 호출하는 방법이기 때문에 이름이 일치하는 것이 중요합니다.

또한 이 버전을 대상으로 하므로 호환성 최소값을 4.2로 설정했습니다. 이후 버전에서는 계속 작동합니다. 최신 Godot 버전을 사용하고 있고 새로운 기능을 사용한다면, 이 값을 사용하는 모든 것을 포함하는 버전 번호로 늘려야 합니다. 자세한 내용은 :ref:`doc_what_is_gdextension_version_compatibility`를 참조하세요.

[libraries] 섹션에서는 다양한 플랫폼의 공유 라이브러리에 대한 경로를 설정했습니다. 여기에는 우리가 예제를 위해 작업 중인 디버그 버전만 있습니다. :ref:`feature 태그 <doc_feature_tags>`를 사용하면 이를 세부적으로 조정하여 릴리스 버전을 제공하고, 더 많은 대상 운영 체제를 추가하고, 32비트 및 64비트 바이너리를 제공할 수 있습니다.

이 파일에 클래스에 대한 라이브러리 종속성과 사용자 정의 아이콘을 추가할 수도 있지만 이는 이 튜토리얼의 범위를 벗어납니다.

파일을 저장한 후 편집기로 돌아갑니다. Godot는 자동으로 확장 기능을 로드해야 합니다. 우리의 확장은 새로운 클래스만 등록하기 때문에 아무것도 보이지 않을 것입니다. 이 클래스를 사용하려면 ``Node2D``를 씬의 루트로 추가하세요. 더 나은 가시성을 위해 뷰포트의 중앙으로 이동하십시오. 그런 다음 새 자식 노드를 루트에 추가하고 만들기 New 노드 대화 상자에서 클래스 이름인 "GDExample"을 검색합니다. 그렇지 않다면 Godot가 확장 기능을 제대로 로드하지 않았다는 의미이므로 편집기를 다시 시작하고 단계를 다시 추적하여 누락된 것이 있는지 확인하세요.

우리의 사용자 정의 클래스는 Sprite2D``에서 파생되었으므로 인스펙터에 **Texture** 속성이 있습니다. 프로젝트를 만들 Godot가 우리를 위해 쉽게 생성한 ``icon.svg 파일로 이것을 설정하세요. 이 씬을 ``main.tscn``로 저장하고 실행하세요. 편의상 메인 씬로 설정할 수도 있습니다.

../../../_images/gdextension_c_running.webp

짜잔! 우리는 Godot에서 실행되는 맞춤형 노드를 가지고 있습니다. 그러나 아무것도 하지 않으며 일반 Sprite2D 노드와 다른 점이 없습니다. 다음에는 사용자 지정 메서드와 속성을 추가하여 이 문제를 수정하겠습니다.

커스텀 메서드

확장의 일반적인 점은 사용자 정의 클래스에 대한 메소드를 생성하고 이를 Godot API에 노출하는 것입니다. 나중에 속성을 바인딩하는 데 필요한 몇 가지 getter 및 setter를 만들 예정입니다.

먼저 amplitudespeed``에 대한 값을 보유하기 위해 구조체에 필드를 추가하겠습니다. 필드는 나중에 노드에 대한 동작을 생성할 사용할 것입니다. ``GDExample 구조체를 변경하여 gdexample.h 파일에 추가합니다.

...

typedef struct
{
    // Public properties.
    double amplitude;
    double speed;
    // Metadata.
    GDExtensionObjectPtr object; // Stores the underlying Godot object.
} GDExample;

...

동일한 파일에서 소멸자 바로 뒤에 getter 및 setter에 대한 선언을 추가합니다.

...

// Destructor for the node.
void gdexample_class_destructor(GDExample *self);

// Properties.
void gdexample_class_set_amplitude(GDExample *self, double amplitude);
double gdexample_class_get_amplitude(const GDExample *self);
void gdexample_class_set_speed(GDExample *self, double speed);
double gdexample_class_get_speed(const GDExample *self);

...

gdexample.c 파일에서는 생성자에서 이러한 값을 초기화하고 매우 간단한 새 함수에 대한 구현을 추가합니다.

void gdexample_class_constructor(GDExample *self)
{
    self->amplitude = 10.0;
    self->speed = 1.0;
}

void gdexample_class_set_amplitude(GDExample *self, double amplitude)
{
    self->amplitude = amplitude;
}

double gdexample_class_get_amplitude(const GDExample *self)
{
    return self->amplitude;
}

void gdexample_class_set_speed(GDExample *self, double speed)
{
    self->speed = speed;
}

double gdexample_class_get_speed(const GDExample *self)
{
    return self->speed;
}

Godot가 호출할 때 이러한 간단한 함수가 작동하도록 하려면, 엔진과 데이터를 적절하게 변환하는 데 도움이 되는 래퍼가 필요합니다.

먼저 ptrcall``에 대한 래퍼를 생성합니다. 이것은 값의 유형이 정확한 것으로 알려졌을 Godot가 사용하는 것이며, 이는 Variant 사용을 피하는 것입니다. 개가 필요합니다. 하나는 인수를 사용하지 않고 ``double``를 반환하는 함수용(getter용)이고 다른 하나는 단일 ``double 인수를 사용하고 아무것도 반환하지 않는 함수용(setter용)입니다.

api.h 파일에 선언을 추가합니다.

void ptrcall_0_args_ret_float(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret);
void ptrcall_1_float_arg_no_ret(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret);

이 두 함수는 gdextension_interface.h``에 정의된 대로 ``GDExtensionClassMethodPtrCall 유형을 따릅니다. 여기서는 float``를 이름으로 사용합니다. 왜냐하면 Godot에서 ``float 유형은 배정밀도를 갖고 있기 때문에 이 규칙을 유지합니다.

그런 다음 api.c 파일에 해당 기능을 구현합니다.

void ptrcall_0_args_ret_float(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret)
{
    // Call the function.
    double (*function)(void *) = method_userdata;
    *((double *)r_ret) = function(p_instance);
}

void ptrcall_1_float_arg_no_ret(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret)
{
    // Call the function.
    void (*function)(void *, double) = method_userdata;
    function(p_instance, *((double *)p_args[0]));
}

method_userdata 인수는 Godot에 제공하는 사용자 정의 값입니다. 이 경우 호출하려는 함수 포인터로 설정합니다. 따라서 먼저 이를 함수 유형으로 변환한 다음 필요할 때 인수를 전달하거나 반환 값을 설정하여 호출합니다.

p_instance 인수에는 객체를 생성할 때 ``object_set_instance()``와 함께 제공한 클래스의 사용자 정의 인스턴스가 포함되어 있습니다.

``p_args``는 인수 배열입니다. 여기에는 값에 대한 **포인터**가 포함되어 있습니다. 이것이 바로 함수에 전달할 때 역참조하는 이유입니다. 인수 수는 함수를 바인딩할 때 선언되며(곧 수행할 예정임) 기본 인수가 있는 경우 항상 포함됩니다.

마지막으로 ``r_ret``는 반환 값을 설정해야 하는 변수에 대한 포인터입니다. 인수와 마찬가지로 선언된 대로 올바른 유형이 됩니다. 반환되지 않는 함수에 대해서는 설정을 피해야 합니다.

유형과 인수 개수가 얼마나 정확한지 확인하세요. 예를 들어 다른 유형이 필요한 경우 더 많은 래퍼를 만들어야 합니다. 이는 일부 코드 생성을 사용하여 자동화할 수 있지만 이는 이 자습서의 범위를 벗어납니다.

ptrcall 함수는 유형이 정확할 때 사용되지만, 때때로 Godot는 그것이 사실인지 알 수 없습니다(호출이 GDScript와 같은 동적으로 유형이 지정된 언어에서 오는 경우). 이러한 상황에서는 일반 call 기능을 사용하므로 바인딩할 때 해당 기능도 제공해야 합니다.

api.h 파일에 두 개의 새로운 래퍼를 만들어 보겠습니다.

void call_0_args_ret_float(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstVariantPtr *p_args, GDExtensionInt p_argument_count, GDExtensionVariantPtr r_return, GDExtensionCallError *r_error);
void call_1_float_arg_no_ret(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstVariantPtr *p_args, GDExtensionInt p_argument_count, GDExtensionVariantPtr r_return, GDExtensionCallError *r_error);

이는 약간 다른 GDExtensionClassMethodCall 유형을 따릅니다. 첫째, 정확한 유형 대신 Variants에 대한 포인터를 받습니다. 문제가 발생할 경우 설정할 수 있는 인수의 양과 오류 구조체도 있습니다.

유형을 확인하고 Variant와의 상호 작용을 추출하려면 GDExtension API의 몇 가지 기능이 더 필요합니다. 이제 래퍼 구조체를 확장해 보겠습니다.

extern struct Constructors {
    ...
    GDExtensionVariantFromTypeConstructorFunc variant_from_float_constructor;
    GDExtensionTypeFromVariantConstructorFunc float_from_variant_constructor;
} constructors;

extern struct API
{
    ...
    GDExtensionInterfaceGetVariantFromTypeConstructor get_variant_from_type_constructor;
    GDExtensionInterfaceGetVariantToTypeConstructor get_variant_to_type_constructor;
    GDExtensionInterfaceVariantGetType variant_get_type;
} api;

이름은 그들이 하는 일에 대해 모두 말해줍니다. Variant에서 부동 소수점 값을 생성하고 추출하는 두 개의 생성자가 있습니다. 또한 이러한 생성자를 실제로 가져오는 몇 가지 도우미와 Variant 유형을 알아내는 함수도 있습니다.

이전에 했던 것처럼 api.c 파일에서 load_api() 함수를 변경하여 API에서 이를 가져옵니다.

void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
    ...

    // API.
    ...
    api.get_variant_from_type_constructor = (GDExtensionInterfaceGetVariantFromTypeConstructor)p_get_proc_address("get_variant_from_type_constructor");
    api.get_variant_to_type_constructor = (GDExtensionInterfaceGetVariantToTypeConstructor)p_get_proc_address("get_variant_to_type_constructor");
    api.variant_get_type = (GDExtensionInterfaceVariantGetType)p_get_proc_address("variant_get_type");
    ...

    // Constructors.
    ...
    constructors.variant_from_float_constructor = api.get_variant_from_type_constructor(GDEXTENSION_VARIANT_TYPE_FLOAT);
    constructors.float_from_variant_constructor = api.get_variant_to_type_constructor(GDEXTENSION_VARIANT_TYPE_FLOAT);
    ...
}

이제 이러한 설정이 완료되었으므로 동일한 파일에 호출 래퍼를 구현할 수 있습니다.

void call_0_args_ret_float(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstVariantPtr *p_args, GDExtensionInt p_argument_count, GDExtensionVariantPtr r_return, GDExtensionCallError *r_error)
{
    // Check argument count.
    if (p_argument_count != 0)
    {
        r_error->error = GDEXTENSION_CALL_ERROR_TOO_MANY_ARGUMENTS;
        r_error->expected = 0;
        return;
    }

    // Call the function.
    double (*function)(void *) = method_userdata;
    double result = function(p_instance);
    // Set resulting Variant.
    constructors.variant_from_float_constructor(r_return, &result);
}

void call_1_float_arg_no_ret(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstVariantPtr *p_args, GDExtensionInt p_argument_count, GDExtensionVariantPtr r_return, GDExtensionCallError *r_error)
{
    // Check argument count.
    if (p_argument_count < 1)
    {
        r_error->error = GDEXTENSION_CALL_ERROR_TOO_FEW_ARGUMENTS;
        r_error->expected = 1;
        return;
    }
    else if (p_argument_count > 1)
    {
        r_error->error = GDEXTENSION_CALL_ERROR_TOO_MANY_ARGUMENTS;
        r_error->expected = 1;
        return;
    }

    // Check the argument type.
    GDExtensionVariantType type = api.variant_get_type(p_args[0]);
    if (type != GDEXTENSION_VARIANT_TYPE_FLOAT)
    {
        r_error->error = GDEXTENSION_CALL_ERROR_INVALID_ARGUMENT;
        r_error->expected = GDEXTENSION_VARIANT_TYPE_FLOAT;
        r_error->argument = 0;
        return;
    }

    // Extract the argument.
    double arg1;
    constructors.float_from_variant_constructor(&arg1, (GDExtensionVariantPtr)p_args[0]);

    // Call the function.
    void (*function)(void *, double) = method_userdata;
    function(p_instance, arg1);
}

이 기능은 조금 길지만 따라하기 쉽습니다. 먼저 인수 개수가 예상대로인지 확인하고 그렇지 않은 경우 오류 구조체를 설정하고 반환합니다. 매개변수가 하나인 경우 인수 유형이 올바른지 확인합니다. Variant에서 추출할 때 일치하지 않는 유형으로 인해 충돌이 발생할 수 있으므로 이는 중요합니다.

그런 다음 이전에 설정한 생성자를 사용하여 인수를 추출합니다. 인수가 없는 쪽은 대신 함수를 호출한 후 반환 값을 설정합니다. double 변수에 대한 포인터를 사용하는 방법에 주목하세요. 왜냐하면 이것이 생성자가 기대하는 것이기 때문입니다.

실제로 메서드를 바인딩하려면 GDExtensionPropertyInfo 인스턴스를 생성하는 방법이 필요합니다. 나중에 구현할 바인딩 함수 내에서 이를 수행할 수도 있지만 속성을 바인딩할 때를 포함하여 여러 번 필요하므로 도우미를 갖는 것이 더 쉽습니다.

api.h 파일에 다음 두 가지 함수를 만들어 보겠습니다.

// Create a PropertyInfo struct.
GDExtensionPropertyInfo make_property(
    GDExtensionVariantType type,
    const char *name);

GDExtensionPropertyInfo make_property_full(
    GDExtensionVariantType type,
    const char *name,
    uint32_t hint,
    const char *hint_string,
    const char *class_name,
    uint32_t usage_flags);

void destruct_property(GDExtensionPropertyInfo *info);

첫 번째는 두 번째의 단순화된 버전입니다. 일반적으로 속성에 대한 모든 인수가 필요하지 않고 기본값을 사용해도 괜찮기 때문입니다. 그런 다음 적절하게 폐기해야 하는 문자열 및 StringName을 생성해야 하므로 PropertyInfo를 파괴하는 함수도 있습니다.

말하자면, 문자열을 생성하고 소멸하는 방법도 필요하므로 동일한 파일의 기존 구조체에 추가할 것입니다. 또한 사용자 정의 메소드를 실제로 바인딩하기 위한 새로운 API 함수도 제공됩니다.

extern struct Constructors
{
    ...
    GDExtensionInterfaceStringNewWithUtf8Chars string_new_with_utf8_chars;
} constructors;

extern struct Destructors
{
    ...
    GDExtensionPtrDestructor string_destructor;
} destructors;

extern struct API
{
    ...
    GDExtensionInterfaceClassdbRegisterExtensionClassMethod classdb_register_extension_class_method;
} api;

이를 구현하기 전에 defs.h 파일에서 잠시 멈추고 String 유형의 크기와 몇 가지 열거형을 포함해 보겠습니다.

// The sizes can be obtained from the extension_api.json file.
#ifdef BUILD_32
#define STRING_SIZE 4
#define STRING_NAME_SIZE 4
#else
#define STRING_SIZE 8
#define STRING_NAME_SIZE 8
#endif

...

typedef struct
{
    uint8_t data[STRING_SIZE];
} String;

// Enums.

typedef enum
{
    PROPERTY_HINT_NONE = 0,
} PropertyHint;

typedef enum
{
    PROPERTY_USAGE_NONE = 0,
    PROPERTY_USAGE_STORAGE = 2,
    PROPERTY_USAGE_EDITOR = 4,
    PROPERTY_USAGE_DEFAULT = PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR,
} PropertyUsageFlags;

``StringName``와 크기는 동일하지만 다른 이름을 사용하는 것이 더 명확합니다.

여기의 열거형은 자신이 나타내는 숫자에 이름을 부여하는 도우미일 뿐입니다. 이에 대한 정보는 extension_api.json 파일에 있습니다. 여기서는 튜토리얼을 더욱 간결하게 유지하기 위해 필요한 항목을 설정했습니다.

이제 ``api.c``로 이동하여 API에 추가한 새 함수에 대한 포인터를 로드해야 합니다.

void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
    ...
    // API
    ...
    api.classdb_register_extension_class_method = (GDExtensionInterfaceClassdbRegisterExtensionClassMethod)p_get_proc_address("classdb_register_extension_class_method");

    // Constructors.
    ...
    constructors.string_new_with_utf8_chars = (GDExtensionInterfaceStringNewWithUtf8Chars)p_get_proc_address("string_new_with_utf8_chars");

    // Destructors.
    ...
    destructors.string_destructor = variant_get_ptr_destructor(GDEXTENSION_VARIANT_TYPE_STRING);
}

그런 다음 PropertyInfo 구조체를 생성하는 함수를 구현할 수도 있습니다.

GDExtensionPropertyInfo make_property(
    GDExtensionVariantType type,
    const char *name)
{

    return make_property_full(type, name, PROPERTY_HINT_NONE, "", "", PROPERTY_USAGE_DEFAULT);
}

GDExtensionPropertyInfo make_property_full(
    GDExtensionVariantType type,
    const char *name,
    uint32_t hint,
    const char *hint_string,
    const char *class_name,
    uint32_t usage_flags)
{

    StringName *prop_name = api.mem_alloc(sizeof(StringName));
    constructors.string_name_new_with_latin1_chars(prop_name, name, false);
    String *prop_hint_string = api.mem_alloc(sizeof(String));
    constructors.string_new_with_utf8_chars(prop_hint_string, hint_string);
    StringName *prop_class_name = api.mem_alloc(sizeof(StringName));
    constructors.string_name_new_with_latin1_chars(prop_class_name, class_name, false);

    GDExtensionPropertyInfo info = {
        .name = prop_name,
        .type = type,
        .hint = hint,
        .hint_string = prop_hint_string,
        .class_name = prop_class_name,
        .usage = usage_flags,
    };

    return info;
}

void destruct_property(GDExtensionPropertyInfo *info)
{
    destructors.string_name_destructor(info->name);
    destructors.string_destructor(info->hint_string);
    destructors.string_name_destructor(info->class_name);
    api.mem_free(info->name);
    api.mem_free(info->hint_string);
    api.mem_free(info->class_name);
}

``make_property()``의 간단한 버전은 몇 가지 기본 인수를 사용하여 더 완전한 버전을 호출합니다. 이러한 값이 정확히 의미하는 바는 이 튜토리얼의 범위를 벗어납니다. 바인딩 메서드 및 속성에 대한 자세한 내용은 :ref:`Object 클래스 <doc_object_class>`에 대한 페이지를 확인하세요.

완전한 버전은 더 복잡합니다. 먼저 메모리를 할당하고 생성자를 호출하여 필요한 필드에 대해 StringStringName``를 생성합니다. 그런 다음 ``GDExtensionPropertyInfo 구조체를 생성하고 제공된 인수로 모든 필드를 설정합니다. 마지막으로 생성된 구조체를 반환합니다.

destruct_property() 함수는 간단합니다. 생성된 객체에 대한 소멸자를 호출하고 할당된 메모리를 해제하기만 하면 됩니다.

실제로 메서드를 바인딩할 함수를 생성하기 위해 api.h 헤더로 다시 돌아가 보겠습니다.

// Version for 0 arguments, with return.
void bind_method_0_r(
    const char *class_name,
    const char *method_name,
    void *function,
    GDExtensionVariantType return_type);

// Version for 1 argument, no return.
void bind_method_1(
    const char *class_name,
    const char *method_name,
    void *function,
    const char *arg1_name,
    GDExtensionVariantType arg1_type);

그런 다음 api.c 파일로 다시 전환하여 다음을 구현합니다.

// Version for 0 arguments, with return.
void bind_method_0_r(
    const char *class_name,
    const char *method_name,
    void *function,
    GDExtensionVariantType return_type)
{
    StringName method_name_string;
    constructors.string_name_new_with_latin1_chars(&method_name_string, method_name, false);

    GDExtensionClassMethodCall call_func = call_0_args_ret_float;
    GDExtensionClassMethodPtrCall ptrcall_func = ptrcall_0_args_ret_float;

    GDExtensionPropertyInfo return_info = make_property(return_type, "");

    GDExtensionClassMethodInfo method_info = {
        .name = &method_name_string,
        .method_userdata = function,
        .call_func = call_func,
        .ptrcall_func = ptrcall_func,
        .method_flags = GDEXTENSION_METHOD_FLAGS_DEFAULT,
        .has_return_value = true,
        .return_value_info = &return_info,
        .return_value_metadata = GDEXTENSION_METHOD_ARGUMENT_METADATA_NONE,
        .argument_count = 0,
    };

    StringName class_name_string;
    constructors.string_name_new_with_latin1_chars(&class_name_string, class_name, false);

    api.classdb_register_extension_class_method(class_library, &class_name_string, &method_info);

    // Destruct things.
    destructors.string_name_destructor(&method_name_string);
    destructors.string_name_destructor(&class_name_string);
    destruct_property(&return_info);
}

// Version for 1 argument, no return.
void bind_method_1(
    const char *class_name,
    const char *method_name,
    void *function,
    const char *arg1_name,
    GDExtensionVariantType arg1_type)
{

    StringName method_name_string;
    constructors.string_name_new_with_latin1_chars(&method_name_string, method_name, false);

    GDExtensionClassMethodCall call_func = call_1_float_arg_no_ret;
    GDExtensionClassMethodPtrCall ptrcall_func = ptrcall_1_float_arg_no_ret;

    GDExtensionPropertyInfo args_info[] = {
        make_property(arg1_type, arg1_name),
    };
    GDExtensionClassMethodArgumentMetadata args_metadata[] = {
        GDEXTENSION_METHOD_ARGUMENT_METADATA_NONE,
    };

    GDExtensionClassMethodInfo method_info = {
        .name = &method_name_string,
        .method_userdata = function,
        .call_func = call_func,
        .ptrcall_func = ptrcall_func,
        .method_flags = GDEXTENSION_METHOD_FLAGS_DEFAULT,
        .has_return_value = false,
        .argument_count = 1,
        .arguments_info = args_info,
        .arguments_metadata = args_metadata,
    };

    StringName class_name_string;
    constructors.string_name_new_with_latin1_chars(&class_name_string, class_name, false);

    api.classdb_register_extension_class_method(class_library, &class_name_string, &method_info);

    // Destruct things.
    destructors.string_name_destructor(&method_name_string);
    destructors.string_name_destructor(&class_name_string);
    destruct_property(&args_info[0]);
}

두 기능 모두 매우 유사합니다. 먼저 메소드 이름으로 StringName``를 생성합니다. 이는 함수가 끝난 후에 보관할 필요가 없기 때문에 스택에 생성됩니다. 그런 다음 이전에 정의한 도우미 함수를 가리키는 ``call_func``ptrcall_func``를 보유하는 지역 변수를 만듭니다.

다음 단계에서는 약간 다릅니다. 첫 번째는 반환 값에 대한 속성을 생성하는데, 이 속성은 필요하지 않기 때문에 이름이 비어 있습니다. 다른 하나는 인수에 대한 속성 배열을 생성하는데, 이 경우에는 단일 요소가 있습니다. 여기에는 인수에 특별한 것이 있는 경우(예: int 값의 길이가 기본값인 64비트가 아닌 32비트인 경우) 사용할 수 있는 메타데이터 배열도 있습니다.

그런 다음 각 사례에 필요한 필드가 포함된 ``GDExtensionClassMethodInfo``를 생성합니다. 그런 다음 메서드를 클래스와 연결하기 위해 클래스 이름에 대해 ``StringName``를 만듭니다. 다음으로 API 함수를 호출하여 이 메서드를 클래스에 실제로 바인딩합니다. 마지막으로 우리가 만든 객체는 더 이상 필요하지 않으므로 삭제합니다.

참고

여기서 바인딩 도우미는 우리가 이전에 만든 호출 도우미를 사용하므로 해당 호출 도우미는 Godot FLOAT 유형(C의 ``double``와 동일)만 허용한다는 점에 유의하세요. 이를 다른 유형에 사용하려면 인수 유형과 반환 유형을 확인하고 적절한 함수 콜백을 선택해야 합니다. 예제가 더 길어지는 것을 방지하기 위해 여기서는 이를 피합니다.

이제 메소드를 바인딩할 수 있는 방법이 있으므로 사용자 정의 클래스에서 실제로 바인딩할 수 있습니다. gdexample.c 파일로 이동하여 gdexample_class_bind_methods() 함수를 입력합니다.

void gdexample_class_bind_methods()
{
    bind_method_0_r("GDExample", "get_amplitude", gdexample_class_get_amplitude, GDEXTENSION_VARIANT_TYPE_FLOAT);
    bind_method_1("GDExample", "set_amplitude", gdexample_class_set_amplitude, "amplitude", GDEXTENSION_VARIANT_TYPE_FLOAT);

    bind_method_0_r("GDExample", "get_speed", gdexample_class_get_speed, GDEXTENSION_VARIANT_TYPE_FLOAT);
    bind_method_1("GDExample", "set_speed", gdexample_class_set_speed, "speed", GDEXTENSION_VARIANT_TYPE_FLOAT);
}

이 함수는 이미 초기화 프로세스에서 호출되고 있으므로 여기서 중지할 수 있습니다. 이 기능은 이 작업을 수행하기 위한 모든 인프라를 만든 후에 훨씬 더 간단해졌습니다. 여기에서 바인딩 기능을 인라인으로 구현하면 약간의 공간이 필요하고 상당히 반복적이라는 것을 알 수 있습니다. 또한 나중에 다른 메소드를 추가하는 것도 더 쉬워집니다.

코드를 컴파일하고 Godot 프로젝트를 다시 열면 처음에는 두 가지 새로운 메소드만 추가했기 때문에 아무것도 달라지지 않습니다. 해당 항목이 올바르게 등록되었는지 확인하려면 편집기 도움말에서 ``GDExample``를 검색하고 문서 페이지에 해당 내용이 있는지 확인할 수 있습니다.

../../../_images/gdextension_c_methods_doc.webp

속성 그루핑(Grouping)

이제 우리는 속성에 대한 getter와 setter를 이미 바인딩했기 때문에 Godot 편집기 검사기에 표시될 실제 속성을 만들기 위해 계속 진행할 수 있습니다.

이전 섹션의 광범위한 설정을 고려하면 속성을 바인딩하는 데 필요한 것은 몇 가지뿐입니다. 먼저 api.h 파일에서 새로운 API 함수를 가져옵니다.

extern struct API {
    ...
    GDExtensionInterfaceClassdbRegisterExtensionClassProperty classdb_register_extension_class_property;
} api;

HUD는 다음의 정보들을 보여줍니다:

void bind_property(
    const char *class_name,
    const char *name,
    GDExtensionVariantType type,
    const char *getter,
    const char *setter);

api.c 파일에서 새 API 함수를 로드할 수 있습니다.

void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
    // API
    ...
    api.classdb_register_extension_class_property = (GDExtensionInterfaceClassdbRegisterExtensionClassProperty)p_get_proc_address("classdb_register_extension_class_property");

    ...
}

그런 다음 동일한 파일에 새로운 도우미 함수를 구현할 수 있습니다.

void bind_property(
    const char *class_name,
    const char *name,
    GDExtensionVariantType type,
    const char *getter,
    const char *setter)
{
    StringName class_string_name;
    constructors.string_name_new_with_latin1_chars(&class_string_name, class_name, false);
    GDExtensionPropertyInfo info = make_property(type, name);
    StringName getter_name;
    constructors.string_name_new_with_latin1_chars(&getter_name, getter, false);
    StringName setter_name;
    constructors.string_name_new_with_latin1_chars(&setter_name, setter, false);

    api.classdb_register_extension_class_property(class_library, &class_string_name, &info, &setter_name, &getter_name);

    // Destruct things.
    destructors.string_name_destructor(&class_string_name);
    destruct_property(&info);
    destructors.string_name_destructor(&getter_name);
    destructors.string_name_destructor(&setter_name);
}

이 기능은 바인딩 방법과 유사합니다. 주요 차이점은 도우미 함수에 의해 생성된 GDExtensionPropertyInfo``를 간단히 사용할 있으므로 추가 구조체가 필요하지 않으므로 간단하다는 것입니다. C 문자열에서 ``StringName 값만 생성하고, 도우미를 사용하여 속성 정보 구조체를 생성하고, API 함수를 호출하여 클래스에 속성을 등록한 다음 생성한 모든 개체를 삭제합니다.

이 작업이 완료되면 gdexample.c 파일에서 gdexample_class_bind_methods() 기능을 확장할 수 있습니다.

void gdexample_class_bind_methods()
{
    bind_method_0_r("GDExample", "get_amplitude", gdexample_class_get_amplitude, GDEXTENSION_VARIANT_TYPE_FLOAT);
    bind_method_1("GDExample", "set_amplitude", gdexample_class_set_amplitude, "amplitude", GDEXTENSION_VARIANT_TYPE_FLOAT);
    bind_property("GDExample", "amplitude", GDEXTENSION_VARIANT_TYPE_FLOAT, "get_amplitude", "set_amplitude");

    bind_method_0_r("GDExample", "get_speed", gdexample_class_get_speed, GDEXTENSION_VARIANT_TYPE_FLOAT);
    bind_method_1("GDExample", "set_speed", gdexample_class_set_speed, "speed", GDEXTENSION_VARIANT_TYPE_FLOAT);
    bind_property("GDExample", "speed", GDEXTENSION_VARIANT_TYPE_FLOAT, "get_speed", "set_speed");
}

scons``로 확장을 빌드하면 Godot 편집기에서 사용자 정의 클래스에 대한 문서 페이지뿐만 아니라 ``GDExample 노드가 선택되었을 때 인스펙터 독에도 표시되는 새 속성을 볼 수 있습니다.

../../../_images/gdextension_c_inspector_properties.webp

가상 메서드 바인딩

사용자 정의 노드에는 이제 작동 방식에 영향을 주는 속성이 있지만 여전히 아무 작업도 수행하지 않습니다. 이 섹션에서는 가상 메서드 :ref:`_process() <class_Node_private_method__process>`를 바인딩하고 사용자 정의 스프라이트를 약간 움직이게 만듭니다.

gdexample.h 파일에서 사용자 정의 _process() 메서드를 나타내는 함수를 추가해 보겠습니다.

// Methods.
void gdexample_class_process(GDExample *self, double delta);

또한 사용자 정의 구조체에 전달된 시간을 추적하기 위해 "private" 필드를 추가할 것입니다. 언어에 액세스 수정자가 없기 때문에 C 측에서는 공개임에도 불구하고 이는 Godot API에 바인딩되지 않는다는 의미에서만 "비공개"입니다.

typedef struct
{
    // Private properties.
    double time_passed;
    ...
} GDExample;

대응 소스 파일 ``gdexample.c``에서 생성자의 새 필드를 초기화해야 합니다.

void gdexample_class_constructor(GDExample *self)
{
    self->time_passed = 0.0;
    self->amplitude = 10.0;
    self->speed = 1.0;
}

그런 다음 _process 메서드에 대한 가장 간단한 구현을 만들 수 있습니다.

void gdexample_class_process(GDExample *self, double delta)
{
    self->time_passed += self->speed * delta;
}

지금은 우리가 만든 비공개 필드를 업데이트하는 것 외에는 아무것도 하지 않습니다. 메서드가 올바르게 바인딩된 후에 이에 대해 다시 설명하겠습니다.

가상 메소드는 일반 바인딩과 약간 다릅니다. 메소드 자체를 명시적으로 등록하는 대신, 우리는 확장 기능에 특정 가상 메소드가 구현되었는지 묻기 위해 Godot가 호출할 특수 함수를 등록할 것입니다. 엔진은 ``StringName``를 인수로 전달하므로 이 튜토리얼의 정신에 따라 C 문자열과 같은지 확인하는 도우미 함수를 만듭니다.

api.h 파일에 선언을 추가해 보겠습니다.

// Compare a StringName with a C string.
bool is_string_name_equal(GDExtensionConstStringNamePtr p_a, const char *p_b);

또한 사용자 정의 연산자에 대한 함수 포인터를 보유하기 위해 이 파일에 새 구조체를 추가할 것입니다.

extern struct Operators
{
    GDExtensionPtrOperatorEvaluator string_name_equal;
} operators;

그런 다음 api.c 파일에서 API의 함수 포인터를 로드합니다.

struct Operators operators;

void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
    // Get helper functions first.
    ...
    GDExtensionInterfaceVariantGetPtrOperatorEvaluator variant_get_ptr_operator_evaluator = (GDExtensionInterfaceVariantGetPtrOperatorEvaluator)p_get_proc_address("variant_get_ptr_operator_evaluator");

    ...

    // Operators.
    operators.string_name_equal = variant_get_ptr_operator_evaluator(GDEXTENSION_VARIANT_OP_EQUAL, GDEXTENSION_VARIANT_TYPE_STRING_NAME, GDEXTENSION_VARIANT_TYPE_STRING_NAME);
}

보시다시피 연산자에 대한 함수 포인터를 가져오려면 여기에 새로운 로컬 도우미가 필요합니다.

이 기능을 사용하면 동일한 파일에서 비교 함수를 쉽게 만들 수 있습니다.

bool is_string_name_equal(GDExtensionConstStringNamePtr p_a, const char *p_b)
{
    // Create a StringName for the C string.
    StringName string_name;
    constructors.string_name_new_with_latin1_chars(&string_name, p_b, false);

    // Compare both StringNames.
    bool is_equal = false;
    operators.string_name_equal(p_a, &string_name, &is_equal);

    // Destroy the created StringName.
    destructors.string_name_destructor(&string_name);

    // Return the result.
    return is_equal;
}

이 함수는 인수로부터 ``StringName``를 생성하고 연산자 함수 포인터를 사용하여 다른 것과 비교하고 결과를 반환합니다. 연산자의 반환 값은 out 참조로 전달되며 이는 API에서 일반적인 것입니다.

gdexample.h 파일로 돌아가서 Godot API에 대한 콜백으로 사용될 몇 가지 함수를 추가해 보겠습니다:

void *gdexample_class_get_virtual_with_data(void *p_class_userdata, GDExtensionConstStringNamePtr p_name);
void gdexample_class_call_virtual_with_data(GDExtensionClassInstancePtr p_instance, GDExtensionConstStringNamePtr p_name, void *p_virtual_call_userdata, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret);

실제로 가상 메서드를 등록하는 방법에는 두 가지가 있습니다. 단 하나의 get 부분이 있는데, Godot에 호출될 적절하게 제작된 함수 포인터를 제공합니다. 이를 위해 각 가상 메서드에 대해 또 다른 도우미를 만들어야 하는데 이는 그리 편리하지 않습니다. 대신, 우리는 어떤 데이터든 반환할 수 있는 두 번째 방법을 사용합니다. 그러면 Godot는 두 번째 콜백을 호출하고 호출 정보와 함께 이 데이터를 우리에게 돌려줄 것입니다. 우리는 단순히 우리 자신의 함수 포인터를 사용자 정의 데이터로 제공한 다음 모든 가상 메서드에 대해 단일 콜백을 가질 수 있습니다. 이 예에서는 한 가지 방법에만 사용하지만 이 방법이 확장하기가 더 간단합니다.

이제 gdexample.c 파일에 이 두 가지 기능을 구현해 보겠습니다.

void *gdexample_class_get_virtual_with_data(void *p_class_userdata, GDExtensionConstStringNamePtr p_name)
{
    // If it is the "_process" method, return a pointer to the gdexample_class_process function.
    if (is_string_name_equal(p_name, "_process"))
    {
        return (void *)gdexample_class_process;
    }
    // Otherwise, return NULL.
    return NULL;
}

void gdexample_class_call_virtual_with_data(GDExtensionClassInstancePtr p_instance, GDExtensionConstStringNamePtr p_name, void *p_virtual_call_userdata, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret)
{
    // If it is the "_process" method, call it with a helper.
    if (p_virtual_call_userdata == &gdexample_class_process)
    {
        ptrcall_1_float_arg_no_ret(p_virtual_call_userdata, p_instance, p_args, r_ret);
    }
}

이러한 기능은 이전에 모든 도우미를 만든 후에도 매우 간단합니다.

첫 번째 경우에는 요청된 함수 이름이 ``_process``인지 확인하고, 그렇다면 구현에 대한 함수 포인터를 반환합니다. 그렇지 않으면 ``NULL``를 반환하여 메서드가 재정의되지 않았음을 알립니다. 이 함수는 하나의 클래스에만 사용되며 이와 관련된 데이터가 없기 때문에 여기서는 ``p_class_userdata``를 사용하지 않습니다.

두 번째도 비슷합니다. _process() 메서드인 경우 지정된 함수 포인터를 사용하여 ptrcall 도우미를 호출하고 호출 인수를 전달합니다. 그렇지 않으면 구현되는 다른 가상 메서드가 없기 때문에 아무 작업도 수행하지 않습니다.

누락된 유일한 것은 클래스가 등록될 때 해당 콜백을 사용하는 것입니다. init.c 파일로 이동하여 이를 포함하도록 class_info 초기화를 변경하고 이전에 사용된 NULL 값을 바꿉니다.

void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
    ...

    GDExtensionClassCreationInfo2 class_info = {
        ...
        .get_virtual_call_data_func = gdexample_class_get_virtual_with_data,
        .call_virtual_with_data_func = gdexample_class_call_virtual_with_data,
        ...
    };

    ...
}

이는 가상 메서드를 바인딩하는 데 충분합니다. 확장 기능을 빌드하고 Godot 프로젝트를 다시 실행하면 _process() 함수가 호출됩니다. 함수 자체에는 아무 것도 표시되지 않으므로 알 수 없습니다. 이제 사용자 정의 노드를 패턴에 따라 이동시켜 이 문제를 해결하겠습니다.

노드가 작업을 수행하도록 하려면 Godot 메소드를 호출해야 합니다. 지금까지 수행했던 것처럼 GDExtension API 기능뿐만 아니라 스크립팅과 마찬가지로 실제 엔진 메서드도 작동합니다. 이를 위해서는 당연히 추가 설정이 필요합니다.

먼저 class_Vector2`를 ``defs.h` 파일에 추가하여 메서드에서 사용할 수 있도록 하겠습니다.

// The sizes can be obtained from the extension_api.json file.
...
#ifdef REAL_T_IS_DOUBLE
#define VECTOR2_SIZE 16
#else
#define VECTOR2_SIZE 8
#endif

...

// Types.

...

typedef struct
{
    uint8_t data[VECTOR2_SIZE];
} Vector2;

REAL_T_IS_DOUBLE 정의는 Godot 버전이 기본값이 아닌 이중 정밀도 지원으로 빌드된 경우에만 필요합니다.

이제 api.h 파일에서 호출할 엔진 메서드를 보관하기 위한 새로운 항목을 포함하여 API 구조체에 몇 가지 항목을 추가하겠습니다.

extern struct Constructors
{
    ...
    GDExtensionPtrConstructor vector2_constructor_x_y;
} constructors;

...

extern struct Methods
{
    GDExtensionMethodBindPtr node2d_set_position;
} methods;

extern struct API
{
    ...
    GDExtensionInterfaceClassdbGetMethodBind classdb_get_method_bind;
    GDExtensionInterfaceObjectMethodBindPtrcall object_method_bind_ptrcall;
} api;

그런 다음 api.c 파일에서 Godot의 함수 포인터를 가져올 수 있습니다:

struct Methods methods;

void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
    // Get helper functions first.
    ...
    GDExtensionInterfaceVariantGetPtrConstructor variant_get_ptr_constructor = (GDExtensionInterfaceVariantGetPtrConstructor)p_get_proc_address("variant_get_ptr_constructor");

    // API.
    ...
    api.classdb_get_method_bind = (GDExtensionInterfaceClassdbGetMethodBind)p_get_proc_address("classdb_get_method_bind");
    api.object_method_bind_ptrcall = (GDExtensionInterfaceObjectMethodBindPtrcall)p_get_proc_address("object_method_bind_ptrcall");

    // Constructors.
    ...
    constructors.vector2_constructor_x_y = variant_get_ptr_constructor(GDEXTENSION_VARIANT_TYPE_VECTOR2, 3); // See extension_api.json for indices.

    ...
}

여기서 주목할 만한 유일한 부분은 Vector2 생성자이며, 이에 대해 인덱스 3``를 요청합니다. 다양한 종류의 인수를 가진 생성자가 여러 있으므로 원하는 것을 지정해야 합니다. 경우 우리는 개의 부동 소수점 숫자를 ``xy 좌표로 사용하는 이름을 얻습니다. 이 색인은 extension_api.json 파일에서 검색할 수 있습니다. 이를 얻으려면 새로운 지역 도우미도 필요합니다.

여기서는 메소드 구조체에 대해 아무것도 얻지 못한다는 점에 유의하세요. 이는 이 함수가 초기화 프로세스 초기에 너무 일찍 호출되어 아직 클래스가 제대로 등록되지 않기 때문입니다.

대신, 사용자 정의 클래스를 등록할 때 초기화 수준 콜백을 사용하여 이를 가져옵니다. init.c 파일에 다음을 추가합니다.

void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
    if (p_level != GDEXTENSION_INITIALIZATION_SCENE)
    {
        return;
    }

    // Get ClassDB methods here because the classes we need are all properly registered now.
    // See extension_api.json for hashes.
    StringName native_class_name;
    StringName method_name;

    constructors.string_name_new_with_latin1_chars(&native_class_name, "Node2D", false);
    constructors.string_name_new_with_latin1_chars(&method_name, "set_position", false);
    methods.node2d_set_position = api.classdb_get_method_bind(&native_class_name, &method_name, 743155724);
    destructors.string_name_destructor(&native_class_name);
    destructors.string_name_destructor(&method_name);

    ...
}

여기서는 가져오려는 클래스와 메서드에 대해 StringName``를 만든 다음 GDExtension API를 사용하여 바인딩된 메서드를 나타내는 개체인 ``MethodBind``를 검색합니다. 파생 클래스인 ``Sprite2D``에서 사용할 예정이지만 등록된 곳이므로 ``Node2D``에서 ``set_position 메서드를 가져옵니다.

바인드를 얻기 위한 겉보기에 난수는 실제로는 메소드 서명의 해시입니다. 이는 향후 Godot 버전에서 이 서명이 변경되더라도 귀하가 요청하는 것과 일치하는 호환성 방법을 제공함으로써 Godot가 귀하가 요청하는 방법과 일치하도록 허용합니다. 이는 엔진이 이전 버전용으로 만들어진 확장을 로드할 수 있도록 하는 시스템 중 하나입니다. extension_api.json 파일에서 이 해시 값을 얻을 수 있습니다.

이 모든 것을 통해 마침내 gdexample.c 파일에 사용자 정의 _process() 메서드를 구현할 수 있습니다.

...

#include <math.h>

...

void gdexample_class_process(GDExample *self, double delta)
{
    self->time_passed += self->speed * delta;

    Vector2 new_position;

    // Set up the arguments for the Vector2 constructor.
    double x = self->amplitude + (self->amplitude * sin(self->time_passed * 2.0));
    double y = self->amplitude + (self->amplitude * cos(self->time_passed * 1.5));
    GDExtensionConstTypePtr args[] = {&x, &y};
    // Call the Vector2 constructor.
    constructors.vector2_constructor_x_y(&new_position, args);

    // Set up the arguments for the set_position method.
    GDExtensionConstTypePtr args2[] = {&new_position};
    // Call the set_position method.
    api.object_method_bind_ptrcall(methods.node2d_set_position, self->object, args2, NULL);
}

speed 속성으로 스케일링된 경과 시간을 업데이트한 후 이를 기반으로 xy 값을 생성하고 역시 amplitude 속성으로 변조합니다. 이것이 패턴 효과를 주는 것입니다. 여기서 사용되는 sin()cos() 기능에는 math.h 헤더가 필요합니다.

그런 다음 Vector2``를 생성하기 위해 인수 배열을 설정하고 생성자를 호출합니다. 다른 인수 배열을 설정하고 이를 사용하여 이전에 획득한 바인드를 통해 ``set_position() 메서드를 호출합니다.

여기에는 메모리를 할당하는 항목이 없으므로 정리할 필요가 없습니다.

이제 확장을 다시 빌드하고 Godot를 다시 열 수 있습니다. 편집기에서도 맞춤 스프라이트가 움직이는 것을 볼 수 있습니다.

../../../_images/gdextension_c_moving_sprite.gif

SpeedAmplitude 속성을 변경해 보고 스프라이트가 어떻게 반응하는지 확인해 보세요.

시그널 등록 및 내보내기

이 튜토리얼을 완료하려면 사용자 정의 시그널를 등록하고 적절한 경우 내보낼 수 있는 방법을 살펴보겠습니다. 짐작할 수 있듯이 API의 함수 포인터와 도우미 함수가 몇 개 더 필요합니다.

api.h 파일에 두 가지를 추가합니다. 하나는 시그널를 등록하는 API 함수이고, 다른 하나는 시그널 바인딩을 래핑하는 도우미 함수입니다.

extern struct API
{
    ...
    GDExtensionInterfaceClassdbRegisterExtensionClassSignal classdb_register_extension_class_signal;
} api;

...

// Version for 1 argument.
void bind_signal_1(
    const char *class_name,
    const char *signal_name,
    const char *arg1_name,
    GDExtensionVariantType arg1_type);

이 경우 우리는 하나의 인수에 대한 버전만 갖게 됩니다. 왜냐하면 그것이 우리가 사용할 것이기 때문입니다.

api.c 파일로 이동하면 이 새로운 함수 포인터를 로드하고 도우미를 구현할 수 있습니다.

void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
    // API.
    ...
    api.classdb_register_extension_class_signal = (GDExtensionInterfaceClassdbRegisterExtensionClassSignal)p_get_proc_address("classdb_register_extension_class_signal");

    ...
}

void bind_signal_1(
    const char *class_name,
    const char *signal_name,
    const char *arg1_name,
    GDExtensionVariantType arg1_type)
{
    StringName class_string_name;
    constructors.string_name_new_with_latin1_chars(&class_string_name, class_name, false);
    StringName signal_string_name;
    constructors.string_name_new_with_latin1_chars(&signal_string_name, signal_name, false);

    GDExtensionPropertyInfo args_info[] = {
        make_property(arg1_type, arg1_name),
    };

    api.classdb_register_extension_class_signal(class_library, &class_string_name, &signal_string_name, args_info, 1);

    // Destruct things.
    destructors.string_name_destructor(&class_string_name);
    destructors.string_name_destructor(&signal_string_name);
    destruct_property(&args_info[0]);
}

이는 메소드를 바인딩하는 기능과 매우 유사합니다. 주요 차이점은 다른 구조체를 채울 필요가 없다는 것입니다. 필요한 이름과 인수 배열만 전달하면 됩니다. 끝에 있는 ``1``는 시그널가 제공하는 인수의 양을 의미합니다.

이를 통해 ``gdexample.c``에 시그널를 바인딩할 수 있습니다.

void gdexample_class_bind_methods()
{
    ...
    bind_signal_1("GDExample", "position_changed", "new_position", GDEXTENSION_VARIANT_TYPE_VECTOR2);
}

시그널를 내보내려면 사용자 정의 노드에서 emit_signal() 메서드를 호출해야 합니다. 이는 vararg 함수(인수 개수 제한 없음)이므로 ``ptrcall``를 사용할 수 없습니다. 정기적인 호출을 수행하려면 변형을 생성해야 하며 이를 완료하려면 몇 가지 배관 단계가 더 필요합니다.

먼저 defs.h 파일에서 Variant에 대한 정의를 만듭니다.

...

// The sizes can be obtained from the extension_api.json file.
...
#ifdef REAL_T_IS_DOUBLE
#define VARIANT_SIZE 40
#define VECTOR2_SIZE 16
#else
#define VARIANT_SIZE 24
#define VECTOR2_SIZE 8
#endif

...

// Types.

...

typedef struct
{
    uint8_t data[VARIANT_SIZE];
} Variant;

먼저 앞서 추가한 Vector2의 크기와 함께 Variant의 크기를 설정합니다. 그런 다음 이를 사용하여 Variant 데이터를 보유하기에 충분한 불투명 구조체를 만듭니다. 다시 말하지만, 공식 Godot 빌드에서는 단정밀도를 사용하므로 이중 정밀도 빌드의 크기를 대체 수단으로 설정했습니다.

emit_signal() 함수는 두 개의 인수를 사용하여 호출됩니다. 첫 번째는 방출될 시그널의 이름이고 두 번째는 바인딩할 때 선언한 Vector2인 시그널 연결에 전달하는 인수입니다. 따라서 이러한 유형으로 MethodBind를 호출할 수 있는 도우미 함수를 만들겠습니다. 무언가(오류 코드)를 반환하더라도 이를 처리할 필요가 없으므로 지금은 그냥 무시하겠습니다.

``api.h``에서는 기존 구조체에 몇 가지 사항과 호출을 위한 새로운 도우미 함수를 추가합니다.

extern struct Constructors
{
    ...
    GDExtensionVariantFromTypeConstructorFunc variant_from_string_name_constructor;
    GDExtensionVariantFromTypeConstructorFunc variant_from_vector2_constructor;
} constructors;

extern struct Destructors
{
    ..
    GDExtensionInterfaceVariantDestroy variant_destroy;
} destructors;

...

extern struct Methods
{
    ...
    GDExtensionMethodBindPtr object_emit_signal;
} methods;

extern struct API
{
    ...
    GDExtensionInterfaceObjectMethodBindCall object_method_bind_call;
} api;

...

// Helper to call with Variant arguments.
void call_2_args_stringname_vector2_no_ret_variant(
    GDExtensionMethodBindPtr p_method_bind,
    GDExtensionObjectPtr p_instance,
    const GDExtensionTypePtr p_arg1,
    const GDExtensionTypePtr p_arg2);

이제 api.c 파일로 전환하여 이러한 새 함수 포인터를 로드하고 도우미 기능을 구현해 보겠습니다.

void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
    // API.
    ...
    api.object_method_bind_call = (GDExtensionInterfaceObjectMethodBindCall)p_get_proc_address("object_method_bind_call");

    // Constructors.
    ...
    constructors.variant_from_string_name_constructor = api.get_variant_from_type_constructor(GDEXTENSION_VARIANT_TYPE_STRING_NAME);
    constructors.variant_from_vector2_constructor = api.get_variant_from_type_constructor(GDEXTENSION_VARIANT_TYPE_VECTOR2);

    // Destructors.
    ...
    destructors.variant_destroy = (GDExtensionInterfaceVariantDestroy)p_get_proc_address("variant_destroy");

    ...
}

...

void call_2_args_stringname_vector2_no_ret_variant(GDExtensionMethodBindPtr p_method_bind, GDExtensionObjectPtr p_instance, const GDExtensionTypePtr p_arg1, const GDExtensionTypePtr p_arg2)
{
    // Set up the arguments for the call.
    Variant arg1;
    constructors.variant_from_string_name_constructor(&arg1, p_arg1);
    Variant arg2;
    constructors.variant_from_vector2_constructor(&arg2, p_arg2);
    GDExtensionConstVariantPtr args[] = {&arg1, &arg2};

    // Add dummy return value storage.
    Variant ret;

    // Call the function.
    api.object_method_bind_call(p_method_bind, p_instance, args, 2, &ret, NULL);

    // Destroy the arguments.
    destructors.variant_destroy(&arg1);
    destructors.variant_destroy(&arg2);
    destructors.variant_destroy(&ret);
}

이 도우미 함수에는 일부 상용구 코드가 있지만 매우 간단합니다. 스택에 할당된 Variants 내에 두 개의 인수를 설정한 다음 이에 대한 포인터가 있는 배열을 만듭니다. 또한 반환 값을 유지하기 위해 또 다른 Variant를 설정합니다. 호출에서는 초기화되지 않을 것으로 예상하므로 생성할 필요가 없습니다.

그런 다음 우리가 제공한 인스턴스와 인수를 사용하여 실제로 MethodBind를 호출합니다. 끝에 있는 NULL``는 ``GDExtensionCallError 구조체에 대한 포인터입니다. 이는 함수를 호출할 때 발생할 수 있는 오류(예: 잘못된 인수)를 처리하는 데 사용할 수 있습니다. 단순화를 위해 여기서는 다루지 않겠습니다.

마지막에는 우리가 만든 Variants를 파괴해야 합니다. 기술적으로 Vector2는 소멸이 필요하지 않지만 모든 것을 정리하는 것이 더 명확합니다.

또한 이전에 수행한 set_position 메서드에 대한 메서드를 로드한 직후 init.c 파일에서 수행할 MethodBind도 로드해야 합니다.

void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
    ...

    constructors.string_name_new_with_latin1_chars(&native_class_name, "Object", false);
    constructors.string_name_new_with_latin1_chars(&method_name, "emit_signal", false);
    methods.object_emit_signal = api.classdb_get_method_bind(&native_class_name, &method_name, 4047867050);
    destructors.string_name_destructor(&native_class_name);
    destructors.string_name_destructor(&method_name);

    // Register class.
    ...
}

여기서는 native_class_namemethod_name 변수를 재사용하므로 새 변수를 선언할 필요가 없습니다.

이제 몇 가지 필드를 추가할 gdexample.h 파일로 이동합니다.

typedef struct
{
    // Private properties.
    ..
    double time_emit;
    ..
    // Metadata.
    StringName position_changed; // For signal.
} GDExample;

첫 번째 항목은 마지막 시그널가 방출된 이후 경과된 시간을 저장합니다. 왜냐하면 우리는 정기적으로 이를 수행할 것이기 때문입니다. 다른 하나는 시그널 이름을 캐시하여 매번 새 StringName을 만들 필요가 없도록 하는 것입니다.

소스 gdexample.c 파일에서 생성자와 소멸자를 변경하여 새 필드를 처리할 수 있습니다.

void gdexample_class_constructor(GDExample *self)
{
    ...
    self->time_emit = 0.0;

    // Construct the StringName for the signal.
    constructors.string_name_new_with_latin1_chars(&self->position_changed, "position_changed", false);
}

void gdexample_class_destructor(GDExample *self)
{
    // Destruct the StringName for the signal.
    destructors.string_name_destructor(&self->position_changed);
}

메모리 누수를 방지하려면 StringName을 삭제하는 것이 중요합니다.

이제 gdexample_class_process() 함수에 추가하여 실제로 시그널를 내보낼 수 있습니다.

void gdexample_class_process(GDExample *self, double delta)
{
    ...

    self->time_emit += delta;
    if (self->time_emit >= 1.0)
    {
        // Call the emit_signal method.
        call_2_args_stringname_vector2_no_ret_variant(methods.object_emit_signal, self->object, &self->position_changed, &new_position);
        self->time_emit = 0.0;
    }
}

이는 시그널 방출에 경과된 시간을 업데이트하고, 1초를 초과하면 현재 인스턴스에서 emit_signal() 함수를 호출하여 시그널의 이름과 새 위치를 인수로 전달합니다.

이제 C GDExtension이 끝났습니다. 한 번 더 빌드하고 편집기에서 Godot 프로젝트를 다시 엽니다.

``GDExample``에 대한 문서 페이지에서 우리가 바인딩한 새로운 시그널를 볼 수 있습니다.

../../../_images/gdextension_c_signal_doc.webp

작동하는지 확인하기 위해 시그널를 수신할 때마다 출력에 위치를 인쇄하는 사용자 정의 루트 노드에 작은 스크립트를 추가해 보겠습니다.

extends Node2D

func _ready():
    $GDExample.position_changed.connect(on_position_changed)

func on_position_changed(new_position):
    prints("New position:", new_position)

프로젝트를 실행하면 편집기의 출력 도크에 인쇄되는 값을 관찰할 수 있습니다.

../../../_images/gdextension_c_signal_print.webp

결론

이 튜토리얼에서는 사용자 정의 메소드, 속성 및 시그널가 포함된 기본 확장을 보여줍니다. 상당한 양의 상용구가 필요하지만 지루한 작업을 처리하는 도우미 함수를 만들어 확장성을 높일 수 있습니다.

이는 GDExtension API를 이해하기 위한 좋은 기반이자 사용자 정의 바인딩 생성기를 생성하기 위한 출발점이 되어야 합니다. 실제로 이러한 유형의 생성기를 사용하여 C에 대한 바인딩을 생성하여 실제 코딩을 이 예의 gdexample.c 파일과 비슷하게 만들 수 있습니다. 이는 매우 간단하고 장황하지 않습니다.

실제 확장을 생성하려면 코드에서 모든 상용구를 제거하므로 C++ 바인딩을 대신 사용하는 것이 좋습니다. :ref:`godot-cpp 문서 <doc_godot_cpp>`를 확인하여 이 작업을 수행하는 방법을 알아보세요.