본문 바로가기
-----ETC-----/C++로 배우는 게임 엔진 개발

[C++로 배우는 게임 엔진 개발] Day 14: 엔티티 컴포넌트 시스템 (ECS) 기초

by cogito21_cpp 2024. 8. 1.
반응형

엔티티 컴포넌트 시스템 (ECS) 기초

오늘은 엔티티 컴포넌트 시스템(ECS)을 사용하여 게임 객체를 효율적으로 관리하는 방법을 학습하겠습니다. ECS는 유연하고 확장 가능한 게임 객체 관리 방법으로, 엔티티, 컴포넌트, 시스템 세 가지 주요 개념으로 구성됩니다.

1. ECS의 개념

  • 엔티티(Entity): 게임에서 고유한 객체를 나타내는 ID입니다. 실제 데이터는 포함하지 않습니다.
  • 컴포넌트(Component): 엔티티의 속성이나 데이터를 나타냅니다. 각 컴포넌트는 특정한 기능이나 데이터를 제공합니다.
  • 시스템(System): 특정 컴포넌트를 가진 엔티티에 대해 동작을 수행하는 로직입니다.

2. ECS 클래스 설계

먼저, 엔티티, 컴포넌트, 시스템을 관리하기 위한 기본 클래스를 설계합니다.

 

헤더 파일 작성

include/ECS.h 파일을 생성하고 다음과 같이 작성합니다.

#ifndef ECS_H
#define ECS_H

#include <unordered_map>
#include <bitset>
#include <vector>
#include <memory>
#include <iostream>
#include <typeindex>

// 최대 컴포넌트 수를 정의합니다.
constexpr std::size_t MaxComponents = 32;

// 각 컴포넌트의 ID를 관리하기 위한 함수입니다.
inline std::size_t GetComponentTypeID() {
    static std::size_t lastID = 0u;
    return lastID++;
}

// 엔티티를 나타내는 클래스입니다.
class Entity {
public:
    explicit Entity(std::size_t id) : id(id) {}

    std::size_t GetID() const { return id; }

private:
    std::size_t id;
};

// 컴포넌트를 나타내는 기본 클래스입니다.
class Component {
public:
    virtual ~Component() = default;
};

// 엔티티 관리자 클래스입니다.
class EntityManager {
public:
    Entity CreateEntity() {
        std::size_t id = nextEntityID++;
        entities.emplace(id, std::make_unique<Entity>(id));
        return *entities[id];
    }

    void DestroyEntity(Entity entity) {
        auto id = entity.GetID();
        entities.erase(id);
    }

private:
    std::unordered_map<std::size_t, std::unique_ptr<Entity>> entities;
    std::size_t nextEntityID = 0;
};

// 컴포넌트 관리자 클래스입니다.
class ComponentManager {
public:
    template<typename T>
    void AddComponent(Entity entity, T component) {
        auto id = entity.GetID();
        auto componentTypeID = GetComponentTypeID<T>();

        if (componentPools.find(componentTypeID) == componentPools.end()) {
            componentPools[componentTypeID] = std::make_unique<ComponentPool<T>>();
        }

        auto componentPool = static_cast<ComponentPool<T>*>(componentPools[componentTypeID].get());
        componentPool->Insert(id, std::move(component));
    }

    template<typename T>
    T& GetComponent(Entity entity) {
        auto id = entity.GetID();
        auto componentTypeID = GetComponentTypeID<T>();

        auto componentPool = static_cast<ComponentPool<T>*>(componentPools[componentTypeID].get());
        return componentPool->Get(id);
    }

private:
    class IComponentPool {
    public:
        virtual ~IComponentPool() = default;
    };

    template<typename T>
    class ComponentPool : public IComponentPool {
    public:
        void Insert(std::size_t entityID, T component) {
            components[entityID] = std::move(component);
        }

        T& Get(std::size_t entityID) {
            return components[entityID];
        }

    private:
        std::unordered_map<std::size_t, T> components;
    };

    template<typename T>
    std::size_t GetComponentTypeID() {
        static std::size_t typeID = GetComponentTypeID();
        return typeID;
    }

    std::unordered_map<std::size_t, std::unique_ptr<IComponentPool>> componentPools;
};

#endif // ECS_H

3. 게임 엔진에 ECS 추가

이제 게임 엔진에 ECS를 통합하고, 간단한 예제 시스템을 구현하여 엔티티와 컴포넌트를 관리하겠습니다.

 

헤더 파일 수정

include/GameEngine.h 파일을 수정하여 ECS 헤더를 포함합니다.

#ifndef GAMEENGINE_H
#define GAMEENGINE_H

#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include "ECS.h"
#include "AudioManager.h"

class GameEngine {
public:
    GameEngine();
    ~GameEngine();
    bool Initialize(const char* title, int width, int height);
    void Run();
    void Shutdown();

private:
    SDL_Window* window;
    SDL_Renderer* renderer;
    AudioManager* audioManager;
    EntityManager entityManager;
    ComponentManager componentManager;
    Entity playerEntity;
    bool isRunning;
    Uint32 frameStart;
    int frameTime;

    void HandleEvents();
    void Update();
    void Render();
};

#endif // GAMEENGINE_H

 

소스 파일 수정

src/GameEngine.cpp 파일을 다음과 같이 수정합니다.

#include "GameEngine.h"
#include <iostream>

const int FPS = 60;
const int FRAME_DELAY = 1000 / FPS;

class PositionComponent : public Component {
public:
    int x, y;
    PositionComponent(int x, int y) : x(x), y(y) {}
};

