My first project in C/SDL2 after completing a short tutorial coding Pong. These 2 projects are my entire experience with both C and SDL2, although I had some previous basic experience with Python.
I also added a "life trail" that shows recently deceased cells in grey. Darker grey = died more recently.
There is also a 1 square wide border of always dead cells around the edge, to avoid run-off.
Code works and produces the desired result, although I'm looking for recommendations on improving the code. The only reference material used for the project was a list of the game of life rules, so I am sure there are many potential improvements.
I tried to make the code as readable as possible and left comments where possible. (Probably too many obvious ones) - If anything is unclear, please let me know.
// Controls
// Left or Right mouse click to set cells alive/dead
// R to reset all cells to dead
// Spacebar to pause/unpause
// Right-arrow key to step 1 frame at a time
#include <SDL2/SDL.h>
#include <stdbool.h>
#define GRID_SIZE 40 // Number of grid squares in X/Y direction
#define SQUARE_SIZE 20 // Pixel size of each square
#define GRID_LINE_WIDTH 1 // Pixel size of gap between squares
#define GRID_BORDER SQUARE_SIZE * 2 // Width of border around game grid
#define TIME_STEP_DELAY 150
const char* WINDOW_TITLE = "Game of Life"; // Game window title
const int SCREEN_WIDTH = SQUARE_SIZE * GRID_SIZE + GRID_BORDER; // Game window width
const int SCREEN_HEIGHT = SQUARE_SIZE * GRID_SIZE + GRID_BORDER; // Game window height
SDL_Window* window = NULL; // SDL Window
SDL_Renderer* renderer = NULL; // SDL Renderer
typedef struct Cell // Cell struct holds data regarding position, alive status and time since last alive for individual cell
{
int x;
int y;
bool isAlive;
int lastAlive;
} Cell;
bool Initialize(void);
void ResetGameGrid(void);
void HandleInput(void);
void DrawGame(void);
void TimeStep(void);
void Shutdown(void);
void AddExampleGlider(void);
Cell GameGrid[GRID_SIZE][GRID_SIZE]; // Current game grid
Cell PrevGameGrid[GRID_SIZE][GRID_SIZE]; // Previous timestep game grid
bool gameRunning = true;
bool gamePaused = true;
int main(int argc, const char* argv[])
{
atexit(Shutdown); // If app closes, cleanup and shutdown
if (!Initialize()) // Initialize SDL, create window and fill game grid with dead cells. If this fails, exit app
{
exit(1);
}
if(GRID_SIZE >= 8)
AddExampleGlider(); // Adds a basic glider to top left of screen on launch
while (gameRunning) // Main game loop - loop until gameRunning == false
{
HandleInput(); // Check if user closes the window with X at top-right - close and quit SDL if so
DrawGame(); // Draw the window and grid
if(!gamePaused)
TimeStep(); // Apply rules and step forward
SDL_Delay(TIME_STEP_DELAY);
}
Shutdown(); // If gameRunning == false, shutdown and quit SDL
return 0;
}
bool Initialize(void)
{
if (SDL_Init(SDL_INIT_VIDEO) != 0) // Init SDL Video
{
printf("Failed to init SDL: %s\n", SDL_GetError());
return false;
}
// Create SDL Window
window = SDL_CreateWindow(WINDOW_TITLE, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN);
if (!window)
{
return false;
}
// Create SDL Renderer
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (!renderer)
{
return false;
}
ResetGameGrid(); // Fill Game Grid with dead cells
memcpy(PrevGameGrid, GameGrid, sizeof(GameGrid)); // Previous game grid = current game grid
return true;
}
void DrawGame()
{
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); // Draw black background
SDL_RenderClear(renderer);
for(int i = 0; i < GRID_SIZE; i++) // Loop through each cell in the grid and draw
{
for(int j = 0; j < GRID_SIZE; j++)
{
SDL_Rect rect = {
GameGrid[i][j].x,
GameGrid[i][j].y,
.w = SQUARE_SIZE - GRID_LINE_WIDTH,
.h = SQUARE_SIZE - GRID_LINE_WIDTH,
};
if(GameGrid[i][j].isAlive == false) // If cell is dead, draw color based on when last alive to show as trail
{
SDL_SetRenderDrawColor(renderer, GameGrid[i][j].lastAlive, GameGrid[i][j].lastAlive, GameGrid[i][j].lastAlive, 255);
} else {
SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
}
SDL_RenderFillRect(renderer, &rect);
}
}
SDL_RenderPresent(renderer); // Present renderer
}
void Shutdown(void)
{
if (renderer) // Destroy renderer if one exists
{
SDL_DestroyRenderer(renderer);
}
if (window) // Destroy window if one exists
{
SDL_DestroyWindow(window);
}
SDL_Quit(); // Quit SDL
}
void HandleInput(void)
{
SDL_Event event;
while (SDL_PollEvent(&event))
{
if (event.type == SDL_QUIT) // If top-right X is clicked, quit game
{
gameRunning = false;
break;
}
else if (event.type == SDL_MOUSEBUTTONDOWN) // Handle mouse input - click any mouse button to flip alive/dead status
{
int x, y;
SDL_GetMouseState(&x, &y);
int gridX, gridY; // Convert mouse x/y co-ords to grid numbers
gridY = (x - SQUARE_SIZE) / SQUARE_SIZE;
gridX = (y - SQUARE_SIZE) / SQUARE_SIZE;
if(GameGrid[gridX][gridY].isAlive) // Flip dead to alive and alive to dead
GameGrid[gridX][gridY].isAlive = false;
else
GameGrid[gridX][gridY].isAlive = true;
}
if(event.type == SDL_KEYDOWN) // Handle keyboard input
{
switch(event.key.keysym.sym)
{
case SDLK_r: // Press R to reset all cells to dead
ResetGameGrid();
break;
case SDLK_SPACE: // Press Space to pause/unpause
if(gamePaused)
gamePaused = false;
else
gamePaused = true;
break;
case SDLK_RIGHT: // Press Right Arrow Key to time step once
TimeStep();
break;
}
}
}
}
void ResetGameGrid()
{
for(int i = 0; i < GRID_SIZE; i++) // Loop through each cell of the grid and reset to default values
{
for(int j = 0; j < GRID_SIZE; j++)
{
GameGrid[i][j].x = SQUARE_SIZE + j * SQUARE_SIZE;
GameGrid[i][j].y = SQUARE_SIZE + i * SQUARE_SIZE;
GameGrid[i][j].isAlive = false;
GameGrid[i][j].lastAlive = 255;
}
}
}
void TimeStep(void)
{
memcpy(PrevGameGrid, GameGrid, sizeof(GameGrid)); // Set value of PrevGameGrid to be that of current GameGrid
for(int i = 1; i < GRID_SIZE - 1; i++)
{
for(int j = 1; j < GRID_SIZE - 1; j++)
{
int aliveNeighbors = 0; // Count number of alive neighbors - Leaving a 1x1 border of dead cells around grid
if(PrevGameGrid[i-1][j-1].isAlive == true)
aliveNeighbors++;
if(PrevGameGrid[i-1][j].isAlive == true)
aliveNeighbors++;
if(PrevGameGrid[i-1][j+1].isAlive == true)
aliveNeighbors++;
if(PrevGameGrid[i][j-1].isAlive == true)
aliveNeighbors++;
if(PrevGameGrid[i][j+1].isAlive == true)
aliveNeighbors++;
if(PrevGameGrid[i+1][j-1].isAlive == true)
aliveNeighbors++;
if(PrevGameGrid[i+1][j].isAlive == true)
aliveNeighbors++;
if(PrevGameGrid[i+1][j+1].isAlive == true)
aliveNeighbors++;
if(PrevGameGrid[i][j].isAlive == true) // Apply logic if cell is currently alive
{
if(aliveNeighbors < 2) // Cell dies due to lonliness if less than 2 alive neighbors
GameGrid[i][j].isAlive = false;
if(aliveNeighbors > 1 && aliveNeighbors < 4) // Cell stays alive if 2 or 3 alive neighbors
GameGrid[i][j].isAlive = true;
if(aliveNeighbors > 3) // Cell dies due to overcrowing with 4 or more alive neighbors
GameGrid[i][j].isAlive = false;
}
if(PrevGameGrid[i][j].isAlive == false) // Apply logic if cell is currently dead
{
if(aliveNeighbors == 3) // Cell comes to life if exactly 3 alive neighbors
GameGrid[i][j].isAlive = true;
}
if(GameGrid[i][j].isAlive) // "Death Trail" Fade from Grey to White over time as time lastAlive grows
GameGrid[i][j].lastAlive = 125;
else
if(GameGrid[i][j].lastAlive < 255) // Cap at 255
{
GameGrid[i][j].lastAlive += 5;
if(GameGrid[i][j].lastAlive > 255)
GameGrid[i][j].lastAlive = 255;
}
}
}
}
void AddExampleGlider(void)
{
GameGrid[2][4].isAlive = true;
GameGrid[3][5].isAlive = true;
GameGrid[4][3].isAlive = true;
GameGrid[4][4].isAlive = true;
GameGrid[4][5].isAlive = true;
}