Skip to content

Instantly share code, notes, and snippets.

@jamonholmgren
Last active February 20, 2025 05:54
Show Gist options
  • Save jamonholmgren/246d51174c2ebbadd7fe3085056ab0ef to your computer and use it in GitHub Desktop.
Save jamonholmgren/246d51174c2ebbadd7fe3085056ab0ef to your computer and use it in GitHub Desktop.
*In-progress, doesn't work (yet).* A simpler sync system for Godot.
class_name JamminSync extends Node
# In-progress, doesn't work (yet).
# A simpler sync system for Godot.
# Add as an autoload named "Sync" or let JamminSync handle it.
# Usage:
# Sync.sync_prop_reliable(self, "prop_name")
# Sync.sync_prop_unreliable(self, "prop_name")
@export var log_syncs: bool = true
@export var log_missing_nodes: bool = true
var _synced_props: Dictionary = {}
# TODO: make this not run on the physics process, but rather call_deferred
func should_sync(obj_path: NodePath, prop: StringName, every: int = 1, sync_mode: StringName = &"on_change") -> bool:
if every < 1: return false
var frames = Engine.get_physics_frames()
var is_on_tick = frames % every == 0
# Always sync means always (on ticks)
if sync_mode == &"always": return is_on_tick
# Initialize cached values
if not _synced_props.has(obj_path): _synced_props[obj_path] = { prop: { &"last_value": null } }
if not _synced_props[obj_path].has(prop): _synced_props[obj_path][prop] = { &"last_value": null }
var is_changed = _synced_props[obj_path][prop][&"last_value"] != get(prop)
# Don't do a sync, even though it's changed, if we're not on the tick
if sync_mode == &"on_change" and not is_changed: return false
if is_changed:
_synced_props[obj_path][prop][&"last_changed"] = frames
_synced_props[obj_path][prop][&"last_value"] = get(prop)
_synced_props[obj_path][prop][&"every"] = every
return true
# No change, so no sync
if sync_mode == &"on_change": return false
# Dynamic mode!
# In this mode, we sync when the value changes, and fairly quickly for
# a while, but then less and less often, until it changes again.
var last_changed = _synced_props[obj_path][prop][&"last_changed"]
var time_since_last_change = frames - last_changed
var should_update = time_since_last_change % _synced_props[obj_path][prop][&"every"] == 0
# Gradually increase the sync delay over time
if should_update and _synced_props[obj_path][prop][&"every"] < every * 10: _synced_props[obj_path][prop][&"every"] += 1
return should_update
func sync_prop_reliable(obj: Node, prop: StringName, every: int = 1, sync_mode: StringName = &"on_change"):
if not Lobby.is_authority(obj): return
# print("Syncing %s.%s" % [obj.get_path(), prop])
var should = should_sync(obj.get_path(), prop, every, sync_mode)
if should: print("Should sync: %s.%s = %s" % [obj.get_path(), prop, should])
if should: remote_sync_prop_reliable.rpc(obj.get_path(), prop, get(prop))
func sync_prop_unreliable(obj: Node, prop: StringName, every: int = 1, sync_mode: StringName = &"on_change"):
if not Lobby.is_authority(obj): return
if should_sync(obj.get_path(), prop, every, sync_mode): remote_sync_prop_unreliable.rpc(obj.get_path(), prop, get(prop))
@rpc("call_local", "reliable", "authority")
func remote_sync_prop_reliable(obj_path: NodePath, prop: StringName, value: Variant):
push_error("Rel Sync: %s.%s = %s" % [obj_path, prop, value])
# TODO: allow optionally lerping to the new value quickly
var obj = get_node(obj_path)
if not obj:
if log_missing_nodes: push_error("Sync: missing node %s" % obj_path)
return
obj.set(prop, value)
@rpc("call_local", "unreliable", "authority")
func remote_sync_prop_unreliable(obj_path: NodePath, prop: StringName, value: Variant):
push_error("Unr Sync: %s.%s = %s" % [obj_path, prop, value])
# TODO: allow optionally lerping to the new value quickly
var obj = get_node(obj_path)
if not obj:
if log_missing_nodes: push_error("Sync: missing node %s" % obj_path)
return
obj.set(prop, value)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment