GameDev in C Part 4: A Snake Clone

Happy Valentines Day!, I made a snake clone in C.

This is a quick interlude from the regular GameDev in C posts. I was making very little progress on the doodle jump style game on which the series is based. So I thought I would take a quick break and code up a snake clone.

I chose snake as I have written this type of game previously in JS and Python. So I didn't really have to figure much out. Just get down to writing some C. I haev included the full source code in this article. One nice feature that I added was smooth transitions between cells. I did this by growing and shortening the head and tail respectively, based on an animation timer. This makes for a nice animation and makes it look like the snake is moving smoothly, rather than jumping a cell at a time. The implementation can be seen in the SnakeDrawHead and SnakeDrawTail methods. Apart from this it's a regular old snake clone.

Thoughts

I really enjoyed coding this, it's the first time I have felt productive in C. I am starting to grasp arrays and pointers. I may even be beginning to think in a C manner. I kept everything in one file for convenience. This helped me think more about what the code did, and less about where the code lived.

Anyway, take a look at the source code below. I'll get back to the main "SlimeJump" game in the next post.

Source code is also available at BrianDouglasIE/snake-c-raylib

#include "raylib.h"
#include "math.h"
#include <stdlib.h>
#include <stdio.h>
#include <time.h>

float percentOf(float percent, float number)
{
    return (percent / number) * 100.0f;
}

float asPercentOf(float number, float percent)
{
    return (percent / 100.0f) * number;
}

int randomInt(int min, int max)
{
    return (rand() % max) + min;
}

enum Direction {
    UP, DOWN, LEFT, RIGHT
};

struct Snake {
    int x;
    int y;
    float speed;
    float moveTimer;
    enum Direction direction;
    enum Direction nextDirection;
};

struct Snake SnakeCreate(int x, int y)
{
    return (struct Snake) {
        .x = x,
        .y = y,
        .speed = 0.2f,
        .moveTimer = 0,
        .direction = RIGHT,
        .nextDirection = RIGHT,
    };
}

void SnakeMoveInCurrentDirection(struct Snake *s)
{
    switch(s->direction)
    {
        case UP:
            s->y -= 1;
            break;
        case DOWN:
            s->y += 1;
            break;
        case LEFT:
            s->x -= 1;
            break;
        case RIGHT:
            s->x += 1;
            break;
        default:
            break;
    }
}

void SnakeScreenWrap(struct Snake *s, int colCount, int rowCount)
{
    if(s->x < 0) {
        s->x = colCount - 1;
        s->direction = LEFT;
    }
    if(s->x > colCount - 1) {
        s->x = 0;
        s->direction = RIGHT;
    }
    if(s->y < 0) {
        s->y = rowCount - 1;
        s->direction = UP;
    }
    if(s->y > rowCount - 1) {
        s->y = 0;
        s->direction = DOWN;
    }
}

void SnakeUpdate(struct Snake *s, float dt, struct Snake *target)
{
    s->moveTimer += dt;
    if(s->moveTimer >= s->speed) {
        s->moveTimer -= s->speed;

        if(target == NULL) {
            if(s->direction != s->nextDirection)
            {
                s->direction = s->nextDirection;
            }

            SnakeMoveInCurrentDirection(s);
        } else {
            s->x = target->x;
            s->y = target->y;
            s->direction = target->direction;
        }
    }
}

void SnakeSetDirection(struct Snake *s, int dirX, int dirY)
{
    if(dirX && dirY) return;
    if(!dirX && !dirY) return;
    if(dirY && (s->direction == UP || s->direction == DOWN)) return;
    if(dirX && (s->direction == LEFT || s->direction == RIGHT)) return;

    if(dirX == -1) {
        s->nextDirection = LEFT;
    }
    if(dirX == 1) {
        s->nextDirection = RIGHT;
    }
    if(dirY == -1) {
        s->nextDirection = UP;
    }
    if(dirY == 1) {
        s->nextDirection = DOWN;
    }
}

void SnakeDrawHead(struct Snake *s, int cellSize, Color color)
{
    int w, h;
    float growthPercent = percentOf(s->moveTimer, s->speed);

    switch(s->direction)
    {
        case UP:
            h = floor(asPercentOf(cellSize, growthPercent));
            DrawRectangle(s->x * cellSize, (s->y * cellSize) + (cellSize - h), cellSize, h, color);
            break;
        case DOWN:
            h = floor(asPercentOf(cellSize, growthPercent));
            DrawRectangle(s->x * cellSize, s->y * cellSize, cellSize, h, color);
            break;
        case RIGHT:
            w = floor(asPercentOf(cellSize, growthPercent));
            DrawRectangle(s->x * cellSize, s->y * cellSize, w, cellSize, color);
            break;
        case LEFT:
            w = floor(asPercentOf(cellSize, growthPercent));
            DrawRectangle((s->x * cellSize) + (cellSize - w), s->y * cellSize, w, cellSize, color);
            break;
        default:
            break;
    }
}

void SnakeDrawTail(struct Snake *s, int cellSize, Color color)
{
    int w, h;
    float growthPercent = percentOf(s->moveTimer, s->speed);

    switch(s->direction)
    {
        case UP:
            h = floor(asPercentOf(cellSize, growthPercent));
            DrawRectangle(s->x * cellSize, s->y * cellSize, cellSize, cellSize - h, color);
            break;
        case DOWN:
            h = floor(asPercentOf(cellSize, growthPercent));
            DrawRectangle(s->x * cellSize, s->y * cellSize + h, cellSize, cellSize - h, color);
            break;
        case RIGHT:
            w = floor(asPercentOf(cellSize, growthPercent));
            DrawRectangle(s->x * cellSize + w, s->y * cellSize, cellSize - w, cellSize, color);
            break;
        case LEFT:
            w = floor(asPercentOf(cellSize, growthPercent));
            DrawRectangle(s->x * cellSize, s->y * cellSize, cellSize - w, cellSize, color);
            break;
        default:
            break;
    }
}

void SnakeDraw(struct Snake *s, int cellSize, Color color)
{
    DrawRectangle(s->x * cellSize, s->y * cellSize, cellSize, cellSize, color);
}

struct Apple {
    int x;
    int y;
    int active;
    float lifeSpan;
    float lifeTimer;
};

struct Apple AppleCreate()
{
    return (struct Apple) {
        .x = 0,
        .y = 0,
        .active = 0,
        .lifeSpan = 0.0f,
        .lifeTimer = 0.0f,
    };
}

void AppleUpdate(struct Apple *a, float dt)
{
    if(!a->active) return;

    a->lifeTimer += dt;
    if(a->lifeTimer > a->lifeSpan)
    {
        a->lifeTimer -= a->lifeSpan;
        a->active = 0;
    }
}

void AppleActivateAt(struct Apple *a, int x, int y)
{
    a->x = x;
    a->y = y;
    a->active = 1;
    a->lifeTimer = 0;
    a->lifeSpan = 5.0f;
}

void AppleDraw(struct Apple *a, int cellSize, Color color)
{
    if(!a->active) return;
    DrawRectangle(a->x * cellSize, a->y * cellSize, cellSize, cellSize, color);
}

int collidesWithSnake(struct Snake s[], int snakeLength, int x, int y)
{
    for(int i = 0; i < snakeLength; i++)
    {
        if(s[i].x == x && s[i].y == y) return 1;
    }

    return 0;
}

Vector2 getRandomCellCoords(int colCount, int rowCount)
{
    int x = randomInt(0, colCount);
    int y = randomInt(0, rowCount);
    return (Vector2) { x, y };
}

int main(void)
{
    srand((unsigned)time(NULL));

    const int screenSize = 600;
    const int screenWidth = screenSize;
    const int screenHeight = screenSize;

    InitWindow(screenWidth, screenHeight, "snake");
    SetTargetFPS(60);

    const int gridSize = 20;
    const int cellSize = floor(screenSize / gridSize);
    const int colCount = floor(screenWidth / cellSize);
    const int rowCount = floor(screenHeight / cellSize);

    int gameInProgress = 1;

    int snakeLength = 4;
    int snakeMaxLength = colCount * rowCount;
    struct Snake snake[snakeMaxLength];
    for(int i = 0; i < snakeLength; i++)
    {
        snake[i] = SnakeCreate(floor(colCount / 2) - i, floor(rowCount / 2));
    }

    struct Apple apple = AppleCreate();

    float appleSpawnInterval = 8.0f;
    float appleSpawnTimer = 0.0f;

    while(!WindowShouldClose())
    {
        // update
        if(gameInProgress) {
            int up, down, left, right, dirX, dirY;

            float dt = GetFrameTime();

            up = IsKeyDown(KEY_UP) ? -1 : 0;
            down = IsKeyDown(KEY_DOWN) ? 1 : 0;
            left = IsKeyDown(KEY_LEFT) ? -1 : 0;
            right = IsKeyDown(KEY_RIGHT) ? 1 : 0;

            dirY = up + down;
            dirX = left + right;

            for(int i = snakeLength; i > 0; i--)
            {
                SnakeUpdate(&snake[i], dt, &snake[i - 1]);
            }

            snake[snakeLength - 1].direction = snake[snakeLength - 2].direction;
            SnakeSetDirection(&snake[0], dirX, dirY);
            SnakeUpdate(&snake[0], dt, NULL);
            SnakeScreenWrap(&snake[0], colCount, rowCount);

            if(apple.active && snake[0].x == apple.x && snake[0].y == apple.y)
            {
                apple.active = 0;
                snake[snakeLength] = snake[snakeLength - 1];
                snakeLength++;
            }

            if(!apple.active)
            {
                appleSpawnTimer += dt;
                if(appleSpawnTimer > appleSpawnInterval)
                {
                    appleSpawnTimer -= appleSpawnInterval;

                    Vector2 newPos = getRandomCellCoords(colCount, rowCount);
                    while(collidesWithSnake(snake, snakeLength, newPos.x, newPos.y)) {
                        newPos = getRandomCellCoords(colCount, rowCount);
                    }
                    AppleActivateAt(&apple, newPos.x, newPos.y);
                }
            }

            AppleUpdate(&apple, dt);

            int tailLength = snakeLength - 1;
            struct Snake tail[tailLength];
            for(int i = 1; i < tailLength; i++)
            {
                tail[i] = snake[i];
            }
            if(collidesWithSnake(tail, tailLength, snake[0].x, snake[0].y))
            {
                gameInProgress = 0;
            }
        }

        // draw
        BeginDrawing();
        ClearBackground(BLACK);

        for(int r = 0; r < rowCount; r++)
        {
            for(int c = 0; c < colCount; c++) {
                Color color = ((r + c) % 2) == 0 ? RAYWHITE : WHITE;
                DrawRectangle(c * cellSize, r * cellSize, cellSize, cellSize, color);
            }
        }

        for(int i = 1; i < snakeLength - 1; i++)
        {
            SnakeDraw(&snake[i], cellSize, DARKGREEN);
        }
        SnakeDrawHead(&snake[0], cellSize, DARKGREEN);
        SnakeDrawTail(&snake[snakeLength - 1], cellSize, DARKGREEN);

        AppleDraw(&apple, cellSize, RED);

        if(!gameInProgress) {
            const char *gameOverText = "GAME OVER";
            int fontSize = 40;
            int textLength = MeasureText(gameOverText, fontSize);

            int posX = floor(screenWidth / 2) - floor(textLength / 2);
            int posY = floor(screenHeight / 2);
            DrawText(gameOverText, posX, posY, fontSize, BLACK);
        }

        EndDrawing();
    }

    CloseWindow();
}

Until next time,

Brian