diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..868c7cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +bin/ +game/data diff --git a/game/game b/game/game index 77ea0fd..165654d 100755 Binary files a/game/game and b/game/game differ diff --git a/game/game.odin b/game/game.odin index f405e7a..aaff83a 100644 --- a/game/game.odin +++ b/game/game.odin @@ -2,39 +2,49 @@ package game import "core:fmt" import rl "vendor:raylib" -import rand "core:math/rand" -import "core:strconv" -import "core:mem" -import "core:strings" +import "core:os" player : Player world : World -camera : rl.Camera2D - 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") + flags : rl.ConfigFlags = {.VSYNC_HINT} rl.SetConfigFlags(flags) 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)} - camera.zoom = 2 - camera.offset = {f32(rl.GetScreenWidth()) / 2, f32(rl.GetScreenHeight()) / 2} + player = { + position = {CELL_SIZE * 10, CELL_SIZE * 10}, + 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() 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() } @@ -50,7 +60,7 @@ game_loop :: proc() { rl.BeginDrawing() rl.ClearBackground(rl.BLACK) - rl.BeginMode2D(camera) + rl.BeginMode2D(player.camera) draw() @@ -59,8 +69,8 @@ game_loop :: proc() { rl.DrawFPS(5,5) player_grid_pos := get_player_grid_position(&player) - player_grid_pos_tile := get_grid_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) + player_grid_pos_tile := get_world_tile(&world, vec2_to_vec2i(player_grid_pos)) + 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) @@ -73,11 +83,7 @@ game_loop :: proc() { } update :: proc() { - - handle_player_input(&player, &world) - handle_window_resize() - camera.target = {player.position.x + (CELL_SIZE / 2), player.position.y + (CELL_SIZE / 2)} - + player_update(&player, &world) } draw :: proc() { @@ -85,16 +91,3 @@ draw :: proc() { 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) - } - } -} diff --git a/game/math.odin b/game/math.odin index d036570..2cb0de4 100644 --- a/game/math.odin +++ b/game/math.odin @@ -1,8 +1,8 @@ package game Vec2i :: struct { - x: u32, - y:u32, + x: int, + y: int, } 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 { - 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 } diff --git a/game/player.odin b/game/player.odin index 40e4b4f..88f06aa 100644 --- a/game/player.odin +++ b/game/player.odin @@ -3,10 +3,13 @@ package game import rl "vendor:raylib" import "core:fmt" +CHUNK_UNLOAD_DISTANCE :: 3 + Player :: struct { position : rl.Vector2, move_timer: f32, - mode: InteractMode + mode: InteractMode, + camera: rl.Camera2D, } InteractMode :: enum { @@ -14,52 +17,118 @@ InteractMode :: enum { 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) dt := rl.GetFrameTime() - move_delay : f32 = 0.15 + move_delay : f32 = 0.2 if p.move_timer > 0 { 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 rl.IsKeyDown(.D) { target_pos.x += 1 - if !will_collide(target_pos, w) { + if !will_collide(w, target_pos) { player.position.x += CELL_SIZE p.move_timer = move_delay + player_update_chunks(p,w) } } if rl.IsKeyDown(.A) { target_pos.x -= 1 - if !will_collide(target_pos, w) { + if !will_collide(w, target_pos) { player.position.x -= CELL_SIZE p.move_timer = move_delay - } + player_update_chunks(p,w) + } } if rl.IsKeyDown(.W) { target_pos.y -= 1 - if !will_collide(target_pos, w) { + if !will_collide(w, target_pos) { player.position.y -= CELL_SIZE p.move_timer = move_delay - } + player_update_chunks(p,w) + } } if rl.IsKeyDown(.S) { target_pos.y += 1 - if !will_collide(target_pos, w) { + if !will_collide(w, target_pos) { p.move_timer = move_delay 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) } -will_collide :: proc(pos:rl.Vector2, w:^World) -> bool { - if pos.y > WORLD_SIZE * CELL_SIZE || pos.x > WORLD_SIZE * CELL_SIZE { - return false - } +will_collide :: proc(w:^World, pos:rl.Vector2) -> bool { + world_grid_pos := vec2_to_vec2i(pos) + 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 { case .WALL: diff --git a/game/tiles.odin b/game/tiles.odin index ac610ba..36fffe6 100644 --- a/game/tiles.odin +++ b/game/tiles.odin @@ -3,21 +3,43 @@ package game import rl "vendor:raylib" 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 { type = .WALL, tilemap_pos = {0,1}, - color = rl.DARKGREEN -} - -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 - } - } - } + color = {17,87,30,255}, + resource = .TREE, + interaction = .RESOURCE, } diff --git a/game/world.odin b/game/world.odin index 616e2af..5044b55 100644 --- a/game/world.odin +++ b/game/world.odin @@ -2,60 +2,228 @@ package game import rl "vendor:raylib" import "core:fmt" - +import "core:os" +import "core:path/filepath" +import "core:mem" CELL_SIZE :: 16 -WORLD_SIZE :: 100 +CHUNK_SIZE :: 32 +WORLD_DATA_PATH :: "data/worlds" World :: struct { - grid: [WORLD_SIZE][WORLD_SIZE]Tile + data_dir: string, + chunks: map[Vec2i]Chunk } -Tile :: struct { - type: TileType, - tilemap_pos:rl.Vector2, - color:rl.Color, +Chunk :: struct #packed { + position: Vec2i, + tiles: [CHUNK_SIZE][CHUNK_SIZE]Tile, } -TileType :: enum { - NOTHING, - WALL, - DOOR, - FLOOR, -} - -set_grid_tile :: proc(w:^World, pos:Vec2i, t:Tile) { - w.grid[pos.x][pos.y] = t -} - -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]) { - // fmt.printfln("Target [%v] outside of world bounds", pos) - return w.grid[0][0] // Default or error tile +create_world :: proc(name:string) -> World { + data_dir := fmt.tprintf("%v/%v", WORLD_DATA_PATH, name) + if !os.is_dir(data_dir) { + fmt.printfln("Data dir: %v does not exist", data_dir) + os.make_directory(data_dir) + } + + chunk_dir := fmt.tprintf("%v/%v", data_dir, "chunks") + if !os.is_dir(chunk_dir) { + os.make_directory(chunk_dir) + } + + return World { + data_dir = data_dir, + chunks = make(map[Vec2i]Chunk), } - return w.grid[pos.x][pos.y] } -fill_world_grid_with_nothing :: proc(w:^World) { - for x in 0..< len(w.grid) { - for y in 0.. World { + dir := fmt.tprintf("%v/%v", WORLD_DATA_PATH, name) + if !os.is_dir(dir) { + panic("Couldnt load world") + } + + 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 { + 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) { - for x in 0..< len(w.grid) { - for y in 0..< len(w.grid) { - tile := w.grid[x][y] - posX := x * TILE_SIZE - posY := y * TILE_SIZE + for chunk_pos, chunk in w.chunks { + for x in 0..