Godot Version
4.4
Question
Hi,
I’m trying to setup manual multiplayer spawning and syncing with rpc calls. I’m collecting player state and trying to attempt to update the client according to a delta with full states being sent when a player connects and disconnects but I am at a loss and haven’t been able to make it work.
Anyone able to give a brief explanation (with examples if possible)
Current Setup
- Note i did not include any RPC calls as I’ve established that they are not the issue and the data is indeed being transferred.
- Yes i know get_tree().get_multiplayer() works but both the client and server have multiple multiplayer connections and I can use that.
- Yes I know that there is no anti cheat measure implemented in the server yet I’m just trying to get the implementation working first.
- No I cannot use the multiplayer spawner or synchronizer as the server structure is drastically different from the client
Server Side
battle.gd:
This is the instance of each battle/match, this is where the world state is managed
var full_world_state: Dictionary
var last_world_state: Dictionary
var world_state_delta: Dictionary
func _physics_process(_delta: float) -> void:
if full_world_state.is_empty(): # Checks if there is any game state
return
else:
if last_world_state.is_empty(): # Checks if we have a last state
last_world_state = full_world_state.duplicate(true)
else:
world_state_delta = calculate_state_delta(last_world_state, full_world_state) # Gets the world state delta
world_state_delta["T"] = (Time.get_unix_time_from_system() * 1000)
get_parent().return_state_delta(world_state_delta, self)
func calculate_state_delta(last_state: Dictionary, current_state: Dictionary) -> Dictionary:
var delta: Dictionary = {}
# Iterate through all items in the current state
for player_id in current_state:
# Get the sub-dictionaries for the current item
var current_item = current_state[player_id]
var last_item = {}
if last_state.has(player_id):
last_item = last_state[player_id]
var item_delta = {}
# Check for changes in Position ("P")
if current_item.has("P"):
if not last_item.has("P") or current_item.P != last_item.P:
item_delta["P"] = current_item.P
# Check for changes in Rotation ("R")
if current_item.has("R"):
if not last_item.has("R") or current_item.R != last_item.R:
item_delta["R"] = current_item.R
# If any changes were found for this item, add it to the main delta
if not item_delta.is_empty():
delta[player_id] = item_delta
return delta
func update_player_state(player_state: Dictionary, player_id: int) -> void:
if full_world_state.has(player_id): # Checks if player state exists
if full_world_state[player_id]["T"] < player_state["T"]: # Checks if player_state is newer
full_world_state[player_id] = player_state
else:
full_world_state[player_id] = player_state # Adds player_state if it doesnt exist
func remove_player_state(player_id: int) -> void:
full_world_state.erase(player_id)
world_state_delta.erase(player_id)
func get_full_world_state() -> Dictionary:
full_world_state["T"] = (Time.get_unix_time_from_system() * 1000)
return full_world_state
matchmaker.gd
This is where each battle is instanced and also determines what data goes to what player
func find_battle(player_id: int) -> void:
if ongoing_battles == {}:
_create_battle()
_join_battle(player_id)
else:
_join_battle(player_id)
func _create_battle() -> void:
var battle_scene: Node = packed_battle_scene.instantiate()
battle_scene.name = "Battle " + str(battle_counter)
battle_scene.map = "dev_map"
battle_scene.team_blue = "united_states"
battle_scene.team_red = "germany"
battle_scene.max_players = 20
matchmaker.add_child(battle_scene, true)
ongoing_battles[battle_scene] = []
battle_counter += 1
func _join_battle(player_id: int) -> void:
var result: bool = false
var map: String
var battle_joined: Node
for battle in ongoing_battles:
var players_in_battle = ongoing_battles[battle]
if players_in_battle.size() < battle.max_players:
ongoing_battles[battle].append(player_id)
map = battle.map
battle_joined = battle
result = true
if battle.blue_team_list.size() <= battle.red_team_list.size():
battle.blue_team_list.append(player_id)
else:
battle.red_team_list.append(player_id)
break
if result:
GameServer.return_join_battle(map, player_id)
GameServer.spawn_new_player(ongoing_battles[battle_joined], player_id, Vector3(1, 1, 1))
if ongoing_battles[battle_joined].size() >= 1:
GameServer.return_full_world_state(battle_joined.get_full_world_state(), player_id)
player_to_battle_map[player_id] = battle_joined
else:
print("No available battles found. Player was not added.")
func player_left(player_id: int) -> void:
var battle: Node
if player_to_battle_map.has(player_id):
battle = player_to_battle_map[player_id]
GameServer.despawn_player(ongoing_battles[battle], player_id)
if battle.blue_team_list.has(player_id):
battle.blue_team_list.erase(player_id)
if battle.red_team_list.has(player_id):
battle.red_team_list.erase(player_id)
ongoing_battles[battle].erase(player_id)
player_to_battle_map.erase(player_id)
battle.remove_player_state(player_id) # Removes the player's state from the battle
var full_world_state: Dictionary = battle.get_full_world_state()
for player in battle:
GameServer.return_full_world_state(full_world_state, player)
func update_state(player_state: Dictionary, player_id: int) -> void:
var battle: Node
if player_to_battle_map.has(player_id):
battle = player_to_battle_map[player_id]
battle.update_player_state(player_state, player_id)
func return_state_delta(world_state_delta: Dictionary, battle: Node) -> void:
for player in ongoing_battles[battle]:
GameServer.return_world_state_delta(world_state_delta, player)
Client Side
Player.gd
this is where the player sends the updates (called by the _physics_process)
func define_player_state() -> void:
player_state = {"T": (Time.get_unix_time_from_system() * 1000), "P": get_global_position(), "R": get_rotation()}
GameServer.send_player_state(player_state)
PlayerHandler
The playerhandler is what ties it all together
var last_state_time: float
var full_state_sent: bool = false
func spawn_new_player(player_id: int, position: Vector3, rotation: Vector3 = Vector3.ZERO) -> void:
if GameServer.get_multiplayer().get_unique_id() == player_id:
return
var new_player: CharacterBody3D = player_template.instantiate()
new_player.position = position
new_player.name = str(player_id)
new_player.rotation_degrees = rotation
player_handler.add_child(new_player, true)
func despawn_player(player_id: int) -> void:
if player_handler.has_node(str(player_id)):
player_handler.get_node(str(player_id)).queue_free()
func update_world_state(world_state: Dictionary, is_full_state: bool) -> void:
if full_state_sent and not is_full_state:
full_state_sent = false
return
elif last_state_time < world_state["T"]:
for player in world_state:
if str(player) == "T":
continue
if player == GameServer.get_multiplayer().get_unique_id():
continue
if player_handler.has_node(str(player)):
var player_node: CharacterBody3D = player_handler.get_node(player)
if player.has("P"):
var new_position: Vector3 = player["P"]
player_node.move_player(new_position)
if player.has("R"):
var new_rotation: Vector3 = player["R"]
player_node.rotate_player(new_rotation)
else:
if player.has("P") and player.has("R"):
spawn_new_player(player, player["P"], player["R"])
elif player.has("P"):
spawn_new_player(player, player["P"])
full_state_sent = is_full_state # Checks if state recieved is full state or a delta
End Goal
- have a setup that buffers world state to be able to lerp
- have the lerping work with world_state_delta updates
- But most of all have a synced multiplayer world