Skip to content
Closed
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
3 changes: 3 additions & 0 deletions addons/netfox/rollback/network-rollback.gd
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@ func mutate(target: Object, p_tick: int = tick) -> void:
## [br][br]
## @experimental: The Mutations API is experimental!
func is_mutated(target: Object, p_tick: int = tick) -> bool:
# Hack to make local prediction work on state.
return true

if _mutated_nodes.has(target):
return p_tick >= _mutated_nodes.get(target)
else:
Expand Down
46 changes: 21 additions & 25 deletions examples/rollback-fps/characters/player.tscn
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
[gd_scene load_steps=17 format=3 uid="uid://dw3xd5a7xnse7"]
[gd_scene format=3 uid="uid://dw3xd5a7xnse7"]

[ext_resource type="Script" path="res://examples/rollback-fps/scripts/player.gd" id="1_bww6v"]
[ext_resource type="Script" path="res://examples/rollback-fps/scripts/player-weapon.gd" id="2_qyetr"]
[ext_resource type="Script" uid="uid://16cr653lwxno" path="res://examples/rollback-fps/scripts/player.gd" id="1_bww6v"]
[ext_resource type="Script" uid="uid://da0u0nkxkqnpt" path="res://examples/rollback-fps/scripts/player-weapon.gd" id="2_qyetr"]
[ext_resource type="AudioStream" uid="uid://cope814m1d7p6" path="res://examples/multiplayer-fps/sounds/fire.mp3" id="3_kq0j8"]
[ext_resource type="Script" path="res://addons/netfox/rewindable-action.gd" id="6_pks3d"]
[ext_resource type="Script" path="res://examples/rollback-fps/scripts/player-input.gd" id="7_2i3bi"]
[ext_resource type="Script" path="res://addons/netfox/rollback/rollback-synchronizer.gd" id="9_vik3r"]
[ext_resource type="Script" path="res://addons/netfox/tick-interpolator.gd" id="10_4mqhe"]
[ext_resource type="Script" uid="uid://efvmt2o8yu0e" path="res://examples/rollback-fps/scripts/player-input.gd" id="7_2i3bi"]
[ext_resource type="Script" uid="uid://d350u8evihs1u" path="res://addons/netfox/rollback/rollback-synchronizer.gd" id="9_vik3r"]
[ext_resource type="Script" uid="uid://dour8fehaaugp" path="res://addons/netfox/tick-interpolator.gd" id="10_4mqhe"]
[ext_resource type="AudioStream" uid="uid://q7ql86p7flj" path="res://examples/multiplayer-fps/sounds/hit.wav" id="15_1n4tg"]

[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_6kydi"]
Expand All @@ -33,65 +32,62 @@ albedo_color = Color(0, 0, 0, 1)
material = SubResource("StandardMaterial3D_khk4l")
size = Vector3(0.2, 0.2, 1)

[node name="player" type="CharacterBody3D"]
[node name="player" type="CharacterBody3D" unique_id=1703987419]
script = ExtResource("1_bww6v")

[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=453880506]
shape = SubResource("CapsuleShape3D_6kydi")

[node name="Body" type="MeshInstance3D" parent="."]
[node name="Body" type="MeshInstance3D" parent="." unique_id=612354052]
mesh = SubResource("CapsuleMesh_xnks4")

[node name="Head" type="Node3D" parent="."]
[node name="Head" type="Node3D" parent="." unique_id=413995421]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.705698, 0)

[node name="Camera3D" type="Camera3D" parent="Head"]
[node name="Camera3D" type="Camera3D" parent="Head" unique_id=1683812604]

[node name="Nose" type="MeshInstance3D" parent="Head"]
[node name="Nose" type="MeshInstance3D" parent="Head" unique_id=1098470409]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -0.25)
mesh = SubResource("BoxMesh_qmeb2")

[node name="PlayerFPSWeapon" type="Node3D" parent="Head"]
[node name="PlayerFPSWeapon" type="Node3D" parent="Head" unique_id=1142600235]
transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 0)
script = ExtResource("2_qyetr")

[node name="TinyGun" type="MeshInstance3D" parent="Head/PlayerFPSWeapon"]
[node name="TinyGun" type="MeshInstance3D" parent="Head/PlayerFPSWeapon" unique_id=1097178372]
transform = Transform3D(0.15, 0, 0, 0, 0.15, 0, 0, 0, 0.15, 0.09, -0.18, -0.3)
mesh = SubResource("BoxMesh_0vqha")

