fix: center ground under chunks and align player spawn with chunk grid
checkpoint / checkpoint (push) Successful in 5m56s

This commit is contained in:
2026-09-17 13:36:59 +00:00
parent bbd9ffc46f
commit 7c77c0b447
4 changed files with 253 additions and 3 deletions
+70 -3
View File
@@ -38,6 +38,7 @@ var _build_editor: InGameEditor = null
var _stream_manager: ChunkStreamManager = null
var _loaded_chunk_coords: Array[Vector3i] = []
func _ready() -> void:
@@ -75,11 +76,75 @@ func _setup_chunk_streaming() -> void:
_stream_manager._load_chunk(Vector3i.ZERO)
func _on_chunk_loaded(_coord: Vector3i, chunk_scene: Node) -> void:
func _on_chunk_loaded(coord: Vector3i, chunk_scene: Node) -> void:
if chunk_scene is ChunkScene:
var cs := chunk_scene as ChunkScene
if not cs.world_objects.is_empty() or not cs.tile_layers.is_empty():
_hide_default_test_props()
_hide_default_test_props()
if not (coord in _loaded_chunk_coords):
_loaded_chunk_coords.append(coord)
_adjust_ground_and_spawn_for_chunks(cs)
func _adjust_ground_and_spawn_for_chunks(cs: ChunkScene) -> void:
var min_cx := 0
var max_cx := 0
var min_cy := 0
var max_cy := 0
for c in _loaded_chunk_coords:
min_cx = mini(min_cx, c.x)
max_cx = maxi(max_cx, c.x)
min_cy = mini(min_cy, c.y)
max_cy = maxi(max_cy, c.y)
var min_x := float(min_cx * Chunk.CHUNK_SIZE_X)
var max_x := float((max_cx + 1) * Chunk.CHUNK_SIZE_X)
var min_z := float(min_cy * Chunk.CHUNK_SIZE_Y)
var max_z := float((max_cy + 1) * Chunk.CHUNK_SIZE_Y)
var center_x := (min_x + max_x) * 0.5
var center_z := (min_z + max_z) * 0.5
var size_x := maxf(40.0, (max_x - min_x) + 8.0)
var size_z := maxf(40.0, (max_z - min_z) + 8.0)
var ground := get_node_or_null("Level/Ground") as StaticBody3D
if ground != null:
ground.position = Vector3(center_x, -0.5, center_z)
var shape_node := ground.find_child("CollisionShape3D", true, false) as CollisionShape3D
if shape_node != null:
var box: BoxShape3D
if shape_node.shape is BoxShape3D:
box = (shape_node.shape as BoxShape3D).duplicate() as BoxShape3D
else:
box = BoxShape3D.new()
box.size = Vector3(size_x, 1.0, size_z)
shape_node.shape = box
var mesh_node := ground.find_child("MeshInstance3D", true, false) as MeshInstance3D
if mesh_node != null:
var box_mesh: BoxMesh
if mesh_node.mesh is BoxMesh:
box_mesh = (mesh_node.mesh as BoxMesh).duplicate() as BoxMesh
else:
box_mesh = BoxMesh.new()
box_mesh.size = Vector3(size_x, 1.0, size_z)
mesh_node.mesh = box_mesh
var spawn_marker := get_node_or_null("Level/PlayerSpawn") as Node3D
var found_spawn: Variant = null
for obj in cs.world_objects:
if obj.type == "spawn_point" or (obj.properties is Dictionary and obj.properties.get("catalog_id") == "spawn_point"):
found_spawn = cs.global_position + Vector3(obj.position.x, 1.5, obj.position.y)
break
if found_spawn != null:
if spawn_marker != null:
spawn_marker.global_position = found_spawn
if not _aligned:
_player.global_position = found_spawn
elif spawn_marker != null and spawn_marker.global_position.is_equal_approx(Vector3(0, 1.5, 0)):
var default_center := Vector3(16.0, 1.5, 16.0)
spawn_marker.global_position = default_center
if not _aligned:
_player.global_position = default_center
func _hide_default_test_props() -> void:
@@ -194,6 +259,7 @@ func _close_build_mode() -> void:
_player.set_process_unhandled_input(true)
_player._refresh_mouse_mode()
_loaded_chunk_coords.clear()
if _stream_manager == null:
_setup_chunk_streaming()
elif is_instance_valid(_stream_manager):
@@ -273,6 +339,7 @@ func _leave_world() -> void:
## Close the ENet connection and stop it from calling us back mid-teardown.
func _teardown_net() -> void:
_loaded_chunk_coords.clear()
if _stream_manager != null and is_instance_valid(_stream_manager):
_stream_manager.clear_all_chunks()
_stream_manager.queue_free()
+61
View File
@@ -213,6 +213,12 @@ func _try_load_real_content() -> Node:
marker.position = spawn_pos
var entries: Array = (index_result.data as Dictionary).get("chunks", [])
var min_cx := 0
var max_cx := 0
var min_cy := 0
var max_cy := 0
var found_spawn_obj: Variant = null
for entry in entries:
var cx: int = int(entry["cx"])
var cy: int = int(entry["cy"])
@@ -229,6 +235,61 @@ func _try_load_real_content() -> Node:
_build_chunk_collision(world_root, Vector3i(cx, cy, cz), decoded)
_chunks_loaded += 1
if _chunks_loaded == 1:
min_cx = cx
max_cx = cx
min_cy = cy
max_cy = cy
else:
min_cx = mini(min_cx, cx)
max_cx = maxi(max_cx, cx)
min_cy = mini(min_cy, cy)
max_cy = maxi(max_cy, cy)
if found_spawn_obj == null:
var chunk_origin := Vector3(
float(cx * Chunk.CHUNK_SIZE_X),
float(cz * Chunk.CHUNK_SIZE_Z),
float(cy * Chunk.CHUNK_SIZE_Y)
)
for obj in decoded.objects:
if obj.type == "spawn_point" or str(obj.properties.get("catalog_id", "")) == "spawn_point":
found_spawn_obj = chunk_origin + Vector3(obj.position.x, 1.5, obj.position.y)
break
if _chunks_loaded > 0:
var props := world_root.find_child("Props", true, false)
if props != null:
props.queue_free()
var chest := world_root.find_child("ScriptedChest", true, false)
if chest != null:
chest.queue_free()
var min_x := float(min_cx * Chunk.CHUNK_SIZE_X)
var max_x := float((max_cx + 1) * Chunk.CHUNK_SIZE_X)
var min_z := float(min_cy * Chunk.CHUNK_SIZE_Y)
var max_z := float((max_cy + 1) * Chunk.CHUNK_SIZE_Y)
var center_x := (min_x + max_x) * 0.5
var center_z := (min_z + max_z) * 0.5
var size_x := maxf(40.0, (max_x - min_x) + 8.0)
var size_z := maxf(40.0, (max_z - min_z) + 8.0)
var ground: StaticBody3D = world_root.find_child("Ground", true, false) as StaticBody3D
if ground != null:
ground.position = Vector3(center_x, -0.5, center_z)
var shape_node := ground.find_child("CollisionShape3D", true, false) as CollisionShape3D
if shape_node != null:
var box := BoxShape3D.new()
box.size = Vector3(size_x, 1.0, size_z)
shape_node.shape = box
if found_spawn_obj != null:
spawn_pos = found_spawn_obj
elif not (room.get("spawn") is Dictionary and (room.get("spawn") as Dictionary).has("pos")):
spawn_pos = Vector3(16.0, 1.5, 16.0)
marker.position = spawn_pos
return world_root
+109
View File
@@ -0,0 +1,109 @@
extends SceneTree
## Regression test for world chunk alignment and ground positioning.
var _passed: int = 0
var _failed: int = 0
func _check(label: String, condition: bool) -> void:
if condition:
print(" PASS: %s" % label)
_passed += 1
else:
printerr(" FAIL: %s" % label)
_failed += 1
func _init() -> void:
call_deferred("_run_tests")
func _run_tests() -> void:
print("--- World Chunk & Ground Alignment Test ---")
await process_frame
var packed: PackedScene = load("res://scenes/game.tscn")
var result := change_scene_to_packed(packed)
if result != OK:
printerr("SMOKE: failed to change scene, error ", result)
quit(1)
return
await scene_changed
var game: Node3D = current_scene as Node3D
_check("game scene ready", game != null)
# Initially, ground is at default test level position (0, -0.5, 0)
var ground: StaticBody3D = game.get_node_or_null("Level/Ground") as StaticBody3D
_check("Ground initially exists", ground != null)
_check("Ground initially at (0, -0.5, 0)", ground.position.is_equal_approx(Vector3(0, -0.5, 0)))
# Create a ChunkScene for chunk (0, 0, 0) with corner objects
var chunk_scene := ChunkScene.new()
chunk_scene.coord = Vector3i(0, 0, 0)
chunk_scene.name = "chunk_0_0_0"
game.add_child(chunk_scene)
# Add lamp post at corner (0.5, 0.5) and corner (31.5, 31.5)
var obj_corner1 := WorldObject.new()
obj_corner1.id = "lamp1"
obj_corner1.type = "prop"
obj_corner1.position = Vector3(0.5, 0.5, 0.0)
obj_corner1.properties = {"catalog_id": "lamp_post_01"}
chunk_scene.add_object(obj_corner1)
var obj_corner2 := WorldObject.new()
obj_corner2.id = "lamp2"
obj_corner2.type = "prop"
obj_corner2.position = Vector3(31.5, 31.5, 0.0)
obj_corner2.properties = {"catalog_id": "lamp_post_01"}
chunk_scene.add_object(obj_corner2)
# Trigger _on_chunk_loaded
game.call("_on_chunk_loaded", Vector3i(0, 0, 0), chunk_scene)
# Verify Ground has moved to center of chunk: (16.0, -0.5, 16.0)
_check("Ground moved to chunk center (16, -0.5, 16)", ground.position.is_equal_approx(Vector3(16.0, -0.5, 16.0)))
var shape_node := ground.find_child("CollisionShape3D", true, false) as CollisionShape3D
_check("Ground CollisionShape3D size >= 40x40", shape_node != null and (shape_node.shape as BoxShape3D).size.x >= 40.0 and (shape_node.shape as BoxShape3D).size.z >= 40.0)
var mesh_node := ground.find_child("MeshInstance3D", true, false) as MeshInstance3D
_check("Ground MeshInstance3D size >= 40x40", mesh_node != null and (mesh_node.mesh as BoxMesh).size.x >= 40.0 and (mesh_node.mesh as BoxMesh).size.z >= 40.0)
# Verify corners are inside ground bounds
var half_x: float = (shape_node.shape as BoxShape3D).size.x * 0.5
var half_z: float = (shape_node.shape as BoxShape3D).size.z * 0.5
var min_gx: float = ground.position.x - half_x
var max_gx: float = ground.position.x + half_x
var min_gz: float = ground.position.z - half_z
var max_gz: float = ground.position.z + half_z
_check("Corner (0.5, 0.5) is on ground", 0.5 >= min_gx and 0.5 <= max_gx and 0.5 >= min_gz and 0.5 <= max_gz)
_check("Corner (31.5, 31.5) is on ground", 31.5 >= min_gx and 31.5 <= max_gx and 31.5 >= min_gz and 31.5 <= max_gz)
# Verify PlayerSpawn defaulted to chunk center (16, 1.5, 16)
var spawn_marker := game.get_node_or_null("Level/PlayerSpawn") as Node3D
_check("PlayerSpawn marker is at (16, 1.5, 16)", spawn_marker != null and spawn_marker.global_position.is_equal_approx(Vector3(16.0, 1.5, 16.0)))
# Now test placed spawn_point object
var chunk_scene2 := ChunkScene.new()
chunk_scene2.coord = Vector3i(0, 0, 0)
chunk_scene2.name = "chunk_with_spawn"
game.add_child(chunk_scene2)
var spawn_obj := WorldObject.new()
spawn_obj.id = "spawn1"
spawn_obj.type = "spawn_point"
spawn_obj.position = Vector3(8.5, 12.5, 0.0)
spawn_obj.properties = {"catalog_id": "spawn_point"}
chunk_scene2.add_object(spawn_obj)
game.call("_on_chunk_loaded", Vector3i(0, 0, 0), chunk_scene2)
_check("PlayerSpawn moved to placed spawn_point at (8.5, 1.5, 12.5)", spawn_marker != null and spawn_marker.global_position.is_equal_approx(Vector3(8.5, 1.5, 12.5)))
print("\n=== SMOKE RESULT: %d/%d passed ===" % [_passed, _passed + _failed])
if _failed > 0:
quit(1)
else:
quit(0)
+13
View File
@@ -55,6 +55,12 @@ func _create_mesh_for_tile(tile_def: TileDef) -> void:
multi_mesh.mesh = _make_unit_cube_mesh()
multi_mesh.instance_count = count
var col_body: StaticBody3D = null
if tile_def.solid:
col_body = StaticBody3D.new()
col_body.name = "collision_%d" % tile_def.id
add_child(col_body)
var instance_idx := 0
for z in range(tile_layer.depth):
for y in range(tile_layer.height):
@@ -66,6 +72,13 @@ func _create_mesh_for_tile(tile_def: TileDef) -> void:
var xform := Transform3D(Basis(), world_pos)
multi_mesh.set_instance_transform(instance_idx, xform)
instance_idx += 1
if col_body != null:
var col_shape := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = Vector3(TILE_SIZE, TILE_SIZE, TILE_SIZE)
col_shape.shape = box
col_shape.position = world_pos
col_body.add_child(col_shape)
var mesh_instance := MultiMeshInstance3D.new()
mesh_instance.multimesh = multi_mesh