Spawning and despawning in rollback
Some nodes that participate in rollback need to be despawned eventually. This is true for projectiles that appear mid-game and then vanish when they hit something, items that are picked up, or even enemies that are slain during gameplay.
Freeing these nodes with free(), queue_free() or even removing them from
the scene tree could cause issues.
It is possible that the game needs to roll back to a tick where the node has still existed. If that node is already destroyed, it cannot be restored to resimulate the tick, causing mispredictions and artifacts.
The opposite can also happen - the game rewinds to a tick where the node did not exist yet, but since it now exists, it will affect the game. For example, a projectile may hit something before it was even fired.
Liveness tracking
To avoid the above situations, the game can track when each node was spawned and despawned. If the game needs to restore a tick where the node was not yet spawned, or was already despawned, the node can be deactivated. If the game now moves to a tick where the node was already spawned, it can be activated.
This is liveness tracking, implemented in netfox.
Reacting to (de)spawning
For a node to be liveness aware, it must implement at least one of these methods:
func _rollback_spawn() -> void:
pass
func _rollback_despawn() -> void:
pass
Whenever a node needs to be deactivated before simulating a tick,
_rollback_despawn() is called. Whenever it needs to be activated,
_rollback_spawn() is called. These are picked up by RollbackSynchronizer or
PredictiveSynchronizer.
What happens when a node is despawned is up to the game. This can range from disabling collisions and hiding the model to temporarily removing the node from the scene tree. Less invasive solutions can result in less complexity.
Implement the functionality necessary to restore a node to its active state, as
it should be seen by the player, in _rollback_spawn().
Optionally, implement the following method as well:
func _rollback_destroy() -> void:
pass
The above is called when the node has been inactive so long that it can be
safely freed. If it is not implemented, queue_free() is used as fallback.
Requesting a despawn
To despawn a node, find the RollbackSynchronizer or PredictiveSynchronizer
node that manages it, and call despawn():
extends Node3D
# [...]
@onready var synchronizer := $PredictiveSynchronizer as PredictiveSynchronizer
# [...]
func _rollback_tick(dt: float, _t: int, _if: bool) -> void:
# [...]
if distance_left < 0:
synchronizer.despawn()
return
This makes sure that every node managed by the synchronizer will be despawned and spawned in unison.
Using servers
When using servers instead of nodes, use the following methods:
RollbackLivenessServer.register()to register the node for liveness tracking- This is where you can specify spawn, despawn and destroy callbacks
RollbackLivenessServer.deregister()before freeing the nodeRollbackLivenessServer.despawn()to despawn
Limitations
Liveness has a start and an end. It is a contiguous interval. It is not
possible to have a node spawn on tick @10, despawn on tick @15, and then spawn
again on tick @18. Think of despawn() as a rollback equivalent to
queue_free().
In some cases, a despawn might be mispredicted. In that case, see
RollbackLiveness.clear_despawn().