perf: harden runtime for weaker hardware

This commit is contained in:
Rijad Zuzo
2026-08-12 10:02:45 +02:00
parent cd29da95e0
commit 34428d836a
47 changed files with 2164 additions and 243 deletions
+156 -131
View File
@@ -40,6 +40,7 @@ var player_system := PlayerCitizenSystem.new()
var npcs: Array[SimNPC] = []
@export var tick_interval := 1.2
@export_range(1, 16, 1) var max_ticks_per_frame := 4
@export var simulation_seed: int = 1337
@export var cycle_duration_seconds := 240.0
@export var debug_logs := false
@@ -72,6 +73,7 @@ var latest_decisions: Dictionary = {}
var action_selector := ActionSelectionSystem.new()
var action_executor := ActionExecutionSystem.new()
var target_resolver := ActionTargetResolver.new()
var last_player_resource_query_stats: Dictionary = {}
var _population_view := SimulationPopulationView.new()
var speed_index := 2
const SPEED_LEVELS := [0.25, 0.5, 1.0, 2.0, 4.0, 10.0]
@@ -115,7 +117,7 @@ func _ready() -> void:
economy.configure(village, debug_logs)
animal_care.configure(economy, active_world_adapter)
player_system.configure(economy, _record_player_event_at)
conflict_system.configure(economy)
conflict_system.configure(economy, tick_interval)
economy.initialize_storage()
village.update_modifiers()
village.update_priorities()
@@ -131,7 +133,8 @@ func _ready() -> void:
func _process(delta: float) -> void:
var ticks_due := clock.advance(delta * SPEED_LEVELS[speed_index])
advance_player_combat_time(delta)
var ticks_due := clock.advance(delta * SPEED_LEVELS[speed_index], max_ticks_per_frame)
for tick in ticks_due:
simulate_tick()
@@ -171,6 +174,10 @@ func generate_npcs() -> void:
if home_count > 0:
for i in range(npcs.size()):
npcs[i].home_position = home_positions[i % home_count]
refresh_population_index()
func refresh_population_index() -> void:
_population_view.rebuild(npcs)
@@ -196,7 +203,7 @@ func simulate_tick() -> void:
advance_player_needs()
_advance_resource_regrowth()
var village_was_changed := false
_population_view.rebuild(npcs)
refresh_population_index()
for npc in npcs:
village_was_changed = _simulate_npc_tick(npc) or village_was_changed
if village_was_changed:
@@ -299,21 +306,31 @@ func _on_conflict_combatant_spawned(combatant_id: StringName) -> void:
func _on_conflict_combatant_died(combatant_id: StringName) -> void:
combatant_died.emit(combatant_id)
var combatant := conflict_system.get_combatant(combatant_id)
if combatant == null or combatant.get_npc_id() < 0:
return
for npc in npcs:
if npc.id != combatant.get_npc_id() or npc.is_dead:
continue
_handle_npc_death(npc, npc.current_task, npc.target_id)
var npc: SimNPC
if combatant != null and combatant.get_npc_id() >= 0:
npc = _find_npc_by_id(combatant.get_npc_id())
if npc != null:
_population_view.refresh_npc(npc)
combatant_died.emit(combatant_id)
if npc == null or not npc.is_dead:
return
var previous_task := npc.last_task if npc.last_task != &"" else SimulationIds.ACTION_IDLE
_handle_npc_death(npc, previous_task, npc.target_id)
func player_attack(combatant_id: StringName) -> float:
return conflict_system.player_attack(combatant_id)
func player_attack_ready() -> bool:
return conflict_system.is_player_attack_ready()
func advance_player_combat_time(delta: float) -> void:
conflict_system.advance_realtime(delta)
func spawn_wolf(position: Vector3) -> StringName:
return conflict_system.spawn_wolf(position)
@@ -450,85 +467,80 @@ func _complete_resource_gather(npc: SimNPC, completed_task: StringName) -> void:
func release_npc_reservation(npc_id: int) -> void:
for npc in npcs:
if npc.id == npc_id:
if npc.target_id != &"":
var resource_state := get_resource_state(npc.target_id)
if resource_state != null:
resource_state.release(npc.id)
var animal_state := animal_care.get_state(npc.target_id)
if animal_state != null:
animal_state.release(npc.id)
npc.target_id = &""
return
var npc := _find_npc_by_id(npc_id)
if npc == null:
return
if npc.target_id != &"":
var resource_state := get_resource_state(npc.target_id)
if resource_state != null:
resource_state.release(npc.id)
var animal_state := animal_care.get_state(npc.target_id)
if animal_state != null:
animal_state.release(npc.id)
npc.target_id = &""
func notify_npc_arrived(npc_id: int) -> void:
for npc in npcs:
if npc.id == npc_id:
if npc.is_dead:
var npc := _find_npc_by_id(npc_id)
if npc == null or npc.is_dead:
return
if npc.task_state == SimNPC.TASK_STATE_TRAVELING:
var definition := SimulationDefinitions.get_action(npc.current_task)
if (
npc.target_id != &""
and definition != null
and definition.target_type == SimulationIds.TARGET_RESOURCE
):
var resource_state := get_resource_state(npc.target_id)
if (
resource_state == null
or not resource_state.can_extract()
or resource_state.get_reserved_by() != npc.id
):
if debug_logs:
NpcTickDebugLog.print_unavailable_resource(npc)
notify_npc_navigation_failed(npc.id)
return
if (
npc.target_id != &""
and definition != null
and definition.target_type == SimulationIds.TARGET_ANIMAL
):
if not animal_care.can_complete_feed(npc.target_id, npc.id):
if debug_logs:
NpcTickDebugLog.print_unavailable_animal(npc)
notify_npc_navigation_failed(npc.id)
return
if animal_care.requires_feed_pickup(npc):
if _continue_animal_feed_delivery(npc):
return
_redirect_npc_to_wander(npc)
return
if npc.task_state == SimNPC.TASK_STATE_TRAVELING:
var definition := SimulationDefinitions.get_action(npc.current_task)
if (
npc.target_id != &""
and definition != null
and definition.target_type == SimulationIds.TARGET_RESOURCE
):
var resource_state := get_resource_state(npc.target_id)
if (
resource_state == null
or not resource_state.can_extract()
or resource_state.get_reserved_by() != npc.id
):
if debug_logs:
NpcTickDebugLog.print_unavailable_resource(npc)
notify_npc_navigation_failed(npc.id)
return
if (
npc.target_id != &""
and definition != null
and definition.target_type == SimulationIds.TARGET_ANIMAL
):
if not animal_care.can_complete_feed(npc.target_id, npc.id):
if debug_logs:
NpcTickDebugLog.print_unavailable_animal(npc)
notify_npc_navigation_failed(npc.id)
return
if animal_care.requires_feed_pickup(npc):
if _continue_animal_feed_delivery(npc):
return
_redirect_npc_to_wander(npc)
return
npc.has_travel_target = false
npc.start_working(
npc.current_task == SimulationIds.ACTION_FEED_ANIMAL and npc.task_progress > 0.0
)
npc.has_travel_target = false
npc.start_working(
npc.current_task == SimulationIds.ACTION_FEED_ANIMAL and npc.task_progress > 0.0
)
var working_speakers: Array[SimNPC] = []
if npc.target_id != &"":
for other in npcs:
if other.id == npc.id or other.is_dead:
continue
if other.target_id != npc.target_id:
continue
if other.task_state in [SimNPC.TASK_STATE_TRAVELING, SimNPC.TASK_STATE_WORKING]:
relationship_system.increase_shared_work_familiarity(npc.id, other.id)
if other.task_state == SimNPC.TASK_STATE_WORKING:
working_speakers.append(other)
working_speakers.sort_custom(_sort_npcs_by_id)
for speaker in working_speakers:
if try_communicate_at_shared_activity(speaker.id, npc.id):
break
var working_speakers: Array[SimNPC] = []
if npc.target_id != &"":
for other in npcs:
if other.id == npc.id or other.is_dead:
continue
if other.target_id != npc.target_id:
continue
if (
other.task_state
in [SimNPC.TASK_STATE_TRAVELING, SimNPC.TASK_STATE_WORKING]
):
relationship_system.increase_shared_work_familiarity(npc.id, other.id)
if other.task_state == SimNPC.TASK_STATE_WORKING:
working_speakers.append(other)
working_speakers.sort_custom(_sort_npcs_by_id)
for speaker in working_speakers:
if try_communicate_at_shared_activity(speaker.id, npc.id):
break
if debug_logs:
NpcTickDebugLog.print_started_work(npc)
return
if debug_logs:
NpcTickDebugLog.print_started_work(npc)
func _continue_animal_feed_delivery(npc: SimNPC) -> bool:
@@ -564,66 +576,57 @@ func _redirect_npc_to_wander(npc: SimNPC) -> void:
func notify_npc_navigation_failed(npc_id: int) -> void:
for npc in npcs:
if npc.id != npc_id:
continue
if npc.is_dead or npc.task_state != SimNPC.TASK_STATE_TRAVELING:
return
var failed_task := npc.current_task
_redirect_npc_to_wander(npc)
if debug_logs:
NpcTickDebugLog.print_navigation_failure(npc, failed_task)
var npc := _find_npc_by_id(npc_id)
if npc == null or npc.is_dead or npc.task_state != SimNPC.TASK_STATE_TRAVELING:
return
var failed_task := npc.current_task
_redirect_npc_to_wander(npc)
if debug_logs:
NpcTickDebugLog.print_navigation_failure(npc, failed_task)
func resolve_npc_target(npc_id: int, origin: Vector3) -> bool:
for npc in npcs:
if npc.id != npc_id:
continue
if npc.is_dead or npc.task_state != SimNPC.TASK_STATE_TRAVELING:
return false
if npc.current_task == SimulationIds.ACTION_SLEEP:
npc.travel_target_position = npc.home_position
npc.has_travel_target = true
npc_travel_requested.emit(npc, npc.travel_target_position)
return true
if active_world_adapter == null:
push_error("SimulationManager: active_world_adapter is missing")
return false
var result := target_resolver.resolve(npc, origin, self, active_world_adapter)
if result.is_empty():
notify_npc_navigation_failed(npc.id)
return false
npc.target_id = StringName(result.get("target_id", ""))
npc.travel_target_position = result["position"]
var npc := _find_npc_by_id(npc_id)
if npc == null or npc.is_dead or npc.task_state != SimNPC.TASK_STATE_TRAVELING:
return false
if npc.current_task == SimulationIds.ACTION_SLEEP:
npc.travel_target_position = npc.home_position
npc.has_travel_target = true
npc_travel_requested.emit(npc, npc.travel_target_position)
return true
return false
if active_world_adapter == null:
push_error("SimulationManager: active_world_adapter is missing")
return false
var result := target_resolver.resolve(npc, origin, self, active_world_adapter)
if result.is_empty():
notify_npc_navigation_failed(npc.id)
return false
npc.target_id = StringName(result.get("target_id", ""))
npc.travel_target_position = result["position"]
npc.has_travel_target = true
npc_travel_requested.emit(npc, npc.travel_target_position)
return true
func request_current_travel(npc_id: int) -> bool:
for npc in npcs:
if npc.id != npc_id:
continue
if npc.task_state != SimNPC.TASK_STATE_TRAVELING:
return false
if npc.has_travel_target:
npc_travel_requested.emit(npc, npc.travel_target_position)
else:
npc_target_requested.emit(npc)
return true
return false
var npc := _find_npc_by_id(npc_id)
if npc == null or npc.task_state != SimNPC.TASK_STATE_TRAVELING:
return false
if npc.has_travel_target:
npc_travel_requested.emit(npc, npc.travel_target_position)
else:
npc_target_requested.emit(npc)
return true
func synchronize_npc_position(npc_id: int, active_position: Vector3) -> bool:
for npc in npcs:
if npc.id == npc_id:
npc.position = active_position
return true
return false
var npc := _find_npc_by_id(npc_id)
if npc == null:
return false
npc.position = active_position
return true
func get_activity_target_claim_count(target_id: StringName, except_npc_id: int = -1) -> int:
@@ -891,6 +894,12 @@ func _get_lasting_event_ids(npc_id: int) -> Array[int]:
func _find_npc_by_id(npc_id: int) -> SimNPC:
var npc := _population_view.get_any(npc_id)
if npc != null:
return npc
# Population growth can introduce an ID between scheduled rebuild points.
# Rebuild once on a miss while keeping ordinary runtime callbacks O(1).
refresh_population_index()
return _population_view.get_any(npc_id)
@@ -1083,7 +1092,21 @@ func release_resource(node_id: StringName, agent_id: int) -> void:
func find_resource_node_for_player(from_position: Vector3, max_distance: float) -> ResourceNode:
var best: ResourceNode
var best_distance := max_distance * max_distance
for node in ResourceNode.get_all():
var candidates: Array[ResourceNode]
var query_mode := &"registry"
if (
active_world_adapter != null
and active_world_adapter.has_method("get_resource_nodes_in_radius")
):
candidates = active_world_adapter.get_resource_nodes_in_radius(from_position, max_distance)
query_mode = &"spatial"
else:
candidates = ResourceNode.get_all()
last_player_resource_query_stats = {
"mode": query_mode,
"candidate_count": candidates.size(),
}
for node in candidates:
var resource_state := get_resource_state(node.node_id)
if (
resource_state == null
@@ -1341,7 +1364,7 @@ func restore_state(record: SimulationStateRecord) -> bool:
npcs.clear()
for npc_record in record.npcs:
npcs.append(npc_record.restore(debug_logs))
_population_view.rebuild(npcs)
refresh_population_index()
relationship_system.restore(record.relationships)
event_knowledge_system.restore(record.event_knowledge)
opportunity_system.restore(record.opportunities, int(record.simulation["next_opportunity_id"]))
@@ -1362,6 +1385,8 @@ func restore_state(record: SimulationStateRecord) -> bool:
player_system.player_state = record.player
conflict_system.restore_state_records(record.combatants, record.factions, tick_count)
conflict_system.register_npc_combatants(npcs)
conflict_system.configure(economy, tick_interval)
_sync_player_combatant()
village_changed.emit(village)
state_restored.emit()
return true