*ฅ^•ﻌ•^ฅ* ✨✨  HWisnu's blog  ✨✨ о ฅ^•ﻌ•^ฅ

C + Raylib: snake game

Introduction

Continuation of the snake game series. This one is using C + Raylib and there will be another version with Zig + Raylib. You might wonder why did I wrote multiple versions of this snake games? My gamedev session is usually done in Godot 4, but I have always been curious how it differs when doing gamedev in Raylib.

What's Raylib?

raylib-logo

Taken from the website: a programming library to enjoy videogames programming; no fancy interface, no visual helpers, no gui tools or editors... just coding in pure spartan-programmers way.

Key point: "pure spartan-programmers way".

Yes compared to using Godot 4, I do feel like a Spartan warrior when using Raylib! Gamedev in Godot is a walk in the park comparatively.

NOTE: you'll see a super similar code compared to the ASCII snake code.

#1 Libraries

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

#2 Consts, structs and global vars

#define GRID_SIZE 25
#define SCREEN_WIDTH 800
#define SCREEN_HEIGHT 600
#define MAX_SCORE 256

typedef struct {
    Vector2 position;
} Vec2;

Vec2 head = {{0, 0}};
Vec2 segments[MAX_SCORE + 1];
Vec2 dir = {{1, 0}};
Vec2 berry;

int score = 0;
char score_message[32];

bool skip = false;
bool is_running = true;

#3 Function declaration

void game_over();
void init();
void quit_game();
void restart_game();
bool collide(Vec2 a, Vec2 b);
bool collide_snake_body(Vec2 point);
Vec2 spawn_berry();
void process_input();
void draw();
void update();

#4 Collision: the collide function

bool collide(Vec2 a, Vec2 b) 
{
    return (a.position.x == b.position.x && a.position.y == b.position.y);
}

bool collide_snake_body(Vec2 point) 
{
    for (int i = 0; i < score; i++) {
        if (collide(point, segments[i])) {
            return true;
        }
    }
    return false;
}

#5 Spawning berries

Generates a random position for a new berry, ensuring it doesn't overlap with the snake's body or head.

Vec2 spawn_berry() 
{
    Vec2 berry = {{ 1 + rand() % (GRID_SIZE - 2), 1 + rand() % (GRID_SIZE - 2) }};
    while (collide(head, berry) || collide_snake_body(berry)) {
        berry.position.x = 1 + rand() % (GRID_SIZE - 2);
        berry.position.y = 1 + rand() % (GRID_SIZE - 2);
    }
    return berry;
}

#6 Quit and Restart

void quit_game() 
{
    CloseWindow();
}

void restart_game() 
{
    head.position.x = 0;
    head.position.y = 0;
    dir.position.x = 1;
    dir.position.y = 0;
    score = 0;
    sprintf(score_message, "[ Score: %d ]", score);
    is_running = true;
}

#7 Init function

Initializes the game by setting up the window, random number generator, and initial game state.

void init() 
{
    srand(time(NULL));
    InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "snake");
    SetTargetFPS(8);
    berry = spawn_berry();
    sprintf(score_message, "[ Score: %d ]", score);
}

#8 Movement - process_input

Handles user input, such as arrow key presses and space/escape key presses.

void process_input() 
{
    if (IsKeyPressed(KEY_LEFT)) {
        if (dir.position.x == 1) return;
        dir.position.x = -1;
        dir.position.y = 0;
    }
    if (IsKeyPressed(KEY_RIGHT)) {
        if (dir.position.x == -1) return;
        dir.position.x = 1;
        dir.position.y = 0;
    }
    if (IsKeyPressed(KEY_UP)) {
        if (dir.position.y == 1) return;
        dir.position.x = 0;
        dir.position.y = -1;
    }
    if (IsKeyPressed(KEY_DOWN)) {
        if (dir.position.y == -1) return;
        dir.position.x = 0;
        dir.position.y = 1;
    }
    if (IsKeyPressed(KEY_SPACE)) {
        if (!is_running) restart_game();
    }
    if (IsKeyPressed(KEY_ESCAPE)) {
        is_running = false;
        quit_game();
    }
}

#9 Game Over state

Displays the game over screen with restart and quit options.

void game_over() 
{
    int gameOverWidth = MeasureText("Game Over", 30);
    int gameOverX = (SCREEN_WIDTH - gameOverWidth) / 2;
    DrawText("GAME OVER", gameOverX - 20, SCREEN_HEIGHT/2 - 20, 36, RED);

    int restartWidth = MeasureText("[SPACE] to restart, [ESC] to quit", 20);
    int restartX = (SCREEN_WIDTH - restartWidth) / 2;
    DrawText("[SPACE] to restart, [ESC] to quit", 
             restartX + 5, SCREEN_HEIGHT/2 + 50, 20, ORANGE);
}

#10 Update game state

Updates the game state by moving the snake, checking for collisions, and updating the score.

void update() 
{
    for (int i = score; i > 0; i--) {
        segments[i] = segments[i - 1];
    }
    segments[0] = head;

    head.position.x += dir.position.x;
    head.position.y += dir.position.y;

    if (collide_snake_body(head) || head.position.x < 0 || head.position.y < 0 \
            || head.position.x >= GRID_SIZE || head.position.y >= GRID_SIZE) {
        is_running = false;
    }

    if (collide(head, berry)) {
        if (score < MAX_SCORE) {
            score += 1;
            sprintf(score_message, "[ Score: %d ]", score);
        } else {
            printf("You Win!");
        }
        berry = spawn_berry();
    }
}

#11 Draw function

Renders the game screen, including the snake, berry, score, and game over screen (if applicable).

void draw() 
{
    BeginDrawing();
    ClearBackground(BLACK);

    DrawRectangle(berry.position.x * SCREEN_WIDTH / GRID_SIZE,
                  berry.position.y * SCREEN_HEIGHT / GRID_SIZE,
                  SCREEN_WIDTH / GRID_SIZE,
                  SCREEN_HEIGHT / GRID_SIZE,
                  BLUE);

    for (int i = 0; i < score; i++) {
        DrawRectangle(segments[i].position.x * SCREEN_WIDTH / GRID_SIZE,
                      segments[i].position.y * SCREEN_HEIGHT / GRID_SIZE,
                      SCREEN_WIDTH / GRID_SIZE,
                      SCREEN_HEIGHT / GRID_SIZE,
                      GREEN);
    }
    DrawRectangle(head.position.x * SCREEN_WIDTH / GRID_SIZE,
                  head.position.y * SCREEN_HEIGHT / GRID_SIZE,
                  SCREEN_WIDTH / GRID_SIZE,
                  SCREEN_HEIGHT / GRID_SIZE,
                  YELLOW);

    DrawRectangleLines(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, WHITE);

    int score_text = MeasureText("[ Score: {} ]", 24);
    int score_x = (SCREEN_WIDTH - score_text) / 2;
    DrawText(score_message, 
             score_x, 
             10, 
             24, 
             WHITE);

    int lang_text = MeasureText("C + Raylib", 24);
    int lang_x = (SCREEN_WIDTH - lang_text) / 2;
    DrawText("C + Raylib", 
             lang_x, 
             SCREEN_HEIGHT - 60, 
             24, 
             WHITE);

    if (!is_running) {
        game_over();
    }

    EndDrawing();
}

#12 Main

Initializing the game and running the game loop.

int main(void)
{
    init();
    while (!WindowShouldClose())
    {
        process_input();
        if (skip) {
            skip = false;
            continue;
        }
        if (is_running) {
            update();
        }
        draw();
    }
    quit_game();

    return 0;
}

Result

cRL-snake