0213

2026년 2월 13일

카테고리 : 게임엔진, 외부 스크립트 통합

0213_0.png

오늘 만든 yaml로 CR 생성하기

0213_1.gif

2월 18일 추가 : H/VContainer 관련 메서드 등록하고 테스트

0213_2.png


yaml이라고

예전부터 눈여겨본 마크업 언어가 있다.

yaml-cpp 라는 cpp라이브러리도 있어서

언젠가는 엔진에 통합해야겠다 생각해왔었다.

특히, godot의

.godot으로 끝나는 프로젝트 파일이나

.tscn 같은 씬 파일은

다 텍스트 기반이었는데,

나는 이게 마음에 들었고

씬을 번거롭게

매번 cpp로 작성하고 재컴파일 할 필요 없이

외부 스크립트로 연동하고 싶었다.

사용방법은 다음과 같다.

꼭 IScene::on_initialized() 안에서 불러줘야 한다.

        auto scntree_yaml =
            R"""(
# current_cam2d는 꼭 지정해줘야 한다.
current_camera_2d: "Cam2D"

# 씬트리는 반드시 Root란 이름으로 시작한다.
Root:
  type: "IObject2D"

  children:
    Cam2D:
      type: "Camera2D"
      world_position: [640, 360]
      projection_viewport_size: [1280, 720]
    CRect:
      type: "ColorRect"
      rect_world_position: [100, 100]
      rect_size: [50, 50]
      color: 0xFF0000       # 빨간색
      debug_rect_drawing_enabled: true
)""";
        dp::InitializeSceneTreeByYAML(this, scntree_yaml, "Root");

        auto* root = this->get_root_object();
        root->print_tree(0, 2);

InitializeSceneTreeByYAML 라는 함수가

씬 안에 루트부터

하위 노드를 yaml 에 맞춰서 할당해주는 것이다.

기본적인 cpp - yaml 인터페이스는

다음처럼 구성된다.

static int yaml_to_int(const YAML::Node& node);
static dp::Vector2f yaml_to_vec2f(const YAML::Node& node);
static dp::Margin yaml_to_margin(const YAML::Node& node);

기본적인 변환 함수들과,

결국에는 InitializeSceneTreeByYAML 내에서

모든게 처리되는 게 아니라,

재귀적으로 해야 한다.

다음과 같은 함수도 있다.

static void register_object_property(dp::IObject* object, const YAML::Node& info);
static void register_object_children(IScene* scene, dp::IObject* object, const YAML::Node& children);

대충 이런 식이다.

처음에 InitializeSceneTreeByYAML 이게 불리면?

root를 일단 제일 처음 만들고,

그 root에 대해서

  1. register_object_children 으로 root 자식을 모두 등록(할당)한다.
  2. register_object_property 로 root 오브젝트 자체의 속성들을 모두 조정한다.

위 2개의 스텝은

register_object_children 내부에서

register_object_children 가 재귀적으로 불릴 때에도

적용되는 절차이다.

이때, 중요한건

엔진 올리기 시작할 때부터 지켜왔던,

자식 추가 -> 프로퍼티 조정

이 순서는 반드시 지켜줘야 한다는 것이다.

저게 안되면 model / world 좌표계가 꼬일 수 있으며,

씬 전체가 예상이 안된다.


내가 이걸 왜 미뤄왔는지 설명해보자면,

yaml 에만 국한된 것이 아닌,

한 엔진에

스크립트 언어를 내장한다는 것은 -

그 엔진에 구현되어 있는

"모든 public 한 메서드 / 기능" 들을

스크립트 내에서

건드릴 수 있도록

(glue. bridge. 매개체.)

추상화를 해주어야 한다는 것이다.

이건 개인적으로 사람이 할 짓이 아니라고 생각한다.

저게 무슨 뜻이냐면,

void set_property(IObject* object, const std::string& property_name, const Variant& value);

위처럼

"특정 오브젝트" 의

"특정 이름을 가진 프로퍼티" 를

"특정 값" 으로 바꾼다

라는 함수를

완벽하게 작동하도록 해야 하는 것이다.

