fix(world): correct 3D chunk coordinate mapping and streaming proximity
checkpoint / checkpoint (push) Successful in 5m55s
checkpoint / checkpoint (push) Successful in 5m55s
This commit is contained in:
+5
-2
@@ -70,11 +70,14 @@ static func compute_bounds(p_coord: Vector3i) -> AABB:
|
||||
|
||||
|
||||
## Compute the chunk coordinate that contains a world-space position.
|
||||
## Maps Godot 3D world space (X = East/West, Y = Height/Up, Z = North/South)
|
||||
## to chunk coordinates (cx = X, cy = Z, cz = Y).
|
||||
static func coord_for_position(p_pos: Vector3) -> Vector3i:
|
||||
var cz: int = int(floor((p_pos.y - CHUNK_ORIGIN.y) / CHUNK_SIZE_Z)) if CHUNK_SIZE_Z > 1 else 0
|
||||
return Vector3i(
|
||||
int(floor((p_pos.x - CHUNK_ORIGIN.x) / CHUNK_SIZE_X)),
|
||||
int(floor((p_pos.y - CHUNK_ORIGIN.y) / CHUNK_SIZE_Y)),
|
||||
int(floor((p_pos.z - CHUNK_ORIGIN.z) / CHUNK_SIZE_Z))
|
||||
int(floor((p_pos.z - CHUNK_ORIGIN.z) / CHUNK_SIZE_Y)),
|
||||
cz
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ var player: Node3D = null
|
||||
|
||||
## The current player chunk coordinate.
|
||||
var _player_chunk_coord: Vector3i = Vector3i.ZERO
|
||||
var _has_player_coord: bool = false
|
||||
|
||||
## Map of loaded chunk coordinates to their scene nodes.
|
||||
var _loaded_chunks: Dictionary = {}
|
||||
@@ -95,7 +96,7 @@ func _process(delta: float) -> void:
|
||||
return
|
||||
|
||||
_update_accumulator += delta
|
||||
if _update_accumulator < UPDATE_INTERVAL:
|
||||
if _has_player_coord and _update_accumulator < UPDATE_INTERVAL:
|
||||
return
|
||||
_update_accumulator = 0.0
|
||||
|
||||
@@ -110,9 +111,10 @@ func _update_streaming() -> void:
|
||||
return
|
||||
|
||||
var current_coord: Vector3i = Chunk.coord_for_position(player.global_position)
|
||||
if current_coord == _player_chunk_coord:
|
||||
if _has_player_coord and current_coord == _player_chunk_coord:
|
||||
return
|
||||
|
||||
_has_player_coord = true
|
||||
_player_chunk_coord = current_coord
|
||||
_rebuild_queues()
|
||||
|
||||
@@ -144,9 +146,11 @@ func _compute_target_chunks() -> Dictionary:
|
||||
"""Compute the set of chunk coordinates that should be loaded."""
|
||||
var target: Dictionary = {}
|
||||
var half: int = streaming_radius
|
||||
var z_min: int = -half if Chunk.CHUNK_SIZE_Z > 1 else 0
|
||||
var z_max: int = half if Chunk.CHUNK_SIZE_Z > 1 else 0
|
||||
for dx in range(-half, half + 1):
|
||||
for dy in range(-half, half + 1):
|
||||
for dz in range(-half, half + 1):
|
||||
for dz in range(z_min, z_max + 1):
|
||||
var coord: Vector3i = Vector3i(
|
||||
_player_chunk_coord.x + dx,
|
||||
_player_chunk_coord.y + dy,
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
extends SceneTree
|
||||
## Smoke test verifying chunk coordinate mapping and streaming behavior.
|
||||
## Regression test for the issue where moving forward in Z unloaded chunk (0,0,0)
|
||||
## because of transposed Godot Y (Height) and Godot Z (Ground) in coord_for_position.
|
||||
|
||||
var passed := 0
|
||||
var failed := 0
|
||||
|
||||
|
||||
func _check(label: String, cond: bool) -> void:
|
||||
if cond:
|
||||
passed += 1
|
||||
print(" PASS: ", label)
|
||||
else:
|
||||
failed += 1
|
||||
printerr(" FAIL: ", label)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_test_coord_for_position()
|
||||
_test_chunk_stream_manager_proximity()
|
||||
print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed])
|
||||
quit(0 if failed == 0 else 1)
|
||||
|
||||
|
||||
func _test_coord_for_position() -> void:
|
||||
print("--- Chunk.coord_for_position coordinate mapping ---")
|
||||
# In Godot 3D: X is East/West (chunk X), Y is Height/Up (chunk Z), Z is North/South (chunk Y).
|
||||
|
||||
# Spawn origin
|
||||
var c0 := Chunk.coord_for_position(Vector3(0.0, 1.5, 0.0))
|
||||
_check("Origin at (0, 1.5, 0) maps to (0, 0, 0)", c0 == Vector3i(0, 0, 0))
|
||||
|
||||
# Moving along Godot Z (North/South) within chunk 0:
|
||||
# Previously at Z=3, coord.z was incorrectly 3, which exceeded streaming_radius and unloaded chunk 0!
|
||||
var c3 := Chunk.coord_for_position(Vector3(0.0, 1.5, 3.0))
|
||||
_check("Walking 3m forward to Z=3 maps to chunk (0, 0, 0)", c3 == Vector3i(0, 0, 0))
|
||||
|
||||
var c20 := Chunk.coord_for_position(Vector3(10.0, 1.5, 20.0))
|
||||
_check("Inside chunk at (10, 1.5, 20) maps to (0, 0, 0)", c20 == Vector3i(0, 0, 0))
|
||||
|
||||
var c31 := Chunk.coord_for_position(Vector3(31.0, 1.5, 31.0))
|
||||
_check("Far corner of chunk at (31, 1.5, 31) maps to (0, 0, 0)", c31 == Vector3i(0, 0, 0))
|
||||
|
||||
# Crossing into adjacent chunks
|
||||
var cz_next := Chunk.coord_for_position(Vector3(0.0, 1.5, 32.0))
|
||||
_check("Crossing Z=32 maps to chunk Y=1 (0, 1, 0)", cz_next == Vector3i(0, 1, 0))
|
||||
|
||||
var cx_next := Chunk.coord_for_position(Vector3(32.0, 1.5, 0.0))
|
||||
_check("Crossing X=32 maps to chunk X=1 (1, 0, 0)", cx_next == Vector3i(1, 0, 0))
|
||||
|
||||
var cz_prev := Chunk.coord_for_position(Vector3(0.0, 1.5, -1.0))
|
||||
_check("Negative Z maps to chunk Y=-1 (0, -1, 0)", cz_prev == Vector3i(0, -1, 0))
|
||||
|
||||
var cx_prev := Chunk.coord_for_position(Vector3(-1.0, 1.5, 0.0))
|
||||
_check("Negative X maps to chunk X=-1 (-1, 0, 0)", cx_prev == Vector3i(-1, 0, 0))
|
||||
|
||||
# Player height / jumping:
|
||||
var c_jump := Chunk.coord_for_position(Vector3(0.0, 5.0, 0.0))
|
||||
_check("Jumping to height Y=5 in flat world stays at cz=0", c_jump == Vector3i(0, 0, 0))
|
||||
|
||||
|
||||
func _test_chunk_stream_manager_proximity() -> void:
|
||||
print("--- ChunkStreamManager streaming proximity ---")
|
||||
var sm := ChunkStreamManager.new()
|
||||
sm.streaming_radius = 2
|
||||
|
||||
var player := Node3D.new()
|
||||
player.position = Vector3(0.0, 1.5, 0.0)
|
||||
root.add_child(player)
|
||||
root.add_child(sm)
|
||||
|
||||
sm.player = player
|
||||
sm.is_active = true
|
||||
|
||||
# Simulate immediate streaming update on spawn
|
||||
sm._update_streaming()
|
||||
var targets: Dictionary = sm._compute_target_chunks()
|
||||
|
||||
_check("Target chunks computed on spawn", targets.size() > 0)
|
||||
_check("Spawn chunk (0,0,0) is in target set", targets.has(Vector3i(0, 0, 0)))
|
||||
|
||||
# Ensure flat world doesn't spam non-zero cz chunks
|
||||
var all_cz_zero := true
|
||||
for coord in targets.keys():
|
||||
if coord.z != 0:
|
||||
all_cz_zero = false
|
||||
break
|
||||
_check("Flat world targets only contain cz=0", all_cz_zero)
|
||||
|
||||
# Simulate moving close to placed lampposts (Z = 1, 2, 3, 4, 5, 10, 20)
|
||||
player.position = Vector3(0.0, 1.5, 3.0)
|
||||
sm._update_streaming()
|
||||
targets = sm._compute_target_chunks()
|
||||
_check("Approaching lamppost at Z=3: chunk (0,0,0) is STILL in target set", targets.has(Vector3i(0, 0, 0)))
|
||||
|
||||
player.position = Vector3(5.0, 1.5, 15.0)
|
||||
sm._update_streaming()
|
||||
targets = sm._compute_target_chunks()
|
||||
_check("Approaching lamppost at Z=15: chunk (0,0,0) is STILL in target set", targets.has(Vector3i(0, 0, 0)))
|
||||
|
||||
player.position = Vector3(0.0, 1.5, 31.0)
|
||||
sm._update_streaming()
|
||||
targets = sm._compute_target_chunks()
|
||||
_check("Approaching lamppost at Z=31: chunk (0,0,0) is STILL in target set", targets.has(Vector3i(0, 0, 0)))
|
||||
|
||||
# Clean up
|
||||
sm.queue_free()
|
||||
player.queue_free()
|
||||
Reference in New Issue
Block a user