From 1c1ff67d889802875aeac234e60bda026f49a87d Mon Sep 17 00:00:00 2001 From: Floppyrj45 Date: Sun, 19 Apr 2026 17:06:35 +0200 Subject: [PATCH] feat(world): voxel chunks + procedural underwater generation + biomes Co-Authored-By: Claude Sonnet 4.6 --- project.godot | 3 + scenes/World.tscn | 10 ++ scripts/world/BlockDatabase.gd | 102 +++++++++++++++++++ scripts/world/Chunk.gd | 175 ++++++++++++++++++++++++++++++++ scripts/world/ChunkManager.gd | 110 ++++++++++++++++++++ scripts/world/WorldGenerator.gd | 137 +++++++++++++++++++++++++ 6 files changed, 537 insertions(+) create mode 100644 scenes/World.tscn create mode 100644 scripts/world/BlockDatabase.gd create mode 100644 scripts/world/Chunk.gd create mode 100644 scripts/world/ChunkManager.gd create mode 100644 scripts/world/WorldGenerator.gd diff --git a/project.godot b/project.godot index 275503c..5022fef 100644 --- a/project.godot +++ b/project.godot @@ -16,5 +16,8 @@ window/size/mode=2 [input] ; Bindings custom — à compléter plus tard par le module dauphin +[autoload] +BlockDatabase="*res://scripts/world/BlockDatabase.gd" + [rendering] environment/defaults/default_clear_color=Color(0.05, 0.15, 0.25, 1) diff --git a/scenes/World.tscn b/scenes/World.tscn new file mode 100644 index 0000000..baa990a --- /dev/null +++ b/scenes/World.tscn @@ -0,0 +1,10 @@ +[gd_scene load_steps=2 format=3 uid="uid://dauphincraft_world"] + +[ext_resource type="Script" path="res://scripts/world/ChunkManager.gd" id="1_chunkman"] + +[node name="World" type="Node3D"] + +[node name="ChunkManager" type="Node3D" parent="." node_paths=PackedStringArray()] +script = ExtResource("1_chunkman") +render_distance = 4 +world_seed = 12345 diff --git a/scripts/world/BlockDatabase.gd b/scripts/world/BlockDatabase.gd new file mode 100644 index 0000000..ce68c2c --- /dev/null +++ b/scripts/world/BlockDatabase.gd @@ -0,0 +1,102 @@ +extends Node + +enum BlockType { + AIR = 0, + WATER = 1, + SAND = 2, + ROCK = 3, + CORAL_RED = 4, + CORAL_BLUE = 5, + KELP = 6, + WRECK_WOOD = 7, + ICE = 8, + BEDROCK = 9 +} + +const _BLOCKS: Dictionary = { + BlockType.AIR: { + "name": "Air", + "color": Color(0.0, 0.0, 0.0, 0.0), + "hardness": 0.0, + "drops": [] + }, + BlockType.WATER: { + "name": "Eau", + "color": Color(0.1, 0.4, 0.8, 0.6), + "hardness": 0.0, + "drops": [] + }, + BlockType.SAND: { + "name": "Sable", + "color": Color(0.76, 0.70, 0.50, 1.0), + "hardness": 0.5, + "drops": [BlockType.SAND] + }, + BlockType.ROCK: { + "name": "Roche", + "color": Color(0.45, 0.45, 0.45, 1.0), + "hardness": 2.0, + "drops": [BlockType.ROCK] + }, + BlockType.CORAL_RED: { + "name": "Corail Rouge", + "color": Color(0.90, 0.25, 0.20, 1.0), + "hardness": 0.3, + "drops": [BlockType.CORAL_RED] + }, + BlockType.CORAL_BLUE: { + "name": "Corail Bleu", + "color": Color(0.20, 0.50, 0.95, 1.0), + "hardness": 0.3, + "drops": [BlockType.CORAL_BLUE] + }, + BlockType.KELP: { + "name": "Algue", + "color": Color(0.15, 0.60, 0.20, 1.0), + "hardness": 0.1, + "drops": [BlockType.KELP] + }, + BlockType.WRECK_WOOD: { + "name": "Bois d'Épave", + "color": Color(0.35, 0.22, 0.12, 1.0), + "hardness": 1.0, + "drops": [BlockType.WRECK_WOOD] + }, + BlockType.ICE: { + "name": "Glace", + "color": Color(0.75, 0.90, 1.0, 0.85), + "hardness": 0.8, + "drops": [] + }, + BlockType.BEDROCK: { + "name": "Bedrock", + "color": Color(0.15, 0.15, 0.15, 1.0), + "hardness": -1.0, + "drops": [] + } +} + +func is_solid(id: int) -> bool: + if id == BlockType.AIR or id == BlockType.WATER or id == BlockType.KELP: + return false + return true + +func get_color(id: int) -> Color: + if _BLOCKS.has(id): + return _BLOCKS[id]["color"] + return Color(1.0, 0.0, 1.0, 1.0) + +func get_block_name(id: int) -> String: + if _BLOCKS.has(id): + return _BLOCKS[id]["name"] + return "Unknown" + +func get_hardness(id: int) -> float: + if _BLOCKS.has(id): + return _BLOCKS[id]["hardness"] + return 1.0 + +func get_drops(id: int) -> Array: + if _BLOCKS.has(id): + return _BLOCKS[id]["drops"] + return [] diff --git a/scripts/world/Chunk.gd b/scripts/world/Chunk.gd new file mode 100644 index 0000000..7a7c4c7 --- /dev/null +++ b/scripts/world/Chunk.gd @@ -0,0 +1,175 @@ +extends Node3D +class_name Chunk + +const CHUNK_SIZE: int = 16 + +var _blocks: PackedInt32Array = PackedInt32Array() +var _mesh_instance: MeshInstance3D = null +var _static_body: StaticBody3D = null + +func _ready() -> void: + if _blocks.size() == 0: + _blocks.resize(4096) + +func init_blocks(data: PackedInt32Array) -> void: + _blocks = data + +func get_block(x: int, y: int, z: int) -> int: + if x < 0 or x >= CHUNK_SIZE or y < 0 or y >= CHUNK_SIZE or z < 0 or z >= CHUNK_SIZE: + return BlockDatabase.BlockType.AIR + return _blocks[x + y * CHUNK_SIZE + z * 256] + +func set_block(x: int, y: int, z: int, id: int) -> void: + if x < 0 or x >= CHUNK_SIZE or y < 0 or y >= CHUNK_SIZE or z < 0 or z >= CHUNK_SIZE: + return + _blocks[x + y * CHUNK_SIZE + z * 256] = id + +func generate_mesh() -> void: + if _mesh_instance != null: + _mesh_instance.queue_free() + _mesh_instance = null + if _static_body != null: + _static_body.queue_free() + _static_body = null + + var st: SurfaceTool = SurfaceTool.new() + st.begin(Mesh.PRIMITIVE_TRIANGLES) + + var has_faces: bool = false + + for x: int in range(CHUNK_SIZE): + for y: int in range(CHUNK_SIZE): + for z: int in range(CHUNK_SIZE): + var block_id: int = get_block(x, y, z) + if not BlockDatabase.is_solid(block_id): + continue + + var color: Color = BlockDatabase.get_color(block_id) + + if not _is_opaque_at(x, y + 1, z): + _add_face_top(st, x, y, z, color) + has_faces = true + if not _is_opaque_at(x, y - 1, z): + _add_face_bottom(st, x, y, z, color) + has_faces = true + if not _is_opaque_at(x + 1, y, z): + _add_face_right(st, x, y, z, color) + has_faces = true + if not _is_opaque_at(x - 1, y, z): + _add_face_left(st, x, y, z, color) + has_faces = true + if not _is_opaque_at(x, y, z + 1): + _add_face_front(st, x, y, z, color) + has_faces = true + if not _is_opaque_at(x, y, z - 1): + _add_face_back(st, x, y, z, color) + has_faces = true + + if not has_faces: + return + + st.generate_normals() + + var mat: StandardMaterial3D = StandardMaterial3D.new() + mat.vertex_color_use_as_albedo = true + mat.shading_mode = BaseMaterial3D.SHADING_MODE_PER_VERTEX + st.set_material(mat) + + var mesh: ArrayMesh = st.commit() + + _mesh_instance = MeshInstance3D.new() + _mesh_instance.mesh = mesh + add_child(_mesh_instance) + + var shape: ConcavePolygonShape3D = ConcavePolygonShape3D.new() + shape.set_faces(mesh.get_faces()) + + var col_shape: CollisionShape3D = CollisionShape3D.new() + col_shape.shape = shape + + _static_body = StaticBody3D.new() + _static_body.add_child(col_shape) + add_child(_static_body) + +func _is_opaque_at(x: int, y: int, z: int) -> bool: + if x < 0 or x >= CHUNK_SIZE or y < 0 or y >= CHUNK_SIZE or z < 0 or z >= CHUNK_SIZE: + return false + return BlockDatabase.is_solid(get_block(x, y, z)) + +func _add_face_top(st: SurfaceTool, x: int, y: int, z: int, color: Color) -> void: + var fx: float = float(x) + var fy: float = float(y) + var fz: float = float(z) + st.set_color(color) + st.add_vertex(Vector3(fx, fy + 1.0, fz)) + st.add_vertex(Vector3(fx + 1.0, fy + 1.0, fz)) + st.add_vertex(Vector3(fx + 1.0, fy + 1.0, fz + 1.0)) + st.set_color(color) + st.add_vertex(Vector3(fx, fy + 1.0, fz)) + st.add_vertex(Vector3(fx + 1.0, fy + 1.0, fz + 1.0)) + st.add_vertex(Vector3(fx, fy + 1.0, fz + 1.0)) + +func _add_face_bottom(st: SurfaceTool, x: int, y: int, z: int, color: Color) -> void: + var fx: float = float(x) + var fy: float = float(y) + var fz: float = float(z) + st.set_color(color) + st.add_vertex(Vector3(fx, fy, fz + 1.0)) + st.add_vertex(Vector3(fx + 1.0, fy, fz + 1.0)) + st.add_vertex(Vector3(fx + 1.0, fy, fz)) + st.set_color(color) + st.add_vertex(Vector3(fx, fy, fz + 1.0)) + st.add_vertex(Vector3(fx + 1.0, fy, fz)) + st.add_vertex(Vector3(fx, fy, fz)) + +func _add_face_right(st: SurfaceTool, x: int, y: int, z: int, color: Color) -> void: + var fx: float = float(x) + var fy: float = float(y) + var fz: float = float(z) + st.set_color(color) + st.add_vertex(Vector3(fx + 1.0, fy, fz)) + st.add_vertex(Vector3(fx + 1.0, fy, fz + 1.0)) + st.add_vertex(Vector3(fx + 1.0, fy + 1.0, fz + 1.0)) + st.set_color(color) + st.add_vertex(Vector3(fx + 1.0, fy, fz)) + st.add_vertex(Vector3(fx + 1.0, fy + 1.0, fz + 1.0)) + st.add_vertex(Vector3(fx + 1.0, fy + 1.0, fz)) + +func _add_face_left(st: SurfaceTool, x: int, y: int, z: int, color: Color) -> void: + var fx: float = float(x) + var fy: float = float(y) + var fz: float = float(z) + st.set_color(color) + st.add_vertex(Vector3(fx, fy, fz + 1.0)) + st.add_vertex(Vector3(fx, fy, fz)) + st.add_vertex(Vector3(fx, fy + 1.0, fz)) + st.set_color(color) + st.add_vertex(Vector3(fx, fy, fz + 1.0)) + st.add_vertex(Vector3(fx, fy + 1.0, fz)) + st.add_vertex(Vector3(fx, fy + 1.0, fz + 1.0)) + +func _add_face_front(st: SurfaceTool, x: int, y: int, z: int, color: Color) -> void: + var fx: float = float(x) + var fy: float = float(y) + var fz: float = float(z) + st.set_color(color) + st.add_vertex(Vector3(fx + 1.0, fy, fz + 1.0)) + st.add_vertex(Vector3(fx, fy, fz + 1.0)) + st.add_vertex(Vector3(fx, fy + 1.0, fz + 1.0)) + st.set_color(color) + st.add_vertex(Vector3(fx + 1.0, fy, fz + 1.0)) + st.add_vertex(Vector3(fx, fy + 1.0, fz + 1.0)) + st.add_vertex(Vector3(fx + 1.0, fy + 1.0, fz + 1.0)) + +func _add_face_back(st: SurfaceTool, x: int, y: int, z: int, color: Color) -> void: + var fx: float = float(x) + var fy: float = float(y) + var fz: float = float(z) + st.set_color(color) + st.add_vertex(Vector3(fx, fy, fz)) + st.add_vertex(Vector3(fx + 1.0, fy, fz)) + st.add_vertex(Vector3(fx + 1.0, fy + 1.0, fz)) + st.set_color(color) + st.add_vertex(Vector3(fx, fy, fz)) + st.add_vertex(Vector3(fx + 1.0, fy + 1.0, fz)) + st.add_vertex(Vector3(fx, fy + 1.0, fz)) diff --git a/scripts/world/ChunkManager.gd b/scripts/world/ChunkManager.gd new file mode 100644 index 0000000..4c26402 --- /dev/null +++ b/scripts/world/ChunkManager.gd @@ -0,0 +1,110 @@ +extends Node3D +class_name ChunkManager + +const CHUNK_SIZE: int = 16 + +@export var render_distance: int = 4 +@export var world_seed: int = 12345 + +var chunks: Dictionary = {} + +func _ready() -> void: + _load_initial_chunks() + +func _load_initial_chunks() -> void: + for cx: int in range(-render_distance, render_distance + 1): + for cy: int in range(-render_distance, render_distance + 1): + for cz: int in range(-render_distance, render_distance + 1): + _load_chunk(Vector3i(cx, cy, cz)) + +func update_player_position(pos: Vector3) -> void: + var player_chunk: Vector3i = world_to_chunk_coord(pos) + + var to_unload: Array[Vector3i] = [] + for coord: Vector3i in chunks.keys(): + var dist: Vector3i = coord - player_chunk + if abs(dist.x) > render_distance or abs(dist.y) > render_distance or abs(dist.z) > render_distance: + to_unload.append(coord) + + for coord: Vector3i in to_unload: + _unload_chunk(coord) + + for cx: int in range(player_chunk.x - render_distance, player_chunk.x + render_distance + 1): + for cy: int in range(player_chunk.y - render_distance, player_chunk.y + render_distance + 1): + for cz: int in range(player_chunk.z - render_distance, player_chunk.z + render_distance + 1): + var coord: Vector3i = Vector3i(cx, cy, cz) + if not chunks.has(coord): + _load_chunk(coord) + +func break_block(world_pos: Vector3) -> int: + var chunk_coord: Vector3i = world_to_chunk_coord(world_pos) + if not chunks.has(chunk_coord): + return 0 + var chunk: Chunk = chunks[chunk_coord] + var local: Vector3i = world_to_local(world_pos) + var old_id: int = chunk.get_block(local.x, local.y, local.z) + if old_id == BlockDatabase.BlockType.AIR: + return 0 + chunk.set_block(local.x, local.y, local.z, BlockDatabase.BlockType.AIR) + chunk.generate_mesh() + return old_id + +func place_block(world_pos: Vector3, block_id: int) -> bool: + var chunk_coord: Vector3i = world_to_chunk_coord(world_pos) + if not chunks.has(chunk_coord): + return false + var chunk: Chunk = chunks[chunk_coord] + var local: Vector3i = world_to_local(world_pos) + var current: int = chunk.get_block(local.x, local.y, local.z) + if current != BlockDatabase.BlockType.AIR and current != BlockDatabase.BlockType.WATER: + return false + chunk.set_block(local.x, local.y, local.z, block_id) + chunk.generate_mesh() + return true + +func get_block(world_pos: Vector3) -> int: + var chunk_coord: Vector3i = world_to_chunk_coord(world_pos) + if not chunks.has(chunk_coord): + return BlockDatabase.BlockType.AIR + var chunk: Chunk = chunks[chunk_coord] + var local: Vector3i = world_to_local(world_pos) + return chunk.get_block(local.x, local.y, local.z) + +func world_to_chunk_coord(pos: Vector3) -> Vector3i: + return Vector3i( + int(floor(pos.x / float(CHUNK_SIZE))), + int(floor(pos.y / float(CHUNK_SIZE))), + int(floor(pos.z / float(CHUNK_SIZE))) + ) + +func chunk_to_world(coord: Vector3i) -> Vector3: + return Vector3( + float(coord.x * CHUNK_SIZE), + float(coord.y * CHUNK_SIZE), + float(coord.z * CHUNK_SIZE) + ) + +func world_to_local(pos: Vector3) -> Vector3i: + var ix: int = int(floor(pos.x)) + var iy: int = int(floor(pos.y)) + var iz: int = int(floor(pos.z)) + return Vector3i( + ((ix % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE, + ((iy % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE, + ((iz % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE + ) + +func _load_chunk(coord: Vector3i) -> void: + var data: PackedInt32Array = WorldGenerator.generate_chunk(coord.x, coord.y, coord.z, world_seed) + var chunk: Chunk = Chunk.new() + add_child(chunk) + chunk.init_blocks(data) + chunk.position = chunk_to_world(coord) + chunk.generate_mesh() + chunks[coord] = chunk + +func _unload_chunk(coord: Vector3i) -> void: + if chunks.has(coord): + var chunk: Chunk = chunks[coord] + chunk.queue_free() + chunks.erase(coord) diff --git a/scripts/world/WorldGenerator.gd b/scripts/world/WorldGenerator.gd new file mode 100644 index 0000000..10cdc20 --- /dev/null +++ b/scripts/world/WorldGenerator.gd @@ -0,0 +1,137 @@ +class_name WorldGenerator + +const CHUNK_SIZE: int = 16 + +static func generate_chunk(chunk_x: int, chunk_y: int, chunk_z: int, seed: int) -> PackedInt32Array: + var data: PackedInt32Array = PackedInt32Array() + data.resize(4096) + + var noise_surface: FastNoiseLite = FastNoiseLite.new() + noise_surface.seed = seed + noise_surface.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH + noise_surface.frequency = 0.015 + + var noise_biome: FastNoiseLite = FastNoiseLite.new() + noise_biome.seed = seed + 1337 + noise_biome.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH + noise_biome.frequency = 0.004 + + var noise_detail: FastNoiseLite = FastNoiseLite.new() + noise_detail.seed = seed + 42 + noise_detail.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH + noise_detail.frequency = 0.05 + + var noise_wreck: FastNoiseLite = FastNoiseLite.new() + noise_wreck.seed = seed + 999 + noise_wreck.noise_type = FastNoiseLite.TYPE_CELLULAR + noise_wreck.frequency = 0.03 + + for lx: int in range(CHUNK_SIZE): + for lz: int in range(CHUNK_SIZE): + var wx: int = chunk_x * CHUNK_SIZE + lx + var wz: int = chunk_z * CHUNK_SIZE + lz + + var surface_val: float = noise_surface.get_noise_2d(float(wx), float(wz)) + var ground_height: int = int(-20.0 + surface_val * 20.0) + + var biome_val: float = noise_biome.get_noise_2d(float(wx), float(wz)) + var biome: int = _get_biome(biome_val, ground_height) + + var wreck_val: float = noise_wreck.get_noise_2d(float(wx), float(wz)) + var is_wreck_center: bool = wreck_val > 0.75 + + for ly: int in range(CHUNK_SIZE): + var wy: int = chunk_y * CHUNK_SIZE + ly + var idx: int = lx + ly * CHUNK_SIZE + lz * 256 + + data[idx] = _get_block_at(wy, ground_height, biome, wx, wz, lx, lz, + is_wreck_center, noise_detail, seed) + + return data + +static func _get_biome(biome_val: float, ground_height: int) -> int: + if biome_val > 0.5: + return 0 + elif biome_val > 0.0: + return 1 + elif biome_val > -0.5: + return 2 + else: + return 3 + +static func _get_block_at(wy: int, ground_height: int, biome: int, + wx: int, wz: int, lx: int, lz: int, + is_wreck_center: bool, noise_detail: FastNoiseLite, seed: int) -> int: + + if wy <= -60: + return BlockDatabase.BlockType.BEDROCK + + if wy < ground_height - 5: + return BlockDatabase.BlockType.ROCK + + if wy < ground_height: + return BlockDatabase.BlockType.SAND + + if wy == ground_height: + return _surface_block(biome, wx, wz, noise_detail) + + if wy == ground_height + 1: + return _decoration_block(biome, wx, wz, noise_detail, seed) + + if wy > ground_height + 1 and wy <= ground_height + 6: + if biome == 1: + var kelp_height: int = _kelp_height(wx, wz, seed) + if wy <= ground_height + kelp_height: + return BlockDatabase.BlockType.KELP + + if wy > 60: + return BlockDatabase.BlockType.AIR + + if wy >= 58: + var ice_noise: float = noise_detail.get_noise_2d(float(wx) * 0.5, float(wz) * 0.5) + if ice_noise > 0.6: + return BlockDatabase.BlockType.ICE + return BlockDatabase.BlockType.WATER + + if wy > ground_height: + if is_wreck_center and biome == 3: + var rel_y: int = wy - ground_height - 1 + if rel_y >= 0 and rel_y < 3: + var rel_x: int = (wx % 5 + 5) % 5 + var rel_z: int = (wz % 3 + 3) % 3 + if rel_x < 5 and rel_z < 3: + if rel_y == 0 or rel_y == 2 or rel_x == 0 or rel_x == 4 or rel_z == 0 or rel_z == 2: + return BlockDatabase.BlockType.WRECK_WOOD + return BlockDatabase.BlockType.WATER + + return BlockDatabase.BlockType.WATER + +static func _surface_block(biome: int, wx: int, wz: int, noise_detail: FastNoiseLite) -> int: + match biome: + 0: + return BlockDatabase.BlockType.SAND + 1: + return BlockDatabase.BlockType.SAND + 2: + return BlockDatabase.BlockType.ROCK + 3: + return BlockDatabase.BlockType.SAND + _: + return BlockDatabase.BlockType.SAND + +static func _decoration_block(biome: int, wx: int, wz: int, noise_detail: FastNoiseLite, seed: int) -> int: + match biome: + 0: + var coral_noise: float = noise_detail.get_noise_2d(float(wx + seed % 100), float(wz + seed % 100)) + if coral_noise > 0.4: + if (wx + wz) % 2 == 0: + return BlockDatabase.BlockType.CORAL_RED + else: + return BlockDatabase.BlockType.CORAL_BLUE + _: + pass + return BlockDatabase.BlockType.WATER + +static func _kelp_height(wx: int, wz: int, seed: int) -> int: + var h: int = ((wx * 73856093) ^ (wz * 19349663) ^ seed) % 4 + return 3 + h