이게 머리가 조금 아픈게 뭐냐면,

  1. 상속 테이블 다 고려해서, 자식 단에 속성 / 메서드가 없으면 부모로 전이해서 검색
  2. 최상단 부모까지 검색했는데 못찾으면 오류
  3. 프로퍼티 이름이 잘못 들어오면 오류
  4. variant 값이 잘못 저장되면 오류
  5. "기능을 추가했는데, 까먹고 프로퍼티 적용 안 했으면 오류"

저 5번이 가장 무섭다.

나는 저게 가장 무섭다.

너무 무섭다.

대충 이런식으로

static bool _set_property_Generic(ObjectTypeID type_id, IObject* object, const std::string& property_name, const Variant& value) {
    if (type_id == IObject::static_type()) { return _set_property_object(object, property_name, value); }
    else if (type_id == IObject2D::static_type()) { return _set_property_object2d(object->as<IObject2D>(), property_name, value); }
    else if (type_id == IControl::static_type()) { return _set_property_control(object->as<IControl>(), property_name, value); }
    else if (type_id == Camera2D::static_type()) { return _set_property_camera2d(object->as<Camera2D>(), property_name, value); }
    else if (type_id == ColorRect::static_type()) { return _set_property_colorrect(object->as<ColorRect>(), property_name, value); }
    else if (type_id == IContainer::static_type()) { return _set_property_container(object->as<IContainer>(), property_name, value); }
    else if (type_id == HContainer::static_type()) { return _set_property_hcontainer(object->as<HContainer>(), property_name, value); }
    else if (type_id == VContainer::static_type()) { return _set_property_vcontainer(object->as<VContainer>(), property_name, value); }
    else if (type_id == IScrollContainer::static_type()) { return _set_property_scrollcontainer(object->as<IScrollContainer>(), property_name, value); }
    else if (type_id == HScrollContainer::static_type()) { return _set_property_hscrollcontainer(object->as<HScrollContainer>(), property_name, value); }
    else if (type_id == VScrollContainer::static_type()) { return _set_property_vscrollcontainer(object->as<VScrollContainer>(), property_name, value); }
    else { DP_ASSERT(false); }

    return true;
}

이렇게 수동으로 만들긴 했는데,

내가 하나라도 빠뜨리면?

클래스를 새로 만들었는데,

그걸 여기다가 안 추가하면?

기존 클래스에 속성 / 메서드 추가했는데,

이 테이블에 적용 안 시키면?

무용지물이다.


나는 이런 방식이 매우 구시대적이란 것은 알고 있다.

나도 처음에는

rttr이라고,

런타임 리플렉션 라이브러리 써서

저짓거리 안 하려고 했다.

근데 그거 쓰니까,

컴파일러 경고 수준을 낮춰야만 컴파일이 되는 문제,

"고작 상속 테이블 뒤지고 문자열로 메서드 매핑" 하는 작업에 비해서

라이브러리가 너무 크다는 문제가 있었다.

cpp 26인가에

컴파일 타임 리플렉션 기능이

공식적으로 지원된다는 것도 알고는 있지만,

잘 모르겠다.


아무튼,

static ObjectSetPropertyMap _object = {
        { "name", PrimitiveDataType::String},
        { "visible", PrimitiveDataType::Boolean},
};

static ObjectSetPropertyMap _object2d = {
    { "model_position", PrimitiveDataType::Vec2f},
    { "world_position", PrimitiveDataType::Vec2f},
    { "rotation", PrimitiveDataType::Angle},
    { "scale", PrimitiveDataType::Vec2f},
};

static ObjectSetPropertyMap _container = {
    { "margin", PrimitiveDataType::Vec4f },
    { "clip_contents", PrimitiveDataType::Boolean},
    { "contain_mode", PrimitiveDataType::Integer},
    { "separator_enabled", PrimitiveDataType::Boolean},
    { "draw_background_enabled", PrimitiveDataType::Boolean},
};

static ObjectSetPropertyMap _hcontainer = {
    { "halign", PrimitiveDataType::Integer},
    { "x_separation", PrimitiveDataType::Float},
};

이런 거

하나하나 다 만들면서

현타가 좀 많이 심하게 왔다.

ObjectTraits.cpp 라는 파일이 있는데,

내가 클래스를 새로 만들어 놓고

나중에 여기다가 등록을 안 해놓으면

스크립트 단에서는 절대로 못 쓴다.

// 나는 다른 것보다
//  여기다가 추가해놓는거 잊어먹어서
//  괴상한 에러가 나는게 너무 무섭다.

//          아니 애초에 이걸 하기 싫었다니까??
//      -> static 변수로 만들고
//          구현부에서 초기화하면 되긴 한데
//          컴파일러따라서 그변수 안쓰면 없어질걸
//          애초에 정의는 헤더에만 하는게 맞음

// 주의 : ISlider같은 중간 인터페이스 클래스도 
//      무조건 등록해줘야 함
//          -> 부모 상속맵 타고 올라가는 매커니즘이
//              set_property / get_property 함수에 쓰이기 때문에...
void SetupObjectTypeRegistry() {
    IObject::static_type();
    IObject2D::static_type();
    IObject3D::static_type();
    IControl::static_type();
    Tween::static_type();
    AudioStreamPlayer::static_type();
    Camera2D::static_type();
    Sprite2D::static_type();
    ...

(2월 18일 추가)

컨테이너 관련 프로퍼티를

다 하나하나 등록을 마쳤고,

나중에는 대충

이런식으로 동작할 것 같다.

        auto scntree_yaml =
            R"""(

CONSTANTS:
  # 색 정의하기!
  _Red:     &RED     0xFF0000       # 빨간색
  _Green:   &GREEN   0x00FF00       # 초록색
  _Blue:    &BLUE    0x0000FF       # 파란색
  _Cyan:    &CYAN    0x00FFFF       # 시안
  _Magenta: &MAGENTA 0xFF00FF       # 마젠타
  _Yellow:  &YELLOW  0xFFFF00       # 노랑
  _White:   &WHITE   0xFFFFFF       # 하양
  _Black:   &BLACK   0x000000       # 검정색

  _HAlign_Left:   &HALIGN_LEFT   0
  _HAlign_Center: &HALIGN_CENTER 1
  _HAlign_Right:  &HALIGN_RIGHT  2

  _VAlign_Top:    &VALIGN_TOP    0
  _VAlign_Center: &VALIGN_CENTER 1
  _VAlign_Bottom: &VALIGN_BOTTOM 2

  _FillMode_None:          &FILLMODE_NONE 0
  _FillMode_Fill:          &FILLMODE_FILL 1
  _FillMode_FillEssential: &FILLMODE_FILLESSENTIAL 2

  _ContainMode_Normal:     &CONTAINMODE_NORMAL   0
  _ContainMode_Wrap:       &CONTAINMODE_WRAP     1
  _ContainMode_Fill:       &CONTAINMODE_FILL     2
  _ContainMode_FillLast:   &CONTAINMODE_FILLLAST 3

# current_cam2d는 꼭 지정해줘야 한다.
current_camera_2d: "Cam2D"

# 씬트리는 반드시 Root란 이름으로 시작한다.
Root:
  type: "IControl"

  children:
    Cam2D:
      type: "Camera2D"
      world_position: [640, 360]
      projection_viewport_size: [1280, 720]

    VContainer:
      type: "VContainer"
      rect_world_position: [0, 0]
      rect_size: [1280,  720]
      rect_min_size: [300,  300]
      margin: [10, 10, 10, 10]
      debug_rect_drawing_enabled: true
      contain_mode: *CONTAINMODE_FILLLAST
      y_separation: 10.0
      separator_enabled: true

      children:
        TestA:
          type: "ColorRect"
          color: *CYAN
          fill_mode: *FILLMODE_NONE
        TestB:
          type: "ColorRect"
          color: *RED
          fill_mode: *FILLMODE_NONE
        TestC:
          type: "ColorRect"
          color: *BLUE
          fill_mode: *FILLMODE_FILL


)""";

마치 스크립팅 언어의

stdlib.lua 같은 느낌으로

사용자 yaml을 분석하기 전에

정의해둔 전역 상수(enum같은것도 여기 들어간다) 스크립트를

위에다가 concat 하고

그걸 읽어주면 된다.