├── .gitignore ├── LICENSE ├── README.md ├── addons └── replicator │ ├── player_location_manager.gd │ ├── plugin.cfg │ ├── plugin.gd │ ├── remote_spawner.gd │ ├── replicated_member.gd │ ├── replicator.gd │ ├── replicator_node_icon.svg │ └── replicator_node_icon.svg.import └── icon.png /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Godot-specific ignores 3 | .import/ 4 | export.cfg 5 | export_presets.cfg 6 | 7 | # Mono-specific ignores 8 | .mono/ 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jummit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Godot Replicator Node Plugin 2 | 3 | Adds a `Replicator` node that can replicates properties with interpolation and spawning and deletion of nodes between server and client without any code. 4 | 5 | ## Usage 6 | 7 | Add a Replicator node to the node that has properties you want to replicate with all clients. This could be a `RigidBody` for example. 8 | 9 | Increase the length of the `members` array by clicking the arrow next to the `length`. 10 | Write the properties you want to replicate in the `name` text field of the Member resource. For example, if you want to replicate the position and rotation of a `RigidBody`, type in `transform`. 11 | 12 | ### Replicator 13 | 14 | | Exported Property | What it does | 15 | | ----------------- | ------------ | 16 | | Replicate Automatically | Call `replicate_members` in a specified interval | 17 | | Replicate Interval | The interval at which to call `replicate_members` | 18 | | Replicate Spawning | Spawn on puppet instances when spawned on the master | 19 | | Replicate Despawning | Despawn on puppet instances when despawned on the | 20 | | Despawn On Disconnect | Despawn when the master disconnects | 21 | | Spawn On JoiningPeers | Spawn on newly joined peers | 22 | | Interpolate Changes | Use a generated Tween sibling to interpolate new | 23 | | Logging | Log changes of members on puppet instances | 24 | 25 | ### Replicated Member 26 | 27 | | Exported Property | What it does | 28 | | ----------------- | ------------ | 29 | | Name | The name of the property | 30 | | Interpolate Changes | If the property should be smoothly interpolated when a new value is received | 31 | | Replicate Automatically | If the property should be automatically replicated in the specified `replicate_interval` | 32 | | Replicate Interval | If `replicate_automatically` is true, how many seconds to wait to send the next snapshot | 33 | | Reliable | Whether to use NetworkedMultiplayerPeer.TRANSFER_MODE_RELIABLE instead of NetworkedMultiplayerPeer.TRANSFER_MODE_UNRELIABLE | 34 | | Logging | Whether to log when an update is received on a puppet peer | 35 | | Max Interpolation Distance | If `replicate_automatically` is true, maximum difference between snapshots that is interpolated | 36 | 37 | ## How it works 38 | 39 | The Replicator node uses Godot's high level networking API. 40 | 41 | It adds Tween siblings if `interpolate_changes` is true, which interpolate the old value to the new value when replicating, and a timer which calls `replicate_members` on timeout. 42 | 43 | The plugin adds an autoload singleton called "RemoteSpawner" to spawn nodes on newly joined peers. 44 | 45 | It also removes "@"s from nodes names to be able to replicate node names, as it's impossible to use "@"s when setting a node name. 46 | 47 | Example: `@Bullet@2@` becomes `Bullet2` 48 | -------------------------------------------------------------------------------- /addons/replicator/player_location_manager.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name PlayerLocationManager, "replicator_node_icon.svg" 3 | 4 | """ 5 | Singleton used to manage player locations 6 | 7 | Player cameras have to be manually registered by each player. 8 | They are used for distance-based replication optimization. 9 | """ 10 | 11 | export var enable_logging := false 12 | 13 | var player_cameras : Dictionary = {} 14 | 15 | func register(camera : Node, peer : int) -> void: 16 | player_cameras[peer] = camera.get_path() 17 | if enable_logging: 18 | print("Registered %s as camera of peer %s" % [camera.get_path(), peer]) 19 | 20 | 21 | func get_distance(from_node : Node, to_id : int) -> float: 22 | if not to_id in player_cameras: 23 | if enable_logging: 24 | print("No camera found for %s" % to_id) 25 | return -1.0 26 | var camera = multiplayer.root_node.get_node(player_cameras[to_id]) 27 | var distance := -1.0 28 | if from_node is Spatial and camera is Spatial: 29 | distance = from_node.global_transform.origin.distance_to( 30 | camera.global_transform.origin) 31 | elif from_node is Node2D and camera is Node2D: 32 | distance = from_node.global_position.distance_to(camera.global_position) 33 | else: 34 | push_error("Types of %s (type %s) and camera %s (type %s) don't match, can't get distance"\ 35 | % [from_node, from_node.get_class(), camera, camera.get_class()]) 36 | if enable_logging: 37 | print("Distance from %s to %s is %s" % [from_node, to_id, distance]) 38 | return distance 39 | -------------------------------------------------------------------------------- /addons/replicator/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Replicator Node" 4 | description="Adds a replicator node that is used to easily replicate properties with interpolation and spawning and deletion of nodes between server and client without any code." 5 | author="Jummit" 6 | version="3.0" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/replicator/plugin.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends EditorPlugin 3 | -------------------------------------------------------------------------------- /addons/replicator/remote_spawner.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name RemoteSpawner, "replicator_node_icon.svg" 3 | 4 | """ 5 | Singleton that manages replicated spawning 6 | """ 7 | 8 | export var enable_logging := false 9 | 10 | func _ready(): 11 | get_tree().connect("node_added", self, "_on_SceneTree_node_added") 12 | 13 | 14 | func replicate_node(node : Node, peer := 0) -> void: 15 | assert(node.filename, "Can't spawn node that isn't root of the scene") 16 | rpc_id(peer, "spawn", node.name, node.get_network_master(), node.filename, 17 | get_path_to(node.get_parent())) 18 | 19 | 20 | remote func spawn(node_name : String, network_master : int, 21 | scene : String, parent : NodePath) -> void: 22 | if enable_logging: 23 | print("Spawning %s named %s on %s" % [scene, node_name, parent]) 24 | # Todo: cache scenes. 25 | var instance : Node = load(scene).instance() 26 | instance.name = node_name 27 | instance.set_network_master(network_master) 28 | 29 | # Use a path relative to multiplayer.root_node to make it possible 30 | # to run server and client on the same machine. 31 | get_node(parent).add_child(instance) 32 | 33 | # Hide the instance as its position may not yet be 34 | # replicated to avoid seeing the instance at the origin. 35 | # Todo: move this to `Replicator`. 36 | if instance.has_method("show") and instance.has_method("hide"): 37 | instance.hide() 38 | yield(get_tree().create_timer(.01), "timeout") 39 | instance.show() 40 | 41 | 42 | # Node names are replicated in `spawn`, 43 | # but there is no way to include "@"s in custom names. 44 | func _on_SceneTree_node_added(node : Node): 45 | node.name = node.name.replace("@", "") 46 | -------------------------------------------------------------------------------- /addons/replicator/replicated_member.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Resource 3 | class_name ReplicatedMember, "replicator_node_icon.svg" 4 | 5 | """ 6 | Member resource to be used in a `Replicator` 7 | 8 | Holds information about which member should be replicated how. 9 | """ 10 | 11 | # The name of the property. 12 | export var name := "" setget set_name 13 | # If the property should be smoothly interpolated when a new value is received. 14 | export var interpolate_changes := false 15 | # If the property should be automatically 16 | # Replicated in the specified `replicate_interval`. 17 | export var replicate_automatically := false 18 | # If `replicate_automatically` is true, 19 | # How many seconds to wait to send the next snapshot. 20 | export var replicate_interval := 0.2 21 | # Whether to use `NetworkedMultiplayerPeer.TRANSFER_MODE_RELIABLE` 22 | # Instead of `NetworkedMultiplayerPeer.TRANSFER_MODE_UNRELIABLE`. 23 | export var reliable := false 24 | # Whether to log when an update is received on a puppet peer. 25 | export var logging := false 26 | # If `replicate_automatically` is true, 27 | # Maximum difference between snapshots that is interpolated. 28 | export var max_interpolation_distance := INF 29 | # The minimum difference a packet needs to have from the current value to. 30 | # Be accepted. 31 | export var min_replication_difference := 0.0 32 | # The node that has the member. 33 | export var node := NodePath("../") 34 | 35 | # How likely it is that this member will be replicated. 36 | var importance := 0.0 37 | 38 | func set_name(to : String) -> void: 39 | name = to 40 | resource_name = name 41 | -------------------------------------------------------------------------------- /addons/replicator/replicator.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Node 3 | class_name Replicator, "replicator_node_icon.svg" 4 | 5 | """ 6 | Replicates spawning, despawning and the properties listed in `members`. 7 | 8 | Supports interpolation and replicating properties at specified intervals and 9 | spawning on joining peers. 10 | 11 | The `subject` is the node whose properties get replicated, and which is 12 | spawned / despawned. It is the parent of the replicator by default, but can be 13 | changed programatically. 14 | 15 | For spawning to work the `subject` must be the root of the scene, as only the 16 | filename of the scene is send over the network. 17 | 18 | `members` is a list of `ReplicatedMember`s which store a number of settings 19 | regarding replication. `replicate_member` can be called to replicate members 20 | manually. 21 | 22 | Even though `PlayerLocationManager` and `RemoteSpawner` are autoloads, they are 23 | fetched manually. This allows for multiple instances at the same time, which is 24 | needed to run server and client simultaniously. 25 | 26 | Members are only replicated when they change, which is detected using the 27 | native `equal_approx` method. 28 | """ 29 | 30 | # Emitted before the master/puppet are set up. This can be used to alter the 31 | # Replicator before it does it's setup. 32 | signal pre_init 33 | 34 | export(Array, ReplicatedMember) var members : Array 35 | # Spawn on puppet instances when spawned on the master instance. 36 | export var replicate_spawning := false 37 | # Despawn on puppet instances when despawned on the master instance. 38 | export var replicate_despawning := false 39 | # Despawn when the master disconnects. 40 | export var despawn_on_disconnect := false 41 | # The maxium distance `subject` can be away from the player and get replicated. 42 | export var max_replication_distance := INF 43 | # Log replication of members on the master instance. 44 | export var enable_logging := false 45 | 46 | var remote_spawner : RemoteSpawner 47 | var player_location_manager : PlayerLocationManager 48 | 49 | # Store which members where replicated, 50 | # to only interpolate if the master sent us a state. 51 | var already_replicated_once : Dictionary = {} 52 | var last_replicated_values : Dictionary = {} 53 | 54 | var NO_MEMBER := ReplicatedMember.new() 55 | 56 | const TYPES_WITH_EQUAL_APPROX_METHOD := [TYPE_VECTOR2, TYPE_RECT2, 57 | TYPE_VECTOR3, TYPE_TRANSFORM2D, TYPE_PLANE, TYPE_QUAT, TYPE_AABB, 58 | TYPE_BASIS, TYPE_TRANSFORM, TYPE_COLOR] 59 | 60 | const PlayerLocationManager = preload("player_location_manager.gd") 61 | const RemoteSpawner = preload("remote_spawner.gd") 62 | const ReplicatedMember = preload("replicated_member.gd") 63 | 64 | onready var subject : Node = get_parent() 65 | 66 | func _ready() -> void: 67 | set_process(Engine.editor_hint) 68 | if Engine.editor_hint: 69 | return 70 | if not multiplayer.connected(): 71 | yield(multiplayer, "connected_to_server") 72 | 73 | emit_signal("pre_init") 74 | 75 | if is_network_master(): 76 | _setup_master() 77 | else: 78 | _setup_puppet() 79 | 80 | # Make members unique so they can be modified on a per-instance basis. 81 | members = members.duplicate() 82 | for member_num in members.size(): 83 | members[member_num] = members[member_num].duplicate() 84 | 85 | for member in members: 86 | setup_member(member) 87 | 88 | 89 | func _process(_delta : float) -> void: 90 | update_configuration_warning() 91 | 92 | 93 | func _get_configuration_warning() -> String: 94 | if replicate_spawning and not subject.filename: 95 | return "Can't replicate spawning if not attached to the root node of the scene." 96 | return "" 97 | 98 | 99 | func setup_member(member : ReplicatedMember) -> void: 100 | if is_network_master(): 101 | if member.replicate_automatically: 102 | var timer := Timer.new() 103 | timer.wait_time = member.replicate_interval 104 | timer.autostart = true 105 | timer.one_shot = false 106 | timer.connect("timeout", self, "_on_ReplicateTimer_timeout", 107 | [member]) 108 | add_child(timer) 109 | else: 110 | var tween := Tween.new() 111 | tween.name = member.name 112 | add_child(tween) 113 | 114 | 115 | func replicate_member(member : ReplicatedMember) -> void: 116 | var last_value = last_replicated_values.get(member.name) 117 | var node := get_node(member.node) 118 | var current_value = node.get(member.name) 119 | 120 | assert(member.name in node, "member %s not found on %s" % [member.name, 121 | node.name]) 122 | 123 | if _is_variant_equal_approx(current_value, last_value): 124 | if member.reliable: 125 | return 126 | else: 127 | if randf() > member.importance: 128 | member.importance += 0.1 129 | return 130 | 131 | if member.logging: 132 | _log("Replicating %s of %s with value of %s" % 133 | [member.name, subject.name, current_value]) 134 | 135 | for peer in multiplayer.get_network_connected_peers(): 136 | if peer == multiplayer.get_network_unique_id(): 137 | continue 138 | if player_location_manager.get_distance(subject, peer) <\ 139 | max_replication_distance: 140 | if member.reliable: 141 | rpc_id(peer, "_set_member_on_puppet", member.name, 142 | current_value) 143 | else: 144 | rpc_unreliable_id(peer, "_set_member_on_puppet", member.name, 145 | current_value) 146 | 147 | last_replicated_values[member.name] = current_value 148 | 149 | 150 | func get_member_configuration(member_name : String) -> ReplicatedMember: 151 | for member in members: 152 | if member.name == member_name: 153 | return member 154 | return NO_MEMBER 155 | 156 | 157 | func _setup_master() -> void: 158 | remote_spawner = _find_node_on_parents(self, "RemoteSpawner") 159 | player_location_manager = _find_node_on_parents(self, 160 | "PlayerLocationManager") 161 | 162 | if replicate_spawning and subject.filename: 163 | multiplayer.connect("network_peer_connected", self, 164 | "_on_network_peer_connected") 165 | if replicate_spawning: 166 | _log("Spawning %s on connected peers" % subject.name) 167 | remote_spawner.replicate_node(subject) 168 | yield(get_tree(), "idle_frame") 169 | for member in members: 170 | replicate_member(member) 171 | if replicate_despawning: 172 | connect("tree_exiting", self, "_on_tree_exiting") 173 | 174 | 175 | func _setup_puppet() -> void: 176 | if despawn_on_disconnect: 177 | multiplayer.connect("network_peer_disconnected", self, 178 | "_on_network_peer_disconnected") 179 | 180 | 181 | puppet func _set_member_on_puppet(member : String, value) -> void: 182 | var configuration := get_member_configuration(member) 183 | var node := get_node(configuration.node) 184 | var old_value = node.get(member) 185 | var difference := _distance(old_value, value) 186 | if configuration.min_replication_difference and\ 187 | difference < configuration.min_replication_difference: 188 | return 189 | if configuration.logging: 190 | _log("%s of %s set to %s" % [member, subject.name, value]) 191 | if configuration.interpolate_changes and\ 192 | difference < configuration.max_interpolation_distance\ 193 | and already_replicated_once.has(member): 194 | get_node(member).interpolate_property(node, member, 195 | node.get(member), value, configuration.replicate_interval) 196 | get_node(member).start() 197 | else: 198 | node.set(member, value) 199 | already_replicated_once[member] = true 200 | 201 | 202 | # Called when the master node exits the tree. 203 | puppet func _despawn() -> void: 204 | subject.queue_free() 205 | _log("%s despawned as master (%s) disconnected" % [subject.name, 206 | subject.get_network_master()]) 207 | 208 | 209 | func _on_tree_exiting() -> void: 210 | rpc("_despawn") 211 | 212 | 213 | func _on_network_peer_connected(id : int) -> void: 214 | _log("Spawning %s on newly connected peer (%s)" % [subject.filename, id]) 215 | remote_spawner.replicate_node(subject, id) 216 | 217 | 218 | func _on_network_peer_disconnected(id : int) -> void: 219 | if id == get_network_master(): 220 | subject.queue_free() 221 | _log("%s despawned as master (%s) disconnected" % [subject.name, 222 | get_network_master()]) 223 | 224 | 225 | func _on_ReplicateTimer_timeout(member : ReplicatedMember) -> void: 226 | if is_inside_tree(): 227 | replicate_member(member) 228 | 229 | 230 | func _log(message : String) -> void: 231 | if enable_logging: 232 | print(message) 233 | 234 | 235 | static func _is_variant_equal_approx(a, b) -> bool: 236 | if typeof(a) in TYPES_WITH_EQUAL_APPROX_METHOD and (typeof(a) == typeof(b)): 237 | return a.is_equal_approx(b) 238 | else: 239 | return a == b 240 | 241 | 242 | static func _distance(a, b) -> float: 243 | if typeof(a) == typeof(b): 244 | match typeof(a): 245 | TYPE_TRANSFORM, TYPE_TRANSFORM2D: 246 | return a.origin.distance_to(b.origin) 247 | TYPE_VECTOR2, TYPE_VECTOR3: 248 | return a.distance_to(b) 249 | if (a is float or a is int) and (b is float or b is int): 250 | return a - b 251 | return INF 252 | 253 | 254 | static func _find_node_on_parents(start_node : Node, node_name : String): 255 | if not start_node.get_parent(): 256 | return false 257 | var node := start_node.get_parent().find_node(node_name, false, false) 258 | if node: 259 | return node 260 | return _find_node_on_parents(start_node.get_parent(), node_name) 261 | -------------------------------------------------------------------------------- /addons/replicator/replicator_node_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 51 | 54 | 55 | 57 | 58 | 60 | image/svg+xml 61 | 63 | 64 | 65 | 66 | 67 | 72 | 77 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /addons/replicator/replicator_node_icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/replicator_node_icon.svg-079fc2b75a1f2213b640ecd7a1cbcb49.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://addons/third_party/replicator/replicator_node_icon.svg" 13 | dest_files=[ "res://.import/replicator_node_icon.svg-079fc2b75a1f2213b640ecd7a1cbcb49.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jummit/replicator-node/e62f84efbee0b9d916872517ece3a0d142ac13b0/icon.png --------------------------------------------------------------------------------