Components

Jihoon Engine은 Unity와 유사한 컴포넌트 기반 아키텍처를 채택하고 있습니다. 컴포넌트는 게임 오브젝트의 기능적 단위로, 각 게임 오브젝트는 다양한 컴포넌트의 조합으로 구성됩니다. 이러한 구조는 코드 재사용성을 높이고 게임 오브젝트의 유연한 확장을 가능하게 합니다.

컴포넌트 기본 구조

  • 컴포넌트 기본 클래스

모든 컴포넌트는 Component 기본 클래스를 상속받습니다:

class Component
{
public:
    Component(ComponentType type);
    ~Component();

    virtual void Start() {};
    virtual void Update() {};
    virtual void LateUpdate() {};

    shared_ptr<GameObject> GetGameObject() {
        if (auto gameObject = _gameObject.lock())
            return gameObject;
        return nullptr;
    }
    shared_ptr<Transform> GetTransform();
    ComponentType GetType() { return _type; }

protected:
    ComponentType _type;
    weak_ptr<GameObject> _gameObject;
    
private:
    friend class GameObject;
    void SetGameObject(shared_ptr<GameObject> gameObject) { _gameObject = gameObject; }
};

이 기본 클래스는:

  1. 컴포넌트 타입을 저장합니다

  2. 소유 게임 오브젝트에 대한 약한 참조(weak_ptr)를 유지합니다

  3. 생명주기 메서드(Start, Update, LateUpdate)를 정의합니다

  4. 소유 게임 오브젝트와 그 Transform 컴포넌트에 접근하는 메서드를 제공합니다

  • 컴포넌트 타입

Jihoon Engine은 다양한 컴포넌트 타입을 지원합니다:

enum class ComponentType : uint8
{
    Transform,
    MeshRenderer,
    Camera,
    Animator,
    Light,
    Collider,
    Terrain,
    UIImage,
    Button,
    Billboard,
    Particle,
    BoxCollider,
    SphereCollider,
    ParticleSystem,
    Script, // MonoBehaviour
    None
};

여기서 고정 개수의 컴포넌트는 별도로 정의됩니다:

enum
{
    FIXED_COMPONENT_COUNT = 13
};

이는 게임 오브젝트가 가질 수 있는 기본 컴포넌트의 종류를 나타냅니다. 스크립트 컴포넌트는 동적으로 추가될 수 있습니다. (Script를 제외한 컴포넌트는 오브젝트당 1개씩만 소유 가능하며 Script컴포넌트는 사용자가 만든 컴포넌트이기 때문에 중복으로 추가 가능하도록 하기 위해 구분)

게임 오브젝트와 컴포넌트 관계

  • 게임 오브젝트 구조

게임 오브젝트는 컴포넌트의 컨테이너 역할을 합니다:

class GameObject : public enable_shared_from_this<GameObject>
{
private:
    vector<shared_ptr<Component>> _components;
    vector<shared_ptr<GameObject>> _children;
    wstring _name;
    GameObjectType _type;
    shared_ptr<GameObject> _parent;
    // ... 기타 멤버 변수들 ...
};

  • 컴포넌트 초기화

게임 오브젝트 생성자에서 컴포넌트 컨테이너가 초기화됩니다:

GameObject::GameObject()
{
    _components.resize(FIXED_COMPONENT_COUNT);
}

기본적으로 고정 개수(FIXED_COMPONENT_COUNT)의 컴포넌트 슬롯이 할당됩니다. 이는 효율적인 메모리 관리와 빠른 컴포넌트 접근을 위한 설계입니다.

  • 컴포넌트 추가

void GameObject::AddComponent(shared_ptr<Component> component)
{
    component->SetGameObject(shared_from_this());
    int index = static_cast<uint8>(component->GetType());
    if (index < FIXED_COMPONENT_COUNT)
    {
        _components[index] = component;
    }
    else
    {
        _components.push_back(component);
    }
}

컴포넌트 추가 과정:

  1. 컴포넌트에 게임 오브젝트 참조 설정

  2. 컴포넌트 타입에 기반한 인덱스 계산

  3. 고정 컴포넌트는 해당 인덱스에 저장, 그 외는 벡터 끝에 추가

  • 컴포넌트 제거

