Merge pull request 'Chunking system, save/load chunks' (#1) from chunking into master

Reviewed-on: https://git.bellsworne.tech/chrisbell/odin-raylib-game/pulls/1
This commit is contained in:
Chris Bell 2025-02-28 14:40:36 +00:00
commit f29fad7168
7 changed files with 374 additions and 107 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
bin/
game/data

BIN
game/game

Binary file not shown.

View File

@ -2,39 +2,49 @@ package game
import "core:fmt" import "core:fmt"
import rl "vendor:raylib" import rl "vendor:raylib"
import rand "core:math/rand" import "core:os"
import "core:strconv"
import "core:mem"
import "core:strings"
player : Player player : Player
world : World world : World
camera : rl.Camera2D
main :: proc() { main :: proc() {
if !os.is_dir("data") {
os.make_directory("data")
}
if !os.is_dir("data/worlds") {
os.make_directory("data/worlds")
}
rl.InitWindow(1280, 720, "Odin game") rl.InitWindow(1280, 720, "Odin game")
flags : rl.ConfigFlags = {.VSYNC_HINT} flags : rl.ConfigFlags = {.VSYNC_HINT}
rl.SetConfigFlags(flags) rl.SetConfigFlags(flags)
rl.SetTargetFPS(60) rl.SetTargetFPS(60)
player.position.x = CELL_SIZE * 5
player.position.y = CELL_SIZE * 5
player.mode = .INTERACT
camera.target = {player.position.x + (CELL_SIZE / 2), player.position.y + (CELL_SIZE / 2)} player = {
camera.zoom = 2 position = {CELL_SIZE * 10, CELL_SIZE * 10},
camera.offset = {f32(rl.GetScreenWidth()) / 2, f32(rl.GetScreenHeight()) / 2} camera = {
zoom = 3,
target = {player.position.x + (CELL_SIZE / 2), player.position.y + (CELL_SIZE / 2)},
offset = {f32(rl.GetScreenWidth()) / 2, f32(rl.GetScreenHeight()) / 2},
},
mode = .INTERACT,
}
load_tilemap() load_tilemap()
defer unload_tilemap() defer unload_tilemap()
fill_world_grid_with_nothing(&world) world = create_world("test_world")
set_tile(&world, tree_tile, {400,400})
save_world(&world)
place_random_trees(&world)
game_loop() game_loop()
} }
@ -50,7 +60,7 @@ game_loop :: proc() {
rl.BeginDrawing() rl.BeginDrawing()
rl.ClearBackground(rl.BLACK) rl.ClearBackground(rl.BLACK)
rl.BeginMode2D(camera) rl.BeginMode2D(player.camera)
draw() draw()
@ -59,8 +69,8 @@ game_loop :: proc() {
rl.DrawFPS(5,5) rl.DrawFPS(5,5)
player_grid_pos := get_player_grid_position(&player) player_grid_pos := get_player_grid_position(&player)
player_grid_pos_tile := get_grid_tile(&world, vec2_to_vec2i(player_grid_pos)) player_grid_pos_tile := get_world_tile(&world, vec2_to_vec2i(player_grid_pos))
status_string := rl.TextFormat("POS: %v : %v | MODE: %v", player_grid_pos, player_grid_pos_tile.type, player.mode) status_string := rl.TextFormat("POS: [%i,%i] : %v | MODE: %v", int(player_grid_pos.x), int(player_grid_pos.y), player_grid_pos_tile.type, player.mode)
rl.DrawText(status_string, 5, 25, 20, rl.RED) rl.DrawText(status_string, 5, 25, 20, rl.RED)
@ -73,11 +83,7 @@ game_loop :: proc() {
} }
update :: proc() { update :: proc() {
player_update(&player, &world)
handle_player_input(&player, &world)
handle_window_resize()
camera.target = {player.position.x + (CELL_SIZE / 2), player.position.y + (CELL_SIZE / 2)}
} }
draw :: proc() { draw :: proc() {
@ -85,16 +91,3 @@ draw :: proc() {
draw_player(&player) draw_player(&player)
} }
handle_window_resize :: proc() {
if rl.IsWindowResized() {
camera.offset = {f32(rl.GetScreenWidth()) / 2, f32(rl.GetScreenHeight()) / 2}
}
}
print_grid :: proc() {
for x in 0..< len(world.grid) {
for y in 0..< len(world.grid) {
fmt.printfln("[%d, %d] %v", x, y, world.grid[x][y].type)
}
}
}

View File

@ -1,8 +1,8 @@
package game package game
Vec2i :: struct { Vec2i :: struct {
x: u32, x: int,
y:u32, y: int,
} }
vec2i_to_vec2 :: proc(v2i:Vec2i) -> [2]f32 { vec2i_to_vec2 :: proc(v2i:Vec2i) -> [2]f32 {
@ -10,5 +10,17 @@ vec2i_to_vec2 :: proc(v2i:Vec2i) -> [2]f32 {
} }
vec2_to_vec2i :: proc(v2:[2]f32) -> Vec2i { vec2_to_vec2i :: proc(v2:[2]f32) -> Vec2i {
return {u32(v2.x), u32(v2.y)} return {int(v2.x), int(v2.y)}
}
to_bytes :: proc(v: $T) -> [size_of(T)]u8 {
val := v
encoded_bytes := (^[size_of(T)]u8)(&val)
return encoded_bytes^
}
from_bytes :: proc($T:typeid, data: [size_of(T)]u8) -> T {
bytes := data
decoded_value := (^T)(&bytes)^
return decoded_value
} }

View File

@ -3,10 +3,13 @@ package game
import rl "vendor:raylib" import rl "vendor:raylib"
import "core:fmt" import "core:fmt"
CHUNK_UNLOAD_DISTANCE :: 3
Player :: struct { Player :: struct {
position : rl.Vector2, position : rl.Vector2,
move_timer: f32, move_timer: f32,
mode: InteractMode mode: InteractMode,
camera: rl.Camera2D,
} }
InteractMode :: enum { InteractMode :: enum {
@ -14,52 +17,118 @@ InteractMode :: enum {
ATTACK, ATTACK,
} }
handle_player_input :: proc(p : ^Player, w: ^World) { handle_player_camera :: proc(p:^Player) {
p.camera.target = {p.position.x + (CELL_SIZE / 2), p.position.y + (CELL_SIZE / 2)}
if rl.IsWindowResized() {
p.camera.offset = {f32(rl.GetScreenWidth()) / 2, f32(rl.GetScreenHeight()) / 2}
}
}
player_update :: proc(p : ^Player, w: ^World) {
handle_player_input(p,w)
handle_player_camera(p)
if rl.IsKeyPressed(.SPACE) {
set_tile(w, tree_tile, vec2_to_vec2i(get_player_grid_position(p)))
}
}
player_update_chunks :: proc(p: ^Player, w: ^World) {
player_grid_pos := get_player_grid_position(p)
current_player_chunk := get_chunk_from_world_pos(w, player_grid_pos)
directions := [8]Vec2i{
Vec2i{ 1, 0 }, Vec2i{ -1, 0 }, // Right, Left
Vec2i{ 0, 1 }, Vec2i{ 0, -1 }, // Down, Up
Vec2i{ 1, 1 }, Vec2i{ -1, -1 }, // Bottom-right, Top-left
Vec2i{ 1, -1 }, Vec2i{ -1, 1 }, // Top-right, Bottom-left
}
// Always ensure the current chunk is loaded
get_chunk(w, current_player_chunk.position)
// Load adjacent chunks
for dir in directions {
adjacent_pos := Vec2i{
current_player_chunk.position.x + dir.x,
current_player_chunk.position.y + dir.y
}
get_chunk(w, adjacent_pos)
}
// Unload non-adjacent chunks
for chunk_pos in w.chunks {
if chunk_pos == current_player_chunk.position {
continue
}
is_adjacent := false
for dir in directions {
check_pos := Vec2i{
current_player_chunk.position.x + dir.x,
current_player_chunk.position.y + dir.y
}
if chunk_pos == check_pos {
is_adjacent = true
break
}
}
if !is_adjacent {
unload_chunk(chunk_pos, w)
}
}
}
handle_player_input :: proc(p:^Player, w:^World) {
target_pos := get_player_grid_position(p) target_pos := get_player_grid_position(p)
dt := rl.GetFrameTime() dt := rl.GetFrameTime()
move_delay : f32 = 0.15 move_delay : f32 = 0.2
if p.move_timer > 0 { if p.move_timer > 0 {
p.move_timer -= dt p.move_timer -= dt
} }
// fmt.printfln("MOVING TO: %v : %v", target_pos, get_grid_tile(w, vec2_to_vec2i(target_pos)).type)
if p.move_timer <= 0 { if p.move_timer <= 0 {
if rl.IsKeyDown(.D) { if rl.IsKeyDown(.D) {
target_pos.x += 1 target_pos.x += 1
if !will_collide(target_pos, w) { if !will_collide(w, target_pos) {
player.position.x += CELL_SIZE player.position.x += CELL_SIZE
p.move_timer = move_delay p.move_timer = move_delay
player_update_chunks(p,w)
} }
} }
if rl.IsKeyDown(.A) { if rl.IsKeyDown(.A) {
target_pos.x -= 1 target_pos.x -= 1
if !will_collide(target_pos, w) { if !will_collide(w, target_pos) {
player.position.x -= CELL_SIZE player.position.x -= CELL_SIZE
p.move_timer = move_delay p.move_timer = move_delay
} player_update_chunks(p,w)
}
} }
if rl.IsKeyDown(.W) { if rl.IsKeyDown(.W) {
target_pos.y -= 1 target_pos.y -= 1
if !will_collide(target_pos, w) { if !will_collide(w, target_pos) {
player.position.y -= CELL_SIZE player.position.y -= CELL_SIZE
p.move_timer = move_delay p.move_timer = move_delay
} player_update_chunks(p,w)
}
} }
if rl.IsKeyDown(.S) { if rl.IsKeyDown(.S) {
target_pos.y += 1 target_pos.y += 1
if !will_collide(target_pos, w) { if !will_collide(w, target_pos) {
p.move_timer = move_delay p.move_timer = move_delay
player.position.y += CELL_SIZE player.position.y += CELL_SIZE
} player_update_chunks(p,w)
}
} }
} }
} }
@ -74,12 +143,13 @@ draw_player :: proc(player:^Player) {
draw_tile({27,0}, player.position, rl.DARKBLUE) draw_tile({27,0}, player.position, rl.DARKBLUE)
} }
will_collide :: proc(pos:rl.Vector2, w:^World) -> bool { will_collide :: proc(w:^World, pos:rl.Vector2) -> bool {
if pos.y > WORLD_SIZE * CELL_SIZE || pos.x > WORLD_SIZE * CELL_SIZE { world_grid_pos := vec2_to_vec2i(pos)
return false chunk_pos := world_pos_to_chunk_pos(pos)
} local_pos := get_local_chunk_pos(world_grid_pos)
tile := get_grid_tile(w, vec2_to_vec2i(pos)) chunk := get_chunk(w, chunk_pos)
tile := get_chunk_tile(chunk, local_pos)
#partial switch tile.type { #partial switch tile.type {
case .WALL: case .WALL:

View File

@ -3,21 +3,43 @@ package game
import rl "vendor:raylib" import rl "vendor:raylib"
import "core:math/rand" import "core:math/rand"
Tile :: struct #packed {
tilemap_pos: Vec2i,
color: [4]u8,
type: TileType,
interaction: InteractionType,
resource: ResourceType,
}
TileType :: enum u8 {
NOTHING,
WALL,
FOLIAGE,
}
ResourceType :: enum u8 {
NOTHING,
TREE,
}
InteractionType :: enum u8 {
NOTHING,
RESOURCE,
ENEMY,
}
nothing_tile := Tile {
type = .FOLIAGE,
tilemap_pos = {1,2},
color = {30,30,0,255},
interaction = .NOTHING,
resource = .NOTHING
}
tree_tile := Tile { tree_tile := Tile {
type = .WALL, type = .WALL,
tilemap_pos = {0,1}, tilemap_pos = {0,1},
color = rl.DARKGREEN color = {17,87,30,255},
} resource = .TREE,
interaction = .RESOURCE,
place_random_trees :: proc(w:^World) {
for x in 0..< len(w.grid) {
for y in 0..< len(w.grid) {
chance := rand.int_max(100)
if chance <= 5 {
w.grid[x][y] = tree_tile
}
}
}
} }

View File

@ -2,60 +2,228 @@ package game
import rl "vendor:raylib" import rl "vendor:raylib"
import "core:fmt" import "core:fmt"
import "core:os"
import "core:path/filepath"
import "core:mem"
CELL_SIZE :: 16 CELL_SIZE :: 16
WORLD_SIZE :: 100 CHUNK_SIZE :: 32
WORLD_DATA_PATH :: "data/worlds"
World :: struct { World :: struct {
grid: [WORLD_SIZE][WORLD_SIZE]Tile data_dir: string,
chunks: map[Vec2i]Chunk
} }
Tile :: struct { Chunk :: struct #packed {
type: TileType, position: Vec2i,
tilemap_pos:rl.Vector2, tiles: [CHUNK_SIZE][CHUNK_SIZE]Tile,
color:rl.Color,
} }
TileType :: enum { create_world :: proc(name:string) -> World {
NOTHING, data_dir := fmt.tprintf("%v/%v", WORLD_DATA_PATH, name)
WALL, if !os.is_dir(data_dir) {
DOOR, fmt.printfln("Data dir: %v does not exist", data_dir)
FLOOR, os.make_directory(data_dir)
} }
set_grid_tile :: proc(w:^World, pos:Vec2i, t:Tile) { chunk_dir := fmt.tprintf("%v/%v", data_dir, "chunks")
w.grid[pos.x][pos.y] = t if !os.is_dir(chunk_dir) {
} os.make_directory(chunk_dir)
}
get_grid_tile :: proc(w: ^World, pos: Vec2i) -> Tile {
if pos.x < 0 || pos.x >= len(w.grid) || pos.y < 0 || pos.y >= len(w.grid[0]) { return World {
// fmt.printfln("Target [%v] outside of world bounds", pos) data_dir = data_dir,
return w.grid[0][0] // Default or error tile chunks = make(map[Vec2i]Chunk),
} }
return w.grid[pos.x][pos.y]
} }
fill_world_grid_with_nothing :: proc(w:^World) { load_world :: proc(name:string) -> World {
for x in 0..< len(w.grid) { dir := fmt.tprintf("%v/%v", WORLD_DATA_PATH, name)
for y in 0..<len(w.grid) { if !os.is_dir(dir) {
w.grid[x][y] = Tile { panic("Couldnt load world")
type = .NOTHING, }
tilemap_pos = {0,0}
} return World {
data_dir = dir,
chunks = make(map[Vec2i]Chunk),
}
}
save_world :: proc(w:^World) {
if !os.is_dir(w.data_dir) {
panic("World has invalid data_path")
}
// fmt.printfln("Saving world %v", w.data_dir)
for chunk in w.chunks {
save_chunk(get_chunk(w, chunk), w)
}
}
save_chunk :: proc(c:^Chunk, w:^World) {
chunk_dir := fmt.tprintf("%v/%v", w.data_dir, "chunks")
filename := fmt.tprintf("%v/%v_%v.chunk", chunk_dir, c.position.x, c.position.y)
// fmt.printfln("Saving chunk: %v", filename)
data := make([dynamic]u8)
// Append Position
for byte in transmute([size_of(int)]u8)c.position.x {append(&data, byte)}
for byte in transmute([size_of(int)]u8)c.position.y {append(&data, byte)}
// Append Tiles
for row in &c.tiles {
for tile in row {
for byte in transmute([size_of(int)]u8)tile.tilemap_pos.x {append(&data, byte)}
for byte in transmute([size_of(int)]u8)tile.tilemap_pos.y {append(&data, byte)}
for byte in transmute([4]u8)tile.color {append(&data, byte)}
for byte in transmute([size_of(TileType)]u8)tile.type {append(&data, byte)}
for byte in transmute([size_of(InteractionType)]u8)tile.interaction {append(&data, byte)}
for byte in transmute([size_of(ResourceType)]u8)tile.resource {append(&data, byte)}
} }
} }
err := os.write_entire_file_or_err(filename, data[:])
} }
load_chunk :: proc(pos:Vec2i, w:^World) -> Chunk {
chunk_dir := fmt.tprintf("%v/%v", w.data_dir, "chunks")
filename := fmt.tprintf("%v/%v_%v.chunk", chunk_dir, pos.x, pos.y)
data, err := os.read_entire_file_from_filename_or_err(filename)
if err != nil {
// fmt.printfln("No chunk %v found, generating new chunk", pos)
chunk := generate_chunk(pos)
save_chunk(&chunk, w)
return chunk
}
chunk: Chunk
offset := 0
// Load Position
mem.copy(transmute([^]u8)&chunk.position.x, &data[offset], size_of(int))
offset += size_of(int)
mem.copy(transmute([^]u8)&chunk.position.y, &data[offset], size_of(int))
offset += size_of(int)
// Load tiles
for row_index := 0; row_index < len(chunk.tiles); row_index += 1 {
for tile_index := 0; tile_index < len(chunk.tiles[row_index]); tile_index += 1 {
tile := &chunk.tiles[row_index][tile_index] // Get address of tile
mem.copy(&tile.tilemap_pos.x, &data[offset], size_of(int))
offset += size_of(int)
mem.copy(&tile.tilemap_pos.y, &data[offset], size_of(int))
offset += size_of(int)
// Load color
color_temp: [4]u8
mem.copy(&color_temp, &data[offset], 4)
tile.color = color_temp
offset += 4
mem.copy(&tile.type, &data[offset], size_of(TileType))
offset += size_of(TileType)
mem.copy(&tile.interaction, &data[offset], size_of(InteractionType))
offset += size_of(InteractionType)
mem.copy(&tile.resource, &data[offset], size_of(ResourceType))
offset += size_of(ResourceType)
}
}
return chunk
}
unload_chunk :: proc(pos:Vec2i, w:^World) {
_, exists := w.chunks[pos]
if exists {
save_chunk(get_chunk(w, pos), w)
delete_key(&w.chunks, pos)
}
}
generate_chunk :: proc(pos:Vec2i) -> Chunk {
chunk := Chunk {position = pos}
for x in 0..<CHUNK_SIZE {
for y in 0..<CHUNK_SIZE {
chunk.tiles[x][y] = nothing_tile
}
}
center_pos := Vec2i{CHUNK_SIZE/2, CHUNK_SIZE/2}
set_chunk_tile(&chunk, tree_tile, center_pos)
return chunk
}
get_chunk :: proc(w:^World, chunk_pos:Vec2i) -> ^Chunk {
chunk, exists := w.chunks[chunk_pos]
if !exists {
w.chunks[chunk_pos] = load_chunk(chunk_pos, w)
}
return &w.chunks[chunk_pos]
}
get_chunk_from_world_pos :: proc(w:^World, pos:rl.Vector2) -> ^Chunk {
chunk_pos := world_pos_to_chunk_pos(pos)
return get_chunk(w, chunk_pos)
}
world_pos_to_chunk_pos :: proc(pos:rl.Vector2) -> Vec2i {
chunk_pos := vec2_to_vec2i({pos.x / CHUNK_SIZE, pos.y / CHUNK_SIZE})
return chunk_pos
}
get_local_chunk_pos :: proc(pos:Vec2i) -> Vec2i {
return Vec2i {
(pos.x % CHUNK_SIZE + CHUNK_SIZE) % CHUNK_SIZE,
(pos.y % CHUNK_SIZE + CHUNK_SIZE) % CHUNK_SIZE,
}
}
get_world_tile :: proc(w:^World, pos:Vec2i) -> ^Tile {
chunk_pos := world_pos_to_chunk_pos(vec2i_to_vec2(pos))
local_pos := get_local_chunk_pos(pos)
chunk := get_chunk(w, chunk_pos)
return get_chunk_tile(chunk, local_pos)
}
get_chunk_tile :: proc(c:^Chunk, pos:Vec2i) -> ^Tile {
return &c.tiles[pos.x][pos.y]
}
set_chunk_tile :: proc(c:^Chunk, t:Tile, pos:Vec2i) {
c.tiles[pos.x][pos.y] = t
}
set_tile :: proc(w:^World, t:Tile, p:Vec2i) {
chunk := get_chunk_from_world_pos(w, vec2i_to_vec2(p))
set_chunk_tile(chunk, t, get_local_chunk_pos(p))
save_chunk(chunk, w)
}
draw_world :: proc(w:^World) { draw_world :: proc(w:^World) {
for x in 0..< len(w.grid) { for chunk_pos, chunk in w.chunks {
for y in 0..< len(w.grid) { for x in 0..<CHUNK_SIZE {
tile := w.grid[x][y] for y in 0..<CHUNK_SIZE {
posX := x * TILE_SIZE tile := chunk.tiles[x][y]
posY := y * TILE_SIZE world_x := chunk_pos.x * CHUNK_SIZE + x
world_y := chunk_pos.y * CHUNK_SIZE + y
pos := rl.Vector2{f32(world_x * CELL_SIZE), f32(world_y * CELL_SIZE)}
if tile.type != .NOTHING { if tile.type != .NOTHING {
draw_tile(tile.tilemap_pos, {f32(posX), f32(posY)}, tile.color) draw_tile(vec2i_to_vec2(tile.tilemap_pos), pos, rl.Color(tile.color))
}
} }
} }
} }