DirectX11/Eternal Return 모작

[DirectX 11 Eternal Return 모작] 5. Component

Vfly 2025. 9. 2. 23:41

전체 코드 : https://github.com/HyangRim/DirectX11-Engine-Client

 

GitHub - HyangRim/DirectX11-Engine-Client

Contribute to HyangRim/DirectX11-Engine-Client development by creating an account on GitHub.

github.com


Component 기반 게임 오브젝트 시스템

개요

이번 프로젝트에서는 Unity엔진과 유사하게 Component 기반으로 객체를 구성하였다.

 

이번 포스트에서는 직접 구현한 Component 시스템을 바탕으로 , GameObject - Component 구조 설계와 타입 안정성 및 성능을 고려한 Component 관리 기법을 알아보겠다.

 

 

1. GameObject - Component 구조 설계

1.1 기본 아키텍쳐 개요

Component 시스템의 핵심은 상속보다는 조합이다. 하나의 거대한 클래스 계층구조 대신, 작은 기능 단위의 컴포넌트들을 조합하여 게임 객체를 만들어낸다.

// GameObject.h - 메인 컨테이너 클래스
class GameObject : public enable_shared_from_this<GameObject>
{
public:
    GameObject();
    virtual ~GameObject();

    virtual void Init();
    virtual void Start();
    virtual void Update();
    virtual void LateUpdate();
    virtual void FixedUpdate();

    // 컴포넌트 관리
    void AddComponent(shared_ptr<Component> _component);
    shared_ptr<Component> GetFixedComponent(ComponentType _type);
    
    // 타입 안전한 컴포넌트 접근
    shared_ptr<Transform> GetTransform();
    shared_ptr<Camera> GetCamera();
    shared_ptr<MeshRenderer> GetMeshRenderer();
    // ... 다른 컴포넌트 접근자들

private:
    // 고정 컴포넌트 배열 (성능 최적화)
    array<shared_ptr<Component>, FIXED_COMPONENT_COUNT> m_components;
    // 동적 스크립트 컴포넌트
    vector<shared_ptr<MonoBehaviour>> m_scripts;
    
    wstring m_Name;
    OBJECTTYPE m_type = OBJECTTYPE::NONE;
    uint32 m_layerIndex = 0;
    bool m_active = true;
};

 

1.2 Component 기본 클래스

모든 컴포넌트의 베이스가 되는 Component 클래스는 생명주기 관리와 GameObject 참조를 담당한다.

// Component.h - 모든 컴포넌트의 기반 클래스
class Component
{
public:
    Component(ComponentType _type);
    virtual ~Component();

    virtual void Init() {}
    virtual void Start() {}
    virtual void Update() {}
    virtual void LateUpdate() {}
    virtual void FixedUpdate() {}
    virtual void OnDestroy() {}

    // GameObject 참조 관리
    shared_ptr<GameObject> GetGameObject();
    shared_ptr<Transform> GetTransform();

protected:
    ComponentType m_type;
    weak_ptr<GameObject> m_gameObject;  // 순환 참조 방지
};

 

핵심 설계 포인트

  • weak_ptr로 GameObject 참조하여 순환 참조 방지
  • 각 컴포넌트는 고유한 ComponentType을 가짐
  • 생명주기 메서드들을 가상함수로 제공하여 하위 클래스에서 재정의 가능

1.3 ComponentType 열거형과 타입 시스템

enum class ComponentType : uint8
{
    Transform,
    MeshRenderer,
    ModelRenderer,
    Camera,
    Animator,
    Light,
    Collider,
    // ... 고정 컴포넌트들
    
    Script,        // MonoBehaviour 경계
    Custom,
    End
};

enum
{
    FIXED_COMPONENT_COUNT = static_cast<uint8>(ComponentType::End) - 1
};

 

이 설계의 장점

  • 컴파일 타임 타입 안정성 : 잘못된 타입 접근을 컴파일 시점에 차단
  • 메모리 효율성 : 고정 컴포넌트는 배열로, 가변 컴포넌트는 벡터로 분리
  • 빠른 접근 속도 : 인덱스 기반 O(1) 접근

 

2. 타입 안정성과 성능을 고려한 컴포넌트 관리

2.1 이중 저장 구조 설계

성능과 유연성을 동시에 확보하기 위해 컴포넌트를 두 가지 방식으로 분리했다.

void GameObject::AddComponent(shared_ptr<Component> _component)
{
    _component->SetGameObject(shared_from_this());

    // MonoBehaviour인지 먼저 확인
    auto script = dynamic_pointer_cast<MonoBehaviour>(_component);
    if (script) {
        m_scripts.push_back(script);
        return;
    }

    uint8 index = static_cast<uint8>(_component->GetType());
    
    if (index < FIXED_COMPONENT_COUNT) {
        // 고정 컴포넌트: 배열에 저장 (O(1) 접근)
        m_components[index] = _component;
    }
    else {
        // 가변 컴포넌트: 벡터에 저장
        m_scripts.push_back(dynamic_pointer_cast<MonoBehaviour>(_component));
    }
}

 

