[2026] C++ Game Engine Basics: ECS, Rendering, Physics, Input, Lua Scripting [#50-3]
이 글의 핵심
Build a 2D game engine from scratch: ECS architecture, SDL rendering with z-index sorting, AABB physics with collision resolution, input system with events, and Lua scripting integration.
Introduction: “Characters fall through floors, collisions bounce weirdly”
Problems encountered when building game engines
Building 2D games from scratch without Unity or Unreal, you encounter:
- Entities fall through floors — collision detection order or AABB boundary calculation errors
- Render order shuffles every frame — no Z-index sorting or layer system
- Input feels sluggish tied to frames — polling only, not event-based
- Game logic mixed with engine code, hard to modify — insufficient scripting separation This guide covers 2D game engine basics integrating rendering, physics, input, and scripting based on ECS architecture. Goals:
- ECS (Entity Component System) architecture
- 2D rendering pipeline (SDL2/SFML)
- Physics simulation (collision detection, rigid body dynamics)
- Input handling and event system
- Lua scripting integration Requirements: C++17+, SDL2 or SFML, Box2D (optional), Lua 5.4
Conceptual Analogy
Think of this topic as multiple interlocking parts of a system. Choices in one layer (storage, networking, observation) affect adjacent layers, so the main text organizes trade-offs with numbers and patterns.
Table of Contents
- ECS Architecture
- Rendering System
- Physics Simulation
- Input and Events
- Scripting Integration
- Complete Game Engine Example
- Common Issues and Solutions
- Performance Optimization Tips
- Production Patterns
1. ECS Architecture
Why ECS?
Traditional inheritance-based game objects cause “multiple inheritance hell” and “diamond inheritance” problems. ECS uses composition, attaching only needed components to entities for flexible extension. 다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TB
subgraph ECS[ECS Architecture]
E[Entity]
C1[TransformComponent]
C2[SpriteComponent]
C3[RigidBodyComponent]
C4[ColliderComponent]
E --> C1
E --> C2
E --> C3
E --> C4
end
subgraph Systems[Systems]
S1[RenderSystem]
S2[PhysicsSystem]
S3[InputSystem]
end
C1 --> S1
C2 --> S1
C3 --> S2
C4 --> S2
Core Component Definitions
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <glm/glm.hpp>
#include <SDL2/SDL.h>
#include <memory>
#include <string>
#include <typeindex>
#include <unordered_map>
using EntityID = uint32_t;
class Component {
public:
virtual ~Component() = default;
};
// Position, rotation, scale — needed for all render/physics entities
struct TransformComponent : Component {
glm::vec2 position{0, 0};
float rotation = 0.0f;
glm::vec2 scale{1, 1};
};
// Sprite rendering
struct SpriteComponent : Component {
std::string texture_id;
SDL_Rect src_rect;
int z_index = 0; // Render order (lower = drawn behind)
};
// Physics velocity, mass
struct RigidBodyComponent : Component {
glm::vec2 velocity{0, 0};
float mass = 1.0f;
bool is_static = false; // Fixed objects like floors, walls
};
// Collision area (AABB) — required by physics system
struct ColliderComponent : Component {
float width = 32.0f;
float height = 32.0f;
bool is_trigger = false; // Passable area (trigger)
};
class Entity {
EntityID id_;
std::unordered_map<std::type_index, std::unique_ptr<Component>> components_;
public:
explicit Entity(EntityID id) : id_(id) {}
template <typename T, typename....Args>
T& add_component(Args&&....args) {
auto component = std::make_unique<T>(std::forward<Args>(args)...);
auto* ptr = component.get();
components_[typeid(T)] = std::move(component);
return *ptr;
}
template <typename T>
T* get_component() {
auto it = components_.find(typeid(T));
return it != components_.end() ? static_cast<T*>(it->second.get()) : nullptr;
}
template <typename T>
bool has_component() const {
return components_.find(typeid(T)) != components_.end();
}
EntityID get_id() const { return id_; }
};
class EntityManager {
std::unordered_map<EntityID, std::unique_ptr<Entity>> entities_;
EntityID next_id_ = 1;
public:
Entity& create_entity() {
auto id = next_id_++;
auto entity = std::make_unique<Entity>(id);
auto* ptr = entity.get();
entities_[id] = std::move(entity);
return *ptr;
}
void destroy_entity(EntityID id) {
entities_.erase(id);
}
template <typename....Components>
std::vector<Entity*> get_entities_with() {
std::vector<Entity*> result;
for (auto& [id, entity] : entities_) {
if ((entity->has_component<Components>() && ...)) {
result.push_back(entity.get());
}
}
return result;
}
};
Caution: get_entities_with allocates new vector every frame. For high performance, optimize with per-component indices.
2. Rendering System
Rendering Pipeline Flow
아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
sequenceDiagram
participant GameLoop
participant RenderSystem
participant SDL
GameLoop->>RenderSystem: update(entities)
RenderSystem->>RenderSystem: Z-index sort
RenderSystem->>SDL: RenderClear
loop Each entity
RenderSystem->>SDL: RenderCopyEx
end
RenderSystem->>SDL: RenderPresent
Implementation
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <algorithm>
#include <SDL2/SDL_image.h>
class RenderSystem {
SDL_Renderer* renderer_;
std::unordered_map<std::string, SDL_Texture*> textures_;
public:
explicit RenderSystem(SDL_Renderer* renderer) : renderer_(renderer) {}
void update(EntityManager& entities) {
auto entities_to_render =
entities.get_entities_with<TransformComponent, SpriteComponent>();
// Sort by z_index ascending (lower value = drawn behind)
std::sort(entities_to_render.begin(), entities_to_render.end(),
[](Entity* a, Entity* b) {
return a->get_component<SpriteComponent>()->z_index <
b->get_component<SpriteComponent>()->z_index;
});
SDL_RenderClear(renderer_);
for (auto* entity : entities_to_render) {
auto* transform = entity->get_component<TransformComponent>();
auto* sprite = entity->get_component<SpriteComponent>();
auto it = textures_.find(sprite->texture_id);
if (it == textures_.end()) continue; // Skip if no texture
SDL_Rect dest_rect = {
static_cast<int>(transform->position.x),
static_cast<int>(transform->position.y),
static_cast<int>(sprite->src_rect.w * transform->scale.x),
static_cast<int>(sprite->src_rect.h * transform->scale.y)};
SDL_RenderCopyEx(renderer_, it->second, &sprite->src_rect,
&dest_rect, transform->rotation, nullptr,
SDL_FLIP_NONE);
}
SDL_RenderPresent(renderer_);
}
void load_texture(const std::string& id, const std::string& path) {
SDL_Surface* surface = IMG_Load(path.c_str());
if (!surface) return;
SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer_, surface);
SDL_FreeSurface(surface);
if (texture) textures_[id] = texture;
}
~RenderSystem() {
for (auto& [id, tex] : textures_) SDL_DestroyTexture(tex);
}
};
3. Physics Simulation
Simple Physics Engine (AABB Collision)
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class PhysicsSystem {
glm::vec2 gravity_{0, 9.8f};
public:
void update(EntityManager& entities, float dt) {
auto physics_entities = entities.get_entities_with<
TransformComponent, RigidBodyComponent, ColliderComponent>();
// 1. Apply gravity
for (auto* entity : physics_entities) {
auto* rb = entity->get_component<RigidBodyComponent>();
if (!rb->is_static) {
rb->velocity += gravity_ * dt;
}
}
// 2. Update positions
for (auto* entity : physics_entities) {
auto* transform = entity->get_component<TransformComponent>();
auto* rb = entity->get_component<RigidBodyComponent>();
if (!rb->is_static) {
transform->position += rb->velocity * dt;
}
}
// 3. Detect and resolve collisions
check_collisions(physics_entities);
}
void check_collisions(const std::vector<Entity*>& entities) {
for (size_t i = 0; i < entities.size(); ++i) {
for (size_t j = i + 1; j < entities.size(); ++j) {
if (check_collision(entities[i], entities[j])) {
resolve_collision(entities[i], entities[j]);
}
}
}
}
bool check_collision(Entity* a, Entity* b) {
auto* ta = a->get_component<TransformComponent>();
auto* tb = b->get_component<TransformComponent>();
auto* ca = a->get_component<ColliderComponent>();
auto* cb = b->get_component<ColliderComponent>();
if (!ca || !cb) return false;
// AABB collision: check if two rectangles overlap
return ta->position.x < tb->position.x + cb->width &&
ta->position.x + ca->width > tb->position.x &&
ta->position.y < tb->position.y + cb->height &&
ta->position.y + ca->height > tb->position.y;
}
void resolve_collision(Entity* a, Entity* b) {
auto* rba = a->get_component<RigidBodyComponent>();
auto* rbb = b->get_component<RigidBodyComponent>();
if (!rba || !rbb) return;
// Triggers have no physical response
auto* ca = a->get_component<ColliderComponent>();
auto* cb = b->get_component<ColliderComponent>();
if (ca->is_trigger || cb->is_trigger) return;
// Reverse velocity on collision with static object
if (rbb->is_static) {
rba->velocity.y = -rba->velocity.y * 0.8f; // Elasticity
} else if (rba->is_static) {
rbb->velocity.y = -rbb->velocity.y * 0.8f;
} else {
// Both dynamic: swap velocities (simple elastic collision)
auto temp = rba->velocity;
rba->velocity = rbb->velocity;
rbb->velocity = temp;
}
}
};
4. Input and Events
Input System (Polling + Events)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <functional>
#include <vector>
class InputSystem {
std::unordered_map<SDL_Keycode, bool> key_states_;
glm::vec2 mouse_position_{0, 0};
bool quit_requested_ = false;
using KeyCallback = std::function<void(SDL_Keycode)>;
std::vector<KeyCallback> key_down_callbacks_;
std::vector<KeyCallback> key_up_callbacks_;
public:
void update() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_QUIT:
quit_requested_ = true;
break;
case SDL_KEYDOWN:
key_states_[event.key.keysym.sym] = true;
for (auto& cb : key_down_callbacks_) cb(event.key.keysym.sym);
break;
case SDL_KEYUP:
key_states_[event.key.keysym.sym] = false;
for (auto& cb : key_up_callbacks_) cb(event.key.keysym.sym);
break;
case SDL_MOUSEMOTION:
mouse_position_ = {static_cast<float>(event.motion.x),
static_cast<float>(event.motion.y)};
break;
}
}
}
bool is_key_pressed(SDL_Keycode key) const {
auto it = key_states_.find(key);
return it != key_states_.end() && it->second;
}
glm::vec2 get_mouse_position() const { return mouse_position_; }
bool is_quit_requested() const { return quit_requested_; }
void on_key_down(KeyCallback cb) { key_down_callbacks_.push_back(std::move(cb)); }
void on_key_up(KeyCallback cb) { key_up_callbacks_.push_back(std::move(cb)); }
};
5. Scripting Integration
Lua API Registration Example
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
extern "C" {
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
}
class ScriptingSystem {
lua_State* L_;
EntityManager* entities_ = nullptr;
public:
ScriptingSystem() {
L_ = luaL_newstate();
luaL_openlibs(L_);
}
void set_entity_manager(EntityManager* em) { entities_ = em; }
void register_api() {
if (!entities_) return;
// create_entity() -> returns entity_id
lua_pushlightuserdata(L_, entities_);
lua_pushcclosure(L_, [](lua_State* L) -> int {
auto* ud = static_cast<EntityManager*>(lua_touserdata(L, lua_upvalueindex(1)));
if (!ud) return 0;
auto& e = ud->create_entity();
lua_pushinteger(L, static_cast<lua_Integer>(e.get_id()));
return 1;
}, 1);
lua_setglobal(L_, "create_entity");
// set_position(entity_id, x, y)
// Implementation omitted: lua_tointeger, get_entity, get_component<Transform>, etc.
}
bool run_script(const std::string& script) {
if (luaL_dostring(L_, script.c_str()) != LUA_OK) {
fprintf(stderr, "Lua error: %s\n", lua_tostring(L_, -1));
lua_pop(L_, 1);
return false;
}
return true;
}
~ScriptingSystem() { lua_close(L_); }
};
Lua Game Logic Example
아래 코드는 lua를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
-- game_init.lua: run at game start
local player_id = create_entity()
add_transform(player_id, 100, 200)
add_sprite(player_id, "player", 0)
add_rigidbody(player_id, 0, 0, 1, false)
add_collider(player_id, 32, 32)
Calling C++ Callbacks from Lua
To handle game logic in Lua and call C++ functions on specific events, use lua_pcall and table-based callback registration.
아래 코드는 lua를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
-- Lua side: register on_collision
function on_collision(a_id, b_id)
if get_entity_tag(a_id) == "player" and get_entity_tag(b_id) == "coin" then
add_score(10)
destroy_entity(b_id)
end
end
아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// C++ side: call Lua callback
void PhysicsSystem::on_collision_detected(Entity* a, Entity* b) {
lua_getglobal(L_, "on_collision");
if (lua_isfunction(L_, -1)) {
lua_pushinteger(L_, a->get_id());
lua_pushinteger(L_, b->get_id());
if (lua_pcall(L_, 2, 0, 0) != LUA_OK) {
fprintf(stderr, "Lua callback error: %s\n", lua_tostring(L_, -1));
}
}
}
6. Complete Game Engine Example
Game Loop Flow
아래 코드는 mermaid를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
flowchart LR
subgraph Frame[One Frame]
A[Input processing] --> B[Physics update]
B --> C[Script update]
C --> D[Rendering]
D --> E[Frame limiting]
end
E --> A
Game Loop Integration
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class GameEngine {
SDL_Window* window_ = nullptr;
SDL_Renderer* renderer_ = nullptr;
EntityManager entities_;
RenderSystem render_system_;
PhysicsSystem physics_system_;
InputSystem input_system_;
ScriptingSystem script_system_;
bool running_ = true;
const float target_dt_ = 1.0f / 60.0f;
public:
bool init() {
if (SDL_Init(SDL_INIT_VIDEO) != 0) return false;
window_ = SDL_CreateWindow("2D Engine", SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED, 800, 600, 0);
if (!window_) return false;
renderer_ = SDL_CreateRenderer(window_, -1, SDL_RENDERER_ACCELERATED);
if (!renderer_) return false;
render_system_ = RenderSystem(renderer_);
script_system_.set_entity_manager(&entities_);
script_system_.register_api();
create_sample_scene();
return true;
}
void create_sample_scene() {
// Player
auto& player = entities_.create_entity();
player.add_component<TransformComponent>().position = {100, 200};
player.add_component<SpriteComponent>();
auto& spr = *player.get_component<SpriteComponent>();
spr.texture_id = "player";
spr.src_rect = {0, 0, 32, 32};
spr.z_index = 1;
player.add_component<RigidBodyComponent>();
player.add_component<ColliderComponent>();
// Floor
auto& floor = entities_.create_entity();
floor.add_component<TransformComponent>().position = {0, 500};
auto& floor_spr = floor.add_component<SpriteComponent>();
floor_spr.texture_id = "floor";
floor_spr.src_rect = {0, 0, 800, 100};
floor_spr.z_index = 0;
auto& floor_rb = floor.add_component<RigidBodyComponent>();
floor_rb.is_static = true;
floor.add_component<ColliderComponent>().width = 800;
floor.get_component<ColliderComponent>()->height = 100;
}
void run() {
Uint64 last = SDL_GetPerformanceCounter();
while (running_) {
Uint64 now = SDL_GetPerformanceCounter();
float dt = static_cast<float>(now - last) / SDL_GetPerformanceFrequency();
last = now;
input_system_.update();
if (input_system_.is_quit_requested()) break;
physics_system_.update(entities_, std::min(dt, target_dt_ * 2));
render_system_.update(entities_);
// Frame limiting
float elapsed = static_cast<float>(SDL_GetPerformanceCounter() - now) /
SDL_GetPerformanceFrequency();
if (elapsed < target_dt_) {
SDL_Delay(static_cast<Uint32>((target_dt_ - elapsed) * 1000));
}
}
}
void shutdown() {
if (renderer_) SDL_DestroyRenderer(renderer_);
if (window_) SDL_DestroyWindow(window_);
SDL_Quit();
}
};
7. Common Issues and Solutions
Issue 1: Entities fall through floor
Cause: Missing ColliderComponent, or check_collision requires ColliderComponent but not added to floor.
Solution:
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ Wrong: floor missing ColliderComponent
auto& floor = entities_.create_entity();
floor.add_component<TransformComponent>();
floor.add_component<RigidBodyComponent>().is_static = true;
// ColliderComponent missing!
// ✅ Correct
floor.add_component<ColliderComponent>().width = 800;
floor.get_component<ColliderComponent>()->height = 100;
Issue 2: Render order changes every frame
Cause: Unstable comparator in std::sort, or random order when z_index identical.
Solution:
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ Stable sort + secondary sort by entity ID
std::stable_sort(entities_to_render.begin(), entities_to_render.end(),
[](Entity* a, Entity* b) {
int za = a->get_component<SpriteComponent>()->z_index;
int zb = b->get_component<SpriteComponent>()->z_index;
if (za != zb) return za < zb;
return a->get_id() < b->get_id(); // Fixed order on same z_index
});
Issue 3: Lua script “attempt to call a nil value”
Cause: Not passing EntityManager* via lua_upvalueindex when registering C function with lua_register.
Solution:
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ✅ Pass context via upvalue
void register_api() {
lua_pushlightuserdata(L_, entities_);
lua_pushcclosure(L_, [](lua_State* L) -> int {
auto* em = static_cast<EntityManager*>(lua_touserdata(L, lua_upvalueindex(1)));
// Use em
return 1;
}, 1);
lua_setglobal(L_, "create_entity");
}
Issue 4: Physics “tunneling” at high speeds (fast objects pass through walls)
Cause: Single-frame movement distance exceeds collider size, skipping collision detection. Solution: Use continuous collision detection (CCD) or substeps. 아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ Substeps: divide dt for multiple physics updates
const int substeps = 4;
float sub_dt = dt / substeps;
for (int i = 0; i < substeps; ++i) {
physics_system_.update(entities_, sub_dt);
}
Issue 5: Black screen on texture load failure
Cause: Not checking IMG_Load failure returning nullptr before calling SDL_CreateTextureFromSurface.
Solution:
다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ✅ Error checking
void load_texture(const std::string& id, const std::string& path) {
SDL_Surface* surface = IMG_Load(path.c_str());
if (!surface) {
SDL_Log("Failed to load %s: %s", path.c_str(), IMG_GetError());
return;
}
SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer_, surface);
SDL_FreeSurface(surface);
if (!texture) {
SDL_Log("Failed to create texture: %s", SDL_GetError());
return;
}
textures_[id] = texture;
}
Issue 6: Physics “looks slow” when game runs below 60 FPS
Cause: Large dt increases single-frame movement distance, destabilizing physics. On low-spec PCs, dt can exceed 1/30 second.
Solution: Cap dt and divide updates when exceeded.
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ dt clamping + fixed timestep interpolation
const float max_dt = 1.0f / 30.0f; // Physics based on max 30 FPS
float accumulated = 0;
accumulated += std::min(dt, max_dt);
while (accumulated >= target_dt_) {
physics_system_.update(entities_, target_dt_);
accumulated -= target_dt_;
}
Issue 7: Crash on entity deletion (use-after-free)
Cause: After destroy_entity call, other systems continue referencing that entity pointer.
Solution: Use deferred destruction pattern.
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ✅ Delete at next frame start
std::vector<EntityID> to_destroy_;
void mark_for_destruction(EntityID id) {
to_destroy_.push_back(id);
}
void process_destruction() {
for (auto id : to_destroy_) {
entities_.destroy_entity(id);
}
to_destroy_.clear();
}
// Call process_destruction() at run() loop start
8. Performance Optimization Tips
1. Optimize queries with component indices
get_entities_with traverses all entities every frame. Maintaining entity ID lists per component type enables near-O(1) lookup.
// Update index on component add/remove
std::unordered_map<std::type_index, std::vector<EntityID>> component_index_;
2. Render batching
Grouping sprites using same texture and drawing together reduces draw calls. 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// Group by texture_id then batch render
std::map<std::string, std::vector<Entity*>> by_texture;
for (auto* e : entities_to_render) {
by_texture[e->get_component<SpriteComponent>()->texture_id].push_back(e);
}
for (auto& [tex_id, list] : by_texture) {
SDL_Texture* tex = textures_[tex_id];
for (auto* e : list) {
// Repeat SDL_RenderCopyEx (same texture)
}
}
3. Optimize collision detection with spatial partitioning
Instead of O(n²) collision checks, use spatial hash or Quad Tree to compare only objects in same area.
// Simple grid-based spatial partitioning
std::unordered_map<std::pair<int,int>, std::vector<Entity*>> spatial_grid_;
// Divide into cells like 64x64, check collisions only in same/adjacent cells
4. Object pooling
Reuse entities/components from pool instead of new/delete every time.
std::vector<std::unique_ptr<Entity>> entity_pool_;
// On destroy, return to pool instead of actual deletion; on create, fetch from pool
Box2D Integration (Production-Grade Physics)
Custom AABB physics suits simple games. For complex collisions (circles, polygons, joints) or stable rigid body simulation, use Box2D.
Box2D and ECS Integration
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <box2d/box2d.h>
class Box2DPhysicsSystem {
b2World world_{{0, 9.8f}};
std::unordered_map<EntityID, b2Body*> entity_to_body_;
public:
void sync_to_physics(EntityManager& entities) {
for (auto* entity : entities.get_entities_with<TransformComponent, RigidBodyComponent, ColliderComponent>()) {
auto* t = entity->get_component<TransformComponent>();
auto* rb = entity->get_component<RigidBodyComponent>();
auto* col = entity->get_component<ColliderComponent>();
b2BodyDef def;
def.position.Set(t->position.x / 100.0f, t->position.y / 100.0f); // Pixels→meters
def.type = rb->is_static ? b2_staticBody : b2_dynamicBody;
b2Body* body = world_.CreateBody(&def);
b2PolygonShape box;
box.SetAsBox(col->width / 200.0f, col->height / 200.0f);
b2FixtureDef fix;
fix.shape = &box;
fix.density = 1.0f;
body->CreateFixture(&fix);
entity_to_body_[entity->get_id()] = body;
}
}
void step(float dt) {
world_.Step(dt, 6, 2); // velocityIterations, positionIterations
}
void sync_from_physics(EntityManager& entities) {
auto physics_entities = entities.get_entities_with<
TransformComponent, RigidBodyComponent, ColliderComponent>();
for (auto* entity : physics_entities) {
auto it = entity_to_body_.find(entity->get_id());
if (it == entity_to_body_.end()) continue;
b2Body* body = it->second;
auto* t = entity->get_component<TransformComponent>();
auto* rb = entity->get_component<RigidBodyComponent>();
auto pos = body->GetPosition();
t->position = {pos.x * 100.0f, pos.y * 100.0f};
auto vel = body->GetLinearVelocity();
rb->velocity = {vel.x * 100.0f, vel.y * 100.0f};
}
}
};
Caution: Box2D uses meter units. Define pixel-to-scale ratio (e.g., 100 pixels = 1 meter) for conversion.
Build and Dependencies
CMakeLists.txt Example
다음은 cmake를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
cmake_minimum_required(VERSION 3.16)
project(game_engine LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(SDL2 REQUIRED)
find_package(SDL2_image REQUIRED)
find_package(glm CONFIG REQUIRED)
find_package(Lua REQUIRED)
add_executable(game_engine
main.cpp
engine/entity.cpp
engine/render_system.cpp
engine/physics_system.cpp
)
target_include_directories(game_engine PRIVATE
${SDL2_INCLUDE_DIRS}
${LUA_INCLUDE_DIR}
)
target_link_libraries(game_engine
SDL2::SDL2
SDL2_image::SDL2_image
glm::glm
Lua::Lua
)
Installing Dependencies with vcpkg
다음은 간단한 bash 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# After installing vcpkg
vcpkg install sdl2 sdl2-image glm lua
cmake -B build -DCMAKE_TOOLCHAIN_FILE=[vcpkg root]/scripts/buildsystems/vcpkg.cmake
cmake --build build
Platform-Specific Notes
| Platform | Notes |
|---|---|
| Windows | SDL2 supports DLL dynamic linking or static linking. Static linking simplifies distribution |
| macOS | brew install sdl2 sdl2_image lua or vcpkg |
| Linux | apt install libsdl2-dev libsdl2-image-dev liblua5.4-dev |
9. Production Patterns
1. Config File Loading (JSON/YAML)
// config.json: {"gravity": [0, 9.8], "target_fps": 60}
// Load with nlohmann/json, etc., then inject into PhysicsSystem, GameEngine
2. Scene Transition System
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class SceneManager {
std::string current_scene_;
std::function<void(EntityManager&)> load_scene_;
public:
void load(const std::string& name) {
entities_.clear(); // Or destroy_all
load_scene_ = scene_registry_[name];
load_scene_(entities_);
current_scene_ = name;
}
};
3. Save/Load (Serialization)
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// Serialize Transform, RigidBody, etc. components to JSON/binary
void save_game(const std::string& path) {
nlohmann::json j;
for (auto& [id, entity] : entities_) {
j[entities].push_back(serialize_entity(*entity));
}
std::ofstream f(path);
f << j.dump();
}
4. Debug Overlay
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// Display FPS, entity count, physics computation time with ImGui or SDL
void render_debug_overlay() {
ImGui::Text("FPS: %.1f", 1.0f / dt_);
ImGui::Text("Entities: %zu", entities_.size());
}
5. Implementation Checklist
- Install and link SDL2/SDL_image (vcpkg:
vcpkg install sdl2 sdl2-image) - Install Lua 5.4 (
vcpkg install lua) - Handle texture path errors (relative path vs execution path)
- Adjust viewport/scale on window resize
- Check memory leaks (Valgrind/ASan)
- Verify NDEBUG and optimization flags in release build
Summary
| System | Role |
|---|---|
| ECS | Entity-component management |
| Rendering | Z-index sorting, texture batching |
| Physics | AABB collision, gravity, substeps |
| Input | Polling + event callbacks |
| Scripting | Lua API registration, upvalue usage |
| One-line summary: Implement core game engine systems with ECS architecture, systematically solving collision, rendering, and input problems. |
References
Next: [C++ Practical Guide #50-4] Database Engine Implementation
Practical Checklist
Before writing code
- Is this technique the best solution for the current problem?
- Can team members understand and maintain this code?
- Does it meet performance requirements?
While writing code
- Have all compiler warnings been resolved?
- Have edge cases been considered?
- Is error handling appropriate?
During code review
- Is the code’s intent clear?
- Are test cases sufficient?
- Is it documented? Use this checklist to reduce mistakes and improve code quality.
Frequently Asked Questions (FAQ)
Q. When do I use this in production?
A. 2D game engine basics: rendering pipeline, physics simulation, input handling, Lua scripting integration, ECS architecture. In production, apply by referring to examples and selection guides in the main text above.
Q. What should I read first?
A. Follow Previous links at bottom of each article to learn in order. Check C++ Series Index for complete flow.
Q. How to study deeper?
A. Refer to cppreference and official library documentation. Also utilize reference links at end of article. Previous: [C++ Practical Guide #50-2] Building REST API Server
Related Articles
- C++ Game Engine Basics | Game Loop, ECS, Scene Graph, Input Handling Complete Guide
- C++ Game Engine Architecture Complete Guide | Game Loop, ECS, Scene Graph, Resource Manager, Physics Integration
Keywords
C++, game engine, ECS, rendering, physics, collision detection, SDL2, Lua scripting, AABB