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();
}