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?
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
- collide: checks if two points (a and b) have the same coordinates.
- collide_snake_body: checks if a given point is part of the snake's body.
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
- quit_game: Exits the game cleanly, restoring the terminal state.
- restart_game: Resets game state to initial values.
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;
}