void GameObject::RemoveComponent(shared_ptr<Component> component)
{
    if (!component)
        return;

    ComponentType type = component->GetType();
    uint8 index = static_cast<uint8>(type);

    // Transform 컴포넌트는 제거할 수 없음
    if (type == ComponentType::Transform)
        return;

    // FIXED_COMPONENT_COUNT 이내의 컴포넌트인 경우
    if (index < FIXED_COMPONENT_COUNT)
    {
        if (_components[index] == component)
        {
            _components[index] = nullptr;
        }
    }
    else
    {
        // FIXED_COMPONENT_COUNT 이후에 추가된 컴포넌트인 경우
        auto it = std::find(_components.begin() + FIXED_COMPONENT_COUNT, _components.end(), component);
        if (it != _components.end())
        {
            _components.erase(it);
        }
    }

    // 컴포넌트의 GameObject 참조 해제
    component->SetGameObject(nullptr);
}

컴포넌트 제거 과정:

  1. Transform 컴포넌트는 제거 불가능 (게임 오브젝트의 필수 요소)

  2. 고정 컴포넌트는 nullptr로 설정하여 "제거"

  3. 동적 컴포넌트는 벡터에서 실제로 제거

  4. 컴포넌트의 게임 오브젝트 참조 해제

  • 컴포넌트 접근

템플릿 메서드를 통해 타입 안전한 컴포넌트 접근을 제공합니다:

template <typename T>
shared_ptr<T> GetComponent()
{
	ComponentType type = ComponentType::None;
	// MonoBehaviour를 상속받는 클래스인지 먼저 확인
	if (std::is_base_of_v<MonoBehaviour, T>)
		type = ComponentType::Script;
	if (std::is_same_v<T, Transform>)
		type = ComponentType::Transform;
	if (std::is_same_v<T, MeshRenderer>)
		type = ComponentType::MeshRenderer;
	if (std::is_same_v<T, Camera>)
		type = ComponentType::Camera;
	if (std::is_same_v<T, Light>)
		type = ComponentType::Light;
	if (std::is_same_v<T, BaseCollider>)
		type = ComponentType::Collider;
	
        // ... 다른 컴포넌트 타입 확인 ...

	uint8 index = static_cast<uint8>(type);
	if (std::is_same_v<T, EditorCamera>)
		index = 13;

	shared_ptr<Component> component = _components[index];
	if (component == nullptr)
		return nullptr;
	shared_ptr<T> castedComponent = static_pointer_cast<T>(component);
	return castedComponent;

}

이 메서드는:

  1. 요청된 타입에 해당하는 컴포넌트 인덱스를 결정

  2. 해당 인덱스의 컴포넌트 검색

  3. 요청된 타입으로 안전하게 캐스팅하여 반환

특히 Transform 컴포넌트는 자주 사용되므로 별도의 헬퍼 메서드가 제공됩니다:

shared_ptr<Transform> GameObject::transform()
{
	int index = static_cast<uint8>(ComponentType::Transform);
	if (_components[index] != nullptr)
	{
		return static_pointer_cast<Transform>(_components[index]);
	}
	else
	{
		shared_ptr<Transform> transform = make_shared<Transform>();
		AddComponent(transform);
	}

	return transform();
}

컴포넌트 생명주기

컴포넌트는 게임 오브젝트의 생명주기 메서드를 통해 관리됩니다:

  • Start

void GameObject::Start()
{
    for (shared_ptr<Component>& component : _components)
    {
        if (ENGINE.GetEngineMode() == EngineMode::Edit)
        {
            if (component)
            {
                if (component->GetType() == ComponentType::Transform
                    || component->GetType() == ComponentType::Camera
                    || component->GetType() == ComponentType::Light
                    || component->GetType() == ComponentType::MeshRenderer
                    || component->GetType() == ComponentType::Collider
                    || (component->GetType() == ComponentType::Script && dynamic_pointer_cast<EditorCamera>(component) != nullptr))
                    component->Start();
            }
        }
        else
        {
            if (component)
            {
                component->Start();
            }
        }
    }
}

Start는 컴포넌트 초기화에 사용되며:

  1. 에디터 모드에서는 특정 컴포넌트만 초기화됩니다

  2. 게임 모드에서는 모든 컴포넌트가 초기화됩니다

  • Update 및 LateUpdate