[node name="Fire Action" type="Node" parent="Head/PlayerFPSWeapon"]
script = ExtResource("6_pks3d")

[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="Head/PlayerFPSWeapon"]
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="Head/PlayerFPSWeapon" unique_id=1344133455]
transform = Transform3D(-1, 0, 8.74228e-08, 0, 1, 0, -8.74228e-08, 0, -1, 0, -0.705698, 0)
stream = ExtResource("3_kq0j8")

[node name="BigGun" type="MeshInstance3D" parent="Head"]
[node name="BigGun" type="MeshInstance3D" parent="Head" unique_id=1568961368]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.439686, -0.406913, -0.200588)
mesh = SubResource("BoxMesh_wqpqp")

[node name="DisplayNameLabel3D" type="Label3D" parent="."]
[node name="DisplayNameLabel3D" type="Label3D" parent="." unique_id=1151499593]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.30427, 0)
pixel_size = 0.002
billboard = 1
fixed_size = true
font_size = 16

[node name="RollbackSynchronizer" type="Node" parent="." node_paths=PackedStringArray("root")]
[node name="RollbackSynchronizer" type="Node" parent="." unique_id=888631003 node_paths=PackedStringArray("root")]
unique_name_in_owner = true
script = ExtResource("9_vik3r")
root = NodePath("..")
state_properties = Array[String]([":transform", ":velocity", ":health", ":deaths", "Head:transform", "Head/PlayerFPSWeapon:last_fire"])
input_properties = Array[String](["Input:movement", "Input:jump", "Input:fire", "Input:look_angle"])

[node name="TickInterpolator" type="Node" parent="." node_paths=PackedStringArray("root")]
[node name="TickInterpolator" type="Node" parent="." unique_id=314606546 node_paths=PackedStringArray("root")]
script = ExtResource("10_4mqhe")
root = NodePath("..")
properties = Array[String]([":transform", "Head:transform"])

[node name="Input" type="Node" parent="."]
[node name="Input" type="Node" parent="." unique_id=809161058]
script = ExtResource("7_2i3bi")
mouse_sensitivity = 0.005

[node name="Hit SFX" type="AudioStreamPlayer3D" parent="."]
[node name="Hit SFX" type="AudioStreamPlayer3D" parent="." unique_id=1362810457]
stream = ExtResource("15_1n4tg")
60 changes: 31 additions & 29 deletions examples/rollback-fps/rollback-fps.tscn

Large diffs are not rendered by default.

60 changes: 26 additions & 34 deletions examples/rollback-fps/scripts/player-weapon.gd
Original file line number Diff line number Diff line change
Expand Up @@ -5,68 +5,60 @@ extends Node3D

@onready var input := $"../../Input" as ExampleRollbackFPS.PlayerInput
@onready var sound := $AudioStreamPlayer3D as AudioStreamPlayer3D
@onready var fire_action := $"Fire Action" as RewindableAction
@onready var rollback_synchronizer := %RollbackSynchronizer as RollbackSynchronizer

var last_fire: int = -1

func _ready():
fire_action.mutate(self) # Mutate self, so firing code can run
fire_action.mutate($"../../") # Mutate player

NetworkTime.after_tick_loop.connect(_after_loop)

func _rollback_tick(_dt, tick: int, _if):
if rollback_synchronizer.is_predicting():
return
if last_fire == tick - 1:
var hit = _fire()
RollbackEffects.record(
[self, &"fire"],
func(ctx: RollbackEffects.EffectContext):
sound.play()
if hit and hit.has_method("damage_effect"):
hit.damage_effect()

ctx.on_revert = func():
# Any undo logic here. Despawn projectiles, etc.
print("hit reverted")
)

if (
input.fire
and _can_fire()
and (is_multiplayer_authority() or input.is_multiplayer_authority())
):
last_fire = tick

fire_action.set_active(input.fire and _can_fire())
match fire_action.get_status():
RewindableAction.CONFIRMING, RewindableAction.ACTIVE:
# Fire if action has just activated or is active
_fire()
RewindableAction.CANCELLING:
# Whoops, turns out we couldn't have fired, undo
_unfire()

func _after_loop():
if fire_action.has_confirmed():
sound.play()

func _can_fire() -> bool:
return NetworkTime.seconds_between(last_fire, NetworkRollback.tick) >= fire_cooldown

func _fire():
last_fire = NetworkRollback.tick

func _fire():
# See what we've hit
var hit := _raycast()
if hit.is_empty():
# No hit, nothing to do
return