성능 최적화 포인트

  • 고정 컴포넌트 : Transform, Renderer 등 자주 접근하는 컴포넌트는 배열에 저장하여 O(1) 접근
  • 가변 컴포넌트 : 사용자 정의 스크립트들은 벡터에 저장하여 동적 관리
  • 메모리 지역성 : 관련 컴포넌트들이 메모리상에서 가깝게 배치

 

2.2 타입 안전한 컴포넌트 접근

각 컴포넌트 타입별로 전용 접근자를 제공하여 런타임 타입 체크 최소화

shared_ptr<Transform> GameObject::GetTransform()
{
    shared_ptr<Component> component = GetFixedComponent(ComponentType::Transform);
    if (component == nullptr) {
        component = make_shared<Transform>();
        AddComponent(component);
    }
    return static_pointer_cast<Transform>(component);
}

shared_ptr<MeshRenderer> GameObject::GetMeshRenderer()
{
    shared_ptr<Component> component = GetFixedComponent(ComponentType::MeshRenderer);
    return static_pointer_cast<MeshRenderer>(component);
}

shared_ptr<Renderer> GameObject::GetRenderer()
{
    // 여러 렌더러 타입 중 하나를 반환
    shared_ptr<Component> renderer = GetFixedComponent(ComponentType::MeshRenderer);
    if (renderer == nullptr)
        renderer = GetFixedComponent(ComponentType::ModelRenderer);
    if (renderer == nullptr)
        renderer = GetFixedComponent(ComponentType::Animator);
    // ...
    
    return static_pointer_cast<Renderer>(renderer);
}

 

 

2.3 메모리 관리와 생명주기

안전한 메모리 관리를 위한 여러 기법

void GameObject::OnDestroy()
{
    m_isDestroyed = true;

    // 1. 모든 컴포넌트들에게 OnDestroy 알림
    for (auto& component : m_components) {
        if (component) {
            component->OnDestroy();
        }
    }

    // 2. 스크립트들에게 OnDestroy 알림
    for (auto& script : m_scripts) {
        if (script) {
            script->OnDestroy();
        }
    }

    // 3. 참조 해제
    ClearReferences();
}

void GameObject::ClearReferences()
{
    // 컴포넌트들의 GameObject 참조 해제
    for (auto& component : m_components) {
        if (component) {
            component->ClearGameObjectRef();
        }
    }
    
    m_Name.clear();
    m_alpha = 1.0f;
    m_alphaChanged = false;
}

 

2.4 컴포넌트 시스템의 확장성

새로운 컴포넌트를 쉽게 추가할 수 있는 구조

// 새로운 컴포넌트 추가 예시
class InventoryManager : public Component
{
public:
    InventoryManager() : Component(ComponentType::Custom) {}
    
    virtual void Update() override 
    {
        // 인벤토리 업데이트 로직
    }
    
    void AddItem(shared_ptr<Item> item) { /* 구현 */ }
    void RemoveItem(int itemId) { /* 구현 */ }
};

// 사용법
auto player = make_shared<GameObject>();
player->AddComponent(make_shared<InventoryManager>());

 

 

3. 실제 활용

3.1 애니메이션 시스템 통합

Component 시스템과 State Machine의 결합 예시

shared_ptr<AnimationStateMachine> GameObject::GetAnimationStateMachine()
{
    shared_ptr<Component> component = GetFixedComponent(ComponentType::AnimationStateMachine);
    return static_pointer_cast<AnimationStateMachine>(component);
}

// 사용 예시
auto player = CreateCharacterBianca();
auto animStateMachine = player->GetAnimationStateMachine();
animStateMachine->RequestStateChange(AnimationStateType::Run);

 

3.2 충돌 시스템과의 연동

void GameObject::OnCollision(shared_ptr<GameObject> _other)
{
    if (GetRigidbody() != nullptr) {
        GetRigidbody()->OnCollision(_other);
    }
}

void GameObject::OnCollisionEnter(shared_ptr<GameObject> _other)
{
    // 모든 컴포넌트에게 충돌 이벤트 전파
    for (auto& script : m_scripts) {
        if (auto behaviorScript = dynamic_pointer_cast<MonoBehaviour>(script)) {
            behaviorScript->OnCollisionEnter(_other);
        }
    }
}

 

 

 

결론

핵심 성과

  • 타입 안정성 : ComponentType 열거형과 전용 접근자로 컴파일 타임 안정성 확보
  • 성능 최적화 : 고정 / 가변 컴포넌트 분리로 메모리 접근 최적화
  • 확장성 : 새로운 컴포넌트 추가가 용이한 구조
  • 유지보수 : 기능별 분리로 코드 이해와 수정이 쉽다.