Skip to content

Instantly share code, notes, and snippets.

@bones-ai
Last active March 5, 2025 05:41
Show Gist options
  • Save bones-ai/1d66f9212715e7a37b0497a0a3d75443 to your computer and use it in GitHub Desktop.
Save bones-ai/1d66f9212715e7a37b0497a0a3d75443 to your computer and use it in GitHub Desktop.
/*
A simple snake game in odin and raylib
- Video: https://youtu.be/b0NLJBoTtvg
- Hot-reload template: https://github.com/karl-zylinski/odin-raylib-hot-reload-game-template
*/
package main
import "core:math/linalg"
import "core:fmt"
import "core:time"
import "core:math/rand"
import rl "vendor:raylib"
// MARK: Consts
WINDOW_SIZE :: 500
BLOCK_SIZE :: 25
UPDATE_INTERVAL_MS :: 100
// MARK: Structs
GameState :: struct {
// Snake
head: Point,
body: [dynamic]Point,
direction: Direction,
food: Point,
last_update_ts: time.Time,
is_game_over: bool
}
Direction :: enum {
LEFT,
RIGHT,
DOWN,
UP
}
Point :: [2]int
// MARK: Globals
g_game_state: ^GameState
// MARK: Main
update :: proc() {
if g_game_state.is_game_over {
if rl.IsKeyPressed(.R) {
g_game_state.is_game_over = false
mem_free()
reset_state()
}
return
}
// Input handling
dir := g_game_state.direction
if rl.IsKeyDown(.W) && dir != .DOWN do g_game_state.direction = .UP
if rl.IsKeyDown(.A) && dir != .RIGHT do g_game_state.direction = .LEFT
if rl.IsKeyDown(.S) && dir != .UP do g_game_state.direction = .DOWN
if rl.IsKeyDown(.D) && dir != .LEFT do g_game_state.direction = .RIGHT
// Snake update
elapsed_ts := time.duration_milliseconds(time.since(g_game_state.last_update_ts))
if elapsed_ts <= UPDATE_INTERVAL_MS {
return
}
g_game_state.last_update_ts = time.now()
prev_pos := g_game_state.head
new_head := g_game_state.head + dir_to_point(g_game_state.direction)
// Wall collision
grid_size := WINDOW_SIZE / BLOCK_SIZE
is_x_valid := new_head.x < 0 || new_head.x >= grid_size
is_y_valid := new_head.y < 0 || new_head.y >= grid_size
if is_x_valid || is_y_valid {
g_game_state.is_game_over = true
return
}
// Snake collision
for pt in g_game_state.body {
if new_head == pt {
g_game_state.is_game_over = true
return
}
}
// Snake movement
g_game_state.head = new_head
for &pt in g_game_state.body {
temp := pt
pt = prev_pos
prev_pos = temp
}
// Food collision
if g_game_state.head == g_game_state.food {
append(&g_game_state.body, g_game_state.food)
g_game_state.food = get_rand_pos()
}
}
draw :: proc() {
rl.BeginDrawing()
defer rl.EndDrawing()
bg_color := rl.DARKGRAY if g_game_state.is_game_over else rl.DARKBROWN
rl.ClearBackground(bg_color)
// Draw grid
num_rows := WINDOW_SIZE / BLOCK_SIZE
grid_color := rl.ColorAlpha(rl.GRAY, 0.5)
for i in 0..<num_rows {
x := f32(i * BLOCK_SIZE)
rl.DrawLineV({x, 0}, {x, WINDOW_SIZE}, grid_color)
rl.DrawLineV({0, x}, {WINDOW_SIZE, x}, grid_color)
}
// Draw score
score := fmt.caprint(len(g_game_state.body))
SCORE_FONT_SIZE :: 150
score_width := rl.MeasureText(score, SCORE_FONT_SIZE)
score_color := rl.ColorAlpha(rl.BEIGE, 0.8)
rl.DrawText(score, (WINDOW_SIZE / 2) - score_width / 2, (WINDOW_SIZE / 2) - 70, SCORE_FONT_SIZE, score_color)
// Draw snake
{
// Head
snake_head := point_to_vec2(g_game_state.head * BLOCK_SIZE)
rl.DrawRectangleV(snake_head, {BLOCK_SIZE, BLOCK_SIZE}, rl.PINK)
// Body
for pt in g_game_state.body {
point := point_to_vec2(pt * BLOCK_SIZE)
rl.DrawRectangleV(point, {BLOCK_SIZE, BLOCK_SIZE}, rl.WHITE)
}
}
// Draw Food
food_pos := point_to_vec2(g_game_state.food * BLOCK_SIZE)
rl.DrawRectangleV(food_pos, {BLOCK_SIZE, BLOCK_SIZE}, rl.SKYBLUE)
// Draw restart text
if g_game_state.is_game_over {
rl.DrawText("Press 'R' to restart", 10, 10, 30, rl.WHITE)
}
}
// MARK: Utils
point_to_vec2 :: proc(point: Point) -> rl.Vector2 {
return { f32(point.x), f32(point.y) }
}
dir_to_point :: proc(dir: Direction) -> (ret: Point) {
switch(dir) {
case .LEFT:
ret = {-1, 0}
case .RIGHT:
ret = {1, 0}
case .DOWN:
ret = {0, 1}
case .UP:
ret = {0, -1}
}
return ret
}
get_rand_pos :: proc() -> Point {
range := i32(WINDOW_SIZE / BLOCK_SIZE)
return {
int(rand.int31_max(range)),
int(rand.int31_max(range)),
}
}
// MARK: Exports
@(export)
init_window :: proc() {
rl.SetConfigFlags({.WINDOW_RESIZABLE})
rl.InitWindow(WINDOW_SIZE, WINDOW_SIZE, "snake")
rl.SetTargetFPS(120)
}
@(export)
tick :: proc() {
update()
draw()
}
@(export)
mem_free :: proc() {
free(g_game_state)
}
@(export)
reset_state :: proc() {
half_screen := (WINDOW_SIZE / BLOCK_SIZE) / 2
g_game_state = new(GameState)
g_game_state^ = GameState {
head = {half_screen - 1, half_screen - 1},
food = get_rand_pos()
}
}
@(export)
state :: proc() -> rawptr {
return g_game_state
}
@(export)
reload_state :: proc(mem: rawptr) {
g_game_state = (^GameState)(mem)
}
@(export)
is_restart :: proc(mem: rawptr) -> bool {
return rl.IsKeyDown(.LEFT_SHIFT) && rl.IsKeyPressed(.R)
}
@(export)
is_close_window :: proc() -> bool {
return rl.WindowShouldClose()
}
@(export)
close_window :: proc() {
rl.CloseWindow()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment