Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions addons/netfox.extras/physics/godot_driver_2d.gd.off
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ class_name PhysicsDriver2D
# Requires a custom build of Godot with https://github.com/godotengine/godot/pull/76462

var scene_collision_objects: Array = []

# RIDs of tracked bodies currently in the tree. Snapshot entries whose
# body has been freed are skipped for state updates in rollback
var live_rids: Dictionary = {}

var collision_objects_snapshots: Dictionary[int, Dictionary] = {}

func _init_physics_space() -> void:
Expand Down Expand Up @@ -33,8 +38,10 @@ func _snapshot_space(tick: int) -> void:
func _rollback_space(tick) -> void:
if snapshots.has(tick):
var rid_states = snapshots[tick]
for rid in rid_states.keys():
set_body_states(rid, rid_states[rid])
for rid in rid_states.keys():
# Skip bodies freed or removed since the snapshot was taken
if live_rids.has(rid):
set_body_states(rid, rid_states[rid])

@albertok albertok Jul 15, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of the looping checks maybe we can clear out the object as soon as it's going to queue free.

Instead of node_removed being called from get_tree().node_removed.connect(node_removed) signal on line 16 we directly hook into the https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-signal-tree-exiting signal from the node itself.

We do this in node_added ( second last function in this class ) as the node is being registered.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of moving away from the loop. I changed it to a dictionary to improve lookup times.

Unfortunately though, to your point, tree_exiting() is called on any tree removal (including remove_child(), which could be a valid gameplay operation for moving RBs around a scene's tree/heirarchy). https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-private-method-exit-tree And alternatively checking is_queued_for_deletion() would miss direct free() calls on RBs. So from what I see we're stuck without a proper (engine/solver-driven) event-based approach on this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. Are those really valid scenarios?

Calling free will definitely break netfox as will re-parenting.

@elementbound I think you've dealt with these life cycles a lot, what are your thoughts?


for body in scene_collision_objects:
if body is CharacterBody2D or body is AnimatableBody2D:
Expand All @@ -58,6 +65,10 @@ func scan_tree():
scene_collision_objects.clear()
scene_collision_objects = get_all_children(get_node('/root'))

live_rids.clear()
for body in scene_collision_objects:
live_rids[body.get_rid()] = true

func get_all_children(in_node: Node) -> Array:
var nodes = []
nodes = in_node.find_children("*", "PhysicsBody2D", true, false)
Expand All @@ -66,7 +77,9 @@ func get_all_children(in_node: Node) -> Array:
func node_added(node: Node) -> void:
if node is PhysicsBody2D:
scene_collision_objects.append(node)
live_rids[node.get_rid()] = true

func node_removed(node: Node) -> void:
if node is PhysicsBody2D:
scene_collision_objects.erase(node)
live_rids.erase(node.get_rid())
19 changes: 15 additions & 4 deletions addons/netfox.extras/physics/godot_driver_3d.gd.off
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ class_name PhysicsDriver3D
# Maps ticks ( int ) to global snapshots ( Dictionary<RID, Array> )
var scene_collision_objects: Array = []

# RIDs of tracked bodies currently in the tree. Snapshot entries whose
# body has been freed are skipped for state updates in rollback
var live_rids: Dictionary = {}

func _init_physics_space() -> void:
physics_space = get_viewport().world_3d.space
PhysicsServer3D.space_set_active(physics_space, false)
Expand All @@ -29,10 +33,12 @@ func _snapshot_space(tick: int) -> void:
snapshots[tick] = rid_states

func _rollback_space(tick) -> void:
if snapshots.has(tick):
var rid_states = snapshots[tick]
for rid in rid_states.keys():
set_body_states(rid, rid_states[rid])
if snapshots.has(tick):
var rid_states = snapshots[tick]
for rid in rid_states.keys():
# Skip bodies freed or removed since the snapshot was taken
if live_rids.has(rid):
set_body_states(rid, rid_states[rid])

for body in scene_collision_objects:
if body is CharacterBody3D or body is AnimatableBody3D:
Expand All @@ -56,6 +62,9 @@ func set_body_states(rid: RID, body_state: Array) -> void:
func scan_tree():
scene_collision_objects.clear()
scene_collision_objects = get_all_children(get_node('/root'))
live_rids.clear()
for body in scene_collision_objects:
live_rids[body.get_rid()] = true

func get_all_children(in_node: Node) -> Array:
var nodes = []
Expand All @@ -65,7 +74,9 @@ func get_all_children(in_node: Node) -> Array:
func node_added(node: Node) -> void:
if node is PhysicsBody3D:
scene_collision_objects.append(node)
live_rids[node.get_rid()] = true

func node_removed(node: Node) -> void:
if node is PhysicsBody3D:
scene_collision_objects.erase(node)
live_rids.erase(node.get_rid())
13 changes: 7 additions & 6 deletions addons/netfox.extras/physics/physics_driver.gd
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ func _ready() -> void:
_init_physics_space()

# Emitted before a tick is run.
func before_tick(_delta: float, tick: int) -> void:
func before_tick(delta: float, tick: int) -> void:
_snapshot_space(tick)
step_physics(_delta)
step_physics(delta, tick)

func on_prepare_tick(tick: int) -> void:
if NetworkRollback._rollback_from == tick:
Expand All @@ -48,22 +48,23 @@ func on_prepare_tick(tick: int) -> void:
# Subsequent ticks are re-writing history.
_snapshot_space(tick)

func on_process_tick(_tick: int) -> void:
step_physics(NetworkTime.ticktime)
func on_process_tick(tick: int) -> void:
step_physics(NetworkTime.ticktime, tick)

func after_tick_loop() -> void:
# Remove old snapshots
for i in snapshots.keys():
if i < NetworkRollback.history_start:
snapshots.erase(i)

func step_physics(_delta: float) -> void:
# tick is either the current tick, or the tick being resimulated (if rollback is active)
func step_physics(delta: float, tick: int) -> void:
# Break up physics into smaller steps if needed
var frac_delta = _delta / physics_factor
var rollback_participants = get_tree().get_nodes_in_group("network_rigid_body")
for i in range(physics_factor):
for net_rigid_body in rollback_participants:
net_rigid_body._physics_rollback_tick(frac_delta, NetworkTime.tick)
net_rigid_body._physics_rollback_tick(frac_delta, tick)

_physics_step(frac_delta)

Expand Down