From 682e1f18e53f35eb933ed19c46bf7d901df89ed8 Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 6 Oct 2026 20:52:04 +0200 Subject: [PATCH] perf(sites): validate the site once and hand collision the field array DiveSite.configure() builds the height field once (TerrainHeightField validates the data) instead of running the definition validation and then the field construction again; the composition root relies on the error returned by configure(). The collision shape takes the field's row-major elevations directly instead of copying 263k cells one by one. Co-Authored-By: Claude Opus 5.5 --- game/composition/vertical_slice/vertical_slice_root.gd | 4 ---- game/domain/sites/terrain_height_field.gd | 5 +++++ game/world/sites/bathymetric_terrain_actor.gd | 7 +------ game/world/sites/dive_site.gd | 8 ++++---- tests/unit/run_site_domain_tests.gd | 6 ++++-- 5 files changed, 14 insertions(+), 16 deletions(-) diff --git a/game/composition/vertical_slice/vertical_slice_root.gd b/game/composition/vertical_slice/vertical_slice_root.gd index e53bbb3..eea2df7 100644 --- a/game/composition/vertical_slice/vertical_slice_root.gd +++ b/game/composition/vertical_slice/vertical_slice_root.gd @@ -130,10 +130,6 @@ func _start_dive() -> void: if definition == null: push_error("dive site definition %s cannot be loaded" % _selected_site_path) return - var definition_error := definition.validation_error() - if not definition_error.is_empty(): - push_error("dive site %s is invalid: %s" % [_selected_site_path, definition_error]) - return var world_scene := load(DIVE_WORLD_SCENE_PATH) as PackedScene if world_scene == null: return diff --git a/game/domain/sites/terrain_height_field.gd b/game/domain/sites/terrain_height_field.gd index 7866b17..610e34a 100644 --- a/game/domain/sites/terrain_height_field.gd +++ b/game/domain/sites/terrain_height_field.gd @@ -59,6 +59,11 @@ func maximum_elevation_m() -> float: return _maximum_elevation_m +## Row-major elevations (index = row * columns + column), the HeightMapShape3D layout. +func elevations_m() -> PackedFloat32Array: + return _data.elevations_m + + func elevation_at_cell(column: int, row: int) -> float: return _data.elevations_m[_data.index_of(_clamp_column(column), _clamp_row(row))] diff --git a/game/world/sites/bathymetric_terrain_actor.gd b/game/world/sites/bathymetric_terrain_actor.gd index a7fca5d..874aee4 100644 --- a/game/world/sites/bathymetric_terrain_actor.gd +++ b/game/world/sites/bathymetric_terrain_actor.gd @@ -116,12 +116,7 @@ func _build_collision() -> void: var shape := HeightMapShape3D.new() shape.map_width = _field.columns() shape.map_depth = _field.rows() - var heights := PackedFloat32Array() - heights.resize(_field.columns() * _field.rows()) - for row in _field.rows(): - for column in _field.columns(): - heights[row * _field.columns() + column] = _field.elevation_at_cell(column, row) - shape.map_data = heights + shape.map_data = _field.elevations_m() _collision = CollisionShape3D.new() _collision.name = "Collision" _collision.shape = shape diff --git a/game/world/sites/dive_site.gd b/game/world/sites/dive_site.gd index 841d86d..d73d778 100644 --- a/game/world/sites/dive_site.gd +++ b/game/world/sites/dive_site.gd @@ -21,10 +21,10 @@ var _placeholder_count := 0 func configure(definition: DiveSiteDefinition) -> String: if definition == null: return "dive site definition is missing" - var error := definition.validation_error() - if not error.is_empty(): - return error - var terrain_error := _terrain.configure(definition.height_field()) + var field := definition.height_field() + if field == null: + return "dive site definition is invalid" + var terrain_error := _terrain.configure(field) if not terrain_error.is_empty(): return terrain_error _definition = definition diff --git a/tests/unit/run_site_domain_tests.gd b/tests/unit/run_site_domain_tests.gd index 6715204..de0fdbe 100644 --- a/tests/unit/run_site_domain_tests.gd +++ b/tests/unit/run_site_domain_tests.gd @@ -7,7 +7,7 @@ const SitePlacement = preload("res://game/domain/sites/site_placement.gd") const SiteRoute = preload("res://game/domain/sites/site_route.gd") const DiveSiteDefinition = preload("res://game/domain/sites/dive_site_definition.gd") -const TEST_COUNT := 38 +const TEST_COUNT := 39 var _failures: Array[String] = [] @@ -83,10 +83,12 @@ func _test_terrain_data_validation() -> void: func _test_height_field_sampling() -> void: - var field := TerrainHeightField.from_data(_make_terrain(3, 4)) + var data := _make_terrain(3, 4) + var field := TerrainHeightField.from_data(data) _expect(field != null, "valid data yields a field") if field == null: return + _expect(field.elevations_m() == data.elevations_m, "elevations are exposed in row-major data order") _expect(field.bounds_m().is_equal_approx(Rect2(-10.0, 100.0, 4.0, 6.0)), "bounds cover (columns-1) by (rows-1) cells") _expect(field.cell_world_position(2, 3).is_equal_approx(Vector3(-6.0, -26.0, 106.0)), "cell position follows origin and cell size") _expect(is_equal_approx(field.elevation_at(-10.0, 100.0), -20.0), "elevation at a vertex is exact")