_on_hit(hit)
return _on_hit(hit)

func _unfire():
fire_action.erase_context()

func _raycast() -> Dictionary:
# Detect hit
var space := get_world_3d().direct_space_state
var origin_xform := global_transform
var query := PhysicsRayQueryParameters3D.create(
origin_xform.origin,
origin_xform.origin + origin_xform.basis.z * 1024.
origin_xform.origin, origin_xform.origin + origin_xform.basis.z * 1024.
)

return space.intersect_ray(query)

func _on_hit(result: Dictionary):
var is_new_hit := false
if not fire_action.has_context():
fire_action.set_context(true)
is_new_hit = true

func _on_hit(result: Dictionary):
if result.collider.has_method("damage"):
result.collider.damage(damage, is_new_hit)
NetworkRollback.mutate(result.collider)
result.collider.damage(damage)
return result.collider
15 changes: 11 additions & 4 deletions examples/rollback-fps/scripts/player.gd
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var _ackd_deaths := 0

var _was_hit := false


func _ready():
display_name.text = name

Expand All @@ -39,9 +40,11 @@ func _ready():
camera.current = true
ExampleRollbackFPS.HUD.set_player(self)


func _before_tick_loop():
_ackd_deaths = deaths


func _after_tick_loop():
if _ackd_deaths != deaths:
tick_interpolator.teleport()
Expand All @@ -51,6 +54,7 @@ func _after_tick_loop():
hit_sfx.play()
_was_hit = false


func _rollback_tick(delta: float, tick: int, is_fresh: bool) -> void:
# Handle respawn
if tick == death_tick:
Expand Down Expand Up @@ -99,19 +103,22 @@ func _rollback_tick(delta: float, tick: int, is_fresh: bool) -> void:
global_position = get_parent().get_next_spawn_point(get_player_id(), deaths)
health = 100


func _force_update_is_on_floor():
var old_velocity = velocity
velocity = Vector3.ZERO
move_and_slide()
velocity = old_velocity

func damage(amount: int, is_new_hit: bool = false):
# Queue hit sound
if is_new_hit:
_was_hit = true

func damage(amount: int):
health -= amount
_logger.info("%s HP now at %s", [name, health])


func damage_effect():
_was_hit = true


func get_player_id() -> int:
return input.get_multiplayer_authority()
87 changes: 87 additions & 0 deletions examples/rollback-fps/scripts/rollback-effects.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
extends Node


class _Effect:
extends RefCounted
var on_revert: Callable
var was_recorded_on_resim: bool = false


class EffectContext:
extends RefCounted
var on_revert: Callable


var _effects: Array[Dictionary] = []
var _ticks: PackedInt32Array = PackedInt32Array()


func _ready() -> void:
_effects.resize(NetworkRollback.history_limit)
for i: int in _effects.size():
_effects[i] = {}

_ticks.resize(NetworkRollback.history_limit)
_ticks.fill(-1)

NetworkRollback.on_prepare_tick.connect(_on_prepare_tick)
NetworkRollback.after_process_tick.connect(_after_process_tick)


func record(key: Variant, apply: Callable, tick: int = NetworkRollback.tick) -> void:
var index := _get_index(tick)
if _ticks[index] != tick:
_effects[index].clear()
_ticks[index] = tick

var tick_effects := _effects[index]
var effect: _Effect = tick_effects.get(key)
if effect != null:
effect.was_recorded_on_resim = true
return

var context: EffectContext = EffectContext.new()
apply.call(context)

effect = _Effect.new()
effect.on_revert = context.on_revert
effect.was_recorded_on_resim = true
tick_effects[key] = effect


func _on_prepare_tick(tick: int) -> void:
var index := _get_index(tick)
if _ticks[index] != tick:
return

for effect: _Effect in _effects[index].values():
effect.was_recorded_on_resim = false


func _after_process_tick(tick: int) -> void:
var index := _get_index(tick)
if _ticks[index] != tick:
return

var tick_effects := _effects[index]
for key: Variant in tick_effects.keys():
var effect: _Effect = tick_effects[key]
if effect.was_recorded_on_resim:
continue

_revert_effect(effect)
tick_effects.erase(key)


func _get_index(tick: int) -> int:
return tick % _effects.size()


func _revert_effect(effect: _Effect) -> void:
if effect == null:
return

if not effect.on_revert.is_valid():
return

effect.on_revert.call()
1 change: 1 addition & 0 deletions examples/rollback-fps/scripts/rollback-effects.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://brdhmrefa6afm
Loading