From b0fdfe0e41d460039d1ec35122a09cd631c3e6b9 Mon Sep 17 00:00:00 2001 From: copygirl Date: Sun, 8 Dec 2024 15:09:16 +0100 Subject: [PATCH] Refactor some code into SyncController class Besides (hopefully) helping with code cleanliness, this also allows us to store extra per-player or per-model information. --- copyMultiplayer.gd | 107 +++++++++++++++------------------------------ sync_controller.gd | 71 ++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 71 deletions(-) create mode 100644 sync_controller.gd diff --git a/copyMultiplayer.gd b/copyMultiplayer.gd index 36cef47..4a416ba 100644 --- a/copyMultiplayer.gd +++ b/copyMultiplayer.gd @@ -1,3 +1,4 @@ +class_name copyMultiplayer extends Mod_Base @export var cache := "" @@ -5,11 +6,11 @@ extends Mod_Base @export var address := "" @export var port := 52410 +var main_controller: ModelController + ## Hardcoded list of bone names that will get syncronized. var tracked_bones: Array[String] -var main_controller: ModelController - # Temporary positioning system. # TODO: Add a setting that allows syncing model positions, as an alternative # to letting the local player choose where each model is going to appear. @@ -17,9 +18,6 @@ var player_order: Array[int] = [] var starting_offset := Vector3(-1.25, 0.0, 0.0) var accumulative_offset := Vector3( 0.35, 0.0, 0.0) -# Allows us to use the "apply_animations" function to apply blendshapes. -var functions_blendshapes: Script = load("res://Mods/MediaPipe/MediaPipeController_BlendShapes.gd") - func _ready() -> void: # FIXME: This is just thrown together. Dunno if this is an accurate list. # TODO: Allow specifying additional bones, with the help of a hierachical list of existing bones in the model. @@ -117,9 +115,12 @@ func peer_connected(id: int) -> void: update_status() print_log(["Player ", id, " connected"]) - var new_controller := ModelController.new() - new_controller.name = str(id) - add_child(new_controller) + var model_controller := ModelController.new() + model_controller.name = str(id) + var sync_controller := SyncController.new() + sync_controller.name = "SyncController" + model_controller.add_child(sync_controller) + add_child(model_controller) player_order.append(id) update_model_transforms() @@ -162,6 +163,14 @@ func update_enabled_state(is_online: bool) -> void: window.get_node("Buttons/Host" ).disabled = is_online window.get_node("Buttons/Disconnect").disabled = !is_online +func update_status() -> void: + var num_players := 1 + multiplayer.get_peers().size() + var side := "Hosting" if multiplayer.is_server() else "Connected" + var s := "s" if num_players != 1 else "" + set_status([side, ": ", num_players, " player", s]) + + +# FIXME: Temporary hardcoded way to assign some offset to other players. func update_model_transforms() -> void: var offset := starting_offset for id in player_order: @@ -178,12 +187,7 @@ func update_model_rotation(controller: ModelController) -> void: var to_2d := Vector2(camera.position.x, camera.position.z) controller.rotation.y = from_2d.angle_to_point(to_2d) / 4 # Magic value, probably depends on FOV. -func update_status() -> void: - var num_players := 1 + multiplayer.get_peers().size() - var side := "Hosting" if multiplayer.is_server() else "Connected" - var s := "s" if num_players != 1 else "" - set_status([side, ": ", num_players, " player", s]) - +## Removes all networked player models. func clear_player_models() -> void: for controller in get_children(): if controller is ModelController: @@ -192,25 +196,8 @@ func clear_player_models() -> void: player_order.clear() -@rpc("any_peer", "reliable") -func change_model(filename: String) -> void: - var player_id := multiplayer.get_remote_sender_id() - var controller := get_node_or_null(str(player_id)) as ModelController - if not controller: return - - if not filename.is_valid_filename(): - print_log(["ERROR: '", filename, "' is not a valid file name!"]) - return - - var full_path := cache.path_join(filename) - if not FileAccess.file_exists(full_path): - print_log(["Player ", player_id, " wanted to switch to '", filename, "', but it could not be found, skipping"]) - return - - if controller.load_vrm(full_path): - print_log(["Player ", player_id, " switched to '", filename, "'"]) - else: - print_log(["ERROR: Model '", filename, "' could not be loaded!"]) +func _process(_delta: float) -> void: + SyncController.send_model_animation(self) # Called when a node is added to the main ModelController. func model_changed(child: Node) -> void: @@ -221,42 +208,20 @@ func model_changed(child: Node) -> void: if filename.is_valid_filename(): change_model.rpc(filename) -# FIXME: This sends way more information than necessary, but works as a proof-of-concept! -@rpc("any_peer", "unreliable_ordered") -func sync_model_animation( - model_transform: Transform3D, - shape_dict: Dictionary, # Dictionary[String, float] - bone_poses: Dictionary, # Dictionary[String, Transform3D] -) -> void: - var player_id := multiplayer.get_remote_sender_id() - var controller := get_node_or_null(str(player_id)) - if not controller: return - - var model := controller.get_node_or_null("Model") as Node3D - if model: - model.transform = model_transform - functions_blendshapes.apply_animations(model, shape_dict) - - var skeleton := controller._get_model_skeleton() as Skeleton3D - if skeleton: - for bone_name in bone_poses: - var pose: Transform3D = bone_poses[bone_name] - var idx := skeleton.find_bone(bone_name) - if idx != -1: skeleton.set_bone_pose(idx, pose) +## Gets the SyncController for the player with the specified peer id. +func get_sync_controller(peer_id: int) -> SyncController: + var model_controller := get_node_or_null(str(peer_id)) as ModelController + if not model_controller: return null + return model_controller.get_node_or_null("SyncController") as SyncController -func _process(_delta: float) -> void: - if multiplayer.get_peers().size() == 0: return - - var model := get_model() - var skeleton := get_skeleton() - var media_pipe_ctrl = $"../MediaPipeController" - if (not model) or (not skeleton) or (not media_pipe_ctrl): return - - var shape_dict = media_pipe_ctrl.blend_shape_last_values - var bone_poses = {} - for bone_name in tracked_bones: - var bone_idx = skeleton.find_bone(bone_name) - if bone_idx == -1: continue - var bone_pose = skeleton.get_bone_pose(bone_idx) - bone_poses[bone_name] = bone_pose - sync_model_animation.rpc(model.transform, shape_dict, bone_poses) +@rpc("any_peer", "reliable") +func change_model(filename: String) -> void: + var peer_id := multiplayer.get_remote_sender_id() + var controller := get_sync_controller(peer_id) + if controller: controller.change_model(filename) + +@rpc("any_peer", "unreliable_ordered") +func sync_model_animation(model_transform: Transform3D, shape_dict: Dictionary, bone_poses: Dictionary) -> void: + var peer_id := multiplayer.get_remote_sender_id() + var controller := get_sync_controller(peer_id) + if controller: controller.sync_model_animation(model_transform, shape_dict, bone_poses) diff --git a/sync_controller.gd b/sync_controller.gd new file mode 100644 index 0000000..96f6ecf --- /dev/null +++ b/sync_controller.gd @@ -0,0 +1,71 @@ +class_name SyncController +extends Node + +var module: copyMultiplayer +var model_controller: ModelController +var peer_id: int + +var nickname: String # FIXME: Not assigned or used. +var model_name: String +var model: Node +var skeleton: Skeleton3D + +# Allows us to use the "apply_animations" function to apply blendshapes to a model. +static var _functions_blendshapes: Script = load("res://Mods/MediaPipe/MediaPipeController_BlendShapes.gd") + +func _ready() -> void: + module = get_parent().get_parent() + model_controller = get_parent() + peer_id = model_controller.name.to_int() + +## Attempts to change the model of this player. +func change_model(filename: String) -> void: + if not filename.is_valid_filename(): + module.print_log("ERROR: '%s' is not a valid file name!" % filename) + return + var full_path := module.cache.path_join(filename) + if not FileAccess.file_exists(full_path): + module.print_log("Player %d wanted to switch to '%s', but it doesn't exist, skipping" % [ peer_id, filename ]) + return + if not model_controller.load_vrm(full_path): + module.print_log("ERROR: Model '%s' could not be loaded!" % filename) + return + module.print_log("Player %d switched to '%s'" % [ peer_id, filename ]) + model_name = filename + model = model_controller.get_node_or_null("Model") + skeleton = model_controller._get_model_skeleton() + +func sync_model_animation( + model_transform: Transform3D, + shape_dict: Dictionary, # Dictionary[String, float] + bone_poses: Dictionary, # Dictionary[String, Transform3D] +) -> void: + if (not model) or (not skeleton): return + model.transform = model_transform + _functions_blendshapes.apply_animations(model, shape_dict) + for bone_name in bone_poses: + var pose: Transform3D = bone_poses[bone_name] + var idx := skeleton.find_bone(bone_name) + if idx != -1: skeleton.set_bone_pose(idx, pose) + +@warning_ignore("shadowed_variable") +static func send_model_animation(module: copyMultiplayer) -> void: + # Check if there's other players we're connected to. + if module.multiplayer.get_peers().size() == 0: return + + var model := module.get_model() + var skeleton := module.get_skeleton() + var media_pipe = module.get_node("../MediaPipeController") + if (not model) or (not skeleton) or (not media_pipe): return + + var shape_dict = media_pipe.blend_shape_last_values + + var bone_poses = {} + for bone_name in module.tracked_bones: + var bone_idx = skeleton.find_bone(bone_name) + if bone_idx == -1: continue + var bone_pose = skeleton.get_bone_pose(bone_idx) + bone_poses[bone_name] = bone_pose + + # FIXME: This sends way more information than necessary, but works as a proof-of-concept! + module.sync_model_animation.rpc(model.transform, shape_dict, bone_poses)