class VelocityComponent : public Component {
public:
    int vx, vy;
    VelocityComponent(int vx, int vy) : vx(vx), vy(vy) {}
};

GameEngine::GameEngine()
    : window(nullptr), renderer(nullptr), audioManager(nullptr), isRunning(false), frameStart(0), frameTime(0) {}

GameEngine::~GameEngine() {
    Shutdown();
}

bool GameEngine::Initialize(const char* title, int width, int height) {
    if (SDL_Init(SDL_INIT_VIDEO) != 0) {
        std::cerr << "SDL_Init Error: " << SDL_GetError() << std::endl;
        return false;
    }

    window = SDL_CreateWindow(title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, SDL_WINDOW_SHOWN);
    if (window == nullptr) {
        std::cerr << "SDL_CreateWindow Error: " << SDL_GetError() << std::endl;
        return false;
    }

    renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
    if (renderer == nullptr) {
        std::cerr << "SDL_CreateRenderer Error: " << SDL_GetError() << std::endl;
        return false;
    }

    audioManager = new AudioManager();
    if (!audioManager->Initialize()) {
        return false;
    }

    audioManager->LoadSound("path/to/your/sound.wav", "sound1");
    audioManager->LoadMusic("path/to/your/music.mp3");

    playerEntity = entityManager.CreateEntity();
    componentManager.AddComponent(playerEntity, PositionComponent(100, 100));
    componentManager.AddComponent(playerEntity, VelocityComponent(2, 2));

    isRunning = true;
    return true;
}

void GameEngine::Run() {
    audioManager->PlayMusic();
    while (isRunning) {
        frameStart = SDL_GetTicks();

        HandleEvents();
        Update();
        Render();

        frameTime = SDL_GetTicks() - frameStart;
        if (FRAME_DELAY > frameTime) {
            SDL_Delay(FRAME_DELAY - frameTime);
        }
    }
}

void GameEngine::HandleEvents() {
    SDL_Event event;
    while (SDL_PollEvent(&event)) {
        if (event.type == SDL_QUIT) {
            isRunning = false;
        }
        if (event.type == SDL_KEYDOWN) {
            switch (event.key.keysym.sym) {
                case SDLK_ESCAPE:
                    isRunning = false;
                    break;
                case SDLK_SPACE:
                    audioManager->PlaySound("sound1");
                    break;
                default:
                    break;
            }
        }
    }
}

void GameEngine::Update() {
    auto& position = componentManager.GetComponent<PositionComponent>(playerEntity);
    auto& velocity = componentManager.GetComponent<VelocityComponent>(playerEntity);

    position.x += velocity.vx;
    position.y += velocity.vy;

    if (position.x < 0 || position.x > 800 || position.y < 0 || position.y > 600) {
        velocity.vx = -velocity.vx;
        velocity.vy = -velocity.vy;
    }
}

void GameEngine::Render() {
    // 화면을 검은색으로 지우기
    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
    SDL_RenderClear(renderer);

    // 플레이어 렌더링
    auto& position = componentManager.GetComponent<PositionComponent>(playerEntity);
    SDL_Rect playerRect = { position.x, position.y, 50, 50 };
    SDL_SetRenderDrawColor(renderer, 0, 0, 255, 255);
    SDL_RenderFillRect(renderer, &playerRect);

    //

 렌더링 결과를 화면에 출력
    SDL_RenderPresent(renderer);
}

void GameEngine::Shutdown() {
    if (audioManager) {
        audioManager->Shutdown();
        delete audioManager;
        audioManager = nullptr;
    }
    if (renderer) {
        SDL_DestroyRenderer(renderer);
        renderer = nullptr;
    }
    if (window) {
        SDL_DestroyWindow(window);
        window = nullptr;
    }
    SDL_Quit();
}

4. 프로젝트 빌드 및 실행

  1. Visual Studio에서 CMake 프로젝트 열기:
    • Visual Studio를 실행하고, File -> Open -> CMake...를 선택합니다.
    • GameEngine 디렉토리를 선택하여 프로젝트를 엽니다.
  2. SDL2_mixer 라이브러리 설치:
    • SDL2_mixer 라이브러리를 프로젝트에 설정합니다.
  3. 프로젝트 빌드:
    • Visual Studio 상단의 Build 메뉴에서 Build All을 선택하여 프로젝트를 빌드합니다.
  4. 프로젝트 실행:
    • Debug 메뉴에서 Start Without Debugging을 선택하여 프로그램을 실행합니다.
    • 윈도우 창이 생성되고, 파란색 사각형(플레이어)이 화면에서 움직이며, 스페이스바를 누르면 사운드가 재생되고, 배경음악이 루프로 재생됩니다.

마무리

오늘은 엔티티 컴포넌트 시스템(ECS)을 사용하여 게임 객체를 효율적으로 관리하는 방법을 학습했습니다. ECS를 통해 유연하고 확장 가능한 게임 구조를 구현하는 기초를 배웠습니다. 다음 단계에서는 쉐이더 프로그래밍 기초를 배우고, 그래픽 효과를 구현하는 방법을 학습하겠습니다.

질문이나 추가적인 피드백이 있으면 언제든지 댓글로 남겨 주세요.

Day 15 예고

다음 날은 "쉐이더 프로그래밍 기초 (GLSL)"에 대해 다룰 것입니다. GLSL을 사용하여 쉐이더를 작성하고, 다양한 그래픽 효과를 구현하는 방법을 배워보겠습니다.

반응형