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:
+70
-3
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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.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
|
||||
|
||||
Reference in New Issue
Block a user