2
\$\begingroup\$

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;
}
\$\endgroup\$
2
\$\begingroup\$
  • We don't need to store the x and y coords in the Cell itself. We can calculate them cheaply when iterating through the grid.

  • We don't need to store the previous game grid. When doing the update in TimeStep we can set lastAlive to 0 for live cells, and increment it for all dead cells (and perhaps clamp to a max value to prevent overflow). It would be better to worry about the details of mapping lastAlive to a color in the DrawGame function, and keep the value in Cell as a simple cycle counter.


  • When testing boolean conditions, e.g. if(GameGrid[i][j].isAlive == false), we don't need to compare with true or false, we can just use the boolean value directly: if(!GameGrid[i][j].isAlive).

  • Similarly, when assigning to a boolean expression, we don't need to use an if statement, for example

                 if(gamePaused)
                     gamePaused = false;
                 else
                     gamePaused = true;
    

    can be gamePaused = !gamePaused; and

                 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;
    

    can be GameGrid[i][j].isAlive = (aliveNeighbors == 2 || aliveNeighbors == 3);


  • Is this correct for all cases?

         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 the user clicks in the border, won't gridX / gridY be negative? It would also be sensible to ignore clicks outside of the window, just in case.

    Note that you can get x and y from event.button.x and event.button.y.


  • When counting the live neighbours, it might be neater to create an array of offsets, and then use a loop:

     struct Coord { int x, y; };
    
     struct Coord offsets[8] =
     {
         { -1, -1 }, {  0, -1 }, {  1, -1 }, 
         { -1,  0 },             {  1,  0 }, 
         {  1,  1 }, {  0,  1 }, {  1,  1 },
     };
    
     ...
    
             size_t live = 0;
             for (size_t o = 0; o != 8; ++i)
                 live += GameGrid[i + offsets[o].x][j + offsets[o].y].isAlive;
    

  • Avoid global variables:

    • It would be nice to support grids with different width and height, and grids of differing sizes. We can allocate the grid memory at run-time as a one-dimensional array of sizeof(Cell) * width * height bytes, and access the cells with cell_index = y * width + x;.

    • Things like WINDOW_TITLE or TIME_STEP_DELAY are not needed globally. These can be local variables.

    • Where necessary, local variables can be passed to other functions by pointer (e.g. gameRunning, gamePaused).

    • Other global variables could be grouped together into a struct and passed to the functions that need them, e.g.:

        struct DrawInfo
        {
            int squarePixels;
            int borderPixels;
            int linePixels;
        };
      
        struct Window
        {
            SDL_Window* window;
            SDL_Renderer* renderer;
        };
      
        struct Grid
        {
            size_t width, height;
            struct Cell* grid;
        };
      
        void DrawGame(struct Window const* window, struct Grid const* grid, struct DrawInfo drawInfo);
      
\$\endgroup\$
1
  • \$\begingroup\$ Thanks a lot user673679, that's exactly the kind of advice I was looking for! Tutorials and docs have been good for basic syntax, but these changes are tough to find without knowing what to look for. I'll implement them and try to incorporate the ideas in future projects :) \$\endgroup\$
    – Togiodi
    Apr 19 '21 at 18:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.