Update와 LateUpdate 메서드도 Start와 유사한 방식으로 처리됩니다:

void GameObject::Update()
{
    for (shared_ptr<Component>& component : _components)
    {
        if (ENGINE.GetEngineMode() == EngineMode::Edit)
        {
            // 에디터 모드에서는 특정 컴포넌트만 업데이트
            if (component)
            {
                if (component->GetType() == ComponentType::Transform 
                    || component->GetType() == ComponentType::Camera
                    || component->GetType() == ComponentType::Light
                    || component->GetType() == ComponentType::MeshRenderer
                    || component->GetType() == ComponentType::Collider
                    || (component->GetType() == ComponentType::Script && dynamic_pointer_cast<EditorCamera>(component) != nullptr))
                    component->Update();
            }
        }
        else
        {
            // 게임 모드에서는 모든 컴포넌트 업데이트
            if (component)
            {
                component->Update();
            }
        }
    }
}

LateUpdate도 유사한 형태로 구현되어 있으며, 일반적으로 Update 이후에 추가적인 처리가 필요한 작업(카메라 추적 등)에 사용됩니다.

XML기반 컴포턴트 저장

void SceneManager::AddComponentToGameObjectAndSaveToXML(const wstring& path,
    const wstring& name,
    const shared_ptr<Component>& component,
    const wstring& material,
    const wstring& mesh,
    const wstring& model)
{
    // XML 파일 로드
    tinyxml2::XMLDocument doc;
    wstring xmlPath = L"../GameCoding/Resource/Scene/" + path + L".xml";
    string strPath(xmlPath.begin(), xmlPath.end());
    doc.LoadFile(strPath.c_str());
    if (doc.Error()) return;

    // 루트 요소 가져오기
    tinyxml2::XMLElement* rootElem = doc.FirstChildElement("Scene");
    if (rootElem == nullptr) return;

    // 게임 오브젝트 찾기
    tinyxml2::XMLElement* objectElem = rootElem->FirstChildElement("GameObject");
    while (objectElem)
    {
        string objectName = objectElem->Attribute("name");
        wstring wObjectName(objectName.begin(), objectName.end());
        if (wObjectName == name)
        {
            // 컴포넌트 타입에 따른 저장 로직
            ComponentType type = component->GetType();
            if (type == ComponentType::MeshRenderer)
            {
                // MeshRenderer 컴포넌트 저장
                tinyxml2::XMLElement* meshRendererElem = doc.NewElement("MeshRenderer");
                if (!mesh.empty())
                {
                    string meshName(mesh.begin(), mesh.end());
                    meshRendererElem->SetAttribute("mesh", meshName.c_str());
                }
                if (!material.empty())
                {
                    string materialName(material.begin(), material.end());
                    meshRendererElem->SetAttribute("material", materialName.c_str());
                }
                objectElem->InsertEndChild(meshRendererElem);
            }
            else if (type == ComponentType::BoxCollider)
            {
                // BoxCollider 컴포넌트 저장
                shared_ptr<BoxCollider> boxCollider = dynamic_pointer_cast<BoxCollider>(component);
                tinyxml2::XMLElement* colliderElem = doc.NewElement("BoxCollider");
                
                // 중심점 저장
                Vec3 center = boxCollider->GetCenter();
                tinyxml2::XMLElement* centerElem = doc.NewElement("Center");
                centerElem->SetAttribute("x", center.x);
                centerElem->SetAttribute("y", center.y);
                centerElem->SetAttribute("z", center.z);
                colliderElem->InsertEndChild(centerElem);
                
                // 크기 저장
                Vec3 size = boxCollider->GetSize();
                tinyxml2::XMLElement* sizeElem = doc.NewElement("Size");
                sizeElem->SetAttribute("x", size.x);
                sizeElem->SetAttribute("y", size.y);
                sizeElem->SetAttribute("z", size.z);
                colliderElem->InsertEndChild(sizeElem);
                
                objectElem->InsertEndChild(colliderElem);
            }
            // 다른 컴포넌트 타입에 대한 처리...
            
            break;
        }
        objectElem = objectElem->NextSiblingElement("GameObject");
    }

    // XML 파일 저장
    doc.SaveFile(strPath.c_str());
}

