basic ECS spawner

This commit is contained in:
2026-01-15 15:27:48 +01:00
parent 24a781f36a
commit eb737b469c
860 changed files with 58621 additions and 32 deletions

View File

@@ -0,0 +1,328 @@
## Observer Performance Tests
## Compares observers vs traditional systems for different use cases
extends GdUnitTestSuite
var runner: GdUnitSceneRunner
var world: World
func before():
runner = scene_runner("res://addons/gecs/tests/test_scene.tscn")
world = runner.get_property("world")
ECS.world = world
func after_test():
if world:
world.purge(false)
## Setup entities with position and velocity for movement tests
func setup_velocity_entities(count: int) -> void:
for i in count:
var entity = Entity.new()
entity.name = "VelocityEntity_%d" % i
entity.add_component(C_TestPosition.new(Vector3(i, 0, 0)))
entity.add_component(C_TestVelocity.new(Vector3(randf() * 10, randf() * 10, randf() * 10)))
world.add_entity(entity, null, false)
## Setup entities for observer add/remove tests
func setup_observer_test_entities(count: int) -> void:
for i in count:
var entity = Entity.new()
entity.name = "ObserverTestEntity_%d" % i
entity.add_component(C_ObserverTest.new(i))
world.add_entity(entity, null, false)
## Test traditional system approach for continuous processing (like velocity)
## This is the IDEAL use case for systems - they excel at continuous per-frame processing
func test_system_continuous_processing(scale: int, test_parameters := [[100], [1000], [10000]]):
setup_velocity_entities(scale)
var system = S_VelocitySystem.new()
world.add_system(system)
var time_ms = PerfHelpers.time_it(func():
# Simulate 60 frames of processing
for i in range(60):
world.process(0.016)
)
PerfHelpers.record_result("system_continuous_velocity", scale, time_ms)
prints("System processed %d entities across 60 frames" % system.process_count)
world.purge(false)
## Test observer detecting component additions
## This is an IDEAL use case for observers - they excel at reacting to state changes
func test_observer_component_additions(scale: int, test_parameters := [[100], [1000], [10000]]):
var observer = O_PerformanceTest.new()
world.add_observer(observer)
var time_ms = PerfHelpers.time_it(func():
# Add components to entities (observers react to additions)
for i in range(scale):
var entity = Entity.new()
entity.add_component(C_ObserverTest.new(i))
world.add_entity(entity, null, false)
)
PerfHelpers.record_result("observer_component_additions", scale, time_ms)
prints("Observer detected %d additions" % observer.added_count)
assert_int(observer.added_count).is_equal(scale)
world.purge(false)
## Test observer detecting component removals
## Another IDEAL use case for observers - reacting to cleanup/removal events
func test_observer_component_removals(scale: int, test_parameters := [[100], [1000], [10000]]):
setup_observer_test_entities(scale)
var observer = O_PerformanceTest.new()
world.add_observer(observer)
var entities = world.query.with_all([C_ObserverTest]).execute()
var time_ms = PerfHelpers.time_it(func():
# Remove components (observers react to removals)
for entity in entities:
entity.remove_component(C_ObserverTest)
)
PerfHelpers.record_result("observer_component_removals", scale, time_ms)
prints("Observer detected %d removals" % observer.removed_count)
assert_int(observer.removed_count).is_equal(scale)
world.purge(false)
## Test observer detecting property changes
## Good use case for observers - reacting to specific property changes
func test_observer_property_changes(scale: int, test_parameters := [[100], [1000], [10000]]):
setup_observer_test_entities(scale)
var observer = O_PerformanceTest.new()
world.add_observer(observer)
observer.reset_counts()
var entities = world.query.with_all([C_ObserverTest]).execute()
var time_ms = PerfHelpers.time_it(func():
# Change properties (observers react to changes)
for entity in entities:
var comp = entity.get_component(C_ObserverTest)
comp.value = comp.value + 1 # Triggers property_changed signal
)
PerfHelpers.record_result("observer_property_changes", scale, time_ms)
prints("Observer detected %d property changes" % observer.changed_count)
assert_int(observer.changed_count).is_equal(scale)
world.purge(false)
## Test system approach for batch property reads
## Systems are better for batch operations without individual reactions
func test_system_batch_property_reads(scale: int, test_parameters := [[100], [1000], [10000]]):
setup_observer_test_entities(scale)
var system = PerformanceTestSystem.new()
world.add_system(system)
var time_ms = PerfHelpers.time_it(func():
# Single process call reads all entities
world.process(0.016)
)
PerfHelpers.record_result("system_batch_property_reads", scale, time_ms)
prints("System processed %d entities in batch" % system.process_count)
world.purge(false)
## Test observer overhead with multiple property changes per entity
## Shows cost of observers when entities change frequently
func test_observer_frequent_changes(scale: int, test_parameters := [[100], [1000], [10000]]):
setup_observer_test_entities(scale)
var observer = O_PerformanceTest.new()
world.add_observer(observer)
observer.reset_counts()
var entities = world.query.with_all([C_ObserverTest]).execute()
var time_ms = PerfHelpers.time_it(func():
# Each entity changes multiple times
for entity in entities:
var comp = entity.get_component(C_ObserverTest)
for j in range(10): # 10 changes per entity
comp.value = comp.value + 1
)
PerfHelpers.record_result("observer_frequent_changes", scale, time_ms)
prints("Observer detected %d property changes (%d entities × 10 changes)" % [observer.changed_count, scale])
assert_int(observer.changed_count).is_equal(scale * 10)
world.purge(false)
## Test system processing the same frequent changes scenario
## Compares continuous polling vs reactive observation
func test_system_simulating_frequent_changes(scale: int, test_parameters := [[100], [1000], [10000]]):
setup_observer_test_entities(scale)
var system = PerformanceTestSystem.new()
world.add_system(system)
var entities = world.query.with_all([C_ObserverTest]).execute()
var time_ms = PerfHelpers.time_it(func():
# Make the changes
for entity in entities:
var comp = entity.get_component(C_ObserverTest)
for j in range(10):
# Direct property change without signal
comp.value = comp.value + 1
# System processes once (doesn't know about individual changes)
world.process(0.016)
)
PerfHelpers.record_result("system_simulating_frequent_changes", scale, time_ms)
prints("System processed %d entities once after changes" % system.process_count)
world.purge(false)
## Test multiple observers watching the same component
## Shows overhead of multiple reactive systems
func test_multiple_observers_same_component(scale: int, test_parameters := [[100], [1000], [10000]]):
setup_observer_test_entities(scale)
var observer1 = O_PerformanceTest.new()
var observer2 = O_PerformanceTest.new()
var observer3 = O_PerformanceTest.new()
world.add_observers([observer1, observer2, observer3])
observer1.reset_counts()
observer2.reset_counts()
observer3.reset_counts()
var entities = world.query.with_all([C_ObserverTest]).execute()
var time_ms = PerfHelpers.time_it(func():
# Change properties (all 3 observers react)
for entity in entities:
var comp = entity.get_component(C_ObserverTest)
comp.value = comp.value + 1
)
PerfHelpers.record_result("multiple_observers_same_component", scale, time_ms)
prints("3 observers each detected %d changes" % observer1.changed_count)
assert_int(observer1.changed_count).is_equal(scale)
assert_int(observer2.changed_count).is_equal(scale)
assert_int(observer3.changed_count).is_equal(scale)
world.purge(false)
## Test observer query filtering performance
## Shows cost of query evaluation for observers
func test_observer_with_complex_query(scale: int, test_parameters := [[100], [1000], [10000]]):
# Create entities with varying component combinations
for i in range(scale):
var entity = Entity.new()
entity.add_component(C_ObserverTest.new(i))
if i % 2 == 0:
entity.add_component(C_ObserverHealth.new())
world.add_entity(entity, null, false)
# Observer with complex query (needs both components)
var observer = O_HealthObserver.new()
world.add_observer(observer)
observer.reset()
var entities_matching = world.query.with_all([C_ObserverTest, C_ObserverHealth]).execute()
var time_ms = PerfHelpers.time_it(func():
# Change health on matching entities
for entity in entities_matching:
var health = entity.get_component(C_ObserverHealth)
health.health = health.health - 1
)
PerfHelpers.record_result("observer_complex_query", scale, time_ms)
prints("Observer with complex query detected %d changes (out of %d total entities)" % [observer.health_changed_count, scale])
world.purge(false)
## Test baseline: Empty observer overhead
## Measures the cost of just having observers in the system
func test_observer_baseline_overhead(scale: int, test_parameters := [[100], [1000], [10000]]):
setup_observer_test_entities(scale)
# Add observer but don't trigger it
var observer = O_PerformanceTest.new()
world.add_observer(observer)
var entities = world.query.with_all([C_ObserverTest]).execute()
var time_ms = PerfHelpers.time_it(func():
# Make changes WITHOUT triggering property_changed signals
for entity in entities:
var comp = entity.get_component(C_ObserverTest)
# Direct property access without signal emission
comp.value = comp.value + 1
)
PerfHelpers.record_result("observer_baseline_overhead", scale, time_ms)
prints("Made %d changes without triggering observer" % scale)
assert_int(observer.changed_count).is_equal(scale) # Observer should have triggered
world.purge(false)
## Test comparison: Observer vs System for sporadic changes
## Real-world scenario: only 10% of entities change per frame
func test_observer_vs_system_sporadic_changes(scale: int, test_parameters := [[100], [1000], [10000]]):
setup_observer_test_entities(scale)
var observer = O_PerformanceTest.new()
world.add_observer(observer)
observer.reset_counts()
var entities = world.query.with_all([C_ObserverTest]).execute()
var changes_per_frame = max(1, scale / 10) # 10% of entities change
var time_ms_observer = PerfHelpers.time_it(func():
# Simulate 60 frames where only 10% of entities change per frame
for frame in range(60):
for i in range(changes_per_frame):
var entity = entities[i % scale]
var comp = entity.get_component(C_ObserverTest)
comp.value = comp.value + 1 # Triggers observer
)
PerfHelpers.record_result("observer_sporadic_changes", scale, time_ms_observer)
prints("Observer detected %d sporadic changes over 60 frames" % observer.changed_count)
# Now test with system approach
world.purge(false)
setup_observer_test_entities(scale)
var system = PerformanceTestSystem.new()
world.add_system(system)
entities = world.query.with_all([C_ObserverTest]).execute()
var time_ms_system = PerfHelpers.time_it(func():
# Same scenario but system processes ALL entities every frame
for frame in range(60):
# Make the same changes
for i in range(changes_per_frame):
var entity = entities[i % scale]
var comp = entity.get_component(C_ObserverTest)
comp.value = comp.value + 1
# System processes ALL entities every frame
world.process(0.016)
)
PerfHelpers.record_result("system_sporadic_changes", scale, time_ms_system)
prints("System processed %d total entities over 60 frames (even though only 10%% changed)" % system.process_count)
world.purge(false)