Skip to content

Commit e525355

Browse files
feat: InterpolationServer (#602)
Switch tick interpolation to use server pattern while keeping all original contracts. Added unit and perf tests. Fixes #538 Fixes #473 Supersedes #539 --------- Co-authored-by: Tamás Gálffy <ezittgtx@gmail.com>
1 parent eb7e23a commit e525355

17 files changed

Lines changed: 666 additions & 82 deletions

addons/netfox.extras/plugin.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
name="netfox.extras"
44
description="Game-specific utilities for Netfox"
55
author="Tamas Galffy and contributors"
6-
version="1.45.0"
6+
version="1.46.0"
77
script="netfox-extras.gd"

addons/netfox.internals/plugin.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
name="netfox.internals"
44
description="Shared internals for netfox addons"
55
author="Tamas Galffy and contributors"
6-
version="1.45.0"
6+
version="1.46.0"
77
script="plugin.gd"

addons/netfox.noray/plugin.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
name="netfox.noray"
44
description="Bulletproof your connectivity with noray integration for netfox"
55
author="Tamas Galffy and contributors"
6-
version="1.45.0"
6+
version="1.46.0"
77
script="netfox-noray.gd"

addons/netfox/interpolators.gd

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
extends Object
22
class_name Interpolators
33

4+
## Tracks interpolation functions for various data types
5+
##
6+
## The interpolation system can be used to interpolate arbitrary values,
7+
## regardless of their types. This is done by keeping a list of interpolators,
8+
## with each handling a specific data type. These can be registered using
9+
## [method register].
10+
## [br][br]
11+
## Later on, these values can either be interpolated using [method interpolate],
12+
## or getting the interpolation function using [method find_for] and calling it.
13+
## [br][br]
14+
## By default, this class supports most built-in data types, aside from objects,
15+
## arrays, and dictionaries. Custom interpolators can be registered, and will
16+
## take precedence over built-in ones.
17+
##
18+
## @tutorial(Interpolators): https://foxssake.github.io/netfox/latest/netfox/guides/interpolators/
19+
420
class Interpolator:
521
var is_applicable: Callable
622
var apply: Callable
@@ -11,37 +27,56 @@ class Interpolator:
1127
result.apply = apply
1228
return result
1329

30+
## Fallback interpolator.
31+
## [br][br]
32+
## Returns the starting value in the first half of the tick, and the target
33+
## value in the other half.
1434
static var DEFAULT_INTERPOLATOR := Interpolator.make(
1535
func (v): return true,
16-
func (a, b, f): return a if f < 0.5 else b
36+
default_apply
1737
)
1838

39+
## List of known interpolators.
40+
## [br][br]
41+
## This list is used to look up interpolators in [method find_for] and [method
42+
## find_interpolator_for]. The list is ordered - the earlier an interpolator
43+
## is in the list, the higher its precedence.
44+
## [br][br]
45+
## Do not modify - use [method register] instead.
1946
static var interpolators: Array[Interpolator]
47+
48+
## Default interpolation function
2049
static var default_apply: Callable = func(a, b, f): a if f < 0.5 else b
2150

2251
## Register an interpolator.
23-
##
52+
## [br][br]
2453
## New interpolators are pushed to the front of the list, making them have
2554
## precedence over existing ones. This can be useful in case you want to override
2655
## the built-in interpolators.
2756
static func register(is_applicable: Callable, apply: Callable) -> void:
2857
interpolators.push_front(Interpolator.make(is_applicable, apply))
2958

3059
## Find the appropriate interpolator for the given value.
31-
##
32-
## If none was found, the default interpolator is returned.
33-
static func find_for(value) -> Callable:
60+
## [br][br]
61+
## If none was found, returns the default interpolator.
62+
static func find_for(value: Variant) -> Callable:
63+
return find_interpolator_for(value).apply
64+
65+
## Find the appropriate interpolator instance for the given value.
66+
## [br][br]
67+
## If none was found, returns the default interpolator.
68+
static func find_interpolator_for(value: Variant) -> Interpolator:
3469
for interpolator in interpolators:
3570
if interpolator.is_applicable.call(value):
36-
return interpolator.apply
71+
return interpolator
3772

38-
return DEFAULT_INTERPOLATOR.apply
73+
return DEFAULT_INTERPOLATOR
3974

4075
## Interpolate between two values.
41-
##
42-
## Note, that it is usually faster to just cache the Callable returned by find_for
43-
## and call that, instead of calling interpolate repeatedly. The latter will have
44-
## to lookup the appropriate interpolator on every call.
76+
## [br][br]
77+
## Note that this method looks up the appropriate interpolator using [method
78+
## find_for] on every call. It is faster to call [method find_for] once, and
79+
## call the resulting [Callable] repeatedly.
4580
static func interpolate(a, b, f: float):
4681
return find_for(a).call(a, b, f)
4782

addons/netfox/netfox.gd

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@ const AUTOLOADS: Array[Dictionary] = [
216216
{
217217
"name": "RollbackLivenessServer",
218218
"path": ROOT + "/servers/rollback-liveness-server.gd"
219+
},
220+
{
221+
"name": "InterpolationServer",
222+
"path": ROOT + "/servers/interpolation-server.gd"
219223
}
220224
]
221225

addons/netfox/network-time.gd

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,8 @@ func _loop() -> void:
562562
_last_process_time = _clock.get_time()
563563
while _next_tick_time < _last_process_time and ticks_in_loop < max_ticks_per_frame:
564564
if ticks_in_loop == 0:
565+
InterpolationServer._clear_teleports()
566+
InterpolationServer._apply_target_state()
565567
before_tick_loop.emit()
566568

567569
before_tick.emit(ticktime, tick)
@@ -579,14 +581,18 @@ func _loop() -> void:
579581
if ticks_in_loop > 0:
580582
after_tick_loop.emit()
581583
NetworkHistoryServer._restore_synchronizer_state(tick)
584+
InterpolationServer._record_next_state()
582585

583586
NetworkIdentityServer.flush_queue()
584587

585588
func _process(delta: float) -> void:
586589
_process_delta = delta
587590

588-
if _is_active() and not sync_to_physics:
589-
_loop()
591+
if _is_active():
592+
if not sync_to_physics:
593+
_loop()
594+
595+
InterpolationServer.interpolate(tick_factor)
590596

591597
func _physics_process(delta: float) -> void:
592598
if _is_active() and sync_to_physics:

addons/netfox/plugin.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
name="netfox"
44
description="Shared internals for netfox addons"
55
author="Tamas Galffy and contributors"
6-
version="1.45.0"
6+
version="1.46.0"
77
script="netfox.gd"

addons/netfox/servers/data/property-pool.gd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ func add(subject: Object, property: NodePath) -> void:
2727
func has(subject: Object, property: NodePath) -> bool:
2828
return (_properties_by_subject.get(subject, []) as Array).has(property)
2929

30+
func has_subject(subject: Object) -> bool:
31+
return _properties_by_subject.has(subject)
32+
3033
func erase(subject: Object, property: NodePath) -> void:
3134
if not _properties_by_subject.has(subject):
3235
return

addons/netfox/servers/data/snapshot.gd

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ func erase_subject(subject: Object) -> void:
135135
_data.erase(subject)
136136
_auth_subjects.erase(subject)
137137

138+
# TODO: Wth, document this class
139+
# NOTE: If there's no data for the subject, it will erase it from target too
140+
func copy_subject_to(subject: Object, target: _Snapshot) -> void:
141+
target.erase_subject(subject)
142+
if has_subject(subject):
143+
target._data[subject] = (_data[subject] as Dictionary).duplicate()
144+
138145
func get_subjects() -> Array:
139146
return _data.keys()
140147

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
extends Node
2+
class_name _InterpolationServer
3+
4+
# @public class
5+
6+
## Manages interpolation between network ticks
7+
##
8+
## Handles interpolation for multiple TickInterpolator nodes, storing snapshots
9+
## and applying interpolation based on the network tick factor.
10+
## [br][br]
11+
## This server can interpolate properties of any arbitrary type.
12+
## See [Interpolators] for specifics on how interpolation is implemented.
13+
## [br][br]
14+
##
15+
16+
var _properties := _PropertyPool.new()
17+
var _interpolators: Dictionary = {} # {subject Node: {property_path String: Interpolator}}
18+
19+
var _state_from := _Snapshot.new(0)
20+
var _state_to := _Snapshot.new(0)
21+
22+
var _enabled := _Set.new()
23+
var _recording_enabled := _Set.new()
24+
var _teleporting := _Set.new()
25+
26+
static var _logger := NetfoxLogger._for_netfox("InterpolationServer")
27+
28+
## Register a [param property] for interpolation on a [param subject] node.
29+
## [br][br]
30+
## If the subject didn't have any properties configured yet, it will be enabled
31+
## for interpolation and recording. Call [method set_enabled] and
32+
## [method set_recording] to configure the subject after registration. If the
33+
## property is already registered for this subject, nothing happens.
34+
func register(subject: Node, property: NodePath, interpolator: Interpolators.Interpolator = null) -> void:
35+
if not _properties.has_subject(subject):
36+
# Subject wasn't registered before, setup defaults
37+
_interpolators[subject] = {}
38+
_enabled.add(subject)
39+
_recording_enabled.add(subject)
40+
41+
if _properties.has(subject, property):
42+
# Property already registered, do nothing
43+
return
44+
45+
_properties.add(subject, property)
46+
if interpolator == null:
47+
var value := subject.get_indexed(property)
48+
_interpolators[subject][property] = Interpolators.find_interpolator_for(value)
49+
else:
50+
_interpolators[subject][property] = interpolator
51+
52+
## Deregister all properties for a [param subject].
53+
func deregister(subject: Node) -> void:
54+
_state_from.erase_subject(subject)
55+
_state_to.erase_subject(subject)
56+
57+
_properties.erase_subject(subject)
58+
_interpolators.erase(subject)
59+
60+
_enabled.erase(subject)
61+
_recording_enabled.erase(subject)
62+
_teleporting.erase(subject)
63+
64+
## Return true if the [param subject] is registered.
65+
func has_subject(subject: Node) -> bool:
66+
return _properties.has_subject(subject)
67+
68+
## Enable or disable interpolation for a [param subject].
69+
## [br][br]
70+
## See [method is_enabled].
71+
func set_enabled(subject: Node, enabled: bool) -> void:
72+
if enabled:
73+
_enabled.add(subject)
74+
else:
75+
_enabled.erase(subject)
76+
77+
## Return true if the [param subject] is enabled for interpolation.
78+
## [br][br]
79+
## If the subject is enabled, it will be interpolated between ticks.
80+
## [br][br]
81+
## See [method set_enabled].
82+
func is_enabled(subject: Node) -> bool:
83+
return _enabled.has(subject)
84+
85+
## Enable or disable automatic state recording for a [param subject].
86+
## [br][br]
87+
## See [method is_recording].
88+
func set_recording(subject: Node, enabled: bool) -> void:
89+
if enabled:
90+
_recording_enabled.add(subject)
91+
else:
92+
_recording_enabled.erase(subject)
93+
94+
## Return true if the [param subject] is enabled for recording.
95+
## [br][br]
96+
## This means that the subject's interpolation states will be updated
97+
## automatically. Use [method push_state] to update manually.
98+
## [br][br]
99+
## See [method set_recording].
100+
func is_recording(subject: Node) -> bool:
101+
return _recording_enabled.has(subject)
102+
103+
## Return true if interpolation can be done for a [param subject].
104+
## [br][br]
105+
## May return false for multiple reasons - subject is unknown, not enabled for
106+
## interpolation, or is currently teleporting.
107+
func can_interpolate(subject: Node) -> bool:
108+
if not has_subject(subject):
109+
# Unknown subject, can't interpolate
110+
return false
111+
if not is_enabled(subject):
112+
# Interpolation is disabled for subject
113+
return false
114+
if is_teleporting(subject):
115+
# Subject is teleporting, just snap to target state
116+
return false
117+
118+
return true
119+
120+
## Record current state for interpolation.
121+
## [br][br]
122+
## Called automatically, unless disabled with [method set_recording].
123+
func push_state(subject: Node) -> void:
124+
if not has_subject(subject):
125+
_logger.warning("Trying to push state for unregistered subject %s", [subject])
126+
return
127+
128+
# Copy to[subject] => from[subject]
129+
_state_to.copy_subject_to(subject, _state_from)
130+
131+
# Capture current as to[subject]
132+
_state_to.erase_subject(subject)
133+
for property in _properties.get_properties_of(subject):
134+
var value := subject.get_indexed(property)
135+
if value == null:
136+
# NOTE: This shouldn't happen?
137+
_logger.warning("Captured null value for interpolation on %s:%s; either a bug or wrong usage", [subject, property])
138+
else:
139+
_state_to.set_property(subject, property, value)
140+
141+
## Skip interpolation for this tick.
142+
func teleport(subject: Node) -> void:
143+
if is_teleporting(subject):
144+
return
145+
if not has_subject(subject):
146+
_logger.warning("Trying to teleport unregistered subject %s", [subject])
147+
return
148+
149+
_teleporting.add(subject)
150+
151+
## Return true if the [param subject] is currently teleporting.
152+
## [br][br]
153+
## See [method teleport].
154+
func is_teleporting(subject: Node) -> bool:
155+
return _teleporting.has(subject)
156+
157+
## Interpolate properties for a [param subject].
158+
## [br][br]
159+
## Called automatically by default.
160+
func interpolate_subject(subject: Node, factor: float) -> void:
161+
if not can_interpolate(subject):
162+
return
163+
164+
var interps := _interpolators.get(subject, {}) as Dictionary
165+
if interps.is_empty():
166+
_logger.debug("No interpolators found for %s", [subject])
167+
168+
for property in _properties.get_properties_of(subject):
169+
if not _state_from.has_property(subject, property) or not _state_to.has_property(subject, property):
170+
continue
171+
172+
var a = _state_from.get_property(subject, property)
173+
var b = _state_to.get_property(subject, property)
174+
var interpolator := interps.get(property, Interpolators.DEFAULT_INTERPOLATOR) as Interpolators.Interpolator
175+
176+
var value := interpolator.apply.call(a, b, factor)
177+
subject.set_indexed(property, value)
178+
179+
## Interpolate all registered subjects.
180+
## [br][br]
181+
## Called automatically by default.
182+
func interpolate(factor: float) -> void:
183+
for subject in _properties.get_subjects():
184+
interpolate_subject(subject, factor)
185+
186+
func _clear_teleports() -> void:
187+
_teleporting.clear()
188+
189+
func _apply_target_state() -> void:
190+
_state_to.apply()
191+
192+
func _record_next_state() -> void:
193+
for subject in _recording_enabled.values():
194+
push_state(subject)

0 commit comments

Comments
 (0)