Creating a 2D Game Engine with C and SDL - Tomario Wars
Building a game engine from scratch teaches fundamental concepts about graphics programming, physics simulation, and network programming. This comprehensive guide details our journey creating Tomario Wars - a multiplayer 2D game built with C and SDL2 featuring custom physics, Entity Component System architecture, and real-time networking.
Why Build Your Own Game Engine?
While there are many game engines available, building your own teaches you:
Setting Up SDL
First, let's set up our development environment with SDL2:
#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdbool.h>
typedef struct {
    SDL_Window* window;
    SDL_Renderer* renderer;
    bool running;
    int screen_width;
    int screen_height;
} GameEngine;
GameEngine* engine_init(const char* title, int width, int height) {
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        printf("SDL initialization failed: %s\n", SDL_GetError());
        return NULL;
    }
    
    GameEngine* engine = malloc(sizeof(GameEngine));
    engine->screen_width = width;
    engine->screen_height = height;
    
    engine->window = SDL_CreateWindow(
        title,
        SDL_WINDOWPOS_CENTERED,
        SDL_WINDOWPOS_CENTERED,
        width, height,
        SDL_WINDOW_SHOWN
    );
    
    engine->renderer = SDL_CreateRenderer(
        engine->window, -1,
        SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC
    );
    
    engine->running = true;
    return engine;
}Core Game Loop
Every game engine needs a main loop that handles input, updates game state, and renders:
void engine_run(GameEngine* engine) {
    Uint32 last_time = SDL_GetTicks();
    const float target_fps = 60.0f;
    const float frame_time = 1000.0f / target_fps;
    
    while (engine->running) {
        Uint32 current_time = SDL_GetTicks();
        float delta_time = (current_time - last_time) / 1000.0f;
        
        handle_events(engine);
        update_game_state(delta_time);
        render_frame(engine);
        
        // Frame rate limiting
        Uint32 frame_duration = SDL_GetTicks() - current_time;
        if (frame_duration < frame_time) {
            SDL_Delay(frame_time - frame_duration);
        }
        
        last_time = current_time;
    }
}Entity Component System Architecture
For flexibility, let's implement an Entity Component System (ECS) that allows us to compose game objects from reusable components:
// Component types
typedef enum {
    COMPONENT_TRANSFORM,
    COMPONENT_PHYSICS,
    COMPONENT_SPRITE,
    COMPONENT_PLAYER,
    COMPONENT_PROJECTILE,
    COMPONENT_MAX
} ComponentType;
// Transform component for position and size
typedef struct {
    float x, y;
    float width, height;
    float rotation;
} TransformComponent;
// Physics component for movement
typedef struct {
    float velocity_x, velocity_y;
    float acceleration_x, acceleration_y;
    float mass;
    bool is_grounded;
} PhysicsComponent;
// Sprite component for rendering
typedef struct {
    SDL_Texture* texture;
    SDL_Rect source_rect;
    int current_frame;
    int total_frames;
    float animation_speed;
} SpriteComponent;
// Entity structure
typedef struct {
    uint32_t id;
    uint32_t component_mask;
    void* components[COMPONENT_MAX];
} Entity;
// Entity manager
typedef struct {
    Entity* entities;
    size_t entity_count;
    size_t max_entities;
    uint32_t next_entity_id;
} EntityManager;
// Create entity with components
Entity* create_player(EntityManager* manager, float x, float y) {
    Entity* player = add_entity(manager);
    
    // Add transform
    TransformComponent* transform = malloc(sizeof(TransformComponent));
    transform->x = x;
    transform->y = y;
    transform->width = 32;
    transform->height = 48;
    add_component(player, COMPONENT_TRANSFORM, transform);
    
    // Add physics
    PhysicsComponent* physics = malloc(sizeof(PhysicsComponent));
    physics->velocity_x = 0;
    physics->velocity_y = 0;
    physics->mass = 1.0f;
    physics->is_grounded = false;
    add_component(player, COMPONENT_PHYSICS, physics);
    
    return player;
}Advanced Physics System
Implement realistic physics for our tomato-throwing game:
#define GRAVITY 9.81f * 60.0f  // Scaled for pixels
#define DRAG_COEFFICIENT 0.99f
#define BOUNCE_DAMPING 0.7f
void physics_system_update(EntityManager* manager, float delta_time) {
    for (size_t i = 0; i < manager->entity_count; i++) {
        Entity* entity = &manager->entities[i];
        
        if (!has_component(entity, COMPONENT_PHYSICS | COMPONENT_TRANSFORM))
            continue;
            
        PhysicsComponent* physics = get_component(entity, COMPONENT_PHYSICS);
        TransformComponent* transform = get_component(entity, COMPONENT_TRANSFORM);
        
        // Apply gravity
        if (!physics->is_grounded) {
            physics->velocity_y += GRAVITY * delta_time;
        }
        
        // Apply drag
        physics->velocity_x *= DRAG_COEFFICIENT;
        physics->velocity_y *= DRAG_COEFFICIENT;
        
        // Update position
        transform->x += physics->velocity_x * delta_time;
        transform->y += physics->velocity_y * delta_time;
        
        // Ground collision
        if (transform->y + transform->height >= GROUND_Y) {
            transform->y = GROUND_Y - transform->height;
            physics->velocity_y *= -BOUNCE_DAMPING;
            
            if (fabs(physics->velocity_y) < 50.0f) {
                physics->velocity_y = 0;
                physics->is_grounded = true;
            }
        }
    }
}
// Projectile physics for tomato throwing
void throw_tomato(EntityManager* manager, float start_x, float start_y, 
                  float angle, float power) {
    Entity* tomato = add_entity(manager);
    
    TransformComponent* transform = malloc(sizeof(TransformComponent));
    transform->x = start_x;
    transform->y = start_y;
    transform->width = 16;
    transform->height = 16;
    add_component(tomato, COMPONENT_TRANSFORM, transform);
    
    PhysicsComponent* physics = malloc(sizeof(PhysicsComponent));
    physics->velocity_x = cos(angle) * power;
    physics->velocity_y = sin(angle) * power;
    physics->mass = 0.1f;
    add_component(tomato, COMPONENT_PHYSICS, physics);
    
    // Add projectile component for damage/effects
    ProjectileComponent* proj = malloc(sizeof(ProjectileComponent));
    proj->damage = 10;
    proj->owner_id = current_player_id;
    add_component(tomato, COMPONENT_PROJECTILE, proj);
}Collision Detection System
Implement efficient AABB collision detection:
typedef struct {
    float x, y;
    float width, height;
} AABB;
bool aabb_intersects(const AABB* a, const AABB* b) {
    return (a->x < b->x + b->width &&
            a->x + a->width > b->x &&
            a->y < b->y + b->height &&
            a->y + a->height > b->y);
}
void collision_system_update(EntityManager* manager) {
    // Spatial partitioning for optimization
    static GridCell grid[GRID_WIDTH][GRID_HEIGHT];
    
    // Clear grid
    memset(grid, 0, sizeof(grid));
    
    // Insert entities into grid
    for (size_t i = 0; i < manager->entity_count; i++) {
        Entity* entity = &manager->entities[i];
        if (has_component(entity, COMPONENT_TRANSFORM)) {
            insert_into_grid(grid, entity);
        }
    }
    
    // Check collisions within grid cells
    for (int x = 0; x < GRID_WIDTH; x++) {
        for (int y = 0; y < GRID_HEIGHT; y++) {
            check_cell_collisions(&grid[x][y]);
        }
    }
}
void handle_collision(Entity* a, Entity* b) {
    // Projectile hits player
    if (has_component(a, COMPONENT_PROJECTILE) && 
        has_component(b, COMPONENT_PLAYER)) {
        
        ProjectileComponent* proj = get_component(a, COMPONENT_PROJECTILE);
        PlayerComponent* player = get_component(b, COMPONENT_PLAYER);
        
        player->health -= proj->damage;
        create_splat_effect(a->transform->x, a->transform->y);
        remove_entity(a);
    }
}Multiplayer Networking
Implement UDP networking for real-time multiplayer:
#include <SDL2/SDL_net.h>
typedef struct {
    uint8_t type;
    uint16_t sequence;
    uint32_t timestamp;
} PacketHeader;
typedef struct {
    PacketHeader header;
    uint32_t player_id;
    float x, y;
    float velocity_x, velocity_y;
    uint8_t state;
} PlayerUpdatePacket;
typedef struct {
    UDPsocket socket;
    IPaddress server_address;
    uint16_t next_sequence;
    PlayerUpdatePacket last_updates[MAX_PLAYERS];
} NetworkManager;
void network_send_player_update(NetworkManager* net, Player* player) {
    PlayerUpdatePacket packet;
    packet.header.type = PACKET_PLAYER_UPDATE;
    packet.header.sequence = net->next_sequence++;
    packet.header.timestamp = SDL_GetTicks();
    
    packet.player_id = player->id;
    packet.x = player->transform->x;
    packet.y = player->transform->y;
    packet.velocity_x = player->physics->velocity_x;
    packet.velocity_y = player->physics->velocity_y;
    packet.state = player->state;
    
    UDPpacket* udp_packet = SDLNet_AllocPacket(sizeof(PlayerUpdatePacket));
    memcpy(udp_packet->data, &packet, sizeof(PlayerUpdatePacket));
    udp_packet->len = sizeof(PlayerUpdatePacket);
    udp_packet->address = net->server_address;
    
    SDLNet_UDP_Send(net->socket, -1, udp_packet);
    SDLNet_FreePacket(udp_packet);
}
// Client-side prediction and interpolation
void interpolate_remote_players(NetworkManager* net, float alpha) {
    for (int i = 0; i < MAX_PLAYERS; i++) {
        if (net->last_updates[i].player_id == 0) continue;
        
        PlayerUpdatePacket* update = &net->last_updates[i];
        Entity* player = find_player_by_id(update->player_id);
        
        if (player) {
            TransformComponent* transform = get_component(player, COMPONENT_TRANSFORM);
            
            // Interpolate position
            float target_x = update->x + update->velocity_x * alpha;
            float target_y = update->y + update->velocity_y * alpha;
            
            transform->x = lerp(transform->x, target_x, 0.2f);
            transform->y = lerp(transform->y, target_y, 0.2f);
        }
    }
}Rendering Pipeline
Efficient rendering with sprite batching:
void render_system(GameEngine* engine, EntityManager* manager) {
    SDL_SetRenderDrawColor(engine->renderer, 135, 206, 235, 255); // Sky blue
    SDL_RenderClear(engine->renderer);
    
    // Sort entities by Y position for depth
    qsort(manager->entities, manager->entity_count, 
          sizeof(Entity), compare_entity_depth);
    
    // Render all sprites
    for (size_t i = 0; i < manager->entity_count; i++) {
        Entity* entity = &manager->entities[i];
        
        if (!has_component(entity, COMPONENT_SPRITE | COMPONENT_TRANSFORM))
            continue;
            
        SpriteComponent* sprite = get_component(entity, COMPONENT_SPRITE);
        TransformComponent* transform = get_component(entity, COMPONENT_TRANSFORM);
        
        SDL_Rect dest = {
            (int)transform->x,
            (int)transform->y,
            (int)transform->width,
            (int)transform->height
        };
        
        SDL_RenderCopyEx(engine->renderer, sprite->texture, 
                        &sprite->source_rect, &dest,
                        transform->rotation, NULL, SDL_FLIP_NONE);
    }
    
    SDL_RenderPresent(engine->renderer);
}Performance Optimization
Key optimizations for smooth 60 FPS gameplay:
// Custom memory pool for entities
typedef struct {
    void* memory;
    size_t block_size;
    size_t block_count;
    uint32_t* free_list;
    size_t free_count;
} MemoryPool;
MemoryPool* create_pool(size_t block_size, size_t block_count) {
    MemoryPool* pool = malloc(sizeof(MemoryPool));
    pool->block_size = block_size;
    pool->block_count = block_count;
    pool->memory = malloc(block_size * block_count);
    pool->free_list = malloc(sizeof(uint32_t) * block_count);
    
    // Initialize free list
    for (size_t i = 0; i < block_count; i++) {
        pool->free_list[i] = i;
    }
    pool->free_count = block_count;
    
    return pool;
}Conclusion
Building a game engine from scratch provides invaluable experience in:
The complete Tomario Wars engine demonstrates these concepts in action, creating a fun multiplayer experience while teaching fundamental game development principles. The modular architecture allows easy extension with new features like power-ups, different projectile types, or environmental hazards.
Key takeaways:
About the Author
The Nordic Oculus team brings together skills in modern software development, cloud architecture, and emerging technologies.
Learn more about our team