The Complete Guide to Graphics and Games Programming with SDL in C
This article covers The Complete Guide to Graphics and Games Programming with SDL in C. The Complete Guide to Graphics and Games Programming with SDL in C. A Comprehensive Journey from Pixels to Professional Game Development.
Table of Contents
- Introduction and Philosophy
- Setting Up Your Development Environment
- Understanding Graphics Fundamentals
- SDL Basics and Architecture
- Pixels, Buffers, and Memory Management
- The Game Loop - Heart of Every Game
- 2D Rendering Techniques
- Input Handling and User Interaction
- Timing and Frame Rate Control
- Sprites and Texture Management
- Collision Detection Systems
- Particle Systems and Effects
- Audio Programming
- Advanced 2D Techniques
- Introduction to 3D Graphics
- Mathematical Foundations
- Performance Optimization
- Software Architecture for Games
- Complete Game Projects
- Best Practices and Professional Development
Chapter 1: Introduction and Philosophy
1.1 Welcome to Graphics Programming
Graphics programming is one of the most rewarding disciplines in computer science. You'll learn to bring pixels to life, create interactive experiences, and build the visual systems that power modern games and applications. This journey will transform you from someone who uses software to someone who creates the very foundation of interactive visual experiences.
Graphics programming sits at the intersection of mathematics, art, computer science, and engineering. It requires understanding how computers represent and manipulate visual information, how to efficiently process millions of pixels per second, and how to architect systems that are both performant and maintainable.
1.2 Why SDL and C?
We're using SDL (Simple DirectMedia Layer) with C for several compelling reasons:
SDL provides:
- Cross-platform compatibility (Windows, Linux, macOS, mobile)
- Direct hardware access without unnecessary abstractions
- Complete control over the rendering pipeline
- Industry-proven stability and performance
- Active community and extensive documentation
C provides:
- Minimal overhead between your code and the hardware
- Manual memory management (teaching you resource control)
- Direct understanding of how computers work at a low level
- Foundation for understanding higher-level languages
- Performance critical for real-time graphics
Many modern game engines use C++ or other languages, but C teaches you the fundamentals without the complexity of object-oriented programming getting in the way. Once you master C and SDL, transitioning to C++, OpenGL, Vulkan, or commercial engines becomes straightforward.
1.3 Learning Philosophy
This guide follows a specific teaching philosophy:
1. Build Understanding from First Principles We won't just show you how to use SDL functions; we'll explain why they exist and what problems they solve. You'll understand the reasoning behind every design decision.
2. Learn by Doing Every concept comes with complete, runnable code examples. You'll build real projects, not just read about them.
3. Embrace Complexity Gradually We start simple and add complexity piece by piece. Each chapter builds on previous knowledge without overwhelming you.
4. Understand Trade-offs Programming is about making informed decisions. We'll discuss why certain approaches work better in different scenarios.
5. Professional Code from Day One You'll learn to write clean, maintainable, well-documented code. Good habits formed early last a lifetime.
1.4 Prerequisites
You should be comfortable with:
- Basic C programming (variables, functions, pointers, structs)
- Compiling and running C programs
- Basic command line usage
- Elementary mathematics (algebra, basic trigonometry)
If you're rusty on C, don't worry - we'll review important concepts as we encounter them.
1.5 What You'll Build
Throughout this guide, you'll create:
- A complete pixel manipulation system
- Multiple 2D rendering engines
- Particle systems and visual effects
- A collision detection library
- Several complete games (Pong, Space Shooter, Platformer)
- Performance profiling tools
- Your own mini game engine
By the end, you'll have a portfolio of projects and the knowledge to create professional-quality 2D games.
Chapter 2: Setting Up Your Development Environment
2.1 Installing SDL2
SDL2 is the current stable version of SDL. Installation varies by platform.
Linux (Ubuntu/Debian):
sudo apt-get update
sudo apt-get install libsdl2-dev
sudo apt-get install libsdl2-image-dev
sudo apt-get install libsdl2-ttf-dev
sudo apt-get install libsdl2-mixer-dev
macOS (using Homebrew):
brew install sdl2
brew install sdl2_image
brew install sdl2_ttf
brew install sdl2_mixer
Windows: Download development libraries from https://www.libsdl.org/download-2.0.php
2.2 Project Structure
A well-organized project structure is crucial:
my_game/
├── src/
│ ├── main.c
│ ├── game.c
│ ├── renderer.c
│ ├── input.c
│ └── ...
├── include/
│ ├── game.h
│ ├── renderer.h
│ ├── input.h
│ └── ...
├── assets/
│ ├── sprites/
│ ├── sounds/
│ └── fonts/
├── build/
├── Makefile
└── README.md
2.3 Your First SDL Program
Let's create the classic "Hello SDL" program that opens a window:
// hello_sdl.c
#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdbool.h>
#define WINDOW_WIDTH 800
#define WINDOW_HEIGHT 600
int main(int argc, char* argv[]) {
// Initialize SDL
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
printf("SDL could not initialize! SDL_Error: %s\n", SDL_GetError());
return 1;
}
// Create window
SDL_Window* window = SDL_CreateWindow(
"My First SDL Program",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
WINDOW_WIDTH,
WINDOW_HEIGHT,
SDL_WINDOW_SHOWN
);
if (window == NULL) {
printf("Window could not be created! SDL_Error: %s\n", SDL_GetError());
SDL_Quit();
return 1;
}
// Get window surface
SDL_Surface* screenSurface = SDL_GetWindowSurface(window);
// Fill the surface with a color (RGB: 64, 128, 255)
SDL_FillRect(screenSurface, NULL,
SDL_MapRGB(screenSurface->format, 64, 128, 255));
// Update the surface
SDL_UpdateWindowSurface(window);
// Wait for 3 seconds
SDL_Delay(3000);
// Cleanup
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
Compile and run:
gcc hello_sdl.c -o hello_sdl -lSDL2
./hello_sdl
2.4 Understanding the Initialization Pattern
Every SDL program follows a pattern:
- Initialize SDL subsystems
- Create resources (windows, renderers)
- Main loop (game logic)
- Cleanup resources
- Quit SDL
This pattern ensures proper resource management and prevents memory leaks.
2.5 Makefile for Efficient Building
A Makefile automates compilation:
# Makefile
CC = gcc
CFLAGS = -Wall -Wextra -std=c99 -O2
LIBS = -lSDL2 -lSDL2_image -lSDL2_ttf -lSDL2_mixer -lm
INCLUDES = -Iinclude
SRC_DIR = src
BUILD_DIR = build
SOURCES = $(wildcard $(SRC_DIR)/*.c)
OBJECTS = $(SOURCES:$(SRC_DIR)/%.c=$(BUILD_DIR)/%.o)
TARGET = game
all: $(TARGET)
$(TARGET): $(OBJECTS)
$(CC) $(OBJECTS) -o $@ $(LIBS)
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(INCLUDES) -c {{CONTENT}}lt; -o $@
clean:
rm -rf $(BUILD_DIR) $(TARGET)
run: $(TARGET)
./$(TARGET)
.PHONY: all clean run
Now you can build with:
make # Compile
make run # Compile and run
make clean # Remove build files
Chapter 3: Understanding Graphics Fundamentals
3.1 What is a Pixel?
A pixel (picture element) is the smallest controllable element of a display. Modern displays contain millions of pixels arranged in a grid. Each pixel has a color composed of Red, Green, and Blue (RGB) components.
RGB Color Model:
- Each component ranges from 0-255 (8 bits per channel)
- (0, 0, 0) = Black
- (255, 255, 255) = White
- (255, 0, 0) = Red
- (0, 255, 0) = Green
- (0, 0, 255) = Blue
RGBA includes Alpha (transparency):
- Alpha = 0: Fully transparent
- Alpha = 255: Fully opaque
3.2 Coordinate Systems
Graphics use a 2D coordinate system:
(0,0)--------> X
|
|
|
v
Y
Top-left origin is standard in computer graphics
X increases rightward
Y increases downward
This differs from mathematical conventions where Y increases upward. Understanding this is crucial for correct positioning.
3.3 Frame Buffer Concept
A frame buffer is a region of memory that holds the pixel data for one complete frame. When you "draw" something, you're modifying values in this buffer. The display hardware reads this buffer to show images on screen.
Double Buffering: To prevent flickering, we use two buffers:
- Front buffer: Currently displayed
- Back buffer: Being drawn to
We draw to the back buffer, then swap buffers when the frame is complete.
3.4 Memory Layout of Pixels
Understanding memory layout is critical for performance:
// A simple pixel buffer
typedef struct {
unsigned char r, g, b, a;
} Pixel;
// Screen buffer: 800x600 = 480,000 pixels
Pixel buffer[600][800];
// Accessing pixel at (x, y)
Pixel pixel = buffer[y][x];
// Memory layout is row-major:
// [Row 0: Pixel 0, Pixel 1, ..., Pixel 799]
// [Row 1: Pixel 0, Pixel 1, ..., Pixel 799]
// ...
3.5 Practical Example: Manual Pixel Manipulation
Let's create a program that directly manipulates pixels:
// pixel_manipulation.c
#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>
#define WIDTH 800
#define HEIGHT 600
// Set a pixel in the surface
void setPixel(SDL_Surface* surface, int x, int y, Uint32 color) {
if (x < 0 || x >= surface->w || y < 0 || y >= surface->h) {
return; // Bounds checking
}
// Lock surface if needed
if (SDL_MUSTLOCK(surface)) {
SDL_LockSurface(surface);
}
// Calculate pixel position in memory
Uint32* pixels = (Uint32*)surface->pixels;
pixels[y * surface->w + x] = color;
// Unlock surface
if (SDL_MUSTLOCK(surface)) {
SDL_UnlockSurface(surface);
}
}
// Get color from surface
Uint32 getPixel(SDL_Surface* surface, int x, int y) {
if (x < 0 || x >= surface->w || y < 0 || y >= surface->h) {
return 0;
}
Uint32* pixels = (Uint32*)surface->pixels;
return pixels[y * surface->w + x];
}
// Draw a line using Bresenham's algorithm
void drawLine(SDL_Surface* surface, int x0, int y0, int x1, int y1, Uint32 color) {
int dx = abs(x1 - x0);
int dy = abs(y1 - y0);
int sx = (x0 < x1) ? 1 : -1;
int sy = (y0 < y1) ? 1 : -1;
int err = dx - dy;
while (true) {
setPixel(surface, x0, y0, color);
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
}
// Draw a circle using midpoint algorithm
void drawCircle(SDL_Surface* surface, int cx, int cy, int radius, Uint32 color) {
int x = radius;
int y = 0;
int radiusError = 1 - x;
while (x >= y) {
// Draw 8 octants
setPixel(surface, cx + x, cy + y, color);
setPixel(surface, cx + y, cy + x, color);
setPixel(surface, cx - x, cy + y, color);
setPixel(surface, cx - y, cy + x, color);
setPixel(surface, cx - x, cy - y, color);
setPixel(surface, cx - y, cy - x, color);
setPixel(surface, cx + x, cy - y, color);
setPixel(surface, cx + y, cy - x, color);
y++;
if (radiusError < 0) {
radiusError += 2 * y + 1;
} else {
x--;
radiusError += 2 * (y - x + 1);
}
}
}
// Fill a circle
void fillCircle(SDL_Surface* surface, int cx, int cy, int radius, Uint32 color) {
for (int y = -radius; y <= radius; y++) {
for (int x = -radius; x <= radius; x++) {
if (x*x + y*y <= radius*radius) {
setPixel(surface, cx + x, cy + y, color);
}
}
}
}
int main(int argc, char* argv[]) {
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
printf("SDL Init Error: %s\n", SDL_GetError());
return 1;
}
SDL_Window* window = SDL_CreateWindow(
"Pixel Manipulation Demo",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
WIDTH, HEIGHT,
SDL_WINDOW_SHOWN
);
if (!window) {
printf("Window Error: %s\n", SDL_GetError());
SDL_Quit();
return 1;
}
SDL_Surface* surface = SDL_GetWindowSurface(window);
// Clear to black
SDL_FillRect(surface, NULL, SDL_MapRGB(surface->format, 0, 0, 0));
// Create colors
Uint32 white = SDL_MapRGB(surface->format, 255, 255, 255);
Uint32 red = SDL_MapRGB(surface->format, 255, 0, 0);
Uint32 green = SDL_MapRGB(surface->format, 0, 255, 0);
Uint32 blue = SDL_MapRGB(surface->format, 0, 0, 255);
Uint32 yellow = SDL_MapRGB(surface->format, 255, 255, 0);
// Draw some lines
drawLine(surface, 100, 100, 700, 100, white);
drawLine(surface, 100, 100, 100, 500, white);
drawLine(surface, 100, 500, 700, 500, white);
drawLine(surface, 700, 100, 700, 500, white);
// Draw circles
drawCircle(surface, 200, 200, 50, red);
fillCircle(surface, 400, 200, 50, green);
drawCircle(surface, 600, 200, 50, blue);
// Draw a pattern
for (int i = 0; i < 50; i++) {
drawLine(surface, 100, 300 + i*2, 700, 300 + i*2, yellow);
}
SDL_UpdateWindowSurface(window);
// Event loop
bool running = true;
SDL_Event event;
while (running) {
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
running = false;
}
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_ESCAPE) {
running = false;
}
}
SDL_Delay(16); // ~60 FPS
}
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
This example demonstrates:
- Direct pixel manipulation
- Bresenham's line algorithm (efficient line drawing)
- Midpoint circle algorithm
- Memory access patterns
- Basic event handling
Chapter 4: SDL Basics and Architecture
4.1 SDL Subsystems
SDL is organized into subsystems that can be initialized independently:
- SDL_INIT_VIDEO: Video and windowing
- SDL_INIT_AUDIO: Audio playback and recording
- SDL_INIT_TIMER: High-precision timers
- SDL_INIT_JOYSTICK: Joystick input
- SDL_INIT_GAMECONTROLLER: Game controller support
- SDL_INIT_EVENTS: Event handling
- SDL_INIT_EVERYTHING: Initialize all subsystems
// Initialize multiple subsystems
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER) < 0) {
printf("SDL Init Error: %s\n", SDL_GetError());
return 1;
}
4.2 Window vs Surface vs Renderer
SDL provides two rendering paths:
1. Surface-based (immediate mode):
SDL_Window* window = SDL_CreateWindow(...);
SDL_Surface* surface = SDL_GetWindowSurface(window);
// Draw to surface
SDL_UpdateWindowSurface(window);
2. Renderer-based (hardware-accelerated):
SDL_Window* window = SDL_CreateWindow(...);
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
// Draw with renderer
SDL_RenderPresent(renderer);
When to use each:
- Surfaces: Simple programs, direct pixel access, software rendering
- Renderers: Performance-critical applications, hardware acceleration, scaling
For games, use the renderer - it's much faster.
4.3 Complete Renderer Example
// renderer_basic.c
#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdbool.h>
#define WIDTH 800
#define HEIGHT 600
typedef struct {
SDL_Window* window;
SDL_Renderer* renderer;
bool running;
} Application;
bool initApplication(Application* app) {
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
printf("SDL Error: %s\n", SDL_GetError());
return false;
}
app->window = SDL_CreateWindow(
"Renderer Example",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
WIDTH, HEIGHT,
SDL_WINDOW_SHOWN
);
if (!app->window) {
printf("Window Error: %s\n", SDL_GetError());
return false;
}
// Create renderer with hardware acceleration and VSync
app->renderer = SDL_CreateRenderer(
app->window,
-1,
SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC
);
if (!app->renderer) {
printf("Renderer Error: %s\n", SDL_GetError());
return false;
}
// Set blend mode for transparency
SDL_SetRenderDrawBlendMode(app->renderer, SDL_BLENDMODE_BLEND);
app->running = true;
return true;
}
void handleEvents(Application* app) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_QUIT:
app->running = false;
break;
case SDL_KEYDOWN:
if (event.key.keysym.sym == SDLK_ESCAPE) {
app->running = false;
}
break;
}
}
}
void render(Application* app) {
// Clear screen (dark blue)
SDL_SetRenderDrawColor(app->renderer, 20, 30, 50, 255);
SDL_RenderClear(app->renderer);
// Draw filled rectangle
SDL_SetRenderDrawColor(app->renderer, 255, 100, 100, 255);
SDL_Rect rect1 = {100, 100, 200, 150};
SDL_RenderFillRect(app->renderer, &rect1);
// Draw outlined rectangle
SDL_SetRenderDrawColor(app->renderer, 100, 255, 100, 255);
SDL_Rect rect2 = {400, 100, 200, 150};
SDL_RenderDrawRect(app->renderer, &rect2);
// Draw line
SDL_SetRenderDrawColor(app->renderer, 255, 255, 100, 255);
SDL_RenderDrawLine(app->renderer, 100, 300, 700, 300);
// Draw points
SDL_SetRenderDrawColor(app->renderer, 255, 255, 255, 255);
for (int i = 0; i < 100; i++) {
SDL_RenderDrawPoint(app->renderer,
100 + i * 6,
400 + (int)(30 * sin(i * 0.1)));
}
// Present the rendered frame
SDL_RenderPresent(app->renderer);
}
void cleanup(Application* app) {
if (app->renderer) SDL_DestroyRenderer(app->renderer);
if (app->window) SDL_DestroyWindow(app->window);
SDL_Quit();
}
int main(int argc, char* argv[]) {
Application app = {0};
if (!initApplication(&app)) {
return 1;
}
while (app.running) {
handleEvents(&app);
render(&app);
}
cleanup(&app);
return 0;
}
4.4 Understanding SDL_Rect
SDL_Rect is fundamental to SDL rendering:
typedef struct {
int x, y; // Top-left corner
int w, h; // Width and height
} SDL_Rect;
// Creating rectangles
SDL_Rect rect = {100, 100, 50, 75}; // x, y, w, h
SDL_Rect rect2 = {.x = 100, .y = 100, .w = 50, .h = 75}; // Named initialization
// Checking collision
bool SDL_HasIntersection(const SDL_Rect* A, const SDL_Rect* B);
// Getting intersection
bool SDL_IntersectRect(const SDL_Rect* A, const SDL_Rect* B, SDL_Rect* result);
// Checking if point is in rect
SDL_bool SDL_PointInRect(const SDL_Point* p, const SDL_Rect* r);
4.5 Color Management
// SDL_Color structure
typedef struct {
Uint8 r, g, b, a;
} SDL_Color;
// Common color definitions
const SDL_Color COLOR_BLACK = {0, 0, 0, 255};
const SDL_Color COLOR_WHITE = {255, 255, 255, 255};
const SDL_Color COLOR_RED = {255, 0, 0, 255};
const SDL_Color COLOR_GREEN = {0, 255, 0, 255};
const SDL_Color COLOR_BLUE = {0, 0, 255, 255};
// Setting renderer draw color
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
// Interpolating colors (for effects)
SDL_Color lerpColor(SDL_Color a, SDL_Color b, float t) {
SDL_Color result;
result.r = (Uint8)(a.r + (b.r - a.r) * t);
result.g = (Uint8)(a.g + (b.g - a.g) * t);
result.b = (Uint8)(a.b + (b.b - a.b) * t);
result.a = (Uint8)(a.a + (b.a - a.a) * t);
return result;
}
Chapter 5: Pixels, Buffers, and Memory Management
5.1 Understanding SDL_Surface
An SDL_Surface represents a rectangular area of pixels in memory:
typedef struct SDL_Surface {
Uint32 flags; // Surface flags
SDL_PixelFormat* format; // Pixel format
int w, h; // Width and height
int pitch; // Length of a row of pixels in bytes
void* pixels; // Actual pixel data
// ... other fields
} SDL_Surface;
Key concepts:
- Pitch: The number of bytes in a row (may include padding)
- Format: Describes how colors are stored (RGB, RGBA, etc.)
- Pixels: Raw memory containing pixel data
5.2 Creating and Managing Surfaces
// surface_management.c
#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdlib.h>
// Create a blank surface
SDL_Surface* createBlankSurface(int width, int height) {
SDL_Surface* surface = SDL_CreateRGBSurface(
0, // Flags (obsolete, use 0)
width, height, // Dimensions
32, // Bits per pixel
0x00FF0000, // Red mask
0x0000FF00, // Green mask
0x000000FF, // Blue mask
0xFF000000 // Alpha mask
);
if (!surface) {
printf("Surface creation failed: %s\n", SDL_GetError());
}
return surface;
}
// Fill surface with gradient
void fillGradient(SDL_Surface* surface, SDL_Color startColor, SDL_Color endColor) {
if (SDL_MUSTLOCK(surface)) {
SDL_LockSurface(surface);
}
Uint32* pixels = (Uint32*)surface->pixels;
for (int y = 0; y < surface->h; y++) {
float t = (float)y / surface->h;
// Interpolate color
Uint8 r = startColor.r + (endColor.r - startColor.r) * t;
Uint8 g = startColor.g + (endColor.g - startColor.g) * t;
Uint8 b = startColor.b + (endColor.b - startColor.b) * t;
Uint32 color = SDL_MapRGB(surface->format, r, g, b);
for (int x = 0; x < surface->w; x++) {
pixels[y * surface->w + x] = color;
}
}
if (SDL_MUSTLOCK(surface)) {
SDL_UnlockSurface(surface);
}
}
// Copy one surface to another with alpha blending
void blitWithAlpha(SDL_Surface* src, SDL_Surface* dst, int dx, int dy, Uint8 alpha) {
SDL_SetSurfaceAlphaMod(src, alpha);
SDL_Rect destRect = {dx, dy, src->w, src->h};
SDL_BlitSurface(src, NULL, dst, &destRect);
}
// Scale surface (simple nearest-neighbor)
SDL_Surface* scaleSurface(SDL_Surface* src, int newWidth, int newHeight) {
SDL_Surface* scaled = createBlankSurface(newWidth, newHeight);
if (!scaled) return NULL;
if (SDL_MUSTLOCK(src)) SDL_LockSurface(src);
if (SDL_MUSTLOCK(scaled)) SDL_LockSurface(scaled);
Uint32* srcPixels = (Uint32*)src->pixels;
Uint32* dstPixels = (Uint32*)scaled->pixels;
float xRatio = (float)src->w / newWidth;
float yRatio = (float)src->h / newHeight;
for (int y = 0; y < newHeight; y++) {
for (int x = 0; x < newWidth; x++) {
int srcX = (int)(x * xRatio);
int srcY = (int)(y * yRatio);
dstPixels[y * newWidth + x] = srcPixels[srcY * src->w + srcX];
}
}
if (SDL_MUSTLOCK(src)) SDL_UnlockSurface(src);
if (SDL_MUSTLOCK(scaled)) SDL_UnlockSurface(scaled);
return scaled;
}
5.3 Texture vs Surface
Surfaces:
- CPU memory
- Direct pixel access
- Slower for rendering
- Good for manipulation
Textures:
- GPU memory
- No direct pixel access
- Fast rendering
- Good for display
// Converting surface to texture
SDL_Texture* textureFromSurface(SDL_Renderer* renderer, SDL_Surface* surface) {
SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
if (!texture) {
printf("Texture creation failed: %s\n", SDL_GetError());
}
return texture;
}
// Render texture
void renderTexture(SDL_Renderer* renderer, SDL_Texture* texture, int x, int y) {
SDL_Rect dest;
SDL_QueryTexture(texture, NULL, NULL, &dest.w, &dest.h);
dest.x = x;
dest.y = y;
SDL_RenderCopy(renderer, texture, NULL, &dest);
}
5.4 Memory Management Best Practices
// Resource manager for textures
#define MAX_TEXTURES 100
typedef struct {
SDL_Texture* texture;
char* name;
int refCount;
} TextureEntry;
typedef struct {
TextureEntry textures[MAX_TEXTURES];
int count;
SDL_Renderer* renderer;
} TextureManager;
void initTextureManager(TextureManager* tm, SDL_Renderer* renderer) {
tm->count = 0;
tm->renderer = renderer;
memset(tm->textures, 0, sizeof(tm->textures));
}
// Load texture with reference counting
SDL_Texture* loadTextureManaged(TextureManager* tm, const char* filename) {
// Check if already loaded
for (int i = 0; i < tm->count; i++) {
if (strcmp(tm->textures[i].name, filename) == 0) {
tm->textures[i].refCount++;
return tm->textures[i].texture;
}
}
// Load new texture
if (tm->count >= MAX_TEXTURES) {
printf("Texture manager full!\n");
return NULL;
}
SDL_Surface* surface = SDL_LoadBMP(filename);
if (!surface) {
printf("Failed to load %s: %s\n", filename, SDL_GetError());
return NULL;
}
SDL_Texture* texture = SDL_CreateTextureFromSurface(tm->renderer, surface);
SDL_FreeSurface(surface);
if (!texture) {
printf("Failed to create texture: %s\n", SDL_GetError());
return NULL;
}
// Add to manager
int idx = tm->count++;
tm->textures[idx].texture = texture;
tm->textures[idx].name = strdup(filename);
tm->textures[idx].refCount = 1;
return texture;
}
// Release texture
void releaseTexture(TextureManager* tm, SDL_Texture* texture) {
for (int i = 0; i < tm->count; i++) {
if (tm->textures[i].texture == texture) {
tm->textures[i].refCount--;
if (tm->textures[i].refCount <= 0) {
SDL_DestroyTexture(tm->textures[i].texture);
free(tm->textures[i].name);
// Shift remaining textures
for (int j = i; j < tm->count - 1; j++) {
tm->textures[j] = tm->textures[j + 1];
}
tm->count--;
}
return;
}
}
}
void destroyTextureManager(TextureManager* tm) {
for (int i = 0; i < tm->count; i++) {
SDL_DestroyTexture(tm->textures[i].texture);
free(tm->textures[i].name);
}
tm->count = 0;
}
5.5 Advanced Pixel Operations
// Apply effect to surface
typedef void (*PixelEffect)(Uint32* pixel, SDL_PixelFormat* format, void* userData);
void applyEffect(SDL_Surface* surface, PixelEffect effect, void* userData) {
if (SDL_MUSTLOCK(surface)) SDL_LockSurface(surface);
Uint32* pixels = (Uint32*)surface->pixels;
for (int i = 0; i < surface->w * surface->h; i++) {
effect(&pixels[i], surface->format, userData);
}
if (SDL_MUSTLOCK(surface)) SDL_UnlockSurface(surface);
}
// Grayscale effect
void grayscaleEffect(Uint32* pixel, SDL_PixelFormat* format, void* userData) {
Uint8 r, g, b, a;
SDL_GetRGBA(*pixel, format, &r, &g, &b, &a);
Uint8 gray = (Uint8)(0.299 * r + 0.587 * g + 0.114 * b);
*pixel = SDL_MapRGBA(format, gray, gray, gray, a);
}
// Brightness effect
typedef struct {
float brightness; // 0.0 to 2.0
} BrightnessData;
void brightnessEffect(Uint32* pixel, SDL_PixelFormat* format, void* userData) {
BrightnessData* data = (BrightnessData*)userData;
Uint8 r, g, b, a;
SDL_GetRGBA(*pixel, format, &r, &g, &b, &a);
r = (Uint8)(fmin(r * data->brightness, 255));
g = (Uint8)(fmin(g * data->brightness, 255));
b = (Uint8)(fmin(b * data->brightness, 255));
*pixel = SDL_MapRGBA(format, r, g, b, a);
}
// Invert colors
void invertEffect(Uint32* pixel, SDL_PixelFormat* format, void* userData) {
Uint8 r, g, b, a;
SDL_GetRGBA(*pixel, format, &r, &g, &b, &a);
*pixel = SDL_MapRGBA(format, 255-r, 255-g, 255-b, a);
}
// Usage example
void demonstrateEffects() {
SDL_Surface* image = SDL_LoadBMP("image.bmp");
// Apply grayscale
applyEffect(image, grayscaleEffect, NULL);
// Apply brightness
BrightnessData brightness = {1.5f};
applyEffect(image, brightnessEffect, &brightness);
SDL_FreeSurface(image);
}
Chapter 6: The Game Loop - Heart of Every Game
6.1 The Fundamental Game Loop
Every game follows this pattern:
Initialize
While game is running:
Handle Input
Update Game State
Render Frame
Cleanup
This loop runs 60+ times per second, creating the illusion of continuous motion.
6.2 Basic Game Loop Implementation
// basic_game_loop.c
#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdbool.h>
typedef struct {
SDL_Window* window;
SDL_Renderer* renderer;
bool running;
int playerX, playerY;
int velocityX, velocityY;
} Game;
bool initGame(Game* game) {
if (SDL_Init(SDL_INIT_VIDEO) < 0) return false;
game->window = SDL_CreateWindow("Game Loop Demo",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
800, 600, SDL_WINDOW_SHOWN);
if (!game->window) return false;
game->renderer = SDL_CreateRenderer(game->window, -1,
SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (!game->renderer) return false;
game->running = true;
game->playerX = 400;
game->playerY = 300;
game->velocityX = 0;
game->velocityY = 0;
return true;
}
void handleInput(Game* game) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
game->running = false;
}
}
// Get keyboard state
const Uint8* keyState = SDL_GetKeyboardState(NULL);
// Reset velocity
game->velocityX = 0;
game->velocityY = 0;
// Check arrow keys
if (keyState[SDL_SCANCODE_LEFT]) game->velocityX = -5;
if (keyState[SDL_SCANCODE_RIGHT]) game->velocityX = 5;
if (keyState[SDL_SCANCODE_UP]) game->velocityY = -5;
if (keyState[SDL_SCANCODE_DOWN]) game->velocityY = 5;
// ESC to quit
if (keyState[SDL_SCANCODE_ESCAPE]) game->running = false;
}
void update(Game* game) {
// Update player position
game->playerX += game->velocityX;
game->playerY += game->velocityY;
// Keep player in bounds
if (game->playerX < 25) game->playerX = 25;
if (game->playerX > 775) game->playerX = 775;
if (game->playerY < 25) game->playerY = 25;
if (game->playerY > 575) game->playerY = 575;
}
void render(Game* game) {
// Clear screen (dark background)
SDL_SetRenderDrawColor(game->renderer, 20, 20, 30, 255);
SDL_RenderClear(game->renderer);
// Draw player (white square)
SDL_SetRenderDrawColor(game->renderer, 255, 255, 255, 255);
SDL_Rect player = {
game->playerX - 25,
game->playerY - 25,
50, 50
};
SDL_RenderFillRect(game->renderer, &player);
// Present
SDL_RenderPresent(game->renderer);
}
void cleanup(Game* game) {
SDL_DestroyRenderer(game->renderer);
SDL_DestroyWindow(game->window);
SDL_Quit();
}
int main(int argc, char* argv[]) {
Game game = {0};
if (!initGame(&game)) {
printf("Initialization failed!\n");
return 1;
}
// GAME LOOP
while (game.running) {
handleInput(&game);
update(&game);
render(&game);
}
cleanup(&game);
return 0;
}
6.3 Frame Rate and Delta Time
The basic loop has a problem: it runs at different speeds on different computers. The solution is delta time - the time elapsed since the last frame.
// delta_time_loop.c
#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdbool.h>
typedef struct {
SDL_Window* window;
SDL_Renderer* renderer;
bool running;
// Player state
float x, y;
float vx, vy;
float speed;
// Timing
Uint64 lastTime;
float deltaTime;
} Game;
bool initGame(Game* game) {
if (SDL_Init(SDL_INIT_VIDEO) < 0) return false;
game->window = SDL_CreateWindow("Delta Time Demo",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
800, 600, SDL_WINDOW_SHOWN);
if (!game->window) return false;
game->renderer = SDL_CreateRenderer(game->window, -1,
SDL_RENDERER_ACCELERATED);
if (!game->renderer) return false;
game->running = true;
game->x = 400.0f;
game->y = 300.0f;
game->vx = 0.0f;
game->vy = 0.0f;
game->speed = 200.0f; // pixels per second
game->lastTime = SDL_GetPerformanceCounter();
game->deltaTime = 0.0f;
return true;
}
void updateDeltaTime(Game* game) {
Uint64 currentTime = SDL_GetPerformanceCounter();
Uint64 frequency = SDL_GetPerformanceFrequency();
game->deltaTime = (float)(currentTime - game->lastTime) / frequency;
game->lastTime = currentTime;
// Cap delta time to prevent spiral of death
if (game->deltaTime > 0.05f) {
game->deltaTime = 0.05f;
}
}
void handleInput(Game* game) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
game->running = false;
}
}
const Uint8* keyState = SDL_GetKeyboardState(NULL);
game->vx = 0.0f;
game->vy = 0.0f;
if (keyState[SDL_SCANCODE_LEFT]) game->vx = -1.0f;
if (keyState[SDL_SCANCODE_RIGHT]) game->vx = 1.0f;
if (keyState[SDL_SCANCODE_UP]) game->vy = -1.0f;
if (keyState[SDL_SCANCODE_DOWN]) game->vy = 1.0f;
// Normalize diagonal movement
if (game->vx != 0.0f && game->vy != 0.0f) {
game->vx *= 0.7071f; // 1/sqrt(2)
game->vy *= 0.7071f;
}
if (keyState[SDL_SCANCODE_ESCAPE]) game->running = false;
}
void update(Game* game) {
// Update position using delta time
// distance = speed * time
game->x += game->vx * game->speed * game->deltaTime;
game->y += game->vy * game->speed * game->deltaTime;
// Bounds checking
if (game->x < 25) game->x = 25;
if (game->x > 775) game->x = 775;
if (game->y < 25) game->y = 25;
if (game->y > 575) game->y = 575;
}
void render(Game* game) {
SDL_SetRenderDrawColor(game->renderer, 20, 20, 30, 255);
SDL_RenderClear(game->renderer);
// Draw player
SDL_SetRenderDrawColor(game->renderer, 255, 255, 255, 255);
SDL_Rect player = {
(int)(game->x - 25),
(int)(game->y - 25),
50, 50
};
SDL_RenderFillRect(game->renderer, &player);
// Draw FPS
char fpsText[32];
snprintf(fpsText, sizeof(fpsText), "FPS: %.1f", 1.0f / game->deltaTime);
// (Would need SDL_ttf to actually render this)
SDL_RenderPresent(game->renderer);
}
void cleanup(Game* game) {
SDL_DestroyRenderer(game->renderer);
SDL_DestroyWindow(game->window);
SDL_Quit();
}
int main(int argc, char* argv[]) {
Game game = {0};
if (!initGame(&game)) {
printf("Initialization failed!\n");
return 1;
}
// GAME LOOP with delta time
while (game.running) {
updateDeltaTime(&game);
handleInput(&game);
update(&game);
render(&game);
}
cleanup(&game);
return 0;
}
Key points about delta time:
- Measures time between frames in seconds
- Multiply velocities by deltaTime
- Makes game run at consistent speed regardless of frame rate
- Cap deltaTime to prevent instability
6.4 Fixed Time Step for Physics
For physics simulations, a fixed time step is more accurate:
// fixed_timestep.c
#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdbool.h>
#define FIXED_DT 0.016f // 60 FPS
typedef struct {
SDL_Window* window;
SDL_Renderer* renderer;
bool running;
// Player physics
float x, y;
float vx, vy;
float ax, ay; // acceleration
// Timing
Uint64 lastTime;
float accumulator;
} Game;
bool initGame(Game* game) {
if (SDL_Init(SDL_INIT_VIDEO) < 0) return false;
game->window = SDL_CreateWindow("Fixed Timestep",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
800, 600, SDL_WINDOW_SHOWN);
if (!game->window) return false;
game->renderer = SDL_CreateRenderer(game->window, -1,
SDL_RENDERER_ACCELERATED);
if (!game->renderer) return false;
game->running = true;
game->x = 400.0f;
game->y = 100.0f;
game->vx = 0.0f;
game->vy = 0.0f;
game->ax = 0.0f;
game->ay = 500.0f; // gravity
game->lastTime = SDL_GetPerformanceCounter();
game->accumulator = 0.0f;
return true;
}
void handleInput(Game* game) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
game->running = false;
}
if (event.type == SDL_KEYDOWN) {
if (event.key.keysym.sym == SDLK_SPACE) {
// Jump (only if on ground)
if (game->y >= 550.0f) {
game->vy = -400.0f;
}
}
if (event.key.keysym.sym == SDLK_ESCAPE) {
game->running = false;
}
}
}
const Uint8* keyState = SDL_GetKeyboardState(NULL);
// Horizontal movement
game->ax = 0.0f;
if (keyState[SDL_SCANCODE_LEFT]) game->ax = -800.0f;
if (keyState[SDL_SCANCODE_RIGHT]) game->ax = 800.0f;
}
void updatePhysics(Game* game, float dt) {
// Apply acceleration
game->vx += game->ax * dt;
game->vy += game->ay * dt;
// Apply friction (horizontal only)
game->vx *= 0.95f;
// Clamp horizontal velocity
if (game->vx > 300.0f) game->vx = 300.0f;
if (game->vx < -300.0f) game->vx = -300.0f;
// Update position
game->x += game->vx * dt;
game->y += game->vy * dt;
// Ground collision
if (game->y > 550.0f) {
game->y = 550.0f;
game->vy = 0.0f;
}
// Wall collisions
if (game->x < 25.0f) {
game->x = 25.0f;
game->vx = 0.0f;
}
if (game->x > 775.0f) {
game->x = 775.0f;
game->vx = 0.0f;
}
}
void update(Game* game) {
// Calculate delta time
Uint64 currentTime = SDL_GetPerformanceCounter();
Uint64 frequency = SDL_GetPerformanceFrequency();
float deltaTime = (float)(currentTime - game->lastTime) / frequency;
game->lastTime = currentTime;
// Cap delta time
if (deltaTime > 0.1f) deltaTime = 0.1f;
// Add to accumulator
game->accumulator += deltaTime;
// Update physics in fixed steps
while (game->accumulator >= FIXED_DT) {
updatePhysics(game, FIXED_DT);
game->accumulator -= FIXED_DT;
}
}
void render(Game* game) {
SDL_SetRenderDrawColor(game->renderer, 30, 30, 50, 255);
SDL_RenderClear(game->renderer);
// Draw ground
SDL_SetRenderDrawColor(game->renderer, 100, 100, 100, 255);
SDL_Rect ground = {0, 550, 800, 50};
SDL_RenderFillRect(game->renderer, &ground);
// Draw player
SDL_SetRenderDrawColor(game->renderer, 255, 100, 100, 255);
SDL_Rect player = {
(int)(game->x - 25),
(int)(game->y - 25),
50, 50
};
SDL_RenderFillRect(game->renderer, &player);
SDL_RenderPresent(game->renderer);
}
void cleanup(Game* game) {
SDL_DestroyRenderer(game->renderer);
SDL_DestroyWindow(game->window);
SDL_Quit();
}
int main(int argc, char* argv[]) {
Game game = {0};
if (!initGame(&game)) {
printf("Initialization failed!\n");
return 1;
}
while (game.running) {
handleInput(&game);
update(&game);
render(&game);
}
cleanup(&game);
return 0;
}
Fixed timestep advantages:
- Deterministic physics
- Reproducible simulations
- Better for networked games
- More stable numerical integration
6.5 Advanced Loop Patterns
// advanced_game_loop.c
// Demonstrates multiple update rates
#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdbool.h>
#define PHYSICS_DT 0.016f // 60 Hz
#define LOGIC_DT 0.033f // 30 Hz
typedef struct {
SDL_Window* window;
SDL_Renderer* renderer;
bool running;
// Multiple accumulators for different update rates
float physicsAccumulator;
float logicAccumulator;
// State
float playerX, playerY;
float velocityX, velocityY;
int score;
Uint64 lastTime;
} Game;
// Fast physics update
void updatePhysics(Game* game, float dt) {
game->playerX += game->velocityX * dt;
game->playerY += game->velocityY * dt;
// Wrap around screen
if (game->playerX < 0) game->playerX += 800;
if (game->playerX > 800) game->playerX -= 800;
if (game->playerY < 0) game->playerY += 600;
if (game->playerY > 600) game->playerY -= 600;
}
// Slower game logic update
void updateLogic(Game* game, float dt) {
// AI, scoring, etc.
// This runs at 30 Hz instead of 60 Hz
game->score++;
}
void update(Game* game) {
Uint64 currentTime = SDL_GetPerformanceCounter();
Uint64 frequency = SDL_GetPerformanceFrequency();
float deltaTime = (float)(currentTime - game->lastTime) / frequency;
game->lastTime = currentTime;
if (deltaTime > 0.1f) deltaTime = 0.1f;
// Update physics at 60 Hz
game->physicsAccumulator += deltaTime;
while (game->physicsAccumulator >= PHYSICS_DT) {
updatePhysics(game, PHYSICS_DT);
game->physicsAccumulator -= PHYSICS_DT;
}
// Update logic at 30 Hz
game->logicAccumulator += deltaTime;
while (game->logicAccumulator >= LOGIC_DT) {
updateLogic(game, LOGIC_DT);
game->logicAccumulator -= LOGIC_DT;
}
}
// ... rest of implementation
This pattern allows different systems to update at different rates, optimizing performance while maintaining accuracy where needed.
Chapter 7: 2D Rendering Techniques
7.1 Understanding the Rendering Pipeline
The 2D rendering pipeline in SDL follows this flow:
Clear Buffer → Draw Primitives → Draw Textures → Apply Effects → Present
Each step builds upon the previous, creating the final frame.
Key concepts:
- Back buffer: Where we draw
- Front buffer: What's displayed
- Swap/Present: Make back buffer visible
7.2 Drawing Primitives
SDL provides basic drawing functions:
// primitives.c - Comprehensive primitive drawing
#include <SDL2/SDL.h>
#include <stdio.h>
#include <math.h>
#include <stdbool.h>
// Draw thick line using rectangles
void drawThickLine(SDL_Renderer* renderer, int x1, int y1, int x2, int y2, int thickness) {
int dx = x2 - x1;
int dy = y2 - y1;
float length = sqrtf(dx*dx + dy*dy);
float angle = atan2f(dy, dx);
for (int i = 0; i < (int)length; i++) {
int x = x1 + (int)(i * cosf(angle));
int y = y1 + (int)(i * sinf(angle));
SDL_Rect rect = {x - thickness/2, y - thickness/2, thickness, thickness};
SDL_RenderFillRect(renderer, &rect);
}
}
// Draw filled triangle
void drawFilledTriangle(SDL_Renderer* renderer, int x1, int y1, int x2, int y2, int x3, int y3) {
// Sort vertices by y-coordinate
if (y1 > y2) { int t = y1; y1 = y2; y2 = t; t = x1; x1 = x2; x2 = t; }
if (y2 > y3) { int t = y2; y2 = y3; y3 = t; t = x2; x2 = x3; x3 = t; }
if (y1 > y2) { int t = y1; y1 = y2; y2 = t; t = x1; x1 = x2; x2 = t; }
// Draw triangle using scanlines
for (int y = y1; y <= y3; y++) {
int xa, xb;
if (y < y2) {
// Top half
xa = x1 + (x2 - x1) * (y - y1) / (y2 - y1 + 1);
xb = x1 + (x3 - x1) * (y - y1) / (y3 - y1 + 1);
} else {
// Bottom half
xa = x2 + (x3 - x2) * (y - y2) / (y3 - y2 + 1);
xb = x1 + (x3 - x1) * (y - y1) / (y3 - y1 + 1);
}
if (xa > xb) { int t = xa; xa = xb; xb = t; }
SDL_RenderDrawLine(renderer, xa, y, xb, y);
}
}
// Draw polygon
void drawPolygon(SDL_Renderer* renderer, SDL_Point* points, int count, bool filled) {
if (filled) {
// Simple convex polygon fill
// Find bounding box
int minY = points[0].y, maxY = points[0].y;
for (int i = 1; i < count; i++) {
if (points[i].y < minY) minY = points[i].y;
if (points[i].y > maxY) maxY = points[i].y;
}
// Scanline fill
for (int y = minY; y <= maxY; y++) {
int intersections[32];
int numIntersections = 0;
for (int i = 0; i < count; i++) {
int j = (i + 1) % count;
int y1 = points[i].y;
int y2 = points[j].y;
if ((y1 <= y && y < y2) || (y2 <= y && y < y1)) {
int x = points[i].x +
(points[j].x - points[i].x) * (y - y1) / (y2 - y1);
intersections[numIntersections++] = x;
}
}
// Sort intersections
for (int i = 0; i < numIntersections - 1; i++) {
for (int j = i + 1; j < numIntersections; j++) {
if (intersections[i] > intersections[j]) {
int t = intersections[i];
intersections[i] = intersections[j];
intersections[j] = t;
}
}
}
// Draw pairs
for (int i = 0; i < numIntersections; i += 2) {
if (i + 1 < numIntersections) {
SDL_RenderDrawLine(renderer, intersections[i], y,
intersections[i+1], y);
}
}
}
} else {
// Draw outline
for (int i = 0; i < count; i++) {
int j = (i + 1) % count;
SDL_RenderDrawLine(renderer, points[i].x, points[i].y,
points[j].x, points[j].y);
}
}
}
// Draw anti-aliased line (Wu's algorithm)
void drawAALine(SDL_Renderer* renderer, float x0, float y0, float x1, float y1) {
bool steep = fabsf(y1 - y0) > fabsf(x1 - x0);
if (steep) {
float t = x0; x0 = y0; y0 = t;
t = x1; x1 = y1; y1 = t;
}
if (x0 > x1) {
float t = x0; x0 = x1; x1 = t;
t = y0; y0 = y1; y1 = t;
}
float dx = x1 - x0;
float dy = y1 - y0;
float gradient = dy / dx;
// Handle first endpoint
float xend = roundf(x0);
float yend = y0 + gradient * (xend - x0);
float xgap = 1.0f - (x0 + 0.5f - floorf(x0 + 0.5f));
int xpxl1 = (int)xend;
int ypxl1 = (int)floorf(yend);
if (steep) {
SDL_SetRenderDrawColor(renderer, 255, 255, 255,
(Uint8)(255 * (1.0f - (yend - floorf(yend))) * xgap));
SDL_RenderDrawPoint(renderer, ypxl1, xpxl1);
SDL_SetRenderDrawColor(renderer, 255, 255, 255,
(Uint8)(255 * (yend - floorf(yend)) * xgap));
SDL_RenderDrawPoint(renderer, ypxl1 + 1, xpxl1);
} else {
SDL_SetRenderDrawColor(renderer, 255, 255, 255,
(Uint8)(255 * (1.0f - (yend - floorf(yend))) * xgap));
SDL_RenderDrawPoint(renderer, xpxl1, ypxl1);
SDL_SetRenderDrawColor(renderer, 255, 255, 255,
(Uint8)(255 * (yend - floorf(yend)) * xgap));
SDL_RenderDrawPoint(renderer, xpxl1, ypxl1 + 1);
}
float intery = yend + gradient;
// Main loop
for (int x = xpxl1 + 1; x < (int)x1; x++) {
if (steep) {
SDL_SetRenderDrawColor(renderer, 255, 255, 255,
(Uint8)(255 * (1.0f - (intery - floorf(intery)))));
SDL_RenderDrawPoint(renderer, (int)intery, x);
SDL_SetRenderDrawColor(renderer, 255, 255, 255,
(Uint8)(255 * (intery - floorf(intery))));
SDL_RenderDrawPoint(renderer, (int)intery + 1, x);
} else {
SDL_SetRenderDrawColor(renderer, 255, 255, 255,
(Uint8)(255 * (1.0f - (intery - floorf(intery)))));
SDL_RenderDrawPoint(renderer, x, (int)intery);
SDL_SetRenderDrawColor(renderer, 255, 255, 255,
(Uint8)(255 * (intery - floorf(intery))));
SDL_RenderDrawPoint(renderer, x, (int)intery + 1);
}
intery += gradient;
}
}
// Demo program
int main(int argc, char* argv[]) {
SDL_Init(SDL_INIT_VIDEO);
SDL_Window* window = SDL_CreateWindow("2D Primitives",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 800, 600, 0);
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1,
SDL_RENDERER_ACCELERATED);
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
bool running = true;
SDL_Event event;
while (running) {
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT ||
(event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_ESCAPE)) {
running = false;
}
}
// Clear
SDL_SetRenderDrawColor(renderer, 20, 20, 30, 255);
SDL_RenderClear(renderer);
// Draw examples
SDL_SetRenderDrawColor(renderer, 255, 100, 100, 255);
drawThickLine(renderer, 100, 100, 300, 200, 5);
SDL_SetRenderDrawColor(renderer, 100, 255, 100, 255);
drawFilledTriangle(renderer, 400, 100, 500, 250, 350, 200);
SDL_Point hexagon[] = {
{650, 150}, {700, 180}, {700, 230},
{650, 260}, {600, 230}, {600, 180}
};
SDL_SetRenderDrawColor(renderer, 100, 100, 255, 255);
drawPolygon(renderer, hexagon, 6, true);
// Anti-aliased line
drawAALine(renderer, 100, 400, 700, 500);
SDL_RenderPresent(renderer);
SDL_Delay(16);
}
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
7.3 Texture Rendering and Transforms
Textures are the foundation of 2D game graphics:
// texture_rendering.c
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <stdio.h>
#include <math.h>
#include <stdbool.h>
typedef struct {
SDL_Texture* texture;
int width, height;
} Sprite;
// Load sprite from file
Sprite loadSprite(SDL_Renderer* renderer, const char* filename) {
Sprite sprite = {0};
SDL_Surface* surface = IMG_Load(filename);
if (!surface) {
printf("Failed to load image: %s\n", IMG_GetError());
return sprite;
}
sprite.texture = SDL_CreateTextureFromSurface(renderer, surface);
sprite.width = surface->w;
sprite.height = surface->h;
SDL_FreeSurface(surface);
return sprite;
}
// Draw sprite with various options
void drawSprite(SDL_Renderer* renderer, Sprite* sprite,
float x, float y, float scale, float rotation,
bool flipH, bool flipV, Uint8 alpha) {
SDL_Rect dest = {
(int)(x - sprite->width * scale / 2),
(int)(y - sprite->height * scale / 2),
(int)(sprite->width * scale),
(int)(sprite->height * scale)
};
SDL_Point center = {dest.w / 2, dest.h / 2};
SDL_SetTextureAlphaMod(sprite->texture, alpha);
SDL_RendererFlip flip = SDL_FLIP_NONE;
if (flipH) flip |= SDL_FLIP_HORIZONTAL;
if (flipV) flip |= SDL_FLIP_VERTICAL;
SDL_RenderCopyEx(renderer, sprite->texture, NULL, &dest,
rotation, ¢er, flip);
}
// Draw partial sprite (sprite sheet)
void drawSpriteFrame(SDL_Renderer* renderer, Sprite* sprite,
int frameX, int frameY, int frameW, int frameH,
float x, float y, float scale) {
SDL_Rect src = {frameX, frameY, frameW, frameH};
SDL_Rect dest = {
(int)(x - frameW * scale / 2),
(int)(y - frameH * scale / 2),
(int)(frameW * scale),
(int)(frameH * scale)
};
SDL_RenderCopy(renderer, sprite->texture, &src, &dest);
}
// Tiled background rendering
void drawTiledBackground(SDL_Renderer* renderer, SDL_Texture* tile,
int tileW, int tileH, int screenW, int screenH,
float offsetX, float offsetY) {
int startX = (int)(-offsetX / tileW) - 1;
int startY = (int)(-offsetY / tileH) - 1;
int endX = startX + (screenW / tileW) + 2;
int endY = startY + (screenH / tileH) + 2;
for (int y = startY; y < endY; y++) {
for (int x = startX; x < endX; x++) {
SDL_Rect dest = {
(int)(x * tileW + offsetX),
(int)(y * tileH + offsetY),
tileW, tileH
};
SDL_RenderCopy(renderer, tile, NULL, &dest);
}
}
}
// Parallax scrolling
typedef struct {
SDL_Texture* texture;
float speed;
float offsetX;
int width, height;
} ParallaxLayer;
void updateParallax(ParallaxLayer* layer, float cameraVelocity, float dt) {
layer->offsetX += cameraVelocity * layer->speed * dt;
// Wrap around
while (layer->offsetX > layer->width) layer->offsetX -= layer->width;
while (layer->offsetX < 0) layer->offsetX += layer->width;
}
void renderParallax(SDL_Renderer* renderer, ParallaxLayer* layer,
int screenW, int screenH) {
int numCopies = (screenW / layer->width) + 2;
for (int i = -1; i < numCopies; i++) {
SDL_Rect dest = {
(int)(i * layer->width - layer->offsetX),
0,
layer->width,
layer->height
};
SDL_RenderCopy(renderer, layer->texture, NULL, &dest);
}
}
// Example: Complete parallax system
typedef struct {
SDL_Window* window;
SDL_Renderer* renderer;
bool running;
ParallaxLayer layers[3];
float cameraX;
float cameraSpeed;
Uint64 lastTime;
} Game;
bool initGame(Game* game) {
SDL_Init(SDL_INIT_VIDEO);
IMG_Init(IMG_INIT_PNG);
game->window = SDL_CreateWindow("Parallax Demo",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 800, 600, 0);
game->renderer = SDL_CreateRenderer(game->window, -1,
SDL_RENDERER_ACCELERATED);
// Load layers (would load actual images)
// Layer 0: Far background (slow)
// Layer 1: Middle ground (medium)
// Layer 2: Foreground (fast)
game->cameraX = 0;
game->cameraSpeed = 100.0f;
game->running = true;
game->lastTime = SDL_GetPerformanceCounter();
return true;
}
void update(Game* game) {
Uint64 currentTime = SDL_GetPerformanceCounter();
float dt = (float)(currentTime - game->lastTime) / SDL_GetPerformanceFrequency();
game->lastTime = currentTime;
if (dt > 0.1f) dt = 0.1f;
// Update camera
const Uint8* keys = SDL_GetKeyboardState(NULL);
float velocity = 0;
if (keys[SDL_SCANCODE_LEFT]) velocity = -game->cameraSpeed;
if (keys[SDL_SCANCODE_RIGHT]) velocity = game->cameraSpeed;
game->cameraX += velocity * dt;
// Update parallax layers
for (int i = 0; i < 3; i++) {
updateParallax(&game->layers[i], velocity, dt);
}
}
void render(Game* game) {
SDL_SetRenderDrawColor(game->renderer, 135, 206, 235, 255); // Sky blue
SDL_RenderClear(game->renderer);
// Render layers back to front
for (int i = 0; i < 3; i++) {
renderParallax(game->renderer, &game->layers[i], 800, 600);
}
SDL_RenderPresent(game->renderer);
}
7.4 Advanced Blending and Effects
// blending_effects.c
#include <SDL2/SDL.h>
#include <math.h>
// Custom blend modes for special effects
void setupCustomBlending(SDL_Renderer* renderer) {
// Additive blending (for glows, lights)
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_ADD);
// Multiplicative blending (for shadows, darkening)
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_MOD);
// Standard alpha blending
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
}
// Glow effect
void drawGlow(SDL_Renderer* renderer, int x, int y, int radius, SDL_Color color) {
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_ADD);
for (int r = radius; r > 0; r -= 2) {
float alpha = (float)r / radius;
SDL_SetRenderDrawColor(renderer,
(Uint8)(color.r * alpha),
(Uint8)(color.g * alpha),
(Uint8)(color.b * alpha),
(Uint8)(color.a * alpha * 0.5f));
// Draw filled circle
for (int dy = -r; dy <= r; dy++) {
int dx = (int)sqrtf(r*r - dy*dy);
SDL_RenderDrawLine(renderer, x - dx, y + dy, x + dx, y + dy);
}
}
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
}
// Motion blur effect
typedef struct {
SDL_Texture* target;
int width, height;
} MotionBlurBuffer;
MotionBlurBuffer createMotionBlurBuffer(SDL_Renderer* renderer, int w, int h) {
MotionBlurBuffer buffer;
buffer.width = w;
buffer.height = h;
buffer.target = SDL_CreateTexture(renderer,
SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_TARGET,
w, h);
SDL_SetTextureBlendMode(buffer.target, SDL_BLENDMODE_BLEND);
return buffer;
}
void applyMotionBlur(SDL_Renderer* renderer, MotionBlurBuffer* buffer,
float persistence) {
// Render current frame to texture
SDL_SetRenderTarget(renderer, buffer->target);
// Draw previous frame with alpha
SDL_SetTextureAlphaMod(buffer->target, (Uint8)(255 * persistence));
SDL_RenderCopy(renderer, buffer->target, NULL, NULL);
// Reset
SDL_SetRenderTarget(renderer, NULL);
}
// Screen shake effect
typedef struct {
float trauma; // 0.0 to 1.0
float decay; // Trauma decay rate
float maxOffset; // Maximum shake distance
float frequency; // Shake frequency
float time; // Internal timer
} ScreenShake;
void initScreenShake(ScreenShake* shake) {
shake->trauma = 0.0f;
shake->decay = 1.0f;
shake->maxOffset = 20.0f;
shake->frequency = 5.0f;
shake->time = 0.0f;
}
void addTrauma(ScreenShake* shake, float amount) {
shake->trauma = fminf(shake->trauma + amount, 1.0f);
}
void updateScreenShake(ScreenShake* shake, float dt) {
shake->trauma = fmaxf(shake->trauma - shake->decay * dt, 0.0f);
shake->time += dt;
}
void getScreenShakeOffset(ScreenShake* shake, int* offsetX, int* offsetY) {
if (shake->trauma <= 0.0f) {
*offsetX = 0;
*offsetY = 0;
return;
}
float shake2 = shake->trauma * shake->trauma;
*offsetX = (int)(shake->maxOffset * shake2 *
sinf(shake->time * shake->frequency * 2.0f * M_PI));
*offsetY = (int)(shake->maxOffset * shake2 *
cosf(shake->time * shake->frequency * 2.0f * M_PI));
}
// Render with shake
void renderWithShake(SDL_Renderer* renderer, ScreenShake* shake) {
int offsetX, offsetY;
getScreenShakeOffset(shake, &offsetX, &offsetY);
SDL_Rect viewport;
SDL_RenderGetViewport(renderer, &viewport);
viewport.x = offsetX;
viewport.y = offsetY;
SDL_RenderSetViewport(renderer, &viewport);
// ... render scene ...
// Reset viewport
viewport.x = 0;
viewport.y = 0;
SDL_RenderSetViewport(renderer, &viewport);
}
// Color grading
void applyColorGrade(SDL_Renderer* renderer, SDL_Texture* scene,
float brightness, float contrast, float saturation) {
// Would typically use shaders, but can approximate with texture modulation
SDL_SetTextureColorMod(scene,
(Uint8)(255 * brightness),
(Uint8)(255 * brightness),
(Uint8)(255 * brightness));
SDL_RenderCopy(renderer, scene, NULL, NULL);
}
// Fade transitions
typedef struct {
float alpha;
float rate;
bool fadingOut;
} FadeEffect;
void updateFade(FadeEffect* fade, float dt) {
if (fade->fadingOut) {
fade->alpha += fade->rate * dt;
if (fade->alpha >= 1.0f) fade->alpha = 1.0f;
} else {
fade->alpha -= fade->rate * dt;
if (fade->alpha <= 0.0f) fade->alpha = 0.0f;
}
}
void renderFade(SDL_Renderer* renderer, FadeEffect* fade) {
SDL_SetRenderDrawColor(renderer, 0, 0, 0, (Uint8)(255 * fade->alpha));
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_Rect fullscreen = {0, 0, 800, 600};
SDL_RenderFillRect(renderer, &fullscreen);
}
7.5 Clipping and Viewports
// clipping_viewports.c
#include <SDL2/SDL.h>
// Split-screen rendering
void renderSplitScreen(SDL_Renderer* renderer) {
SDL_Rect viewport1 = {0, 0, 400, 600}; // Left half
SDL_Rect viewport2 = {400, 0, 400, 600}; // Right half
// Render player 1 view
SDL_RenderSetViewport(renderer, &viewport1);
// ... render player 1 scene ...
// Render player 2 view
SDL_RenderSetViewport(renderer, &viewport2);
// ... render player 2 scene ...
// Reset to full screen
SDL_RenderSetViewport(renderer, NULL);
}
// Minimap rendering
void renderMinimap(SDL_Renderer* renderer, int x, int y, int w, int h) {
SDL_Rect minimapViewport = {x, y, w, h};
// Save current viewport
SDL_Rect oldViewport;
SDL_RenderGetViewport(renderer, &oldViewport);
// Set minimap viewport
SDL_RenderSetViewport(renderer, &minimapViewport);
// Render minimap content (scaled down world)
// ... render world at smaller scale ...
// Draw border
SDL_RenderSetViewport(renderer, NULL);
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
SDL_RenderDrawRect(renderer, &minimapViewport);
// Restore viewport
SDL_RenderSetViewport(renderer, &oldViewport);
}
// Clipping for UI elements
void renderClippedUI(SDL_Renderer* renderer) {
// Enable clipping
SDL_Rect clipRect = {100, 100, 300, 200};
SDL_RenderSetClipRect(renderer, &clipRect);
// Draw long text that gets clipped
// ... draw UI elements ...
// Disable clipping
SDL_RenderSetClipRect(renderer, NULL);
}
Chapter 8: Input Handling and User Interaction
8.1 Keyboard Input
// keyboard_input.c
#include <SDL2/SDL.h>
#include <stdbool.h>
#include <stdio.h>
// Keyboard state manager
typedef struct {
const Uint8* currentState;
Uint8 previousState[SDL_NUM_SCANCODES];
int numKeys;
} KeyboardState;
void initKeyboardState(KeyboardState* kb) {
kb->currentState = SDL_GetKeyboardState(&kb->numKeys);
memset(kb->previousState, 0, sizeof(kb->previousState));
}
void updateKeyboardState(KeyboardState* kb) {
memcpy(kb->previousState, kb->currentState, kb->numKeys);
kb->currentState = SDL_GetKeyboardState(NULL);
}
// Check if key is currently held
bool isKeyDown(KeyboardState* kb, SDL_Scancode key) {
return kb->currentState[key];
}
// Check if key was just pressed this frame
bool isKeyPressed(KeyboardState* kb, SDL_Scancode key) {
return kb->currentState[key] && !kb->previousState[key];
}
// Check if key was just released this frame
bool isKeyReleased(KeyboardState* kb, SDL_Scancode key) {
return !kb->currentState[key] && kb->previousState[key];
}
// Text input handling
typedef struct {
char buffer[256];
int length;
bool active;
} TextInput;
void initTextInput(TextInput* input) {
input->buffer[0] = '\0';
input->length = 0;
input->active = false;
}
void startTextInput(TextInput* input) {
input->active = true;
SDL_StartTextInput();
}
void stopTextInput(TextInput* input) {
input->active = false;
SDL_StopTextInput();
}
void handleTextInput(TextInput* input, SDL_Event* event) {
if (!input->active) return;
if (event->type == SDL_TEXTINPUT) {
// Add character to buffer
int addLen = strlen(event->text.text);
if (input->length + addLen < 255) {
strcpy(input->buffer + input->length, event->text.text);
input->length += addLen;
}
}
else if (event->type == SDL_KEYDOWN) {
if (event->key.keysym.sym == SDLK_BACKSPACE && input->length > 0) {
input->buffer[--input->length] = '\0';
}
else if (event->key.keysym.sym == SDLK_RETURN) {
// Handle enter key
printf("Input submitted: %s\n", input->buffer);
}
}
}
// Example: Player movement with keyboard
typedef struct {
float x, y;
float speed;
KeyboardState keyboard;
} Player;
void updatePlayerInput(Player* player, float dt) {
float vx = 0, vy = 0;
if (isKeyDown(&player->keyboard, SDL_SCANCODE_W)) vy = -1;
if (isKeyDown(&player->keyboard, SDL_SCANCODE_S)) vy = 1;
if (isKeyDown(&player->keyboard, SDL_SCANCODE_A)) vx = -1;
if (isKeyDown(&player->keyboard, SDL_SCANCODE_D)) vx = 1;
// Normalize diagonal movement
if (vx != 0 && vy != 0) {
vx *= 0.7071f;
vy *= 0.7071f;
}
player->x += vx * player->speed * dt;
player->y += vy * player->speed * dt;
// Check for action button press
if (isKeyPressed(&player->keyboard, SDL_SCANCODE_SPACE)) {
printf("Action!\n");
}
}
8.2 Mouse Input
// mouse_input.c
#include <SDL2/SDL.h>
#include <stdbool.h>
typedef struct {
int x, y; // Current position
int prevX, prevY; // Previous position
int deltaX, deltaY; // Movement since last frame
bool leftButton; // Current state
bool rightButton;
bool middleButton;
bool leftPressed; // Just pressed
bool leftReleased; // Just released
bool rightPressed;
bool rightReleased;
int wheelX, wheelY; // Scroll wheel
} MouseState;
void initMouseState(MouseState* mouse) {
Uint32 buttons = SDL_GetMouseState(&mouse->x, &mouse->y);
mouse->prevX = mouse->x;
mouse->prevY = mouse->y;
mouse->deltaX = 0;
mouse->deltaY = 0;
mouse->leftButton = buttons & SDL_BUTTON(SDL_BUTTON_LEFT);
mouse->rightButton = buttons & SDL_BUTTON(SDL_BUTTON_RIGHT);
mouse->middleButton = buttons & SDL_BUTTON(SDL_BUTTON_MIDDLE);
mouse->leftPressed = false;
mouse->leftReleased = false;
mouse->rightPressed = false;
mouse->rightReleased = false;
mouse->wheelX = 0;
mouse->wheelY = 0;
}
void updateMouseState(MouseState* mouse) {
mouse->prevX = mouse->x;
mouse->prevY = mouse->y;
Uint32 buttons = SDL_GetMouseState(&mouse->x, &mouse->y);
mouse->deltaX = mouse->x - mouse->prevX;
mouse->deltaY = mouse->y - mouse->prevY;
bool prevLeft = mouse->leftButton;
bool prevRight = mouse->rightButton;
mouse->leftButton = buttons & SDL_BUTTON(SDL_BUTTON_LEFT);
mouse->rightButton = buttons & SDL_BUTTON(SDL_BUTTON_RIGHT);
mouse->middleButton = buttons & SDL_BUTTON(SDL_BUTTON_MIDDLE);
mouse->leftPressed = mouse->leftButton && !prevLeft;
mouse->leftReleased = !mouse->leftButton && prevLeft;
mouse->rightPressed = mouse->rightButton && !prevRight;
mouse->rightReleased = !mouse->rightButton && prevRight;
mouse->wheelX = 0;
mouse->wheelY = 0;
}
void handleMouseEvent(MouseState* mouse, SDL_Event* event) {
if (event->type == SDL_MOUSEWHEEL) {
mouse->wheelX = event->wheel.x;
mouse->wheelY = event->wheel.y;
}
}
// Mouse-based camera control
typedef struct {
float x, y;
float zoom;
bool dragging;
int dragStartX, dragStartY;
float dragStartCamX, dragStartCamY;
} Camera;
void updateCameraWithMouse(Camera* cam, MouseState* mouse) {
// Right-click drag to pan
if (mouse->rightPressed) {
cam->dragging = true;
cam->dragStartX = mouse->x;
cam->dragStartY = mouse->y;
cam->dragStartCamX = cam->x;
cam->dragStartCamY = cam->y;
}
if (mouse->rightReleased) {
cam->dragging = false;
}
if (cam->dragging) {
cam->x = cam->dragStartCamX - (mouse->x - cam->dragStartX) / cam->zoom;
cam->y = cam->dragStartCamY - (mouse->y - cam->dragStartY) / cam->zoom;
}
// Scroll wheel to zoom
if (mouse->wheelY != 0) {
float oldZoom = cam->zoom;
cam->zoom *= (mouse->wheelY > 0) ? 1.1f : 0.9f;
// Clamp zoom
if (cam->zoom < 0.1f) cam->zoom = 0.1f;
if (cam->zoom > 5.0f) cam->zoom = 5.0f;
// Zoom toward mouse position
float worldX = cam->x + mouse->x / oldZoom;
float worldY = cam->y + mouse->y / oldZoom;
cam->x = worldX - mouse->x / cam->zoom;
cam->y = worldY - mouse->y / cam->zoom;
}
}
// UI button system
typedef struct {
SDL_Rect rect;
bool hovered;
bool pressed;
void (*onClick)(void* userData);
void* userData;
} Button;
void updateButton(Button* button, MouseState* mouse) {
button->hovered = (mouse->x >= button->rect.x &&
mouse->x < button->rect.x + button->rect.w &&
mouse->y >= button->rect.y &&
mouse->y < button->rect.y + button->rect.h);
if (button->hovered && mouse->leftPressed) {
button->pressed = true;
}
if (button->pressed && mouse->leftReleased) {
if (button->hovered && button->onClick) {
button->onClick(button->userData);
}
button->pressed = false;
}
}
void renderButton(SDL_Renderer* renderer, Button* button, const char* text) {
// Draw button background
if (button->pressed) {
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
} else if (button->hovered) {
SDL_SetRenderDrawColor(renderer, 150, 150, 150, 255);
} else {
SDL_SetRenderDrawColor(renderer, 120, 120, 120, 255);
}
SDL_RenderFillRect(renderer, &button->rect);
// Draw border
SDL_SetRenderDrawColor(renderer, 200, 200, 200, 255);
SDL_RenderDrawRect(renderer, &button->rect);
// Would draw text here with SDL_ttf
}
8.3 Gamepad/Controller Input
// gamepad_input.c
#include <SDL2/SDL.h>
#include <stdbool.h>
#define MAX_GAMEPADS 4
typedef struct {
SDL_GameController* controller;
SDL_Joystick* joystick;
SDL_JoystickID instanceID;
bool connected;
// Button states
bool buttons[SDL_CONTROLLER_BUTTON_MAX];
bool buttonsPressed[SDL_CONTROLLER_BUTTON_MAX];
bool buttonsReleased[SDL_CONTROLLER_BUTTON_MAX];
// Analog sticks (normalized -1 to 1)
float leftStickX, leftStickY;
float rightStickX, rightStickY;
// Triggers (0 to 1)
float leftTrigger, rightTrigger;
// Haptic/rumble
SDL_Haptic* haptic;
} Gamepad;
typedef struct {
Gamepad gamepads[MAX_GAMEPADS];
int numGamepads;
} GamepadSystem;
void initGamepadSystem(GamepadSystem* system) {
SDL_Init(SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC);
system->numGamepads = 0;
// Open all connected controllers
for (int i = 0; i < SDL_NumJoysticks() && system->numGamepads < MAX_GAMEPADS; i++) {
if (SDL_IsGameController(i)) {
Gamepad* pad = &system->gamepads[system->numGamepads];
pad->controller = SDL_GameControllerOpen(i);
pad->joystick = SDL_GameControllerGetJoystick(pad->controller);
pad->instanceID = SDL_JoystickInstanceID(pad->joystick);
pad->connected = true;
// Try to open haptic
pad->haptic = SDL_HapticOpenFromJoystick(pad->joystick);
if (pad->haptic) {
SDL_HapticRumbleInit(pad->haptic);
}
memset(pad->buttons, 0, sizeof(pad->buttons));
memset(pad->buttonsPressed, 0, sizeof(pad->buttonsPressed));
memset(pad->buttonsReleased, 0, sizeof(pad->buttonsReleased));
system->numGamepads++;
printf("Gamepad %d connected: %s\n", system->numGamepads,
SDL_GameControllerName(pad->controller));
}
}
}
float normalizeAxis(Sint16 value, int deadzone) {
if (abs(value) < deadzone) return 0.0f;
if (value < 0) {
return (float)(value + deadzone) / (32768.0f - deadzone);
} else {
return (float)(value - deadzone) / (32767.0f - deadzone);
}
}
void updateGamepad(Gamepad* pad) {
if (!pad->connected) return;
// Update buttons
for (int i = 0; i < SDL_CONTROLLER_BUTTON_MAX; i++) {
bool prevState = pad->buttons[i];
pad->buttons[i] = SDL_GameControllerGetButton(pad->controller, i);
pad->buttonsPressed[i] = pad->buttons[i] && !prevState;
pad->buttonsReleased[i] = !pad->buttons[i] && prevState;
}
// Update analog sticks with deadzone
const int DEADZONE = 8000;
Sint16 leftX = SDL_GameControllerGetAxis(pad->controller,
SDL_CONTROLLER_AXIS_LEFTX);
Sint16 leftY = SDL_GameControllerGetAxis(pad->controller,
SDL_CONTROLLER_AXIS_LEFTY);
Sint16 rightX = SDL_GameControllerGetAxis(pad->controller,
SDL_CONTROLLER_AXIS_RIGHTX);
Sint16 rightY = SDL_GameControllerGetAxis(pad->controller,
SDL_CONTROLLER_AXIS_RIGHTY);
pad->leftStickX = normalizeAxis(leftX, DEADZONE);
pad->leftStickY = normalizeAxis(leftY, DEADZONE);
pad->rightStickX = normalizeAxis(rightX, DEADZONE);
pad->rightStickY = normalizeAxis(rightY, DEADZONE);
// Update triggers
Sint16 leftTrig = SDL_GameControllerGetAxis(pad->controller,
SDL_CONTROLLER_AXIS_TRIGGERLEFT);
Sint16 rightTrig = SDL_GameControllerGetAxis(pad->controller,
SDL_CONTROLLER_AXIS_TRIGGERRIGHT);
pad->leftTrigger = leftTrig / 32767.0f;
pad->rightTrigger = rightTrig / 32767.0f;
}
void rumbleGamepad(Gamepad* pad, float strength, Uint32 duration) {
if (pad->haptic) {
SDL_HapticRumblePlay(pad->haptic, strength, duration);
}
}
void handleGamepadEvent(GamepadSystem* system, SDL_Event* event) {
switch (event->type) {
case SDL_CONTROLLERDEVICEADDED:
if (system->numGamepads < MAX_GAMEPADS) {
Gamepad* pad = &system->gamepads[system->numGamepads];
int deviceIndex = event->cdevice.which;
pad->controller = SDL_GameControllerOpen(deviceIndex);
pad->joystick = SDL_GameControllerGetJoystick(pad->controller);
pad->instanceID = SDL_JoystickInstanceID(pad->joystick);
pad->connected = true;
pad->haptic = SDL_HapticOpenFromJoystick(pad->joystick);
if (pad->haptic) {
SDL_HapticRumbleInit(pad->haptic);
}
system->numGamepads++;
printf("Gamepad connected\n");
}
break;
case SDL_CONTROLLERDEVICEREMOVED:
for (int i = 0; i < system->numGamepads; i++) {
if (system->gamepads[i].instanceID == event->cdevice.which) {
if (system->gamepads[i].haptic) {
SDL_HapticClose(system->gamepads[i].haptic);
}
SDL_GameControllerClose(system->gamepads[i].controller);
system->gamepads[i].connected = false;
printf("Gamepad disconnected\n");
break;
}
}
break;
}
}
// Example: Character movement with gamepad
void updatePlayerWithGamepad(Player* player, Gamepad* pad, float dt) {
if (!pad->connected) return;
// Movement with left stick
player->x += pad->leftStickX * player->speed * dt;
player->y += pad->leftStickY * player->speed * dt;
// Jump with A button
if (pad->buttonsPressed[SDL_CONTROLLER_BUTTON_A]) {
// Jump logic
rumbleGamepad(pad, 0.3f, 100);
}
// Sprint with right trigger
if (pad->rightTrigger > 0.5f) {
player->speed *= 1.5f;
}
// Camera with right stick
// ... camera control ...
}
8.4 Touch Input (Mobile)
// touch_input.c
#include <SDL2/SDL.h>
#define MAX_TOUCHES 10
typedef struct {
SDL_FingerID fingerID;
float x, y; // Normalized 0-1
float dx, dy; // Delta since last frame
float pressure;
bool active;
} Touch;
typedef struct {
Touch touches[MAX_TOUCHES];
int numTouches;
bool multiTouchEnabled;
} TouchSystem;
void initTouchSystem(TouchSystem* system) {
system->numTouches = 0;
system->multiTouchEnabled = true;
memset(system->touches, 0, sizeof(system->touches));
}
int findTouchIndex(TouchSystem* system, SDL_FingerID fingerID) {
for (int i = 0; i < system->numTouches; i++) {
if (system->touches[i].fingerID == fingerID) {
return i;
}
}
return -1;
}
void handleTouchEvent(TouchSystem* system, SDL_Event* event) {
switch (event->type) {
case SDL_FINGERDOWN: {
if (system->numTouches >= MAX_TOUCHES) break;
Touch* touch = &system->touches[system->numTouches++];
touch->fingerID = event->tfinger.fingerID;
touch->x = event->tfinger.x;
touch->y = event->tfinger.y;
touch->dx = event->tfinger.dx;
touch->dy = event->tfinger.dy;
touch->pressure = event->tfinger.pressure;
touch->active = true;
break;
}
case SDL_FINGERUP: {
int idx = findTouchIndex(system, event->tfinger.fingerID);
if (idx >= 0) {
// Remove touch by shifting remaining touches
for (int i = idx; i < system->numTouches - 1; i++) {
system->touches[i] = system->touches[i + 1];
}
system->numTouches--;
}
break;
}
case SDL_FINGERMOTION: {
int idx = findTouchIndex(system, event->tfinger.fingerID);
if (idx >= 0) {
Touch* touch = &system->touches[idx];
touch->dx = event->tfinger.dx;
touch->dy = event->tfinger.dy;
touch->x = event->tfinger.x;
touch->y = event->tfinger.y;
touch->pressure = event->tfinger.pressure;
}
break;
}
}
}
// Pinch-to-zoom gesture
typedef struct {
float initialDistance;
float currentDistance;
float scale;
bool active;
} PinchGesture;
void updatePinchGesture(PinchGesture* gesture, TouchSystem* system) {
if (system->numTouches == 2) {
Touch* t1 = &system->touches[0];
Touch* t2 = &system->touches[1];
float dx = t2->x - t1->x;
float dy = t2->y - t1->y;
float distance = sqrtf(dx*dx + dy*dy);
if (!gesture->active) {
gesture->initialDistance = distance;
gesture->active = true;
}
gesture->currentDistance = distance;
gesture->scale = distance / gesture->initialDistance;
} else {
gesture->active = false;
}
}
// Swipe gesture detection
typedef enum {
SWIPE_NONE,
SWIPE_UP,
SWIPE_DOWN,
SWIPE_LEFT,
SWIPE_RIGHT
} SwipeDirection;
typedef struct {
float startX, startY;
float endX, endY;
bool swiping;
float minDistance;
SwipeDirection detected;
} SwipeGesture;
void initSwipeGesture(SwipeGesture* swipe) {
swipe->swiping = false;
swipe->minDistance = 0.1f; // 10% of screen
swipe->detected = SWIPE_NONE;
}
void updateSwipeGesture(SwipeGesture* swipe, TouchSystem* system) {
if (system->numTouches == 1) {
Touch* touch = &system->touches[0];
if (!swipe->swiping) {
swipe->startX = touch->x;
swipe->startY = touch->y;
swipe->swiping = true;
swipe->detected = SWIPE_NONE;
}
swipe->endX = touch->x;
swipe->endY = touch->y;
} else if (swipe->swiping) {
// Touch ended, check for swipe
float dx = swipe->endX - swipe->startX;
float dy = swipe->endY - swipe->startY;
float distance = sqrtf(dx*dx + dy*dy);
if (distance >= swipe->minDistance) {
if (fabsf(dx) > fabsf(dy)) {
swipe->detected = (dx > 0) ? SWIPE_RIGHT : SWIPE_LEFT;
} else {
swipe->detected = (dy > 0) ? SWIPE_DOWN : SWIPE_UP;
}
}
swipe->swiping = false;
}
}
Chapter 9: Timing and Frame Rate Control
9.1 Understanding Time in Games
Time management is crucial for:
- Consistent gameplay across different hardware
- Smooth animations
- Physics simulation
- Frame-rate independent logic
9.2 High-Precision Timing
// timing.c - Comprehensive timing system
#include <SDL2/SDL.h>
#include <stdio.h>
typedef struct {
Uint64 frequency;
Uint64 startTime;
Uint64 currentTime;
Uint64 lastFrameTime;
float deltaTime;
float totalTime;
int frameCount;
float fpsTimer;
float fps;
float timeScale; // For slow-motion/fast-forward
} Timer;
void initTimer(Timer* timer) {
timer->frequency = SDL_GetPerformanceFrequency();
timer->startTime = SDL_GetPerformanceCounter();
timer->lastFrameTime = timer->startTime;
timer->currentTime = timer->startTime;
timer->deltaTime = 0.0f;
timer->totalTime = 0.0f;
timer->frameCount = 0;
timer->fpsTimer = 0.0f;
timer->fps = 0.0f;
timer->timeScale = 1.0f;
}
void updateTimer(Timer* timer) {
timer->currentTime = SDL_GetPerformanceCounter();
// Calculate delta time in seconds
Uint64 elapsed = timer->currentTime - timer->lastFrameTime;
timer->deltaTime = (float)elapsed / (float)timer->frequency;
// Cap delta time to prevent spiral of death
if (timer->deltaTime > 0.1f) {
timer->deltaTime = 0.1f;
}
// Apply time scale
timer->deltaTime *= timer->timeScale;
// Update total time
timer->totalTime += timer->deltaTime;
// Calculate FPS
timer->frameCount++;
timer->fpsTimer += timer->deltaTime;
if (timer->fpsTimer >= 1.0f) {
timer->fps = timer->frameCount / timer->fpsTimer;
timer->frameCount = 0;
timer->fpsTimer = 0.0f;
}
timer->lastFrameTime = timer->currentTime;
}
float getScaledDeltaTime(Timer* timer) {
return timer->deltaTime;
}
float getUnscaledDeltaTime(Timer* timer) {
Uint64 elapsed = timer->currentTime - timer->lastFrameTime;
return (float)elapsed / (float)timer->frequency;
}
void setTimeScale(Timer* timer, float scale) {
timer->timeScale = scale;
}
// Frame rate limiter
typedef struct {
float targetFPS;
float targetFrameTime;
Uint64 lastFrameEnd;
} FrameLimiter;
void initFrameLimiter(FrameLimiter* limiter, float targetFPS) {
limiter->targetFPS = targetFPS;
limiter->targetFrameTime = 1.0f / targetFPS;
limiter->lastFrameEnd = SDL_GetPerformanceCounter();
}
void limitFrameRate(FrameLimiter* limiter) {
Uint64 currentTime = SDL_GetPerformanceCounter();
Uint64 frequency = SDL_GetPerformanceFrequency();
float frameTime = (float)(currentTime - limiter->lastFrameEnd) / frequency;
if (frameTime < limiter->targetFrameTime) {
float sleepTime = limiter->targetFrameTime - frameTime;
SDL_Delay((Uint32)(sleepTime * 1000.0f));
}
limiter->lastFrameEnd = SDL_GetPerformanceCounter();
}
// Countdown timer
typedef struct {
float duration;
float elapsed;
bool active;
bool loop;
void (*onComplete)(void* userData);
void* userData;
} Countdown;
void startCountdown(Countdown* countdown, float duration, bool loop,
void (*onComplete)(void*), void* userData) {
countdown->duration = duration;
countdown->elapsed = 0.0f;
countdown->active = true;
countdown->loop = loop;
countdown->onComplete = onComplete;
countdown->userData = userData;
}
void updateCountdown(Countdown* countdown, float dt) {
if (!countdown->active) return;
countdown->elapsed += dt;
if (countdown->elapsed >= countdown->duration) {
if (countdown->onComplete) {
countdown->onComplete(countdown->userData);
}
if (countdown->loop) {
countdown->elapsed -= countdown->duration;
} else {
countdown->active = false;
}
}
}
float getCountdownProgress(Countdown* countdown) {
return countdown->elapsed / countdown->duration;
}
// Stopwatch
typedef struct {
float elapsed;
bool running;
} Stopwatch;
void startStopwatch(Stopwatch* sw) {
sw->elapsed = 0.0f;
sw->running = true;
}
void pauseStopwatch(Stopwatch* sw) {
sw->running = false;
}
void resumeStopwatch(Stopwatch* sw) {
sw->running = true;
}
void resetStopwatch(Stopwatch* sw) {
sw->elapsed = 0.0f;
}
void updateStopwatch(Stopwatch* sw, float dt) {
if (sw->running) {
sw->elapsed += dt;
}
}
// Interpolation/Tween system
typedef enum {
EASE_LINEAR,
EASE_IN_QUAD,
EASE_OUT_QUAD,
EASE_IN_OUT_QUAD,
EASE_IN_CUBIC,
EASE_OUT_CUBIC,
EASE_IN_OUT_CUBIC,
EASE_ELASTIC
} EaseType;
float easeFunction(float t, EaseType type) {
switch (type) {
case EASE_LINEAR:
return t;
case EASE_IN_QUAD:
return t * t;
case EASE_OUT_QUAD:
return t * (2.0f - t);
case EASE_IN_OUT_QUAD:
return (t < 0.5f) ? (2.0f * t * t) : (-1.0f + (4.0f - 2.0f * t) * t);
case EASE_IN_CUBIC:
return t * t * t;
case EASE_OUT_CUBIC: {
float f = t - 1.0f;
return f * f * f + 1.0f;
}
case EASE_IN_OUT_CUBIC:
return (t < 0.5f) ? (4.0f * t * t * t) :
((t - 1.0f) * (2.0f * t - 2.0f) * (2.0f * t - 2.0f) + 1.0f);
case EASE_ELASTIC: {
if (t == 0.0f || t == 1.0f) return t;
float p = 0.3f;
return -powf(2.0f, 10.0f * (t - 1.0f)) *
sinf((t - 1.1f) * 2.0f * M_PI / p);
}
default:
return t;
}
}
typedef struct {
float* value;
float startValue;
float endValue;
float duration;
float elapsed;
EaseType easeType;
bool active;
void (*onComplete)(void* userData);
void* userData;
} Tween;
void startTween(Tween* tween, float* value, float endValue, float duration,
EaseType easeType, void (*onComplete)(void*), void* userData) {
tween->value = value;
tween->startValue = *value;
tween->endValue = endValue;
tween->duration = duration;
tween->elapsed = 0.0f;
tween->easeType = easeType;
tween->active = true;
tween->onComplete = onComplete;
tween->userData = userData;
}
void updateTween(Tween* tween, float dt) {
if (!tween->active) return;
tween->elapsed += dt;
if (tween->elapsed >= tween->duration) {
*tween->value = tween->endValue;
tween->active = false;
if (tween->onComplete) {
tween->onComplete(tween->userData);
}
} else {
float t = tween->elapsed / tween->duration;
float easedT = easeFunction(t, tween->easeType);
*tween->value = tween->startValue +
(tween->endValue - tween->startValue) * easedT;
}
}
// Example usage
void demonstrateTimingSystem() {
Timer timer;
initTimer(&timer);
FrameLimiter limiter;
initFrameLimiter(&limiter, 60.0f);
float playerX = 100.0f;
Tween moveTween;
startTween(&moveTween, &playerX, 700.0f, 2.0f, EASE_IN_OUT_QUAD, NULL, NULL);
bool running = true;
while (running) {
updateTimer(&timer);
updateTween(&moveTween, timer.deltaTime);
// Use playerX for rendering
limitFrameRate(&limiter);
}
}
9.3 Animation Timing
// animation_timing.c
#include <SDL2/SDL.h>
typedef struct {
int numFrames;
float frameDuration;
float timer;
int currentFrame;
bool loop;
bool finished;
} Animation;
void initAnimation(Animation* anim, int numFrames, float fps, bool loop) {
anim->numFrames = numFrames;
anim->frameDuration = 1.0f / fps;
anim->timer = 0.0f;
anim->currentFrame = 0;
anim->loop = loop;
anim->finished = false;
}
void updateAnimation(Animation* anim, float dt) {
if (anim->finished && !anim->loop) return;
anim->timer += dt;
while (anim->timer >= anim->frameDuration) {
anim->timer -= anim->frameDuration;
anim->currentFrame++;
if (anim->currentFrame >= anim->numFrames) {
if (anim->loop) {
anim->currentFrame = 0;
} else {
anim->currentFrame = anim->numFrames - 1;
anim->finished = true;
}
}
}
}
void resetAnimation(Animation* anim) {
anim->currentFrame = 0;
anim->timer = 0.0f;
anim->finished = false;
}
int getCurrentFrame(Animation* anim) {
return anim->currentFrame;
}
// Sprite animation system
typedef struct {
SDL_Texture* texture;
Animation animation;
int frameWidth;
int frameHeight;
int framesPerRow;
} AnimatedSprite;
void renderAnimatedSprite(SDL_Renderer* renderer, AnimatedSprite* sprite,
int x, int y) {
int frame = getCurrentFrame(&sprite->animation);
int row = frame / sprite->framesPerRow;
int col = frame % sprite->framesPerRow;
SDL_Rect src = {
col * sprite->frameWidth,
row * sprite->frameHeight,
sprite->frameWidth,
sprite->frameHeight
};
SDL_Rect dest = {
x - sprite->frameWidth / 2,
y - sprite->frameHeight / 2,
sprite->frameWidth,
sprite->frameHeight
};
SDL_RenderCopy(renderer, sprite->texture, &src, &dest);
}
Chapter 10: Sprites and Texture Management
10.1 Sprite Fundamentals
A sprite is a 2D image or animation that represents a game object. Managing sprites efficiently is essential for performance.
// sprite_system.c
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <string.h>
#include <stdbool.h>
typedef struct {
SDL_Texture* texture;
int width;
int height;
char name[64];
} SpriteData;
typedef struct {
SpriteData* data;
float x, y;
float rotation;
float scaleX, scaleY;
SDL_Color tint;
Uint8 alpha;
bool flipH, flipV;
} Sprite;
typedef struct {
SpriteData sprites[256];
int count;
SDL_Renderer* renderer;
} SpriteManager;
void initSpriteManager(SpriteManager* manager, SDL_Renderer* renderer) {
manager->count = 0;
manager->renderer = renderer;
memset(manager->sprites, 0, sizeof(manager->sprites));
}
SpriteData* loadSpriteData(SpriteManager* manager, const char* filename,
const char* name) {
// Check if already loaded
for (int i = 0; i < manager->count; i++) {
if (strcmp(manager->sprites[i].name, name) == 0) {
return &manager->sprites[i];
}
}
if (manager->count >= 256) {
printf("Sprite manager full!\n");
return NULL;
}
SDL_Surface* surface = IMG_Load(filename);
if (!surface) {
printf("Failed to load %s: %s\n", filename, IMG_GetError());
return NULL;
}
SpriteData* data = &manager->sprites[manager->count++];
data->texture = SDL_CreateTextureFromSurface(manager->renderer, surface);
data->width = surface->w;
data->height = surface->h;
strncpy(data->name, name, sizeof(data->name) - 1);
SDL_FreeSurface(surface);
return data;
}
void createSprite(Sprite* sprite, SpriteData* data, float x, float y) {
sprite->data = data;
sprite->x = x;
sprite->y = y;
sprite->rotation = 0.0f;
sprite->scaleX = 1.0f;
sprite->scaleY = 1.0f;
sprite->tint = (SDL_Color){255, 255, 255, 255};
sprite->alpha = 255;
sprite->flipH = false;
sprite->flipV = false;
}
void renderSprite(SDL_Renderer* renderer, Sprite* sprite) {
if (!sprite->data || !sprite->data->texture) return;
SDL_Rect dest = {
(int)(sprite->x - sprite->data->width * sprite->scaleX / 2),
(int)(sprite->y - sprite->data->height * sprite->scaleY / 2),
(int)(sprite->data->width * sprite->scaleX),
(int)(sprite->data->height * sprite->scaleY)
};
SDL_Point center = {dest.w / 2, dest.h / 2};
// Apply tint and alpha
SDL_SetTextureColorMod(sprite->data->texture,
sprite->tint.r, sprite->tint.g, sprite->tint.b);
SDL_SetTextureAlphaMod(sprite->data->texture, sprite->alpha);
SDL_RendererFlip flip = SDL_FLIP_NONE;
if (sprite->flipH) flip |= SDL_FLIP_HORIZONTAL;
if (sprite->flipV) flip |= SDL_FLIP_VERTICAL;
SDL_RenderCopyEx(renderer, sprite->data->texture, NULL, &dest,
sprite->rotation, ¢er, flip);
}
void destroySpriteManager(SpriteManager* manager) {
for (int i = 0; i < manager->count; i++) {
if (manager->sprites[i].texture) {
SDL_DestroyTexture(manager->sprites[i].texture);
}
}
manager->count = 0;
}
10.2 Sprite Sheets and Atlases
Sprite sheets combine multiple sprites into one texture for efficiency:
// sprite_sheet.c
#include <SDL2/SDL.h>
typedef struct {
int x, y, w, h;
int originX, originY; // Pivot point
} SpriteFrame;
typedef struct {
SDL_Texture* texture;
SpriteFrame* frames;
int frameCount;
} SpriteSheet;
SpriteSheet* createSpriteSheet(SDL_Renderer* renderer, const char* filename,
int frameWidth, int frameHeight, int numFrames) {
SpriteSheet* sheet = malloc(sizeof(SpriteSheet));
SDL_Surface* surface = IMG_Load(filename);
if (!surface) {
free(sheet);
return NULL;
}
sheet->texture = SDL_CreateTextureFromSurface(renderer, surface);
int columns = surface->w / frameWidth;
int rows = (numFrames + columns - 1) / columns;
sheet->frames = malloc(sizeof(SpriteFrame) * numFrames);
sheet->frameCount = numFrames;
for (int i = 0; i < numFrames; i++) {
int row = i / columns;
int col = i % columns;
sheet->frames[i].x = col * frameWidth;
sheet->frames[i].y = row * frameHeight;
sheet->frames[i].w = frameWidth;
sheet->frames[i].h = frameHeight;
sheet->frames[i].originX = frameWidth / 2;
sheet->frames[i].originY = frameHeight / 2;
}
SDL_FreeSurface(surface);
return sheet;
}
void renderSpriteSheetFrame(SDL_Renderer* renderer, SpriteSheet* sheet,
int frameIndex, float x, float y,
float scale, float rotation) {
if (frameIndex < 0 || frameIndex >= sheet->frameCount) return;
SpriteFrame* frame = &sheet->frames[frameIndex];
SDL_Rect src = {frame->x, frame->y, frame->w, frame->h};
SDL_Rect dest = {
(int)(x - frame->originX * scale),
(int)(y - frame->originY * scale),
(int)(frame->w * scale),
(int)(frame->h * scale)
};
SDL_Point center = {
(int)(frame->originX * scale),
(int)(frame->originY * scale)
};
SDL_RenderCopyEx(renderer, sheet->texture, &src, &dest,
rotation, ¢er, SDL_FLIP_NONE);
}
void destroySpriteSheet(SpriteSheet* sheet) {
if (sheet) {
SDL_DestroyTexture(sheet->texture);
free(sheet->frames);
free(sheet);
}
}
// Texture atlas with named sprites
typedef struct {
char name[64];
SDL_Rect rect;
int originX, originY;
} AtlasEntry;
typedef struct {
SDL_Texture* texture;
AtlasEntry* entries;
int entryCount;
} TextureAtlas;
TextureAtlas* loadTextureAtlas(SDL_Renderer* renderer, const char* imageFile,
const char* dataFile) {
TextureAtlas* atlas = malloc(sizeof(TextureAtlas));
// Load texture
SDL_Surface* surface = IMG_Load(imageFile);
if (!surface) {
free(atlas);
return NULL;
}
atlas->texture = SDL_CreateTextureFromSurface(renderer, surface);
SDL_FreeSurface(surface);
// Load atlas data (custom format)
FILE* file = fopen(dataFile, "r");
if (!file) {
SDL_DestroyTexture(atlas->texture);
free(atlas);
return NULL;
}
fscanf(file, "%d", &atlas->entryCount);
atlas->entries = malloc(sizeof(AtlasEntry) * atlas->entryCount);
for (int i = 0; i < atlas->entryCount; i++) {
AtlasEntry* entry = &atlas->entries[i];
fscanf(file, "%s %d %d %d %d %d %d",
entry->name,
&entry->rect.x, &entry->rect.y,
&entry->rect.w, &entry->rect.h,
&entry->originX, &entry->originY);
}
fclose(file);
return atlas;
}
AtlasEntry* findAtlasEntry(TextureAtlas* atlas, const char* name) {
for (int i = 0; i < atlas->entryCount; i++) {
if (strcmp(atlas->entries[i].name, name) == 0) {
return &atlas->entries[i];
}
}
return NULL;
}
void renderAtlasSprite(SDL_Renderer* renderer, TextureAtlas* atlas,
const char* spriteName, float x, float y) {
AtlasEntry* entry = findAtlasEntry(atlas, spriteName);
if (!entry) return;
SDL_Rect dest = {
(int)(x - entry->originX),
(int)(y - entry->originY),
entry->rect.w,
entry->rect.h
};
SDL_RenderCopy(renderer, atlas->texture, &entry->rect, &dest);
}
10.3 Sprite Batching for Performance
// sprite_batch.c
#include <SDL2/SDL.h>
#define MAX_BATCH_SIZE 10000
typedef struct {
SDL_Texture* texture;
SDL_Rect src;
SDL_Rect dest;
float rotation;
SDL_Point center;
SDL_Color color;
Uint8 alpha;
} BatchedSprite;
typedef struct {
BatchedSprite sprites[MAX_BATCH_SIZE];
int count;
SDL_Renderer* renderer;
SDL_Texture* currentTexture;
} SpriteBatch;
void initSpriteBatch(SpriteBatch* batch, SDL_Renderer* renderer) {
batch->count = 0;
batch->renderer = renderer;
batch->currentTexture = NULL;
}
void beginBatch(SpriteBatch* batch) {
batch->count = 0;
batch->currentTexture = NULL;
}
void addToBatch(SpriteBatch* batch, SDL_Texture* texture,
SDL_Rect* src, SDL_Rect* dest,
float rotation, SDL_Point* center,
SDL_Color color, Uint8 alpha) {
if (batch->count >= MAX_BATCH_SIZE) {
flushBatch(batch);
}
// If texture changed, flush
if (batch->currentTexture && batch->currentTexture != texture) {
flushBatch(batch);
}
batch->currentTexture = texture;
BatchedSprite* sprite = &batch->sprites[batch->count++];
sprite->texture = texture;
sprite->src = src ? *src : (SDL_Rect){0, 0, 0, 0};
sprite->dest = *dest;
sprite->rotation = rotation;
sprite->center = center ? *center : (SDL_Point){0, 0};
sprite->color = color;
sprite->alpha = alpha;
}
void flushBatch(SpriteBatch* batch) {
if (batch->count == 0) return;
for (int i = 0; i < batch->count; i++) {
BatchedSprite* sprite = &batch->sprites[i];
SDL_SetTextureColorMod(sprite->texture,
sprite->color.r, sprite->color.g, sprite->color.b);
SDL_SetTextureAlphaMod(sprite->texture, sprite->alpha);
SDL_RenderCopyEx(batch->renderer, sprite->texture,
&sprite->src, &sprite->dest,
sprite->rotation, &sprite->center,
SDL_FLIP_NONE);
}
batch->count = 0;
}
void endBatch(SpriteBatch* batch) {
flushBatch(batch);
}
Chapter 11: Collision Detection Systems
11.1 Collision Detection Fundamentals
Collision detection determines when two game objects intersect. Different shapes require different algorithms.
// collision.c
#include <SDL2/SDL.h>
#include <math.h>
#include <stdbool.h>
// Point-Rectangle collision
bool pointInRect(int px, int py, SDL_Rect* rect) {
return px >= rect->x && px < rect->x + rect->w &&
py >= rect->y && py < rect->y + rect->h;
}
// Rectangle-Rectangle collision (AABB)
bool rectIntersect(SDL_Rect* a, SDL_Rect* b) {
return a->x < b->x + b->w &&
a->x + a->w > b->x &&
a->y < b->y + b->h &&
a->y + a->h > b->y;
}
// Circle-Circle collision
typedef struct {
float x, y;
float radius;
} Circle;
bool circleIntersect(Circle* a, Circle* b) {
float dx = b->x - a->x;
float dy = b->y - a->y;
float distanceSquared = dx * dx + dy * dy;
float radiusSum = a->radius + b->radius;
return distanceSquared < radiusSum * radiusSum;
}
// Circle-Rectangle collision
bool circleRectIntersect(Circle* circle, SDL_Rect* rect) {
// Find closest point on rectangle to circle center
float closestX = fmax(rect->x, fmin(circle->x, rect->x + rect->w));
float closestY = fmax(rect->y, fmin(circle->y, rect->y + rect->h));
// Calculate distance
float dx = circle->x - closestX;
float dy = circle->y - closestY;
float distanceSquared = dx * dx + dy * dy;
return distanceSquared < circle->radius * circle->radius;
}
// Line-Line intersection
typedef struct {
float x1, y1, x2, y2;
} Line;
bool lineIntersect(Line* a, Line* b, float* outX, float* outY) {
float s1_x = a->x2 - a->x1;
float s1_y = a->y2 - a->y1;
float s2_x = b->x2 - b->x1;
float s2_y = b->y2 - b->y1;
float denom = (-s2_x * s1_y + s1_x * s2_y);
if (fabs(denom) < 0.0001f) return false; // Parallel
float s = (-s1_y * (a->x1 - b->x1) + s1_x * (a->y1 - b->y1)) / denom;
float t = ( s2_x * (a->y1 - b->y1) - s2_y * (a->x1 - b->x1)) / denom;
if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
// Collision detected
if (outX) *outX = a->x1 + (t * s1_x);
if (outY) *outY = a->y1 + (t * s1_y);
return true;
}
return false;
}
// Polygon collision (SAT - Separating Axis Theorem)
typedef struct {
float x, y;
} Vector2;
typedef struct {
Vector2* vertices;
int vertexCount;
} Polygon;
void projectPolygon(Polygon* poly, Vector2* axis, float* min, float* max) {
float dot = poly->vertices[0].x * axis->x + poly->vertices[0].y * axis->y;
*min = *max = dot;
for (int i = 1; i < poly->vertexCount; i++) {
dot = poly->vertices[i].x * axis->x + poly->vertices[i].y * axis->y;
if (dot < *min) *min = dot;
if (dot > *max) *max = dot;
}
}
bool polygonIntersect(Polygon* a, Polygon* b) {
// Check axes from polygon A
for (int i = 0; i < a->vertexCount; i++) {
int j = (i + 1) % a->vertexCount;
// Get edge vector
float edgeX = a->vertices[j].x - a->vertices[i].x;
float edgeY = a->vertices[j].y - a->vertices[i].y;
// Get perpendicular (normal)
Vector2 axis = {-edgeY, edgeX};
// Normalize
float length = sqrtf(axis.x * axis.x + axis.y * axis.y);
axis.x /= length;
axis.y /= length;
// Project both polygons onto axis
float minA, maxA, minB, maxB;
projectPolygon(a, &axis, &minA, &maxA);
projectPolygon(b, &axis, &minB, &maxB);
// Check for gap
if (maxA < minB || maxB < minA) {
return false; // Separating axis found
}
}
// Check axes from polygon B
for (int i = 0; i < b->vertexCount; i++) {
int j = (i + 1) % b->vertexCount;
float edgeX = b->vertices[j].x - b->vertices[i].x;
float edgeY = b->vertices[j].y - b->vertices[i].y;
Vector2 axis = {-edgeY, edgeX};
float length = sqrtf(axis.x * axis.x + axis.y * axis.y);
axis.x /= length;
axis.y /= length;
float minA, maxA, minB, maxB;
projectPolygon(a, &axis, &minA, &maxA);
projectPolygon(b, &axis, &minB, &maxB);
if (maxA < minB || maxB < minA) {
return false;
}
}
return true; // No separating axis found, polygons intersect
}
11.2 Collision Response
// collision_response.c
#include <SDL2/SDL.h>
#include <math.h>
// Resolve AABB collision
void resolveAABBCollision(SDL_Rect* moving, SDL_Rect* stationary,
float* vx, float* vy) {
// Calculate overlap
int overlapX, overlapY;
if (moving->x < stationary->x) {
overlapX = (moving->x + moving->w) - stationary->x;
} else {
overlapX = stationary->x + stationary->w - moving->x;
}
if (moving->y < stationary->y) {
overlapY = (moving->y + moving->h) - stationary->y;
} else {
overlapY = stationary->y + stationary->h - moving->y;
}
// Resolve along shortest axis
if (overlapX < overlapY) {
if (moving->x < stationary->x) {
moving->x -= overlapX;
} else {
moving->x += overlapX;
}
*vx = -*vx * 0.5f; // Bounce with energy loss
} else {
if (moving->y < stationary->y) {
moving->y -= overlapY;
} else {
moving->y += overlapY;
}
*vy = -*vy * 0.5f;
}
}
// Elastic collision between circles
void resolveCircleCollision(Circle* a, Circle* b,
float* vx_a, float* vy_a,
float* vx_b, float* vy_b,
float mass_a, float mass_b) {
float dx = b->x - a->x;
float dy = b->y - a->y;
float distance = sqrtf(dx * dx + dy * dy);
if (distance == 0) return;
// Normalize collision normal
float nx = dx / distance;
float ny = dy / distance;
// Relative velocity
float dvx = *vx_a - *vx_b;
float dvy = *vy_a - *vy_b;
// Relative velocity along normal
float dvn = dvx * nx + dvy * ny;
// Don't resolve if moving apart
if (dvn > 0) return;
// Coefficient of restitution (bounciness)
float restitution = 0.8f;
// Calculate impulse
float impulse = (-(1.0f + restitution) * dvn) / (1.0f/mass_a + 1.0f/mass_b);
// Apply impulse
*vx_a += (impulse / mass_a) * nx;
*vy_a += (impulse / mass_a) * ny;
*vx_b -= (impulse / mass_b) * nx;
*vy_b -= (impulse / mass_b) * ny;
// Separate circles
float overlap = (a->radius + b->radius) - distance;
float separationRatio = overlap / distance;
a->x -= nx * separationRatio * a->radius;
a->y -= ny * separationRatio * a->radius;
b->x += nx * separationRatio * b->radius;
b->y += ny * separationRatio * b->radius;
}
// Sweep test for continuous collision
bool sweepAABB(SDL_Rect* moving, float vx, float vy,
SDL_Rect* stationary, float* hitTime) {
// Minkowski difference
SDL_Rect expanded = {
stationary->x - moving->w,
stationary->y - moving->h,
stationary->w + moving->w,
stationary->h + moving->h
};
// Ray cast
float tmin = 0.0f;
float tmax = 1.0f;
if (fabsf(vx) < 0.0001f && fabsf(vy) < 0.0001f) {
return false; // Not moving
}
// X axis
if (fabsf(vx) > 0.0001f) {
float t1 = (expanded.x - moving->x) / vx;
float t2 = (expanded.x + expanded.w - moving->x) / vx;
if (t1 > t2) { float t = t1; t1 = t2; t2 = t; }
tmin = fmax(tmin, t1);
tmax = fmin(tmax, t2);
if (tmin > tmax) return false;
}
// Y axis
if (fabsf(vy) > 0.0001f) {
float t1 = (expanded.y - moving->y) / vy;
float t2 = (expanded.y + expanded.h - moving->y) / vy;
if (t1 > t2) { float t = t1; t1 = t2; t2 = t; }
tmin = fmax(tmin, t1);
tmax = fmin(tmax, t2);
if (tmin > tmax) return false;
}
*hitTime = tmin;
return tmin >= 0.0f && tmin <= 1.0f;
}
11.3 Spatial Partitioning - Grid
// spatial_grid.c
#include <SDL2/SDL.h>
#include <stdlib.h>
#define GRID_CELL_SIZE 64
typedef struct GameObject GameObject;
struct GameObject {
SDL_Rect bounds;
int id;
GameObject* nextInCell; // Linked list for grid cell
};
typedef struct {
GameObject* objects;
int width, height;
int cellSize;
int gridWidth, gridHeight;
} SpatialGrid;
void initSpatialGrid(SpatialGrid* grid, int worldWidth, int worldHeight, int cellSize) {
grid->width = worldWidth;
grid->height = worldHeight;
grid->cellSize = cellSize;
grid->gridWidth = (worldWidth + cellSize - 1) / cellSize;
grid->gridHeight = (worldHeight + cellSize - 1) / cellSize;
grid->objects = calloc(grid->gridWidth * grid->gridHeight, sizeof(GameObject));
}
int getCellIndex(SpatialGrid* grid, int x, int y) {
if (x < 0 || x >= grid->gridWidth || y < 0 || y >= grid->gridHeight) {
return -1;
}
return y * grid->gridWidth + x;
}
void insertObject(SpatialGrid* grid, GameObject* obj) {
int cellX = obj->bounds.x / grid->cellSize;
int cellY = obj->bounds.y / grid->cellSize;
int idx = getCellIndex(grid, cellX, cellY);
if (idx < 0) return;
obj->nextInCell = grid->objects[idx].nextInCell;
grid->objects[idx].nextInCell = obj;
}
void queryGrid(SpatialGrid* grid, SDL_Rect* area,
void (*callback)(GameObject* obj, void* userData),
void* userData) {
int startX = area->x / grid->cellSize;
int startY = area->y / grid->cellSize;
int endX = (area->x + area->w) / grid->cellSize;
int endY = (area->y + area->h) / grid->cellSize;
for (int y = startY; y <= endY; y++) {
for (int x = startX; x <= endX; x++) {
int idx = getCellIndex(grid, x, y);
if (idx < 0) continue;
GameObject* obj = grid->objects[idx].nextInCell;
while (obj) {
if (rectIntersect(&obj->bounds, area)) {
callback(obj, userData);
}
obj = obj->nextInCell;
}
}
}
}
void clearGrid(SpatialGrid* grid) {
for (int i = 0; i < grid->gridWidth * grid->gridHeight; i++) {
grid->objects[i].nextInCell = NULL;
}
}
void destroySpatialGrid(SpatialGrid* grid) {
free(grid->objects);
}
11.4 Quadtree
// quadtree.c
#include <SDL2/SDL.h>
#include <stdlib.h>
#define MAX_OBJECTS_PER_NODE 4
#define MAX_LEVELS 5
typedef struct QuadtreeNode QuadtreeNode;
typedef struct {
SDL_Rect bounds;
void* userData;
} QuadObject;
struct QuadtreeNode {
int level;
SDL_Rect bounds;
QuadObject objects[MAX_OBJECTS_PER_NODE];
int objectCount;
QuadtreeNode* children[4]; // NW, NE, SW, SE
};
QuadtreeNode* createQuadtreeNode(int level, SDL_Rect bounds) {
QuadtreeNode* node = calloc(1, sizeof(QuadtreeNode));
node->level = level;
node->bounds = bounds;
node->objectCount = 0;
for (int i = 0; i < 4; i++) {
node->children[i] = NULL;
}
return node;
}
void subdivide(QuadtreeNode* node) {
int subWidth = node->bounds.w / 2;
int subHeight = node->bounds.h / 2;
int x = node->bounds.x;
int y = node->bounds.y;
// Northwest
node->children[0] = createQuadtreeNode(node->level + 1,
(SDL_Rect){x, y, subWidth, subHeight});
// Northeast
node->children[1] = createQuadtreeNode(node->level + 1,
(SDL_Rect){x + subWidth, y, subWidth, subHeight});
// Southwest
node->children[2] = createQuadtreeNode(node->level + 1,
(SDL_Rect){x, y + subHeight, subWidth, subHeight});
// Southeast
node->children[3] = createQuadtreeNode(node->level + 1,
(SDL_Rect){x + subWidth, y + subHeight, subWidth, subHeight});
}
int getChildIndex(QuadtreeNode* node, SDL_Rect* objBounds) {
int midX = node->bounds.x + node->bounds.w / 2;
int midY = node->bounds.y + node->bounds.h / 2;
bool topHalf = (objBounds->y + objBounds->h < midY);
bool bottomHalf = (objBounds->y >= midY);
bool leftHalf = (objBounds->x + objBounds->w < midX);
bool rightHalf = (objBounds->x >= midX);
if (topHalf && leftHalf) return 0; // NW
if (topHalf && rightHalf) return 1; // NE
if (bottomHalf && leftHalf) return 2; // SW
if (bottomHalf && rightHalf) return 3; // SE
return -1; // Doesn't fit in one quadrant
}
void insertIntoQuadtree(QuadtreeNode* node, QuadObject object) {
// If we have children, try to insert into them
if (node->children[0]) {
int index = getChildIndex(node, &object.bounds);
if (index != -1) {
insertIntoQuadtree(node->children[index], object);
return;
}
}
// Add to this node
node->objects[node->objectCount++] = object;
// Split if necessary
if (node->objectCount > MAX_OBJECTS_PER_NODE &&
node->level < MAX_LEVELS &&
!node->children[0]) {
subdivide(node);
// Redistribute objects
int i = 0;
while (i < node->objectCount) {
int index = getChildIndex(node, &node->objects[i].bounds);
if (index != -1) {
insertIntoQuadtree(node->children[index], node->objects[i]);
// Remove from this node
node->objects[i] = node->objects[--node->objectCount];
} else {
i++;
}
}
}
}
void queryQuadtree(QuadtreeNode* node, SDL_Rect* area,
QuadObject* results, int* resultCount, int maxResults) {
if (!rectIntersect(&node->bounds, area)) return;
// Check objects in this node
for (int i = 0; i < node->objectCount && *resultCount < maxResults; i++) {
if (rectIntersect(&node->objects[i].bounds, area)) {
results[(*resultCount)++] = node->objects[i];
}
}
// Check children
if (node->children[0]) {
for (int i = 0; i < 4 && *resultCount < maxResults; i++) {
queryQuadtree(node->children[i], area, results, resultCount, maxResults);
}
}
}
void destroyQuadtree(QuadtreeNode* node) {
if (node->children[0]) {
for (int i = 0; i < 4; i++) {
destroyQuadtree(node->children[i]);
}
}
free(node);
}
Chapter 12: Particle Systems and Effects
12.1 Particle System Fundamentals
Particle systems create effects like fire, smoke, explosions, and magic by managing thousands of small sprites.
// particle_system.c
#include <SDL2/SDL.h>
#include <stdlib.h>
#include <math.h>
#include <stdbool.h>
typedef struct {
float x, y;
float vx, vy;
float ax, ay;
float life; // Current life
float lifetime; // Total lifetime
float size;
float rotation;
float rotationSpeed;
SDL_Color color;
float alpha;
bool active;
} Particle;
typedef struct {
Particle* particles;
int maxParticles;
int activeParticles;
// Emitter properties
float emitX, emitY;
float emitRate; // Particles per second
float emitTimer;
// Particle initialization ranges
float lifetimeMin, lifetimeMax;
float sizeMin, sizeMax;
float speedMin, speedMax;
float angleMin, angleMax; // Emission angle in radians
float gravityX, gravityY;
SDL_Color colorStart;
SDL_Color colorEnd;
bool active;
} ParticleSystem;
ParticleSystem* createParticleSystem(int maxParticles) {
ParticleSystem* ps = malloc(sizeof(ParticleSystem));
ps->particles = malloc(sizeof(Particle) * maxParticles);
ps->maxParticles = maxParticles;
ps->activeParticles = 0;
// Default values
ps->emitX = 0;
ps->emitY = 0;
ps->emitRate = 100.0f;
ps->emitTimer = 0.0f;
ps->lifetimeMin = 1.0f;
ps->lifetimeMax = 3.0f;
ps->sizeMin = 2.0f;
ps->sizeMax = 5.0f;
ps->speedMin = 50.0f;
ps->speedMax = 150.0f;
ps->angleMin = 0.0f;
ps->angleMax = 2.0f * M_PI;
ps->gravityX = 0.0f;
ps->gravityY = 0.0f;
ps->colorStart = (SDL_Color){255, 255, 255, 255};
ps->colorEnd = (SDL_Color){255, 255, 255, 0};
ps->active = true;
// Initialize particles as inactive
for (int i = 0; i < maxParticles; i++) {
ps->particles[i].active = false;
}
return ps;
}
float randomFloat(float min, float max) {
return min + (max - min) * ((float)rand() / RAND_MAX);
}
void emitParticle(ParticleSystem* ps) {
// Find inactive particle
for (int i = 0; i < ps->maxParticles; i++) {
if (!ps->particles[i].active) {
Particle* p = &ps->particles[i];
// Initialize particle
p->x = ps->emitX;
p->y = ps->emitY;
float angle = randomFloat(ps->angleMin, ps->angleMax);
float speed = randomFloat(ps->speedMin, ps->speedMax);
p->vx = cosf(angle) * speed;
p->vy = sinf(angle) * speed;
p->ax = ps->gravityX;
p->ay = ps->gravityY;
p->lifetime = randomFloat(ps->lifetimeMin, ps->lifetimeMax);
p->life = p->lifetime;
p->size = randomFloat(ps->sizeMin, ps->sizeMax);
p->rotation = randomFloat(0, 2.0f * M_PI);
p->rotationSpeed = randomFloat(-2.0f, 2.0f);
p->color = ps->colorStart;
p->alpha = 1.0f;
p->active = true;
ps->activeParticles++;
return;
}
}
}
void updateParticleSystem(ParticleSystem* ps, float dt) {
// Emit particles
if (ps->active) {
ps->emitTimer += dt;
float emitInterval = 1.0f / ps->emitRate;
while (ps->emitTimer >= emitInterval) {
emitParticle(ps);
ps->emitTimer -= emitInterval;
}
}
// Update particles
for (int i = 0; i < ps->maxParticles; i++) {
Particle* p = &ps->particles[i];
if (!p->active) continue;
// Update life
p->life -= dt;
if (p->life <= 0) {
p->active = false;
ps->activeParticles--;
continue;
}
// Update physics
p->vx += p->ax * dt;
p->vy += p->ay * dt;
p->x += p->vx * dt;
p->y += p->vy * dt;
p->rotation += p->rotationSpeed * dt;
// Interpolate color
float t = 1.0f - (p->life / p->lifetime);
p->color.r = (Uint8)(ps->colorStart.r + (ps->colorEnd.r - ps->colorStart.r) * t);
p->color.g = (Uint8)(ps->colorStart.g + (ps->colorEnd.g - ps->colorStart.g) * t);
p->color.b = (Uint8)(ps->colorStart.b + (ps->colorEnd.b - ps->colorStart.b) * t);
p->alpha = 1.0f - t;
}
}
void renderParticleSystem(SDL_Renderer* renderer, ParticleSystem* ps) {
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
for (int i = 0; i < ps->maxParticles; i++) {
Particle* p = &ps->particles[i];
if (!p->active) continue;
SDL_SetRenderDrawColor(renderer,
p->color.r, p->color.g, p->color.b,
(Uint8)(p->alpha * 255));
// Draw as filled circle (approximate with rectangles)
int segments = 8;
for (int j = 0; j < segments; j++) {
float angle1 = (2.0f * M_PI * j) / segments;
float angle2 = (2.0f * M_PI * (j + 1)) / segments;
int x1 = (int)(p->x + cosf(angle1) * p->size);
int y1 = (int)(p->y + sinf(angle1) * p->size);
int x2 = (int)(p->x + cosf(angle2) * p->size);
int y2 = (int)(p->y + sinf(angle2) * p->size);
SDL_RenderDrawLine(renderer, x1, y1, x2, y2);
}
}
}
void destroyParticleSystem(ParticleSystem* ps) {
free(ps->particles);
free(ps);
}
// Preset: Fire effect
ParticleSystem* createFireEffect(float x, float y) {
ParticleSystem* fire = createParticleSystem(500);
fire->emitX = x;
fire->emitY = y;
fire->emitRate = 200.0f;
fire->lifetimeMin = 0.5f;
fire->lifetimeMax = 1.5f;
fire->speedMin = 20.0f;
fire->speedMax = 50.0f;
fire->angleMin = -M_PI / 4;
fire->angleMax = -3 * M_PI / 4;
fire->gravityY = -30.0f;
fire->sizeMin = 3.0f;
fire->sizeMax = 7.0f;
fire->colorStart = (SDL_Color){255, 255, 100, 255};
fire->colorEnd = (SDL_Color){255, 50, 0, 0};
return fire;
}
// Preset: Explosion
void createExplosion(ParticleSystem* ps, float x, float y) {
ps->emitX = x;
ps->emitY = y;
ps->active = false; // One-shot
// Emit burst of particles
for (int i = 0; i < 100; i++) {
emitParticle(ps);
}
}
// Preset: Smoke trail
ParticleSystem* createSmokeTrail() {
ParticleSystem* smoke = createParticleSystem(200);
smoke->emitRate = 50.0f;
smoke->lifetimeMin = 1.0f;
smoke->lifetimeMax = 2.0f;
smoke->speedMin = 10.0f;
smoke->speedMax = 30.0f;
smoke->angleMin = -M_PI / 6;
smoke->angleMax = -5 * M_PI / 6;
smoke->gravityY = -20.0f;
smoke->sizeMin = 2.0f;
smoke->sizeMax = 8.0f;
smoke->colorStart = (SDL_Color){200, 200, 200, 200};
smoke->colorEnd = (SDL_Color){100, 100, 100, 0};
return smoke;
}
12.2 Advanced Particle Techniques
// advanced_particles.c
#include <SDL2/SDL.h>
// Particle with texture
typedef struct {
float x, y;
float vx, vy;
float life, lifetime;
float size;
float rotation;
SDL_Texture* texture;
SDL_Color tint;
float alpha;
bool active;
} TexturedParticle;
typedef struct {
TexturedParticle* particles;
SDL_Texture** textures;
int numTextures;
int maxParticles;
} TexturedParticleSystem;
void renderTexturedParticles(SDL_Renderer* renderer, TexturedParticleSystem* ps) {
for (int i = 0; i < ps->maxParticles; i++) {
TexturedParticle* p = &ps->particles[i];
if (!p->active || !p->texture) continue;
int w, h;
SDL_QueryTexture(p->texture, NULL, NULL, &w, &h);
SDL_Rect dest = {
(int)(p->x - w * p->size / 2),
(int)(p->y - h * p->size / 2),
(int)(w * p->size),
(int)(h * p->size)
};
SDL_SetTextureColorMod(p->texture, p->tint.r, p->tint.g, p->tint.b);
SDL_SetTextureAlphaMod(p->texture, (Uint8)(p->alpha * 255));
SDL_Point center = {dest.w / 2, dest.h / 2};
SDL_RenderCopyEx(renderer, p->texture, NULL, &dest,
p->rotation * 180.0 / M_PI, ¢er, SDL_FLIP_NONE);
}
}
// Particle forces
typedef struct {
float x, y; // Position
float strength; // Positive = attract, negative = repel
float radius;
} ParticleForce;
void applyForceToParticle(Particle* p, ParticleForce* force) {
float dx = force->x - p->x;
float dy = force->y - p->y;
float distSq = dx * dx + dy * dy;
if (distSq > force->radius * force->radius || distSq < 0.1f) return;
float dist = sqrtf(distSq);
float forceMagnitude = force->strength / distSq;
p->ax += (dx / dist) * forceMagnitude;
p->ay += (dy / dist) * forceMagnitude;
}
// Particle affector system
typedef enum {
AFFECTOR_GRAVITY,
AFFECTOR_WIND,
AFFECTOR_TURBULENCE,
AFFECTOR_ATTRACTOR,
AFFECTOR_VORTEX
} AffectorType;
typedef struct {
AffectorType type;
float x, y;
float strength;
float radius;
void* customData;
} ParticleAffector;
void applyAffector(Particle* p, ParticleAffector* affector, float dt) {
switch (affector->type) {
case AFFECTOR_GRAVITY:
p->ay += affector->strength;
break;
case AFFECTOR_WIND:
p->ax += affector->strength * dt;
break;
case AFFECTOR_TURBULENCE: {
float noise = ((float)rand() / RAND_MAX) * 2.0f - 1.0f;
p->ax += noise * affector->strength;
p->ay += noise * affector->strength;
break;
}
case AFFECTOR_ATTRACTOR: {
float dx = affector->x - p->x;
float dy = affector->y - p->y;
float distSq = dx * dx + dy * dy;
if (distSq < affector->radius * affector->radius && distSq > 0.1f) {
float dist = sqrtf(distSq);
float force = affector->strength / distSq;
p->ax += (dx / dist) * force;
p->ay += (dy / dist) * force;
}
break;
}
case AFFECTOR_VORTEX: {
float dx = p->x - affector->x;
float dy = p->y - affector->y;
float distSq = dx * dx + dy * dy;
if (distSq < affector->radius * affector->radius && distSq > 0.1f) {
float dist = sqrtf(distSq);
// Tangential force
p->ax += -dy / dist * affector->strength;
p->ay += dx / dist * affector->strength;
}
break;
}
}
}
// Particle collision with environment
void updateParticleCollisions(Particle* p, SDL_Rect* bounds, float restitution) {
// Bottom
if (p->y + p->size > bounds->y + bounds->h) {
p->y = bounds->y + bounds->h - p->size;
p->vy = -p->vy * restitution;
p->vx *= 0.95f; // Friction
}
// Top
if (p->y - p->size < bounds->y) {
p->y = bounds->y + p->size;
p->vy = -p->vy * restitution;
}
// Right
if (p->x + p->size > bounds->x + bounds->w) {
p->x = bounds->x + bounds->w - p->size;
p->vx = -p->vx * restitution;
}
// Left
if (p->x - p->size < bounds->x) {
p->x = bounds->x + p->size;
p->vx = -p->vx * restitution;
}
}
Chapter 13: Audio Programming
13.1 SDL_mixer Basics
Audio is crucial for immersive games. SDL_mixer provides easy audio playback.
// audio_system.c
#include <SDL2/SDL.h>
#include <SDL2/SDL_mixer.h>
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#define MAX_SOUNDS 64
#define MAX_MUSIC 8
typedef struct {
Mix_Chunk* chunk;
char name[64];
int refCount;
} SoundEffect;
typedef struct {
Mix_Music* music;
char name[64];
} MusicTrack;
typedef struct {
SoundEffect sounds[MAX_SOUNDS];
int numSounds;
MusicTrack music[MAX_MUSIC];
int numMusic;
float masterVolume;
float musicVolume;
float sfxVolume;
bool initialized;
} AudioSystem;
bool initAudioSystem(AudioSystem* audio) {
if (SDL_Init(SDL_INIT_AUDIO) < 0) {
printf("SDL_Init failed: %s\n", SDL_GetError());
return false;
}
// Initialize SDL_mixer
if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) < 0) {
printf("Mix_OpenAudio failed: %s\n", Mix_GetError());
return false;
}
// Allocate mixing channels
Mix_AllocateChannels(16);
audio->numSounds = 0;
audio->numMusic = 0;
audio->masterVolume = 1.0f;
audio->musicVolume = 0.7f;
audio->sfxVolume = 1.0f;
audio->initialized = true;
// Set volumes
Mix_VolumeMusic((int)(MIX_MAX_VOLUME * audio->musicVolume * audio->masterVolume));
return true;
}
SoundEffect* loadSound(AudioSystem* audio, const char* filename, const char* name) {
if (audio->numSounds >= MAX_SOUNDS) {
printf("Audio system full!\n");
return NULL;
}
// Check if already loaded
for (int i = 0; i < audio->numSounds; i++) {
if (strcmp(audio->sounds[i].name, name) == 0) {
audio->sounds[i].refCount++;
return &audio->sounds[i];
}
}
// Load new sound
Mix_Chunk* chunk = Mix_LoadWAV(filename);
if (!chunk) {
printf("Failed to load sound %s: %s\n", filename, Mix_GetError());
return NULL;
}
SoundEffect* sound = &audio->sounds[audio->numSounds++];
sound->chunk = chunk;
strncpy(sound->name, name, sizeof(sound->name) - 1);
sound->refCount = 1;
return sound;
}
MusicTrack* loadMusic(AudioSystem* audio, const char* filename, const char* name) {
if (audio->numMusic >= MAX_MUSIC) {
printf("Music array full!\n");
return NULL;
}
Mix_Music* music = Mix_LoadMUS(filename);
if (!music) {
printf("Failed to load music %s: %s\n", filename, Mix_GetError());
return NULL;
}
MusicTrack* track = &audio->music[audio->numMusic++];
track->music = music;
strncpy(track->name, name, sizeof(track->name) - 1);
return track;
}
int playSound(AudioSystem* audio, const char* name, int loops) {
for (int i = 0; i < audio->numSounds; i++) {
if (strcmp(audio->sounds[i].name, name) == 0) {
int channel = Mix_PlayChannel(-1, audio->sounds[i].chunk, loops);
// Apply volume
if (channel >= 0) {
int volume = (int)(MIX_MAX_VOLUME * audio->sfxVolume *
audio->masterVolume);
Mix_Volume(channel, volume);
}
return channel;
}
}
printf("Sound %s not found\n", name);
return -1;
}
void playMusic(AudioSystem* audio, const char* name, int loops) {
for (int i = 0; i < audio->numMusic; i++) {
if (strcmp(audio->music[i].name, name) == 0) {
Mix_PlayMusic(audio->music[i].music, loops);
return;
}
}
printf("Music %s not found\n", name);
}
void stopMusic() {
Mix_HaltMusic();
}
void pauseMusic() {
Mix_PauseMusic();
}
void resumeMusic() {
Mix_ResumeMusic();
}
void setMasterVolume(AudioSystem* audio, float volume) {
audio->masterVolume = volume;
Mix_VolumeMusic((int)(MIX_MAX_VOLUME * audio->musicVolume * audio->masterVolume));
}
void setMusicVolume(AudioSystem* audio, float volume) {
audio->musicVolume = volume;
Mix_VolumeMusic((int)(MIX_MAX_VOLUME * audio->musicVolume * audio->masterVolume));
}
void setSFXVolume(AudioSystem* audio, float volume) {
audio->sfxVolume = volume;
}
void fadeInMusic(AudioSystem* audio, const char* name, int ms) {
for (int i = 0; i < audio->numMusic; i++) {
if (strcmp(audio->music[i].name, name) == 0) {
Mix_FadeInMusic(audio->music[i].music, -1, ms);
return;
}
}
}
void fadeOutMusic(int ms) {
Mix_FadeOutMusic(ms);
}
bool isMusicPlaying() {
return Mix_PlayingMusic();
}
void cleanupAudioSystem(AudioSystem* audio) {
// Free sounds
for (int i = 0; i < audio->numSounds; i++) {
Mix_FreeChunk(audio->sounds[i].chunk);
}
// Free music
for (int i = 0; i < audio->numMusic; i++) {
Mix_FreeMusic(audio->music[i].music);
}
Mix_CloseAudio();
audio->initialized = false;
}
13.2 3D Positional Audio
// positional_audio.c
#include <SDL2/SDL_mixer.h>
#include <math.h>
typedef struct {
float x, y;
} Position2D;
typedef struct {
Position2D listenerPos;
float maxHearingDistance;
} AudioListener;
void setListenerPosition(AudioListener* listener, float x, float y) {
listener->listenerPos.x = x;
listener->listenerPos.y = y;
}
int playSoundAtPosition(AudioSystem* audio, const char* name,
Position2D sourcePos, AudioListener* listener) {
// Calculate distance
float dx = sourcePos.x - listener->listenerPos.x;
float dy = sourcePos.y - listener->listenerPos.y;
float distance = sqrtf(dx * dx + dy * dy);
// Calculate volume based on distance
float volume = 1.0f - (distance / listener->maxHearingDistance);
if (volume < 0.0f) volume = 0.0f;
// Play sound
int channel = playSound(audio, name, 0);
if (channel >= 0) {
// Set volume
int sdlVolume = (int)(MIX_MAX_VOLUME * volume * audio->sfxVolume *
audio->masterVolume);
Mix_Volume(channel, sdlVolume);
// Set panning (left-right)
float angle = atan2f(dy, dx);
Uint8 left = (Uint8)(127 * (1.0f - cosf(angle)));
Uint8 right = 255 - left;
Mix_SetPanning(channel, left, right);
}
return channel;
}
void updateSoundPosition(int channel, Position2D sourcePos,
AudioListener* listener) {
float dx = sourcePos.x - listener->listenerPos.x;
float dy = sourcePos.y - listener->listenerPos.y;
float distance = sqrtf(dx * dx + dy * dy);
float volume = 1.0f - (distance / listener->maxHearingDistance);
if (volume < 0.0f) volume = 0.0f;
Mix_Volume(channel, (int)(MIX_MAX_VOLUME * volume));
float angle = atan2f(dy, dx);
Uint8 left = (Uint8)(127 * (1.0f - cosf(angle)));
Uint8 right = 255 - left;
Mix_SetPanning(channel, left, right);
}
13.3 Sound Effects and Processing
// sound_effects.c
#include <SDL2/SDL_mixer.h>
// Doppler effect approximation
typedef struct {
Position2D position;
Position2D velocity;
int channel;
float baseFrequency;
} MovingSound;
void updateDopplerEffect(MovingSound* sound, AudioListener* listener,
float speedOfSound) {
// Calculate relative velocity
float dx = sound->position.x - listener->listenerPos.x;
float dy = sound->position.y - listener->listenerPos.y;
float distance = sqrtf(dx * dx + dy * dy);
if (distance < 0.1f) return;
// Velocity toward listener
float vr = (sound->velocity.x * dx + sound->velocity.y * dy) / distance;
// Doppler shift
float frequencyRatio = (speedOfSound) / (speedOfSound - vr);
// SDL_mixer doesn't support frequency change directly,
// but we can approximate with pitch shifting in some audio libraries
}
// Echo/Reverb system (simplified)
typedef struct {
Mix_Chunk* delayBuffer[8];
int delayTimes[8]; // in ms
float decayFactors[8];
int numEchoes;
} ReverbEffect;
void initReverb(ReverbEffect* reverb) {
reverb->numEchoes = 4;
reverb->delayTimes[0] = 50;
reverb->delayTimes[1] = 100;
reverb->delayTimes[2] = 200;
reverb->delayTimes[3] = 400;
reverb->decayFactors[0] = 0.8f;
reverb->decayFactors[1] = 0.6f;
reverb->decayFactors[2] = 0.4f;
reverb->decayFactors[3] = 0.2f;
}
// Music transition system
typedef struct {
Mix_Music* currentMusic;
Mix_Music* nextMusic;
bool transitioning;
float transitionDuration;
float transitionTime;
} MusicManager;
void startMusicTransition(MusicManager* mgr, Mix_Music* nextMusic,
float duration) {
mgr->nextMusic = nextMusic;
mgr->transitioning = true;
mgr->transitionDuration = duration;
mgr->transitionTime = 0.0f;
// Start fade out
Mix_FadeOutMusic((int)(duration * 1000));
}
void updateMusicManager(MusicManager* mgr, float dt) {
if (mgr->transitioning) {
mgr->transitionTime += dt;
if (mgr->transitionTime >= mgr->transitionDuration) {
// Start new music
Mix_FadeInMusic(mgr->nextMusic, -1,
(int)(mgr->transitionDuration * 1000));
mgr->currentMusic = mgr->nextMusic;
mgr->transitioning = false;
}
}
}
Chapter 14: Advanced 2D Techniques
14.1 Lighting and Shadows
// lighting_2d.c
#include <SDL2/SDL.h>
#include <math.h>
typedef struct {
float x, y;
float radius;
SDL_Color color;
float intensity;
} Light2D;
// Create light texture (radial gradient)
SDL_Texture* createLightTexture(SDL_Renderer* renderer, int size) {
SDL_Surface* surface = SDL_CreateRGBSurface(0, size, size, 32,
0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000);
SDL_LockSurface(surface);
Uint32* pixels = (Uint32*)surface->pixels;
int centerX = size / 2;
int centerY = size / 2;
float maxDist = size / 2.0f;
for (int y = 0; y < size; y++) {
for (int x = 0; x < size; x++) {
float dx = x - centerX;
float dy = y - centerY;
float dist = sqrtf(dx * dx + dy * dy);
float intensity = 1.0f - (dist / maxDist);
if (intensity < 0.0f) intensity = 0.0f;
Uint8 alpha = (Uint8)(255 * intensity);
pixels[y * size + x] = SDL_MapRGBA(surface->format,
255, 255, 255, alpha);
}
}
SDL_UnlockSurface(surface);
SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_ADD);
SDL_FreeSurface(surface);
return texture;
}
void renderLight(SDL_Renderer* renderer, Light2D* light,
SDL_Texture* lightTexture) {
int size = (int)(light->radius * 2);
SDL_Rect dest = {
(int)(light->x - light->radius),
(int)(light->y - light->radius),
size, size
};
SDL_SetTextureColorMod(lightTexture,
light->color.r, light->color.g, light->color.b);
SDL_SetTextureAlphaMod(lightTexture,
(Uint8)(light->intensity * 255));
SDL_RenderCopy(renderer, lightTexture, NULL, &dest);
}
// Shadow casting from line segments
typedef struct {
float x1, y1, x2, y2;
} Segment;
void castShadow(SDL_Renderer* renderer, Segment* segment,
Light2D* light, float shadowLength) {
// Calculate shadow polygon vertices
float dx1 = segment->x1 - light->x;
float dy1 = segment->y1 - light->y;
float len1 = sqrtf(dx1 * dx1 + dy1 * dy1);
float dx2 = segment->x2 - light->x;
float dy2 = segment->y2 - light->y;
float len2 = sqrtf(dx2 * dx2 + dy2 * dy2);
// Extend points away from light
float ext1_x = segment->x1 + (dx1 / len1) * shadowLength;
float ext1_y = segment->y1 + (dy1 / len1) * shadowLength;
float ext2_x = segment->x2 + (dx2 / len2) * shadowLength;
float ext2_y = segment->y2 + (dy2 / len2) * shadowLength;
// Draw shadow quad
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 128);
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
// Would use filled polygon here
// Approximate with triangles
}
// Ambient occlusion
void renderAmbientOcclusion(SDL_Renderer* renderer, SDL_Rect* objects,
int numObjects, float aoStrength) {
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
for (int i = 0; i < numObjects; i++) {
// Draw soft shadow around object edges
int expansion = 10;
SDL_Rect shadow = objects[i];
for (int e = 0; e < expansion; e++) {
shadow.x -= 1;
shadow.y -= 1;
shadow.w += 2;
shadow.h += 2;
Uint8 alpha = (Uint8)((expansion - e) * aoStrength * 255 / expansion);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, alpha);
SDL_RenderDrawRect(renderer, &shadow);
}
}
}
14.2 Camera System
// camera_2d.c
#include <SDL2/SDL.h>
#include <math.h>
typedef struct {
float x, y; // Position in world space
float zoom; // Zoom level (1.0 = normal)
float rotation; // Rotation in radians
int screenWidth;
int screenHeight;
// Camera bounds
float minX, maxX;
float minY, maxY;
// Smooth follow
float followSpeed;
float targetX, targetY;
// Screen shake
float trauma;
float shakeOffset;
} Camera2D;
void initCamera(Camera2D* cam, int screenWidth, int screenHeight) {
cam->x = 0;
cam->y = 0;
cam->zoom = 1.0f;
cam->rotation = 0.0f;
cam->screenWidth = screenWidth;
cam->screenHeight = screenHeight;
cam->minX = -INFINITY;
cam->maxX = INFINITY;
cam->minY = -INFINITY;
cam->maxY = INFINITY;
cam->followSpeed = 5.0f;
cam->targetX = 0;
cam->targetY = 0;
cam->trauma = 0;
cam->shakeOffset = 10.0f;
}
void setCameraBounds(Camera2D* cam, float minX, float maxX,
float minY, float maxY) {
cam->minX = minX;
cam->maxX = maxX;
cam->minY = minY;
cam->maxY = maxY;
}
void followTarget(Camera2D* cam, float targetX, float targetY) {
cam->targetX = targetX;
cam->targetY = targetY;
}
void updateCamera(Camera2D* cam, float dt) {
// Smooth follow
cam->x += (cam->targetX - cam->x) * cam->followSpeed * dt;
cam->y += (cam->targetY - cam->y) * cam->followSpeed * dt;
// Apply bounds
float halfWidth = cam->screenWidth / (2.0f * cam->zoom);
float halfHeight = cam->screenHeight / (2.0f * cam->zoom);
if (cam->x - halfWidth < cam->minX) cam->x = cam->minX + halfWidth;
if (cam->x + halfWidth > cam->maxX) cam->x = cam->maxX - halfWidth;
if (cam->y - halfHeight < cam->minY) cam->y = cam->minY + halfHeight;
if (cam->y + halfHeight > cam->maxY) cam->y = cam->maxY - halfHeight;
// Update screen shake
if (cam->trauma > 0) {
cam->trauma -= dt * 2.0f;
if (cam->trauma < 0) cam->trauma = 0;
}
}
void addCameraTrauma(Camera2D* cam, float amount) {
cam->trauma += amount;
if (cam->trauma > 1.0f) cam->trauma = 1.0f;
}
// Convert world coordinates to screen coordinates
void worldToScreen(Camera2D* cam, float worldX, float worldY,
int* screenX, int* screenY) {
// Apply camera transformation
float relX = worldX - cam->x;
float relY = worldY - cam->y;
// Apply zoom
relX *= cam->zoom;
relY *= cam->zoom;
// Apply rotation
if (cam->rotation != 0) {
float cos_r = cosf(-cam->rotation);
float sin_r = sinf(-cam->rotation);
float rotX = relX * cos_r - relY * sin_r;
float rotY = relX * sin_r + relY * cos_r;
relX = rotX;
relY = rotY;
}
// Add screen shake
if (cam->trauma > 0) {
float shake = cam->trauma * cam->trauma;
relX += (((float)rand() / RAND_MAX) * 2.0f - 1.0f) *
cam->shakeOffset * shake;
relY += (((float)rand() / RAND_MAX) * 2.0f - 1.0f) *
cam->shakeOffset * shake;
}
// Center on screen
*screenX = (int)(relX + cam->screenWidth / 2);
*screenY = (int)(relY + cam->screenHeight / 2);
}
// Convert screen coordinates to world coordinates
void screenToWorld(Camera2D* cam, int screenX, int screenY,
float* worldX, float* worldY) {
float relX = screenX - cam->screenWidth / 2.0f;
float relY = screenY - cam->screenHeight / 2.0f;
// Reverse rotation
if (cam->rotation != 0) {
float cos_r = cosf(cam->rotation);
float sin_r = sinf(cam->rotation);
float rotX = relX * cos_r - relY * sin_r;
float rotY = relX * sin_r + relY * cos_r;
relX = rotX;
relY = rotY;
}
// Reverse zoom
relX /= cam->zoom;
relY /= cam->zoom;
// Add camera position
*worldX = relX + cam->x;
*worldY = relY + cam->y;
}
// Apply camera transform to renderer
void applyCameraTransform(SDL_Renderer* renderer, Camera2D* cam) {
// Set viewport center
SDL_Rect viewport = {0, 0, cam->screenWidth, cam->screenHeight};
SDL_RenderSetViewport(renderer, &viewport);
// Note: SDL doesn't support rotation/zoom on viewport directly
// You'll need to transform each object manually using worldToScreen
}
14.3 Tilemap System
// tilemap.c
#include <SDL2/SDL.h>
#include <stdlib.h>
typedef struct {
SDL_Texture* tileset;
int tileWidth;
int tileHeight;
int tilesPerRow;
int* tiles; // Tile indices
int mapWidth;
int mapHeight;
} Tilemap;
Tilemap* createTilemap(SDL_Renderer* renderer, const char* tilesetFile,
int tileWidth, int tileHeight,
int mapWidth, int mapHeight) {
Tilemap* map = malloc(sizeof(Tilemap));
SDL_Surface* surface = IMG_Load(tilesetFile);
if (!surface) {
free(map);
return NULL;
}
map->tileset = SDL_CreateTextureFromSurface(renderer, surface);
map->tilesPerRow = surface->w / tileWidth;
SDL_FreeSurface(surface);
map->tileWidth = tileWidth;
map->tileHeight = tileHeight;
map->mapWidth = mapWidth;
map->mapHeight = mapHeight;
map->tiles = calloc(mapWidth * mapHeight, sizeof(int));
return map;
}
void setTile(Tilemap* map, int x, int y, int tileIndex) {
if (x >= 0 && x < map->mapWidth && y >= 0 && y < map->mapHeight) {
map->tiles[y * map->mapWidth + x] = tileIndex;
}
}
int getTile(Tilemap* map, int x, int y) {
if (x >= 0 && x < map->mapWidth && y >= 0 && y < map->mapHeight) {
return map->tiles[y * map->mapWidth + x];
}
return -1;
}
void renderTilemap(SDL_Renderer* renderer, Tilemap* map, Camera2D* cam) {
// Calculate visible tile range
int startX = (int)((cam->x - cam->screenWidth / (2.0f * cam->zoom)) /
map->tileWidth);
int startY = (int)((cam->y - cam->screenHeight / (2.0f * cam->zoom)) /
map->tileHeight);
int endX = startX + (int)(cam->screenWidth / (map->tileWidth * cam->zoom)) + 2;
int endY = startY + (int)(cam->screenHeight / (map->tileHeight * cam->zoom)) + 2;
// Clamp to map bounds
if (startX < 0) startX = 0;
if (startY < 0) startY = 0;
if (endX > map->mapWidth) endX = map->mapWidth;
if (endY > map->mapHeight) endY = map->mapHeight;
// Render visible tiles
for (int y = startY; y < endY; y++) {
for (int x = startX; x < endX; x++) {
int tileIndex = getTile(map, x, y);
if (tileIndex < 0) continue;
// Calculate tile position in tileset
int tileX = (tileIndex % map->tilesPerRow) * map->tileWidth;
int tileY = (tileIndex / map->tilesPerRow) * map->tileHeight;
SDL_Rect src = {tileX, tileY, map->tileWidth, map->tileHeight};
// Calculate screen position
float worldX = x * map->tileWidth + map->tileWidth / 2.0f;
float worldY = y * map->tileHeight + map->tileHeight / 2.0f;
int screenX, screenY;
worldToScreen(cam, worldX, worldY, &screenX, &screenY);
SDL_Rect dest = {
screenX - (int)(map->tileWidth * cam->zoom / 2),
screenY - (int)(map->tileHeight * cam->zoom / 2),
(int)(map->tileWidth * cam->zoom),
(int)(map->tileHeight * cam->zoom)
};
SDL_RenderCopy(renderer, map->tileset, &src, &dest);
}
}
}
void destroyTilemap(Tilemap* map) {
if (map) {
SDL_DestroyTexture(map->tileset);
free(map->tiles);
free(map);
}
}
Chapter 15: Introduction to 3D Graphics Concepts
15.1 3D Coordinate Systems
While SDL is primarily for 2D, understanding 3D concepts prepares you for OpenGL/Vulkan.
// 3d_basics.c
#include <math.h>
typedef struct {
float x, y, z;
} Vector3;
typedef struct {
float m[4][4]; // 4x4 matrix
} Matrix4;
// Vector operations
Vector3 vec3Add(Vector3 a, Vector3 b) {
return (Vector3){a.x + b.x, a.y + b.y, a.z + b.z};
}
Vector3 vec3Sub(Vector3 a, Vector3 b) {
return (Vector3){a.x - b.x, a.y - b.y, a.z - b.z};
}
Vector3 vec3Scale(Vector3 v, float s) {
return (Vector3){v.x * s, v.y * s, v.z * s};
}
float vec3Dot(Vector3 a, Vector3 b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
Vector3 vec3Cross(Vector3 a, Vector3 b) {
return (Vector3){
a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x
};
}
float vec3Length(Vector3 v) {
return sqrtf(v.x * v.x + v.y * v.y + v.z * v.z);
}
Vector3 vec3Normalize(Vector3 v) {
float len = vec3Length(v);
if (len > 0.0f) {
return vec3Scale(v, 1.0f / len);
}
return v;
}
// Matrix operations
Matrix4 mat4Identity() {
Matrix4 m = {0};
m.m[0][0] = m.m[1][1] = m.m[2][2] = m.m[3][3] = 1.0f;
return m;
}
Matrix4 mat4Multiply(Matrix4 a, Matrix4 b) {
Matrix4 result = {0};
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
for (int k = 0; k < 4; k++) {
result.m[i][j] += a.m[i][k] * b.m[k][j];
}
}
}
return result;
}
// Transform matrices
Matrix4 mat4Translation(float x, float y, float z) {
Matrix4 m = mat4Identity();
m.m[3][0] = x;
m.m[3][1] = y;
m.m[3][2] = z;
return m;
}
Matrix4 mat4RotationY(float angle) {
Matrix4 m = mat4Identity();
float c = cosf(angle);
float s = sinf(angle);
m.m[0][0] = c;
m.m[0][2] = s;
m.m[2][0] = -s;
m.m[2][2] = c;
return m;
}
Matrix4 mat4Scale(float x, float y, float z) {
Matrix4 m = {0};
m.m[0][0] = x;
m.m[1][1] = y;
m.m[2][2] = z;
m.m[3][3] = 1.0f;
return m;
}
// Perspective projection
Matrix4 mat4Perspective(float fov, float aspect, float near, float far) {
Matrix4 m = {0};
float tanHalfFov = tanf(fov / 2.0f);
m.m[0][0] = 1.0f / (aspect * tanHalfFov);
m.m[1][1] = 1.0f / tanHalfFov;
m.m[2][2] = -(far + near) / (far - near);
m.m[2][3] = -1.0f;
m.m[3][2] = -(2.0f * far * near) / (far - near);
return m;
}
// Transform point by matrix
Vector3 mat4TransformPoint(Matrix4 m, Vector3 v) {
float w = m.m[0][3] * v.x + m.m[1][3] * v.y +
m.m[2][3] * v.z + m.m[3][3];
if (w != 0.0f) {
w = 1.0f / w;
}
return (Vector3){
(m.m[0][0] * v.x + m.m[1][0] * v.y + m.m[2][0] * v.z + m.m[3][0]) * w,
(m.m[0][1] * v.x + m.m[1][1] * v.y + m.m[2][1] * v.z + m.m[3][1]) * w,
(m.m[0][2] * v.x + m.m[1][2] * v.y + m.m[2][2] * v.z + m.m[3][2]) * w
};
}
15.2 Simple 3D to 2D Projection
// 3d_projection.c
#include <SDL2/SDL.h>
typedef struct {
Vector3 position;
float fov;
float near;
float far;
} Camera3D;
// Project 3D point to 2D screen
void project3DTo2D(Vector3 worldPos, Camera3D* camera,
int screenWidth, int screenHeight,
int* screenX, int* screenY) {
// Transform to camera space
Vector3 relPos = vec3Sub(worldPos, camera->position);
// Perspective division
if (relPos.z > camera->near) {
float scale = camera->fov / relPos.z;
*screenX = (int)(screenWidth / 2 + relPos.x * scale);
*screenY = (int)(screenHeight / 2 - relPos.y * scale);
}
}
// Render wireframe cube
void renderWireCube(SDL_Renderer* renderer, Vector3 center, float size,
Camera3D* camera, int screenW, int screenH) {
// Cube vertices
Vector3 vertices[8] = {
{center.x - size, center.y - size, center.z - size},
{center.x + size, center.y - size, center.z - size},
{center.x + size, center.y + size, center.z - size},
{center.x - size, center.y + size, center.z - size},
{center.x - size, center.y - size, center.z + size},
{center.x + size, center.y - size, center.z + size},
{center.x + size, center.y + size, center.z + size},
{center.x - size, center.y + size, center.z + size}
};
// Project vertices
int screenVerts[8][2];
for (int i = 0; i < 8; i++) {
project3DTo2D(vertices[i], camera, screenW, screenH,
&screenVerts[i][0], &screenVerts[i][1]);
}
// Draw edges
int edges[][2] = {
{0,1},{1,2},{2,3},{3,0}, // Front face
{4,5},{5,6},{6,7},{7,4}, // Back face
{0,4},{1,5},{2,6},{3,7} // Connecting edges
};
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
for (int i = 0; i < 12; i++) {
SDL_RenderDrawLine(renderer,
screenVerts[edges[i][0]][0],
screenVerts[edges[i][0]][1],
screenVerts[edges[i][1]][0],
screenVerts[edges[i][1]][1]);
}
}
Chapter 16: Mathematical Foundations
16.1 Interpolation Functions
// math_utils.c
#include <math.h>
// Linear interpolation
float lerp(float a, float b, float t) {
return a + (b - a) * t;
}
// Smooth step (smoothed lerp)
float smoothstep(float a, float b, float t) {
t = t * t * (3.0f - 2.0f * t);
return a + (b - a) * t;
}
// Smoother step
float smootherstep(float a, float b, float t) {
t = t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f);
return a + (b - a) * t;
}
// Cosine interpolation
float cosineInterp(float a, float b, float t) {
float cos_t = (1.0f - cosf(t * M_PI)) * 0.5f;
return a + (b - a) * cos_t;
}
// Clamp value
float clamp(float value, float min, float max) {
if (value < min) return min;
if (value > max) return max;
return value;
}
// Map value from one range to another
float map(float value, float inMin, float inMax, float outMin, float outMax) {
return outMin + (outMax - outMin) * ((value - inMin) / (inMax - inMin));
}
16.2 Bezier Curves
// bezier.c
#include <math.h>
typedef struct {
float x, y;
} Point2D;
// Quadratic Bezier
Point2D quadraticBezier(Point2D p0, Point2D p1, Point2D p2, float t) {
float u = 1.0f - t;
float tt = t * t;
float uu = u * u;
Point2D result;
result.x = uu * p0.x + 2 * u * t * p1.x + tt * p2.x;
result.y = uu * p0.y + 2 * u * t * p1.y + tt * p2.y;
return result;
}
// Cubic Bezier
Point2D cubicBezier(Point2D p0, Point2D p1, Point2D p2, Point2D p3, float t) {
float u = 1.0f - t;
float tt = t * t;
float uu = u * u;
float ttt = tt * t;
float uuu = uu * u;
Point2D result;
result.x = uuu * p0.x + 3 * uu * t * p1.x +
3 * u * tt * p2.x + ttt * p3.x;
result.y = uuu * p0.y + 3 * uu * t * p1.y +
3 * u * tt * p2.y + ttt * p3.y;
return result;
}
// Draw Bezier curve
void drawQuadraticBezier(SDL_Renderer* renderer,
Point2D p0, Point2D p1, Point2D p2, int segments) {
for (int i = 0; i < segments; i++) {
float t1 = (float)i / segments;
float t2 = (float)(i + 1) / segments;
Point2D pt1 = quadraticBezier(p0, p1, p2, t1);
Point2D pt2 = quadraticBezier(p0, p1, p2, t2);
SDL_RenderDrawLine(renderer,
(int)pt1.x, (int)pt1.y,
(int)pt2.x, (int)pt2.y);
}
}
16.3 Perlin Noise
// perlin_noise.c
#include <math.h>
#include <stdlib.h>
#define PERLIN_SIZE 256
static int permutation[PERLIN_SIZE * 2];
static bool perlinInitialized = false;
void initPerlin() {
// Initialize permutation table
for (int i = 0; i < PERLIN_SIZE; i++) {
permutation[i] = i;
}
// Shuffle
for (int i = 0; i < PERLIN_SIZE; i++) {
int j = rand() % PERLIN_SIZE;
int temp = permutation[i];
permutation[i] = permutation[j];
permutation[j] = temp;
}
// Duplicate
for (int i = 0; i < PERLIN_SIZE; i++) {
permutation[PERLIN_SIZE + i] = permutation[i];
}
perlinInitialized = true;
}
float fade(float t) {
return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f);
}
float grad(int hash, float x, float y) {
int h = hash & 15;
float u = h < 8 ? x : y;
float v = h < 4 ? y : (h == 12 || h == 14 ? x : 0);
return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
}
float perlinNoise2D(float x, float y) {
if (!perlinInitialized) initPerlin();
int X = (int)floorf(x) & (PERLIN_SIZE - 1);
int Y = (int)floorf(y) & (PERLIN_SIZE - 1);
x -= floorf(x);
y -= floorf(y);
float u = fade(x);
float v = fade(y);
int A = permutation[X] + Y;
int B = permutation[X + 1] + Y;
return lerp(lerp(grad(permutation[A], x, y),
grad(permutation[B], x - 1, y), u),
lerp(grad(permutation[A + 1], x, y - 1),
grad(permutation[B + 1], x - 1, y - 1), u), v);
}
// Octave noise (fractal Brownian motion)
float perlinOctave(float x, float y, int octaves, float persistence) {
float total = 0.0f;
float frequency = 1.0f;
float amplitude = 1.0f;
float maxValue = 0.0f;
for (int i = 0; i < octaves; i++) {
total += perlinNoise2D(x * frequency, y * frequency) * amplitude;
maxValue += amplitude;
amplitude *= persistence;
frequency *= 2.0f;
}
return total / maxValue;
}
Chapter 17: Performance Optimization
17.1 Profiling and Measurement
// profiler.c
#include <SDL2/SDL.h>
#include <stdio.h>
#include <string.h>
#define MAX_PROFILE_SECTIONS 32
typedef struct {
char name[64];
Uint64 startTime;
Uint64 totalTime;
int callCount;
} ProfileSection;
typedef struct {
ProfileSection sections[MAX_PROFILE_SECTIONS];
int sectionCount;
Uint64 frequency;
} Profiler;
static Profiler globalProfiler = {0};
void initProfiler() {
globalProfiler.sectionCount = 0;
globalProfiler.frequency = SDL_GetPerformanceFrequency();
}
void beginProfile(const char* name) {
// Find or create section
int idx = -1;
for (int i = 0; i < globalProfiler.sectionCount; i++) {
if (strcmp(globalProfiler.sections[i].name, name) == 0) {
idx = i;
break;
}
}
if (idx == -1) {
if (globalProfiler.sectionCount >= MAX_PROFILE_SECTIONS) return;
idx = globalProfiler.sectionCount++;
strncpy(globalProfiler.sections[idx].name, name,
sizeof(globalProfiler.sections[idx].name) - 1);
globalProfiler.sections[idx].totalTime = 0;
globalProfiler.sections[idx].callCount = 0;
}
globalProfiler.sections[idx].startTime = SDL_GetPerformanceCounter();
}
void endProfile(const char* name) {
Uint64 endTime = SDL_GetPerformanceCounter();
for (int i = 0; i < globalProfiler.sectionCount; i++) {
if (strcmp(globalProfiler.sections[i].name, name) == 0) {
Uint64 elapsed = endTime - globalProfiler.sections[i].startTime;
globalProfiler.sections[i].totalTime += elapsed;
globalProfiler.sections[i].callCount++;
break;
}
}
}
void printProfileResults() {
printf("\n=== Profile Results ===\n");
for (int i = 0; i < globalProfiler.sectionCount; i++) {
ProfileSection* section = &globalProfiler.sections[i];
float avgMs = (section->totalTime / (float)section->callCount) /
(globalProfiler.frequency / 1000.0f);
printf("%s: %.3f ms avg (%d calls)\n",
section->name, avgMs, section->callCount);
}
}
void resetProfiler() {
for (int i = 0; i < globalProfiler.sectionCount; i++) {
globalProfiler.sections[i].totalTime = 0;
globalProfiler.sections[i].callCount = 0;
}
}
// Usage example
void gameUpdate() {
beginProfile("Physics");
// ... physics code ...
endProfile("Physics");
beginProfile("AI");
// ... AI code ...
endProfile("AI");
beginProfile("Rendering");
// ... rendering code ...
endProfile("Rendering");
}
17.2 Memory Pool
// memory_pool.c
#include <stdlib.h>
#include <string.h>
typedef struct PoolBlock {
struct PoolBlock* next;
} PoolBlock;
typedef struct {
void* memory;
PoolBlock* freeList;
size_t blockSize;
size_t blockCount;
size_t allocatedBlocks;
} MemoryPool;
MemoryPool* createMemoryPool(size_t blockSize, size_t blockCount) {
MemoryPool* pool = malloc(sizeof(MemoryPool));
pool->blockSize = (blockSize < sizeof(PoolBlock)) ?
sizeof(PoolBlock) : blockSize;
pool->blockCount = blockCount;
pool->allocatedBlocks = 0;
// Allocate memory
pool->memory = malloc(pool->blockSize * blockCount);
// Initialize free list
pool->freeList = NULL;
for (size_t i = 0; i < blockCount; i++) {
PoolBlock* block = (PoolBlock*)((char*)pool->memory + i * pool->blockSize);
block->next = pool->freeList;
pool->freeList = block;
}
return pool;
}
void* poolAlloc(MemoryPool* pool) {
if (!pool->freeList) {
return NULL; // Pool exhausted
}
PoolBlock* block = pool->freeList;
pool->freeList = block->next;
pool->allocatedBlocks++;
return block;
}
void poolFree(MemoryPool* pool, void* ptr) {
if (!ptr) return;
PoolBlock* block = (PoolBlock*)ptr;
block->next = pool->freeList;
pool->freeList = block;
pool->allocatedBlocks--;
}
void destroyMemoryPool(MemoryPool* pool) {
free(pool->memory);
free(pool);
}
// Usage example
typedef struct {
float x, y;
float vx, vy;
float life;
} PooledParticle;
MemoryPool* particlePool;
void initParticleSystem() {
particlePool = createMemoryPool(sizeof(PooledParticle), 10000);
}
PooledParticle* createParticle() {
return (PooledParticle*)poolAlloc(particlePool);
}
void destroyParticle(PooledParticle* p) {
poolFree(particlePool, p);
}
17.3 Optimization Techniques
// optimization_tips.c
// 1. Object pooling for frequently created/destroyed objects
typedef struct Entity Entity;
struct Entity {
bool active;
float x, y;
// ... other data
Entity* next; // For free list
};
static Entity entityPool[1000];
static Entity* freeList = NULL;
void initEntityPool() {
for (int i = 0; i < 1000; i++) {
entityPool[i].active = false;
entityPool[i].next = (i < 999) ? &entityPool[i + 1] : NULL;
}
freeList = &entityPool[0];
}
Entity* spawnEntity() {
if (!freeList) return NULL;
Entity* entity = freeList;
freeList = entity->next;
entity->active = true;
return entity;
}
void despawnEntity(Entity* entity) {
entity->active = false;
entity->next = freeList;
freeList = entity;
}
// 2. Batch similar operations
void updateEntitiesBatched(Entity* entities, int count, float dt) {
// Process all position updates together
for (int i = 0; i < count; i++) {
if (entities[i].active) {
entities[i].x += entities[i].vx * dt;
entities[i].y += entities[i].vy * dt;
}
}
// Process all collision checks together
for (int i = 0; i < count; i++) {
if (entities[i].active) {
// Check collisions
}
}
}
// 3. Avoid redundant calculations
typedef struct {
float x, y;
float lastCos, lastSin;
float lastAngle;
} OptimizedEntity;
void rotateEntity(OptimizedEntity* e, float angle) {
if (angle != e->lastAngle) {
e->lastAngle = angle;
e->lastCos = cosf(angle);
e->lastSin = sinf(angle);
}
// Use cached sin/cos values
}
// 4. Early exit conditions
bool checkComplexCondition(Entity* e) {
// Check simple conditions first
if (!e->active) return false;
if (e->x < 0 || e->x > 800) return false;
// More expensive checks only if needed
// ... complex logic ...
return true;
}
// 5. Use appropriate data structures
// For sparse data: hash table
// For sequential access: array
// For frequent insertion/deletion: linked list
// For spatial queries: quadtree/grid
Chapter 18: Software Architecture for Games
18.1 Entity Component System (ECS)
// ecs.c - Simple ECS implementation
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#define MAX_ENTITIES 1000
#define MAX_COMPONENTS 32
typedef unsigned int EntityID;
typedef unsigned int ComponentMask;
// Component types
typedef enum {
COMPONENT_POSITION = 1 << 0,
COMPONENT_VELOCITY = 1 << 1,
COMPONENT_SPRITE = 1 << 2,
COMPONENT_COLLISION = 1 << 3,
COMPONENT_HEALTH = 1 << 4
} ComponentType;
// Component data
typedef struct {
float x, y;
} PositionComponent;
typedef struct {
float vx, vy;
} VelocityComponent;
typedef struct {
SDL_Texture* texture;
SDL_Rect srcRect;
} SpriteComponent;
typedef struct {
SDL_Rect bounds;
bool isTrigger;
} CollisionComponent;
typedef struct {
int current;
int maximum;
} HealthComponent;
// ECS World
typedef struct {
ComponentMask entityMasks[MAX_ENTITIES];
bool entityActive[MAX_ENTITIES];
PositionComponent positions[MAX_ENTITIES];
VelocityComponent velocities[MAX_ENTITIES];
SpriteComponent sprites[MAX_ENTITIES];
CollisionComponent collisions[MAX_ENTITIES];
HealthComponent healths[MAX_ENTITIES];
EntityID nextEntity;
} ECSWorld;
void initECS(ECSWorld* world) {
memset(world, 0, sizeof(ECSWorld));
world->nextEntity = 0;
}
EntityID createEntity(ECSWorld* world) {
if (world->nextEntity >= MAX_ENTITIES) {
return MAX_ENTITIES; // Invalid
}
EntityID id = world->nextEntity++;
world->entityActive[id] = true;
world->entityMasks[id] = 0;
return id;
}
void destroyEntity(ECSWorld* world, EntityID entity) {
world->entityActive[entity] = false;
world->entityMasks[entity] = 0;
}
void addComponent(ECSWorld* world, EntityID entity, ComponentType type,
void* data) {
world->entityMasks[entity] |= type;
switch (type) {
case COMPONENT_POSITION:
world->positions[entity] = *(PositionComponent*)data;
break;
case COMPONENT_VELOCITY:
world->velocities[entity] = *(VelocityComponent*)data;
break;
case COMPONENT_SPRITE:
world->sprites[entity] = *(SpriteComponent*)data;
break;
case COMPONENT_COLLISION:
world->collisions[entity] = *(CollisionComponent*)data;
break;
case COMPONENT_HEALTH:
world->healths[entity] = *(HealthComponent*)data;
break;
}
}
bool hasComponents(ECSWorld* world, EntityID entity, ComponentMask mask) {
return (world->entityMasks[entity] & mask) == mask;
}
// System: Movement
void movementSystem(ECSWorld* world, float dt) {
ComponentMask required = COMPONENT_POSITION | COMPONENT_VELOCITY;
for (EntityID e = 0; e < world->nextEntity; e++) {
if (!world->entityActive[e]) continue;
if (!hasComponents(world, e, required)) continue;
world->positions[e].x += world->velocities[e].vx * dt;
world->positions[e].y += world->velocities[e].vy * dt;
}
}
// System: Rendering
void renderSystem(ECSWorld* world, SDL_Renderer* renderer) {
ComponentMask required = COMPONENT_POSITION | COMPONENT_SPRITE;
for (EntityID e = 0; e < world->nextEntity; e++) {
if (!world->entityActive[e]) continue;
if (!hasComponents(world, e, required)) continue;
PositionComponent* pos = &world->positions[e];
SpriteComponent* sprite = &world->sprites[e];
SDL_Rect dest = {
(int)(pos->x - sprite->srcRect.w / 2),
(int)(pos->y - sprite->srcRect.h / 2),
sprite->srcRect.w,
sprite->srcRect.h
};
SDL_RenderCopy(renderer, sprite->texture, &sprite->srcRect, &dest);
}
}
// System: Collision
void collisionSystem(ECSWorld* world) {
ComponentMask required = COMPONENT_POSITION | COMPONENT_COLLISION;
for (EntityID e1 = 0; e1 < world->nextEntity; e1++) {
if (!world->entityActive[e1]) continue;
if (!hasComponents(world, e1, required)) continue;
for (EntityID e2 = e1 + 1; e2 < world->nextEntity; e2++) {
if (!world->entityActive[e2]) continue;
if (!hasComponents(world, e2, required)) continue;
// Check collision
SDL_Rect* bounds1 = &world->collisions[e1].bounds;
SDL_Rect* bounds2 = &world->collisions[e2].bounds;
if (SDL_HasIntersection(bounds1, bounds2)) {
// Handle collision
}
}
}
}
18.2 State Machine
// state_machine.c
#include <stdio.h>
typedef struct GameState GameState;
typedef void (*StateFunc)(GameState* state, void* game);
struct GameState {
StateFunc onEnter;
StateFunc onUpdate;
StateFunc onExit;
void* data;
};
typedef struct {
GameState* currentState;
GameState* previousState;
} StateMachine;
void changeState(StateMachine* sm, GameState* newState, void* game) {
if (sm->currentState && sm->currentState->onExit) {
sm->currentState->onExit(sm->currentState, game);
}
sm->previousState = sm->currentState;
sm->currentState = newState;
if (sm->currentState && sm->currentState->onEnter) {
sm->currentState->onEnter(sm->currentState, game);
}
}
void updateStateMachine(StateMachine* sm, void* game) {
if (sm->currentState && sm->currentState->onUpdate) {
sm->currentState->onUpdate(sm->currentState, game);
}
}
// Example states
void menuEnter(GameState* state, void* game) {
printf("Entering menu\n");
}
void menuUpdate(GameState* state, void* game) {
// Menu logic
}
void menuExit(GameState* state, void* game) {
printf("Exiting menu\n");
}
void gameplayEnter(GameState* state, void* game) {
printf("Starting game\n");
}
void gameplayUpdate(GameState* state, void* game) {
// Game logic
}
void gameplayExit(GameState* state, void* game) {
printf("Ending game\n");
}
GameState menuState = {menuEnter, menuUpdate, menuExit, NULL};
GameState gameplayState = {gameplayEnter, gameplayUpdate, gameplayExit, NULL};
18.3 Event System
// event_system.c
#include <stdlib.h>
#include <string.h>
#define MAX_LISTENERS 32
typedef enum {
EVENT_PLAYER_DIED,
EVENT_ENEMY_SPAWNED,
EVENT_LEVEL_COMPLETE,
EVENT_SCORE_CHANGED,
EVENT_COUNT
} EventType;
typedef struct {
EventType type;
void* data;
} Event;
typedef void (*EventListener)(Event* event, void* userData);
typedef struct {
EventListener listeners[MAX_LISTENERS];
void* userData[MAX_LISTENERS];
int listenerCount;
} EventChannel;
typedef struct {
EventChannel channels[EVENT_COUNT];
} EventSystem;
static EventSystem globalEventSystem;
void initEventSystem() {
memset(&globalEventSystem, 0, sizeof(EventSystem));
}
void registerListener(EventType type, EventListener listener, void* userData) {
EventChannel* channel = &globalEventSystem.channels[type];
if (channel->listenerCount < MAX_LISTENERS) {
channel->listeners[channel->listenerCount] = listener;
channel->userData[channel->listenerCount] = userData;
channel->listenerCount++;
}
}
void triggerEvent(EventType type, void* data) {
Event event = {type, data};
EventChannel* channel = &globalEventSystem.channels[type];
for (int i = 0; i < channel->listenerCount; i++) {
channel->listeners[i](&event, channel->userData[i]);
}
}
// Usage example
void onPlayerDied(Event* event, void* userData) {
printf("Player died! Game over.\n");
}
void onScoreChanged(Event* event, void* userData) {
int* score = (int*)event->data;
printf("Score: %d\n", *score);
}
void setupGameEvents() {
registerListener(EVENT_PLAYER_DIED, onPlayerDied, NULL);
registerListener(EVENT_SCORE_CHANGED, onScoreChanged, NULL);
}
Chapter 19: Complete Game Projects
19.1 Project: Pong
// pong_complete.c
#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdbool.h>
#include <math.h>
#define SCREEN_WIDTH 800
#define SCREEN_HEIGHT 600
#define PADDLE_WIDTH 15
#define PADDLE_HEIGHT 80
#define BALL_SIZE 10
#define BALL_SPEED 300.0f
#define PADDLE_SPEED 400.0f
typedef struct {
float x, y;
float vx, vy;
} Ball;
typedef struct {
float y;
int score;
} Paddle;
typedef struct {
SDL_Window* window;
SDL_Renderer* renderer;
bool running;
Paddle leftPaddle;
Paddle rightPaddle;
Ball ball;
Uint64 lastTime;
float deltaTime;
} PongGame;
bool initPong(PongGame* game) {
if (SDL_Init(SDL_INIT_VIDEO) < 0) return false;
game->window = SDL_CreateWindow("Pong",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
SCREEN_WIDTH, SCREEN_HEIGHT, 0);
if (!game->window) return false;
game->renderer = SDL_CreateRenderer(game->window, -1,
SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (!game->renderer) return false;
// Initialize game state
game->leftPaddle.y = SCREEN_HEIGHT / 2;
game->leftPaddle.score = 0;
game->rightPaddle.y = SCREEN_HEIGHT / 2;
game->rightPaddle.score = 0;
game->ball.x = SCREEN_WIDTH / 2;
game->ball.y = SCREEN_HEIGHT / 2;
game->ball.vx = BALL_SPEED;
game->ball.vy = 0;
game->running = true;
game->lastTime = SDL_GetPerformanceCounter();
return true;
}
void resetBall(PongGame* game) {
game->ball.x = SCREEN_WIDTH / 2;
game->ball.y = SCREEN_HEIGHT / 2;
// Random angle
float angle = ((float)rand() / RAND_MAX - 0.5f) * M_PI / 3;
game->ball.vx = cosf(angle) * BALL_SPEED * ((rand() % 2) ? 1 : -1);
game->ball.vy = sinf(angle) * BALL_SPEED;
}
void handleInput(PongGame* game) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
game->running = false;
}
}
const Uint8* keys = SDL_GetKeyboardState(NULL);
// Left paddle
if (keys[SDL_SCANCODE_W]) {
game->leftPaddle.y -= PADDLE_SPEED * game->deltaTime;
}
if (keys[SDL_SCANCODE_S]) {
game->leftPaddle.y += PADDLE_SPEED * game->deltaTime;
}
// Right paddle
if (keys[SDL_SCANCODE_UP]) {
game->rightPaddle.y -= PADDLE_SPEED * game->deltaTime;
}
if (keys[SDL_SCANCODE_DOWN]) {
game->rightPaddle.y += PADDLE_SPEED * game->deltaTime;
}
if (keys[SDL_SCANCODE_ESCAPE]) {
game->running = false;
}
}
void updatePong(PongGame* game) {
// Update delta time
Uint64 currentTime = SDL_GetPerformanceCounter();
game->deltaTime = (float)(currentTime - game->lastTime) /
SDL_GetPerformanceFrequency();
if (game->deltaTime > 0.05f) game->deltaTime = 0.05f;
game->lastTime = currentTime;
// Clamp paddles
if (game->leftPaddle.y < PADDLE_HEIGHT/2)
game->leftPaddle.y = PADDLE_HEIGHT/2;
if (game->leftPaddle.y > SCREEN_HEIGHT - PADDLE_HEIGHT/2)
game->leftPaddle.y = SCREEN_HEIGHT - PADDLE_HEIGHT/2;
if (game->rightPaddle.y < PADDLE_HEIGHT/2)
game->rightPaddle.y = PADDLE_HEIGHT/2;
if (game->rightPaddle.y > SCREEN_HEIGHT - PADDLE_HEIGHT/2)
game->rightPaddle.y = SCREEN_HEIGHT - PADDLE_HEIGHT/2;
// Update ball
game->ball.x += game->ball.vx * game->deltaTime;
game->ball.y += game->ball.vy * game->deltaTime;
// Ball collision with top/bottom
if (game->ball.y - BALL_SIZE/2 < 0 ||
game->ball.y + BALL_SIZE/2 > SCREEN_HEIGHT) {
game->ball.vy = -game->ball.vy;
}
// Ball collision with paddles
SDL_Rect ballRect = {
(int)(game->ball.x - BALL_SIZE/2),
(int)(game->ball.y - BALL_SIZE/2),
BALL_SIZE, BALL_SIZE
};
SDL_Rect leftPaddleRect = {
20,
(int)(game->leftPaddle.y - PADDLE_HEIGHT/2),
PADDLE_WIDTH, PADDLE_HEIGHT
};
SDL_Rect rightPaddleRect = {
SCREEN_WIDTH - 20 - PADDLE_WIDTH,
(int)(game->rightPaddle.y - PADDLE_HEIGHT/2),
PADDLE_WIDTH, PADDLE_HEIGHT
};
if (SDL_HasIntersection(&ballRect, &leftPaddleRect)) {
game->ball.vx = fabsf(game->ball.vx);
float relativeY = (game->ball.y - game->leftPaddle.y) / (PADDLE_HEIGHT/2);
game->ball.vy = relativeY * BALL_SPEED;
}
if (SDL_HasIntersection(&ballRect, &rightPaddleRect)) {
game->ball.vx = -fabsf(game->ball.vx);
float relativeY = (game->ball.y - game->rightPaddle.y) / (PADDLE_HEIGHT/2);
game->ball.vy = relativeY * BALL_SPEED;
}
// Score
if (game->ball.x < 0) {
game->rightPaddle.score++;
resetBall(game);
}
if (game->ball.x > SCREEN_WIDTH) {
game->leftPaddle.score++;
resetBall(game);
}
}
void renderPong(PongGame* game) {
// Clear
SDL_SetRenderDrawColor(game->renderer, 0, 0, 0, 255);
SDL_RenderClear(game->renderer);
// Draw center line
SDL_SetRenderDrawColor(game->renderer, 100, 100, 100, 255);
for (int y = 0; y < SCREEN_HEIGHT; y += 20) {
SDL_RenderDrawLine(game->renderer, SCREEN_WIDTH/2, y,
SCREEN_WIDTH/2, y + 10);
}
// Draw paddles
SDL_SetRenderDrawColor(game->renderer, 255, 255, 255, 255);
SDL_Rect leftPaddle = {
20, (int)(game->leftPaddle.y - PADDLE_HEIGHT/2),
PADDLE_WIDTH, PADDLE_HEIGHT
};
SDL_RenderFillRect(game->renderer, &leftPaddle);
SDL_Rect rightPaddle = {
SCREEN_WIDTH - 20 - PADDLE_WIDTH,
(int)(game->rightPaddle.y - PADDLE_HEIGHT/2),
PADDLE_WIDTH, PADDLE_HEIGHT
};
SDL_RenderFillRect(game->renderer, &rightPaddle);
// Draw ball
SDL_Rect ball = {
(int)(game->ball.x - BALL_SIZE/2),
(int)(game->ball.y - BALL_SIZE/2),
BALL_SIZE, BALL_SIZE
};
SDL_RenderFillRect(game->renderer, &ball);
SDL_RenderPresent(game->renderer);
}
void cleanupPong(PongGame* game) {
SDL_DestroyRenderer(game->renderer);
SDL_DestroyWindow(game->window);
SDL_Quit();
}
int main(int argc, char* argv[]) {
PongGame game = {0};
if (!initPong(&game)) {
printf("Failed to initialize Pong!\n");
return 1;
}
while (game.running) {
handleInput(&game);
updatePong(&game);
renderPong(&game);
}
cleanupPong(&game);
return 0;
}
Chapter 20: Best Practices and Professional Development
20.1 Code Organization
// Good project structure:
// project/
// src/
// main.c
// game/
// game.c
// game.h
// player.c
// player.h
// enemy.c
// enemy.h
// engine/
// renderer.c
// renderer.h
// audio.c
// audio.h
// input.c
// input.h
// utils/
// math_utils.c
// math_utils.h
// include/
// assets/
// build/
// Header guards
#ifndef GAME_H
#define GAME_H
// Forward declarations
typedef struct Game Game;
// Public API
Game* createGame(void);
void updateGame(Game* game, float dt);
void renderGame(Game* game);
void destroyGame(Game* game);
#endif // GAME_H
20.2 Error Handling
// error_handling.c
// Return error codes
typedef enum {
SUCCESS = 0,
ERROR_SDL_INIT = -1,
ERROR_WINDOW_CREATE = -2,
ERROR_RENDERER_CREATE = -3,
ERROR_ASSET_LOAD = -4
} ErrorCode;
ErrorCode initGame(Game* game) {
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
fprintf(stderr, "SDL Init failed: %s\n", SDL_GetError());
return ERROR_SDL_INIT;
}
game->window = SDL_CreateWindow(...);
if (!game->window) {
fprintf(stderr, "Window creation failed: %s\n", SDL_GetError());
SDL_Quit();
return ERROR_WINDOW_CREATE;
}
// ... more initialization ...
return SUCCESS;
}
// Cleanup on error
void cleanupOnError(Game* game) {
if (game->texture) SDL_DestroyTexture(game->texture);
if (game->renderer) SDL_DestroyRenderer(game->renderer);
if (game->window) SDL_DestroyWindow(game->window);
SDL_Quit();
}
20.3 Documentation
/**
* @brief Creates a new particle emitter
*
* Allocates and initializes a particle emitter with the specified parameters.
* The emitter must be freed with destroyEmitter() when no longer needed.
*
* @param x X position of the emitter
* @param y Y position of the emitter
* @param maxParticles Maximum number of particles
* @param emitRate Particles emitted per second
* @return Pointer to the new emitter, or NULL on failure
*
* @see destroyEmitter
* @see updateEmitter
*/
ParticleEmitter* createEmitter(float x, float y, int maxParticles, float emitRate);
/**
* @brief Updates the particle emitter
*
* Updates all active particles and emits new ones based on the emit rate.
* Should be called once per frame with the delta time.
*
* @param emitter The emitter to update
* @param dt Delta time in seconds
*/
void updateEmitter(ParticleEmitter* emitter, float dt);
20.4 Testing
// Simple unit testing
typedef struct {
int passed;
int failed;
} TestResults;
TestResults globalResults = {0, 0};
#define TEST(name) \
void test_##name()
#define ASSERT(condition, message) \
if (!(condition)) { \
printf("FAIL: %s - %s\n", __func__, message); \
globalResults.failed++; \
return; \
}
#define RUN_TEST(name) \
printf("Running %s...\n", #name); \
test_##name(); \
globalResults.passed++;
// Example tests
TEST(vector_addition) {
Vector2 a = {3.0f, 4.0f};
Vector2 b = {1.0f, 2.0f};
Vector2 result = vec2Add(a, b);
ASSERT(result.x == 4.0f, "X component incorrect");
ASSERT(result.y == 6.0f, "Y component incorrect");
}
TEST(rect_collision) {
SDL_Rect a = {0, 0, 10, 10};
SDL_Rect b = {5, 5, 10, 10};
ASSERT(rectIntersect(&a, &b), "Should intersect");
b.x = 20;
ASSERT(!rectIntersect(&a, &b), "Should not intersect");
}
void runAllTests() {
RUN_TEST(vector_addition);
RUN_TEST(rect_collision);
printf("\nResults: %d passed, %d failed\n",
globalResults.passed, globalResults.failed);
}
20.5 Performance Guidelines
- Profile before optimizing - Measure what's slow
- Optimize the right things - Focus on hot paths
- Cache-friendly data - Use structs of arrays vs arrays of structs
- Batch draw calls - Minimize state changes
- Use spatial partitioning - Don't check everything against everything
- Object pooling - Reuse objects instead of allocating
- Fixed time step physics - For deterministic simulations
- Delta time for rendering - For smooth visuals
- Lazy evaluation - Don't compute what you don't need
- Multi-threading - For independent systems
20.6 Debugging Tips
// Debug drawing
#ifdef DEBUG
void debugDrawColliders(SDL_Renderer* renderer, Entity* entities, int count) {
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 128);
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
for (int i = 0; i < count; i++) {
if (entities[i].active && entities[i].hasCollider) {
SDL_RenderDrawRect(renderer, &entities[i].collider);
}
}
}
#endif
// Assert macro
#ifdef DEBUG
#define ASSERT(condition, message) \
if (!(condition)) { \
fprintf(stderr, "Assertion failed: %s (%s:%d)\n", \
message, __FILE__, __LINE__); \
abort(); \
}
#else
#define ASSERT(condition, message)
#endif
// Logging system
typedef enum {
LOG_DEBUG,
LOG_INFO,
LOG_WARNING,
LOG_ERROR
} LogLevel;
void logMessage(LogLevel level, const char* format, ...) {
const char* levelStr[] = {"DEBUG", "INFO", "WARN", "ERROR"};
printf("[%s] ", levelStr[level]);
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
printf("\n");
}
Conclusion
You've completed this comprehensive guide to graphics and games programming with SDL in C. You've learned:
- Graphics fundamentals and SDL architecture
- Game loops and timing systems
- Input handling across multiple devices
- 2D rendering techniques and effects
- Sprite and texture management
- Collision detection and response
- Particle systems
- Audio programming
- Advanced techniques (lighting, cameras, tilemaps)
- 3D graphics concepts
- Mathematical foundations
- Performance optimization
- Software architecture patterns
- Complete game examples
- Professional best practices
Next Steps:
- Build Your Own Games - Start with simple projects and gradually increase complexity
- Study Open Source Games - Learn from real-world code
- Explore Advanced Topics - OpenGL, Vulkan, shaders, networking
- Join Communities - Share your work, get feedback, help others
- Never Stop Learning - Graphics programming is vast and ever-evolving
Remember: The best way to learn is by doing. Start coding, make mistakes, learn from them, and build amazing games!
Additional Resources:
- SDL2 Documentation: https://wiki.libsdl.org/
- Lazy Foo's SDL Tutorials: https://lazyfoo.net/tutorials/SDL/
- Game Programming Patterns: https://gameprogrammingpatterns.com/
- Red Blob Games: https://www.redblobgames.com/
- Mathematics for Computer Graphics (textbooks)
- Graphics programming communities on Reddit, Discord, Stack Overflow
Good luck on your journey to becoming an exceptional graphics programmer!
I'll continue creating this comprehensive guide. Let me move it to the outputs directory so you can access it.