Tillbaka till bloggen

Creating a 2D Game Engine with C and SDL - Tomario Wars

Teknisk genomgång
Nordic Oculus Team
CSDLGame DevelopmentGraphicsNetworking

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:

  • Low-level graphics programming
  • Memory management in C
  • Game loop architecture
  • Physics and collision detection
  • Network programming for multiplayer
  • Setting Up SDL

    First, let's set up our development environment with SDL2:

    c
    #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:

    c
    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:

    c
    // 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:

    c
    #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:

    c
    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:

    c
    #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:

    c
    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:

  • **Memory Management**: Custom memory pools for frequent allocations
  • **Spatial Partitioning**: Grid-based collision detection
  • **Component Arrays**: Cache-friendly data layout
  • **Fixed Timestep**: Consistent physics simulation
  • c
    // 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:

  • Systems programming and memory management
  • Real-time physics simulation
  • Network programming for multiplayer games
  • Performance optimization techniques
  • Software architecture patterns (ECS)
  • 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:

  • ECS provides flexibility and performance
  • Fixed timestep ensures consistent physics
  • UDP networking enables real-time multiplayer
  • Spatial partitioning optimizes collision detection
  • Memory pools reduce allocation overhead
  • About the Author

    The Nordic Oculus team brings together skills in modern software development, cloud architecture, and emerging technologies.

    Learn more about our team