From Scratch: Building a Lightweight Game Engine Core with C++

A game engine serves as a bridge between game creativity and hardware performance. Although there are many mature game engines available on the market, understanding and implementing a lightweight game engine core remains a dream for many game developers and C++ enthusiasts. This process not only hones low-level programming skills but also helps clarify the key elements of game architecture design.

This article focuses on implementing a concise yet functional game engine skeleton in C++, covering scene management, component systems, and a basic rendering loop. Code examples will be written in modern C++ to balance clarity and practicality.

Why Build a Lightweight Engine Yourself?

Writing a game engine may sound complex, but when broken down into modules, the core is not difficult:

  • Understanding the design of the game loop
  • Experiencing the flexibility of the Entity-Component System (ECS)
  • Mastering the basic concepts of resource management and event systems

Moreover, implementing it yourself helps to understand the principles behind mainstream engines rather than simply using a black box.

1. Basics of the Game Loop

The core of a game engine is the “game loop,” which determines the rhythm of game state updates and rendering. A typical game loop looks like this:

while(running) {
    processInput();
    update(deltaTime);
    render();
}

Here, <span>deltaTime</span> is the time difference between two frames, used to maintain time consistency in game logic.

Example implementation:

#include <chrono>
#include <thread>
#include <iostream>

class Game {
public:
    void run() {
        using clock = std::chrono::high_resolution_clock;
        auto lastTime = clock::now();

        while (running) {
            auto currentTime = clock::now();
            std::chrono::duration<float> delta = currentTime - lastTime;
            lastTime = currentTime;

            processInput();
            update(delta.count());
            render();

            std::this_thread::sleep_for(std::chrono::milliseconds(16)); // Limit frame rate to about 60FPS
        }
    }

    void processInput() {
        // Handle input, to be expanded later
    }

    void update(float deltaTime) {
        // Update game state
        std::cout << "Updating game state with deltaTime = " << deltaTime << " seconds.\n";
    }

    void render() {
        // Draw the current frame
        std::cout << "Rendering frame...\n";
    }

    void stop() { running = false; }

private:
    bool running = true;
};

int main() {
    Game game;
    game.run();
    return 0;
}

This code, though simple, is the “heartbeat” of the game engine.

2. Introduction to the Entity-Component System (ECS)

The traditional inheritance system can lead to code bloat and coupling, while modern game engines often adopt ECS design: an Entity is an identifier for specific objects in the game, Components contain data, and Systems handle logic.

Simple Entity and Component Design

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

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

class PositionComponent : public Component {
public:
    float x, y;
    PositionComponent(float x_, float y_) : x(x_), y(y_) {}
};

class VelocityComponent : public Component {
public:
    float vx, vy;
    VelocityComponent(float vx_, float vy_) : vx(vx_), vy(vy_) {}
};

class Entity {
public:
    template<typename T, typename... Args>
    void addComponent(Args&&... args) {
        components[typeid(T)] = std::make_shared<T>(std::forward<Args>(args)...);
    }

    template<typename T>
    std::shared_ptr<T> getComponent() {
        auto it = components.find(typeid(T));
        if (it != components.end())
            return std::static_pointer_cast<T>(it->second);
        return nullptr;
    }

private:
    std::unordered_map<std::type_index, std::shared_ptr<Component>> components;
};

This design allows us to flexibly add various components without writing dedicated classes for each entity.

System Example: Movement System

#include <vector>

class MovementSystem {
public:
    void update(std::vector<Entity>& entities, float deltaTime) {
        for (auto& entity : entities) {
            auto pos = entity.getComponent<PositionComponent>();
            auto vel = entity.getComponent<VelocityComponent>();
            if (pos && vel) {
                pos->x += vel->vx * deltaTime;
                pos->y += vel->vy * deltaTime;
            }
        }
    }
};

By operating on components through systems, business logic and data storage are separated, resulting in a clearer code structure.

3. Basic Rendering Loop

Here we assume no complex graphics libraries are used, and we demonstrate “rendering” with text:

void renderEntities(const std::vector<Entity>& entities) {
    for (const auto& entity : entities) {
        auto pos = entity.getComponent<PositionComponent>();
        if (pos) {
            std::cout << "Entity at (" << pos->x << ", " << pos->y << ")\n";
        }
    }
}

In actual projects, this can be integrated with OpenGL, Vulkan, or DirectX for graphical rendering.

4. Integration Example: A Simple Game World

int main() {
    Game game;

    std::vector<Entity> entities;

    Entity player;
    player.addComponent<PositionComponent>(0.0f, 0.0f);
    player.addComponent<VelocityComponent>(1.0f, 1.5f);
    entities.push_back(player);

    MovementSystem movementSystem;

    using clock = std::chrono::high_resolution_clock;
    auto lastTime = clock::now();

    for (int frame = 0; frame < 5; ++frame) {
        auto currentTime = clock::now();
        std::chrono::duration<float> delta = currentTime - lastTime;
        lastTime = currentTime;

        movementSystem.update(entities, delta.count());

        renderEntities(entities);

        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }

    return 0;
}

This code constructs a simple world containing entities, moving them each frame and printing their positions.

5. Future Directions and Considerations

The core of a lightweight game engine can be continuously expanded:

  • Add an event system to support event-driven architecture
  • A resource management module to unify loading and caching of textures and models
  • Script support to enhance extensibility
  • Multithreading optimizations to improve performance

Understanding and practicing these fundamentals will help build an engine framework that meets your project needs.

Building a lightweight game engine core focuses on clarifying the game loop, entity-component system, and rendering process. Using modern C++ features, writing a concise and flexible architecture is entirely feasible. Implementing it not only improves programming skills but also deepens understanding of the entire game development process.

I hope this example helps you explore game engines. The code does not need to be complex, but it should clearly convey the core ideas.

Recommended Reading:

C++ Direct Access to Major Companies

Code reviews are not just professional; they help students discover blind spots and fill gaps

What you lack is not just interview opportunities, but the ability to anticipate in advance

Leave a Comment