perf: harden runtime for weaker hardware
This commit is contained in:
+156
-131
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user