fix: center ground under chunks and align player spawn with chunk grid
checkpoint / checkpoint (push) Successful in 5m56s
checkpoint / checkpoint (push) Successful in 5m56s
This commit is contained in:
+69
-2
@@ -38,6 +38,7 @@ var _build_editor: InGameEditor = null
|
|||||||
|
|
||||||
|
|
||||||
var _stream_manager: ChunkStreamManager = null
|
var _stream_manager: ChunkStreamManager = null
|
||||||
|
var _loaded_chunk_coords: Array[Vector3i] = []
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -75,11 +76,75 @@ func _setup_chunk_streaming() -> void:
|
|||||||
_stream_manager._load_chunk(Vector3i.ZERO)
|
_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:
|
if chunk_scene is ChunkScene:
|
||||||
var cs := chunk_scene as 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:
|
func _hide_default_test_props() -> void:
|
||||||
@@ -194,6 +259,7 @@ func _close_build_mode() -> void:
|
|||||||
_player.set_process_unhandled_input(true)
|
_player.set_process_unhandled_input(true)
|
||||||
_player._refresh_mouse_mode()
|
_player._refresh_mouse_mode()
|
||||||
|
|
||||||
|
_loaded_chunk_coords.clear()
|
||||||
if _stream_manager == null:
|
if _stream_manager == null:
|
||||||
_setup_chunk_streaming()
|
_setup_chunk_streaming()
|
||||||
elif is_instance_valid(_stream_manager):
|
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.
|
## Close the ENet connection and stop it from calling us back mid-teardown.
|
||||||
func _teardown_net() -> void:
|
func _teardown_net() -> void:
|
||||||
|
_loaded_chunk_coords.clear()
|
||||||
if _stream_manager != null and is_instance_valid(_stream_manager):
|
if _stream_manager != null and is_instance_valid(_stream_manager):
|
||||||
_stream_manager.clear_all_chunks()
|
_stream_manager.clear_all_chunks()
|
||||||
_stream_manager.queue_free()
|
_stream_manager.queue_free()
|
||||||
|
|||||||
@@ -213,6 +213,12 @@ func _try_load_real_content() -> Node:
|
|||||||
marker.position = spawn_pos
|
marker.position = spawn_pos
|
||||||
|
|
||||||
var entries: Array = (index_result.data as Dictionary).get("chunks", [])
|
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:
|
for entry in entries:
|
||||||
var cx: int = int(entry["cx"])
|
var cx: int = int(entry["cx"])
|
||||||
var cy: int = int(entry["cy"])
|
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)
|
_build_chunk_collision(world_root, Vector3i(cx, cy, cz), decoded)
|
||||||
_chunks_loaded += 1
|
_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
|
return world_root
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -55,6 +55,12 @@ func _create_mesh_for_tile(tile_def: TileDef) -> void:
|
|||||||
multi_mesh.mesh = _make_unit_cube_mesh()
|
multi_mesh.mesh = _make_unit_cube_mesh()
|
||||||
multi_mesh.instance_count = count
|
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
|
var instance_idx := 0
|
||||||
for z in range(tile_layer.depth):
|
for z in range(tile_layer.depth):
|
||||||
for y in range(tile_layer.height):
|
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)
|
var xform := Transform3D(Basis(), world_pos)
|
||||||
multi_mesh.set_instance_transform(instance_idx, xform)
|
multi_mesh.set_instance_transform(instance_idx, xform)
|
||||||
instance_idx += 1
|
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()
|
var mesh_instance := MultiMeshInstance3D.new()
|
||||||
mesh_instance.multimesh = multi_mesh
|
mesh_instance.multimesh = multi_mesh
|
||||||
|
|||||||
Reference in New Issue
Block a user