이 함수는:

  1. 현재 Scene의 XML 파일을 로드합니다

  2. 해당 게임 오브젝트를 찾습니다

  3. 컴포넌트 타입에 따라 적절한 XML 요소를 생성합니다

  4. 컴포넌트의 속성을 XML 속성으로 저장합니다

  5. 수정된 XML을 파일에 저장합니다

컴포넌트 제거도 유사한 방식으로 구현이 되어있으며 저장된 형태는 아래와 같습니다:

<Transform posX="35.395309" posY="2.171416" posZ="39.140205" rotX="0" rotY="0" rotZ="0" scaleX="1" scaleY="1" scaleZ="1"/>
<MeshRenderer useEnvironmentMap="false" fillMode="3" cullMode="3" frontCounterClockwise="false" material="SolidWhiteMaterial" mesh="Sphere">
<RenderPass pass="0" depthStencilState="0"/>
</MeshRenderer>
<SphereCollider radius="0.5" centerX="0" centerY="0" centerZ="0"/>

ComponentFactory를 활용한 사용자 정의 컴포넌트 관리

ComponentFactory는 사용자 정의 스크립트 컴포넌트(MonoBehaviour를 상속한 클래스)를 등록하고 생성하는 역할을 합니다.

  • ComponentFactory 구조

class ComponentFactory {
protected:
    ComponentFactory() {}
public:
    static ComponentFactory& GetInstance() {
        static ComponentFactory instance;
        return instance;
    }

    using CreateScriptFunc = std::function<shared_ptr<MonoBehaviour>()>;
    struct ScriptInfo {
        string displayName;
        CreateScriptFunc createFunc;
    };

    void RegisterScript(const string& typeName, const ScriptInfo& info) {
        _scriptTypes[typeName] = info;
    }

    const map<string, ScriptInfo>& GetRegisteredScripts() const { return _scriptTypes; }

private:
    map<string, ScriptInfo> _scriptTypes;
};

이 클래스는:

  1. 싱글톤 패턴으로 구현되어 있습니다

  2. 스크립트 타입 이름과 정보(표시 이름, 생성 함수)를 매핑합니다

  3. RegisterScript 메서드를 통해 새로운 스크립트 타입을 등록합니다

  • REGISTER_SCRIPT 매크로 활용

#define REGISTER_SCRIPT(Type, DisplayName) \
    namespace { \
        struct Type##Registrar { \
            Type##Registrar() { \
                ComponentFactory::ScriptInfo info; \
                info.displayName = DisplayName; \
                info.createFunc = []() { return make_shared<Type>(); }; \
                ComponentFactory::GetInstance().RegisterScript(#Type, info); \
            } \
        }; \
        static Type##Registrar Type##AutoRegister; \
    }

이 매크로는 사용자 정의 스크립트를 자동으로 등록하는 기능을 제공합니다:

  1. 매크로는 Type(클래스 이름)과 DisplayName(UI에 표시될 이름)을 인자로 받습니다

  2. 익명 네임스페이스 내에 등록자(Registrar) 구조체를 정의합니다

  3. 구조체의 생성자에서 스크립트 정보를 설정하고 ComponentFactory에 등록합니다

  4. 정적 변수로 등록자 인스턴스를 생성하여 프로그램 시작 시 자동으로 등록되도록 합니다

  • 사용자 정의 컴포넌트 등록 예시

// MoveObject.h
#pragma once
#include "MonoBehaviour.h"

class MoveObject : public MonoBehaviour
{
public:
    MoveObject();
    virtual ~MoveObject();

    virtual void Start() override;
    virtual void Update() override;

private:
    float _speed = 5.0f;
    Vec3 _direction = Vec3::Right;
};

// 스크립트 자동 등록
REGISTER_SCRIPT(MoveObject, "Move Object");

이 코드는:

  1. MonoBehaviour를 상속받아 MoveObject 컴포넌트를 정의합니다

  2. REGISTER_SCRIPT 매크로를 사용하여 ComponentFactory에 자동 등록합니다

  3. "Move Object"라는 표시 이름으로 에디터 UI에 나타나게 됩니다

Last updated