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