|
| 1 | +@tool |
| 2 | +extends Node |
| 3 | +class_name PredictiveSynchronizer |
| 4 | + |
| 5 | +## Similar to [RollbackSynchronizer], this class manages local variables in a |
| 6 | +## rollback context for predictive simulation without networking. |
| 7 | +## |
| 8 | +## This is a simplified version that focuses on local state management. |
| 9 | +## [br][br] |
| 10 | +## Like [RollbackSynchronizer], it automatically discovers nodes |
| 11 | +## with a [code]_rollback_tick(delta: float, tick: int)[/code] |
| 12 | +## method and calls them during the prediction phase. |
| 13 | + |
| 14 | +## The root node for resolving node paths in properties. Defaults to the parent |
| 15 | +## node. |
| 16 | +@export var root: Node = get_parent() |
| 17 | + |
| 18 | +@export_group("State") |
| 19 | +## Properties that define the game state. |
| 20 | +## [br][br] |
| 21 | +## State properties are recorded for each tick and restored during rollback. |
| 22 | +## State is restored before every rollback tick, and recorded after simulating |
| 23 | +## the tick. |
| 24 | +@export var state_properties: Array[String] |
| 25 | + |
| 26 | +var _state_property_config: _PropertyConfig = _PropertyConfig.new() |
| 27 | +var _property_cache := PropertyCache.new(root) |
| 28 | +var _freshness_store := RollbackFreshnessStore.new() |
| 29 | + |
| 30 | +var _states := _PropertyHistoryBuffer.new() |
| 31 | +var _nodes: Array[Node] = [] |
| 32 | +var _skipset: _Set = _Set.new() |
| 33 | + |
| 34 | +var _properties_dirty: bool = false |
| 35 | + |
| 36 | +# Composition |
| 37 | +var _history_recorder: _RollbackHistoryRecorder |
| 38 | + |
| 39 | +## Process settings. |
| 40 | +## |
| 41 | +## Call this after any change to configuration. |
| 42 | +func process_settings() -> void: |
| 43 | + _property_cache.root = root |
| 44 | + _property_cache.clear() |
| 45 | + _freshness_store.clear() |
| 46 | + |
| 47 | + _nodes.clear() |
| 48 | + _states.clear() |
| 49 | + |
| 50 | + # Gather all prediction-aware nodes to call during prediction ticks |
| 51 | + _nodes = root.find_children("*") |
| 52 | + _nodes.push_front(root) |
| 53 | + _nodes = _nodes.filter(func(n): return NetworkRollback.is_rollback_aware(n)) |
| 54 | + _nodes.erase(self) |
| 55 | + |
| 56 | + _state_property_config.set_properties_from_paths(state_properties, _property_cache) |
| 57 | + |
| 58 | + if _history_recorder == null: |
| 59 | + _history_recorder = _RollbackHistoryRecorder.new() |
| 60 | + |
| 61 | + var _inputs := _PropertyHistoryBuffer.new() |
| 62 | + var _input_property_config := _PropertyConfig.new() |
| 63 | + _history_recorder.configure(_states, _inputs, _state_property_config, _input_property_config, _property_cache, _skipset) |
| 64 | + |
| 65 | +func _ready() -> void: |
| 66 | + if Engine.is_editor_hint(): |
| 67 | + return |
| 68 | + |
| 69 | + if not NetworkTime.is_initial_sync_done(): |
| 70 | + # Wait for time sync to complete |
| 71 | + await NetworkTime.after_sync |
| 72 | + |
| 73 | + process_settings.call_deferred() |
| 74 | + |
| 75 | +func _connect_signals() -> void: |
| 76 | + NetworkTime.before_tick.connect(_before_tick) |
| 77 | + NetworkTime.after_tick.connect(_after_tick) |
| 78 | + |
| 79 | + NetworkRollback.on_prepare_tick.connect(_on_prepare_tick) |
| 80 | + NetworkRollback.on_process_tick.connect(_run_prediction_tick) |
| 81 | + NetworkRollback.on_record_tick.connect(_on_record_tick) |
| 82 | + |
| 83 | +func _disconnect_signals() -> void: |
| 84 | + NetworkTime.before_tick.disconnect(_before_tick) |
| 85 | + NetworkTime.after_tick.disconnect(_after_tick) |
| 86 | + |
| 87 | + NetworkRollback.on_prepare_tick.disconnect(_on_prepare_tick) |
| 88 | + NetworkRollback.on_process_tick.disconnect(_run_prediction_tick) |
| 89 | + NetworkRollback.on_record_tick.disconnect(_on_record_tick) |
| 90 | + |
| 91 | +func _before_tick(_dt: float, tick: int) -> void: |
| 92 | + _history_recorder.apply_state(tick) |
| 93 | + |
| 94 | +func _after_tick(_dt: float, tick: int) -> void: |
| 95 | + _history_recorder.trim_history() |
| 96 | + _freshness_store.trim() |
| 97 | + |
| 98 | +func _on_prepare_tick(tick: int) -> void: |
| 99 | + _history_recorder.apply_tick(tick) |
| 100 | + |
| 101 | +func _on_record_tick(tick: int) -> void: |
| 102 | + _history_recorder.record_state(tick) |
| 103 | + |
| 104 | +func _run_prediction_tick(tick: int) -> void: |
| 105 | + for node in _nodes: |
| 106 | + var is_fresh := _freshness_store.is_fresh(node, tick) |
| 107 | + NetworkRollback.process_rollback(node, NetworkTime.ticktime, tick, is_fresh) |
| 108 | + _freshness_store.notify_processed(node, tick) |
| 109 | + |
| 110 | +func _enter_tree() -> void: |
| 111 | + if Engine.is_editor_hint(): |
| 112 | + return |
| 113 | + |
| 114 | + if not NetworkTime.is_initial_sync_done(): |
| 115 | + # Wait for time sync to complete |
| 116 | + await NetworkTime.after_sync |
| 117 | + _connect_signals.call_deferred() |
| 118 | + process_settings.call_deferred() |
| 119 | + |
| 120 | +func _exit_tree() -> void: |
| 121 | + if Engine.is_editor_hint(): |
| 122 | + return |
| 123 | + |
| 124 | + _disconnect_signals() |
| 125 | + |
| 126 | +func _reprocess_settings() -> void: |
| 127 | + if not _properties_dirty or Engine.is_editor_hint(): |
| 128 | + return |
| 129 | + |
| 130 | + _properties_dirty = false |
| 131 | + process_settings() |
| 132 | + |
| 133 | +## Add a state property. |
| 134 | +## [br][br] |
| 135 | +## Settings will be automatically updated. The [param node] may be a string or |
| 136 | +## [NodePath] pointing to a node, or an actual [Node] instance. If the given |
| 137 | +## property is already tracked, this method does nothing. |
| 138 | +func add_state(node: Variant, property: String): |
| 139 | + var property_path := PropertyEntry.make_path(root, node, property) |
| 140 | + if not property_path or state_properties.has(property_path): |
| 141 | + return |
| 142 | + |
| 143 | + state_properties.push_back(property_path) |
| 144 | + _properties_dirty = true |
| 145 | + _reprocess_settings.call_deferred() |
| 146 | + |
| 147 | +func _notification(what: int) -> void: |
| 148 | + if what == NOTIFICATION_EDITOR_PRE_SAVE: |
| 149 | + update_configuration_warnings() |
| 150 | + |
| 151 | +func _get_configuration_warnings() -> PackedStringArray: |
| 152 | + if not root: |
| 153 | + root = get_parent() |
| 154 | + |
| 155 | + # Explore state properties |
| 156 | + if not root: |
| 157 | + return ["No valid root node found!"] |
| 158 | + |
| 159 | + var result := PackedStringArray() |
| 160 | + result.append_array(_NetfoxEditorUtils.gather_properties(root, "_get_rollback_state_properties", |
| 161 | + func(node, prop): |
| 162 | + add_state(node, prop) |
| 163 | + )) |
| 164 | + |
| 165 | + return result |
0 commit comments