Adjust player position via dragging

Changes the mod settings UI to use a tabbed system,
split up into "Connect", "Settings" and "Players".

In the "Players" tab, the position of each remote
player can be changed via the provided input controls.
Additionally, while this tab is active, you can also
click and drag players directly to reposition them.

Removed the temporary positioning system.
main
copygirl 2 months ago
parent d4f7fa604a
commit e35eb0c9c8
  1. 20
      Resources/copy_multiplayer_settings.gd
  2. 113
      Resources/copy_multiplayer_settings.tscn
  3. 24
      Scenes/copy_multiplayer_settings.gd
  4. 159
      Scenes/copy_multiplayer_settings.tscn
  5. 41
      Scenes/player_settings.gd
  6. 179
      Scenes/player_settings.tscn
  7. 108
      Scenes/sync_controller.gd
  8. 9
      Scenes/sync_controller.tscn
  9. 0
      Utility/stream_buffer.gd
  10. 100
      copyMultiplayer.gd

@ -1,20 +0,0 @@
extends Container
@export var visible_icon: Texture2D
@export var hidden_icon: Texture2D
func _on_cache_dir_dialog_pressed() -> void:
var widget: LineEdit = $"Cache/LineEdit"
var dialog: FileDialog = $"Cache/FileDialog"
dialog.size = get_window().size / 2
dialog.position = get_window().size / 4
dialog.current_dir = widget.text
dialog.popup()
widget.text = await dialog.dir_selected
widget.text_changed.emit(widget.text)
func _on_show_hide_address_toggled(toggled_on: bool) -> void:
$"Host/Address".secret = !toggled_on
$"Host/ShowHide".icon = visible_icon if toggled_on else hidden_icon

@ -1,113 +0,0 @@
[gd_scene load_steps=4 format=3 uid="uid://cdxnwsgmevndb"]
[ext_resource type="Script" path="res://Mods/copyMultiplayer/Resources/copy_multiplayer_settings.gd" id="1_7d55i"]
[ext_resource type="Texture2D" uid="uid://qbho5oyu1kfa" path="res://Mods/copyMultiplayer/Resources/hidden.png" id="2_1u5pu"]
[ext_resource type="Texture2D" uid="uid://dcmljlb2v6p16" path="res://Mods/copyMultiplayer/Resources/visible.png" id="2_ibe7i"]
[node name="copyMultiplayerSettings" type="VBoxContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("1_7d55i")
visible_icon = ExtResource("2_ibe7i")
hidden_icon = ExtResource("2_1u5pu")
[node name="Cache" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="Label" type="Label" parent="Cache"]
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
text = "Cache"
[node name="LineEdit" type="LineEdit" parent="Cache"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "The directory other players' models will be loaded from.
Filenames must match exactly for this to work."
[node name="Button" type="Button" parent="Cache"]
layout_mode = 2
tooltip_text = "Browse Directory"
text = " ... "
[node name="FileDialog" type="FileDialog" parent="Cache"]
title = "Open a Directory"
ok_button_text = "Select Current Folder"
file_mode = 2
access = 2
[node name="Name" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="Label" type="Label" parent="Name"]
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
text = "Name"
[node name="LineEdit" type="LineEdit" parent="Name"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Host" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="Label" type="Label" parent="Host"]
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
text = "Host"
[node name="Address" type="LineEdit" parent="Host"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "Address or IP of the host player to connect to.
Not used when you're hosting."
placeholder_text = "127.0.0.1"
secret = true
[node name="ShowHide" type="Button" parent="Host"]
layout_mode = 2
tooltip_text = "Show / Hide Address"
toggle_mode = true
icon = ExtResource("2_1u5pu")
flat = true
[node name="Port" type="SpinBox" parent="Host"]
layout_mode = 2
tooltip_text = "Port to connect to / listen on.
Host must forward this port."
min_value = 1024.0
max_value = 65000.0
value = 52410.0
alignment = 2
[node name="Buttons" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="Join" type="Button" parent="Buttons"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "Join an existing session."
text = "Join"
[node name="Host" type="Button" parent="Buttons"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "Open a new session, allowing
users to connect to your public IP."
text = "Host"
[node name="Disconnect" type="Button" parent="Buttons"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "Disconnect from / close the current session."
disabled = true
text = "Disconnect
"
[connection signal="pressed" from="Cache/Button" to="." method="_on_cache_dir_dialog_pressed"]
[connection signal="toggled" from="Host/ShowHide" to="." method="_on_show_hide_address_toggled"]

@ -0,0 +1,24 @@
class_name copyMultiplayerSettings
extends TabContainer
@export var visible_icon: Texture2D
@export var hidden_icon: Texture2D
func is_tab_selected(tab_name: String) -> bool:
return get_current_tab_control().name == tab_name
func _on_cache_dir_dialog_pressed() -> void:
var widget: LineEdit = $"Settings/VBoxContainer/Cache/LineEdit"
var dialog: FileDialog = $"Settings/VBoxContainer/Cache/FileDialog"
dialog.size = get_window().size / 2
dialog.position = get_window().size / 4
dialog.current_dir = widget.text
dialog.popup()
widget.text = await dialog.dir_selected
widget.text_changed.emit(widget.text)
func _on_show_hide_address_toggled(toggled_on: bool) -> void:
$"Connect/VBoxContainer/Host/Address".secret = !toggled_on
$"Connect/VBoxContainer/Host/ShowHide".icon = visible_icon if toggled_on else hidden_icon

@ -0,0 +1,159 @@
[gd_scene load_steps=4 format=3 uid="uid://cdxnwsgmevndb"]
[ext_resource type="Script" path="res://Mods/copyMultiplayer/Scenes/copy_multiplayer_settings.gd" id="1_7d55i"]
[ext_resource type="Texture2D" uid="uid://qbho5oyu1kfa" path="res://Mods/copyMultiplayer/Resources/hidden.png" id="2_1u5pu"]
[ext_resource type="Texture2D" uid="uid://dcmljlb2v6p16" path="res://Mods/copyMultiplayer/Resources/visible.png" id="2_ibe7i"]
[node name="copyMultiplayerSettings" type="TabContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
current_tab = 2
script = ExtResource("1_7d55i")
visible_icon = ExtResource("2_ibe7i")
hidden_icon = ExtResource("2_1u5pu")
[node name="Connect" type="MarginContainer" parent="."]
visible = false
layout_mode = 2
theme_override_constants/margin_left = 4
theme_override_constants/margin_top = 4
theme_override_constants/margin_right = 4
theme_override_constants/margin_bottom = 4
metadata/_tab_index = 0
[node name="VBoxContainer" type="VBoxContainer" parent="Connect"]
layout_mode = 2
[node name="Join" type="HBoxContainer" parent="Connect/VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="Connect/VBoxContainer/Join"]
custom_minimum_size = Vector2(80, 0)
layout_mode = 2
text = "Address:"
horizontal_alignment = 2
[node name="Address" type="LineEdit" parent="Connect/VBoxContainer/Join"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "Address or IP of the host player to connect to.
Not used when you're hosting."
placeholder_text = "127.0.0.1"
secret = true
[node name="ShowHide" type="Button" parent="Connect/VBoxContainer/Join"]
layout_mode = 2
tooltip_text = "Show / Hide Address"
toggle_mode = true
icon = ExtResource("2_1u5pu")
flat = true
[node name="Button" type="Button" parent="Connect/VBoxContainer/Join"]
custom_minimum_size = Vector2(120, 0)
layout_mode = 2
tooltip_text = "Join an existing session."
text = "Join"
[node name="Host" type="HBoxContainer" parent="Connect/VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="Connect/VBoxContainer/Host"]
custom_minimum_size = Vector2(80, 0)
layout_mode = 2
text = "Port:"
horizontal_alignment = 2
[node name="Port" type="SpinBox" parent="Connect/VBoxContainer/Host"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "Port to connect to / listen on.
Host must forward this port."
min_value = 1024.0
max_value = 65000.0
value = 52410.0
alignment = 2
[node name="Button" type="Button" parent="Connect/VBoxContainer/Host"]
custom_minimum_size = Vector2(120, 0)
layout_mode = 2
tooltip_text = "Open a new session, allowing
users to connect to your public IP."
text = "Host"
[node name="Disconnect" type="Button" parent="Connect/VBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "Disconnect from / close the current session."
disabled = true
text = "Disconnect
"
[node name="Settings" type="MarginContainer" parent="."]
visible = false
layout_mode = 2
theme_override_constants/margin_left = 4
theme_override_constants/margin_top = 4
theme_override_constants/margin_right = 4
theme_override_constants/margin_bottom = 4
metadata/_tab_index = 1
[node name="VBoxContainer" type="VBoxContainer" parent="Settings"]
layout_mode = 2
[node name="Cache" type="HBoxContainer" parent="Settings/VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="Settings/VBoxContainer/Cache"]
custom_minimum_size = Vector2(80, 0)
layout_mode = 2
text = "Cache:"
horizontal_alignment = 2
[node name="LineEdit" type="LineEdit" parent="Settings/VBoxContainer/Cache"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "The directory other players' models will be loaded from.
Filenames must match exactly for this to work."
[node name="Button" type="Button" parent="Settings/VBoxContainer/Cache"]
layout_mode = 2
tooltip_text = "Browse Directory"
text = " ... "
[node name="FileDialog" type="FileDialog" parent="Settings/VBoxContainer/Cache"]
title = "Open a Directory"
ok_button_text = "Select Current Folder"
file_mode = 2
access = 2
[node name="Name" type="HBoxContainer" parent="Settings/VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="Settings/VBoxContainer/Name"]
custom_minimum_size = Vector2(80, 0)
layout_mode = 2
text = "Name:"
horizontal_alignment = 2
[node name="LineEdit" type="LineEdit" parent="Settings/VBoxContainer/Name"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Players" type="MarginContainer" parent="."]
layout_mode = 2
theme_override_constants/margin_left = 4
theme_override_constants/margin_top = 4
theme_override_constants/margin_right = 4
theme_override_constants/margin_bottom = 4
metadata/_tab_index = 2
[node name="VBoxContainer" type="VBoxContainer" parent="Players"]
layout_mode = 2
[connection signal="toggled" from="Connect/VBoxContainer/Join/ShowHide" to="." method="_on_show_hide_address_toggled"]
[connection signal="pressed" from="Settings/VBoxContainer/Cache/Button" to="." method="_on_cache_dir_dialog_pressed"]

@ -0,0 +1,41 @@
class_name PlayerSettings
extends Container
signal value_changed(Transform3D)
@export var offset_x: SpinBox
@export var offset_y: SpinBox
@export var offset_z: SpinBox
@export var rotation_x: SpinBox
@export var rotation_y: SpinBox
@export var rotation_z: SpinBox
@export var scale_xyz: SpinBox
func set_nickname(value: String) -> void:
$Nickname.text = value
func on_transform_changed(value: Transform3D) -> void:
offset_x.value = value.origin.x
offset_y.value = value.origin.y
offset_z.value = value.origin.z
var rot := value.basis.get_euler()
rotation_x.value = rad_to_deg(rot.x)
rotation_y.value = rad_to_deg(rot.y)
rotation_z.value = rad_to_deg(rot.z)
scale_xyz.value = value.basis.get_scale().x
func _on_value_changed(_value: float) -> void:
var origin := Vector3(offset_x.value, offset_y.value, offset_z.value)
var rot := Vector3(rotation_x.value, rotation_y.value, rotation_z.value) / 360 * TAU
var basis := Basis.from_euler(rot) * Basis.from_scale(Vector3.ONE * scale_xyz.value)
value_changed.emit(Transform3D(basis, origin))
# Ensure that rotation inputs are always in 0-359 range.
rotation_x.set_value_no_signal(fposmod(rotation_x.value, 360))
rotation_y.set_value_no_signal(fposmod(rotation_y.value, 360))
rotation_z.set_value_no_signal(fposmod(rotation_z.value, 360))

@ -0,0 +1,179 @@
[gd_scene load_steps=2 format=3 uid="uid://fbfasiqs3d88"]
[ext_resource type="Script" path="res://Mods/copyMultiplayer/Scenes/player_settings.gd" id="1_ybttq"]
[node name="PlayerSettings" type="VBoxContainer" node_paths=PackedStringArray("offset_x", "offset_y", "offset_z", "rotation_x", "rotation_y", "rotation_z", "scale_xyz")]
script = ExtResource("1_ybttq")
offset_x = NodePath("PanelContainer/MarginContainer/VBoxContainer/Offset/X")
offset_y = NodePath("PanelContainer/MarginContainer/VBoxContainer/Offset/Y")
offset_z = NodePath("PanelContainer/MarginContainer/VBoxContainer/Offset/Z")
rotation_x = NodePath("PanelContainer/MarginContainer/VBoxContainer/Rotation/X")
rotation_y = NodePath("PanelContainer/MarginContainer/VBoxContainer/Rotation/Y")
rotation_z = NodePath("PanelContainer/MarginContainer/VBoxContainer/Rotation/Z")
scale_xyz = NodePath("PanelContainer/MarginContainer/VBoxContainer/Scale/SpinBox")
[node name="Nickname" type="Label" parent="."]
layout_mode = 2
text = "Nickname"
horizontal_alignment = 1
[node name="PanelContainer" type="PanelContainer" parent="."]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer"]
layout_mode = 2
theme_override_constants/margin_left = 4
theme_override_constants/margin_top = 4
theme_override_constants/margin_right = 4
theme_override_constants/margin_bottom = 4
[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer"]
layout_mode = 2
[node name="Offset" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/Offset"]
custom_minimum_size = Vector2(80, 0)
layout_mode = 2
text = "Offset:"
horizontal_alignment = 2
[node name="LabelX" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/Offset"]
custom_minimum_size = Vector2(20, 0)
layout_mode = 2
theme_override_colors/font_color = Color(0.917647, 0.211765, 0.317647, 1)
text = "X"
horizontal_alignment = 2
[node name="X" type="SpinBox" parent="PanelContainer/MarginContainer/VBoxContainer/Offset"]
layout_mode = 2
size_flags_horizontal = 3
min_value = -100.0
step = 0.01
alignment = 2
custom_arrow_step = 0.05
[node name="LabelY" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/Offset"]
custom_minimum_size = Vector2(20, 0)
layout_mode = 2
theme_override_colors/font_color = Color(0.529412, 0.839216, 0.0117647, 1)
text = "Y"
horizontal_alignment = 2
[node name="Y" type="SpinBox" parent="PanelContainer/MarginContainer/VBoxContainer/Offset"]
layout_mode = 2
size_flags_horizontal = 3
min_value = -100.0
step = 0.01
alignment = 2
custom_arrow_step = 0.05
[node name="LabelZ" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/Offset"]
custom_minimum_size = Vector2(20, 0)
layout_mode = 2
theme_override_colors/font_color = Color(0.180392, 0.529412, 0.917647, 1)
text = "Z"
horizontal_alignment = 2
[node name="Z" type="SpinBox" parent="PanelContainer/MarginContainer/VBoxContainer/Offset"]
layout_mode = 2
size_flags_horizontal = 3
min_value = -100.0
step = 0.01
alignment = 2
custom_arrow_step = 0.05
[node name="Rotation" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/Rotation"]
custom_minimum_size = Vector2(80, 0)
layout_mode = 2
text = "Rotation:"
horizontal_alignment = 2
[node name="LabelX" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/Rotation"]
custom_minimum_size = Vector2(20, 0)
layout_mode = 2
theme_override_colors/font_color = Color(0.917647, 0.211765, 0.317647, 1)
text = "X"
horizontal_alignment = 2
[node name="X" type="SpinBox" parent="PanelContainer/MarginContainer/VBoxContainer/Rotation"]
layout_mode = 2
size_flags_horizontal = 3
max_value = 359.0
step = 0.1
allow_greater = true
allow_lesser = true
alignment = 2
custom_arrow_step = 2.0
[node name="LabelY" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/Rotation"]
custom_minimum_size = Vector2(20, 0)
layout_mode = 2
theme_override_colors/font_color = Color(0.529412, 0.839216, 0.0117647, 1)
text = "Y"
horizontal_alignment = 2
[node name="Y" type="SpinBox" parent="PanelContainer/MarginContainer/VBoxContainer/Rotation"]
layout_mode = 2
size_flags_horizontal = 3
max_value = 359.0
step = 0.1
allow_greater = true
allow_lesser = true
alignment = 2
custom_arrow_step = 2.0
[node name="LabelZ" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/Rotation"]
custom_minimum_size = Vector2(20, 0)
layout_mode = 2
theme_override_colors/font_color = Color(0.180392, 0.529412, 0.917647, 1)
text = "Z"
horizontal_alignment = 2
[node name="Z" type="SpinBox" parent="PanelContainer/MarginContainer/VBoxContainer/Rotation"]
layout_mode = 2
size_flags_horizontal = 3
max_value = 359.0
step = 0.1
allow_greater = true
allow_lesser = true
alignment = 2
custom_arrow_step = 2.0
[node name="Scale" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/Scale"]
custom_minimum_size = Vector2(80, 0)
layout_mode = 2
text = "Scale:"
horizontal_alignment = 2
[node name="Padding" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/Scale"]
custom_minimum_size = Vector2(20, 0)
layout_mode = 2
theme_override_colors/font_color = Color(0.917647, 0.211765, 0.317647, 1)
text = "
"
horizontal_alignment = 2
[node name="SpinBox" type="SpinBox" parent="PanelContainer/MarginContainer/VBoxContainer/Scale"]
layout_mode = 2
size_flags_horizontal = 3
min_value = 0.5
max_value = 2.0
step = 0.01
alignment = 2
custom_arrow_step = 0.05
[connection signal="value_changed" from="PanelContainer/MarginContainer/VBoxContainer/Offset/X" to="." method="_on_value_changed"]
[connection signal="value_changed" from="PanelContainer/MarginContainer/VBoxContainer/Offset/Y" to="." method="_on_value_changed"]
[connection signal="value_changed" from="PanelContainer/MarginContainer/VBoxContainer/Offset/Z" to="." method="_on_value_changed"]
[connection signal="value_changed" from="PanelContainer/MarginContainer/VBoxContainer/Rotation/X" to="." method="_on_value_changed"]
[connection signal="value_changed" from="PanelContainer/MarginContainer/VBoxContainer/Rotation/Y" to="." method="_on_value_changed"]
[connection signal="value_changed" from="PanelContainer/MarginContainer/VBoxContainer/Rotation/Z" to="." method="_on_value_changed"]
[connection signal="value_changed" from="PanelContainer/MarginContainer/VBoxContainer/Scale/SpinBox" to="." method="_on_value_changed"]

@ -1,9 +1,11 @@
class_name SyncController class_name SyncController
extends Node extends Area3D
@export var shape: CollisionShape3D
var peer_id: int
var module: copyMultiplayer var module: copyMultiplayer
var model_controller: ModelController var model_controller: ModelController
var peer_id: int
var nickname: String var nickname: String
@ -16,14 +18,27 @@ var model : Node
var skeleton : Skeleton3D var skeleton : Skeleton3D
var anim_player : AnimationPlayer var anim_player : AnimationPlayer
var anim_root : Node var anim_root : Node
var settings : PlayerSettings
var is_dragging := false
var drag_current: Vector3
# Reusable buffer to write data for synchronizing models. # Reusable buffer to write data for synchronizing models.
static var write_stream: StreamBuffer = StreamBuffer.with_capacity(2048) static var write_stream: StreamBuffer = StreamBuffer.with_capacity(2048)
func _ready() -> void: func _ready() -> void:
module = get_parent().get_parent() peer_id = name.to_int()
model_controller = get_parent() module = get_parent()
peer_id = model_controller.name.to_int() model_controller = ModelController.new()
model_controller.name = "ModelController"
add_child(model_controller)
func _process(_delta: float) -> void:
update_collision_shape_position()
handle_dragging()
func _exit_tree() -> void:
if settings: settings.queue_free()
func get_display_name() -> String: func get_display_name() -> String:
if nickname: return "Player '%s' (%d)" % [ nickname, peer_id ] if nickname: return "Player '%s' (%d)" % [ nickname, peer_id ]
@ -32,9 +47,16 @@ func get_display_name() -> String:
func change_nickname(new_nickname: String) -> void: func change_nickname(new_nickname: String) -> void:
new_nickname = new_nickname.strip_edges() new_nickname = new_nickname.strip_edges()
if new_nickname == "": return # Ignore empty nicknames. if new_nickname == "": return # Ignore empty nicknames.
if new_nickname == nickname: return # Ignore unchanged nicknames.
module.print_log("%s is now known as '%s'" % [ get_display_name(), new_nickname ]) module.print_log("%s is now known as '%s'" % [ get_display_name(), new_nickname ])
nickname = new_nickname nickname = new_nickname
if !settings:
settings = module.new_player_settings()
settings.on_transform_changed(transform)
settings.value_changed.connect(func(value): transform = value)
settings.set_nickname(nickname)
## Attempts to change the model of this player. ## Attempts to change the model of this player.
func change_model( func change_model(
new_version: int, new_version: int,
@ -65,6 +87,8 @@ func change_model(
anim_player = model.find_child("AnimationPlayer", false, false) anim_player = model.find_child("AnimationPlayer", false, false)
anim_root = anim_player.get_node(anim_player.root_node) anim_root = anim_player.get_node(anim_player.root_node)
recalculate_collision_shape()
module.print_log("%s switched to '%s'" % [ get_display_name(), filename ]) module.print_log("%s switched to '%s'" % [ get_display_name(), filename ])
func sync_model_animation( func sync_model_animation(
@ -158,3 +182,77 @@ static func send_model_animation(module: copyMultiplayer) -> void:
# module.set_status("Packet size: %d bytes (%d uncompressed)" % [ compressed_buffer.size(), write_stream.size ]) # module.set_status("Packet size: %d bytes (%d uncompressed)" % [ compressed_buffer.size(), write_stream.size ])
module.sync_model_animation.rpc(module.version, write_stream.size, compressed_buffer) module.sync_model_animation.rpc(module.version, write_stream.size, compressed_buffer)
write_stream.clear() write_stream.clear()
# Updates the collision shapes used for allowing the player model to be
# repositioned based on the current model's skeleton. This functionality
# is only enabled when the "Players" tab is selected.
func recalculate_collision_shape() -> void:
var head_y := _get_bone_position("Head").y
var left_foot_y := _get_bone_position("LeftFoot").y
var right_foot_y := _get_bone_position("RightFoot").y
var avg_foot_y := (left_foot_y + right_foot_y) / 2
var shoulder_x := _get_bone_position("LeftUpperArm").x
var capsule := CapsuleShape3D.new()
capsule.height = (head_y - avg_foot_y) * 1.5
capsule.radius = shoulder_x * 1.75
shape.shape = capsule
func update_collision_shape_position() -> void:
if not skeleton: return
var head := _get_bone_position("Head")
var left_foot := _get_bone_position("LeftFoot")
var right_foot := _get_bone_position("RightFoot")
var avg_foot := (left_foot + right_foot) / 2
shape.global_position = skeleton.global_position + (head + avg_foot) / 2
func _get_bone_position(bone_name: String) -> Vector3:
var idx := skeleton.find_bone(bone_name)
var pose := skeleton.get_bone_global_pose(idx)
return pose.origin
func _input_event(_camera: Node, event: InputEvent,
pos: Vector3, _normal: Vector3, _idx: int) -> void:
if (module.can_move_players
&& event is InputEventMouseButton
&& event.button_index == MOUSE_BUTTON_LEFT
&& event.pressed):
is_dragging = true
drag_current = pos
func _unhandled_input(event: InputEvent) -> void:
if (is_dragging
&& event is InputEventMouseButton
&& event.button_index == MOUSE_BUTTON_LEFT
&& not event.pressed):
is_dragging = false
get_viewport().set_input_as_handled()
func handle_dragging() -> void:
if not is_dragging: return
var camera := get_viewport().get_camera_3d()
var mouse := get_viewport().get_mouse_position()
var origin := camera.project_ray_origin(mouse)
var dir := camera.project_ray_normal(mouse)
if dir.z == 0: return
var distance := (drag_current.z - origin.z) / dir.z
var target := origin + dir * distance
position += target - drag_current
drag_current = target
sortof_face_the_camera()
settings.on_transform_changed(transform)
# FIXME: Kind of a hack, find a better way.
## Rotates the model a little bit towards the camera
## so it doesn't appear to be staring into nowhere.
func sortof_face_the_camera() -> void:
var camera := get_viewport().get_camera_3d()
var from_2d := Vector2(global_position.x, global_position.z)
var to_2d := Vector2(camera.global_position.x, camera.global_position.z)
rotation.y = -from_2d.angle_to_point(to_2d) + TAU/4

@ -0,0 +1,9 @@
[gd_scene load_steps=2 format=3 uid="uid://b8t8fgu7ht8rn"]
[ext_resource type="Script" path="res://Mods/copyMultiplayer/Scenes/sync_controller.gd" id="1_v4dbb"]
[node name="SyncController" type="Area3D" node_paths=PackedStringArray("shape")]
script = ExtResource("1_v4dbb")
shape = NodePath("CollisionShape3D")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]

@ -15,12 +15,10 @@ var version: int = -1
var bone_lookup: Array[String] = [] var bone_lookup: Array[String] = []
var blendshape_lookup: Array[NodePath] = [] var blendshape_lookup: Array[NodePath] = []
# Temporary positioning system. # TODO: Remember the position of each remote player by name.
# TODO: Add a setting that allows syncing model positions, as an alternative ## Whether the position of remote players can be ajusted
# to letting the local player choose where each model is going to appear. ## by clicking their controller's collider and dragging.
var player_order: Array[int] = [] var can_move_players := false
var starting_offset := Vector3(-1.25, 0.0, 0.0)
var accumulative_offset := Vector3( 0.35, 0.0, 0.0)
# FIXME: There is an edge case where you can load the settings while connected, # FIXME: There is an edge case where you can load the settings while connected,
# resulting in out-of-sync information with connected players, but so far # resulting in out-of-sync information with connected players, but so far
@ -49,14 +47,16 @@ func _ready() -> void:
for phalange in [ "Proximal", "Intermediate", "Distal" ]: for phalange in [ "Proximal", "Intermediate", "Distal" ]:
tracked_bones.append("%s%s%s" % [ side, finger, phalange ]) tracked_bones.append("%s%s%s" % [ side, finger, phalange ])
setup_setting_widget("cache" , "Cache/LineEdit", true ) setup_setting_widget("address" , "Connect/VBoxContainer/Join/Address" , true )
setup_setting_widget("nickname", "Name/LineEdit" , false) setup_setting_widget("port" , "Connect/VBoxContainer/Host/Port" , true )
setup_setting_widget("address" , "Host/Address" , true )
setup_setting_widget("port" , "Host/Port" , true ) setup_setting_widget("cache" , "Settings/VBoxContainer/Cache/LineEdit", true )
setup_setting_widget("nickname", "Settings/VBoxContainer/Name/LineEdit" , false)
setup_button_connections() setup_button_connections()
# Filter whitespace characters from nickname before saving it to "nickname" field. # Filter whitespace characters from nickname before saving it to "nickname" field.
var nickname_widget: LineEdit = get_settings_window().get_node("Name/LineEdit") var nickname_widget: LineEdit = get_settings_window().get_node("Settings/VBoxContainer/Name/LineEdit")
nickname_widget.text_changed.connect(func(new_text): modify_setting("nickname", new_text.strip_edges())) nickname_widget.text_changed.connect(func(new_text): modify_setting("nickname", new_text.strip_edges()))
nickname_widget.text_submitted.connect(func(_new_text): nickname_widget.text = nickname) nickname_widget.text_submitted.connect(func(_new_text): nickname_widget.text = nickname)
nickname_widget.focus_exited.connect(func(): nickname_widget.text = nickname) nickname_widget.focus_exited.connect(func(): nickname_widget.text = nickname)
@ -71,7 +71,7 @@ func _exit_tree() -> void:
multiplayer.multiplayer_peer.close() multiplayer.multiplayer_peer.close()
func _create_settings_window() -> Control: func _create_settings_window() -> Control:
return load("res://Mods/copyMultiplayer/Resources/copy_multiplayer_settings.tscn").instantiate() return load("res://Mods/copyMultiplayer/Scenes/copy_multiplayer_settings.tscn").instantiate()
func setup_setting_widget(setting_name: String, path: NodePath, setup_events: bool) -> void: func setup_setting_widget(setting_name: String, path: NodePath, setup_events: bool) -> void:
var settings = get_settings_window() var settings = get_settings_window()
@ -88,13 +88,20 @@ func setup_setting_widget(setting_name: String, path: NodePath, setup_events: bo
func setup_button_connections() -> void: func setup_button_connections() -> void:
var window = get_settings_window() var window = get_settings_window()
window.get_node("Buttons/Join").pressed.connect(on_join_pressed) window.get_node("Connect/VBoxContainer/Join/Button").pressed.connect(on_join_pressed)
window.get_node("Buttons/Host").pressed.connect(on_host_pressed) window.get_node("Connect/VBoxContainer/Host/Button").pressed.connect(on_host_pressed)
window.get_node("Buttons/Disconnect").pressed.connect(on_disconnect_pressed) window.get_node("Connect/VBoxContainer/Disconnect" ).pressed.connect(on_disconnect_pressed)
var player_settings_scene: PackedScene = load("res://Mods/copyMultiplayer/Scenes/player_settings.tscn")
func new_player_settings() -> PlayerSettings:
var container = get_settings_window().get_node("Players/VBoxContainer")
var result := player_settings_scene.instantiate()
container.add_child(result)
return result
func on_join_pressed() -> void: func on_join_pressed() -> void:
var address_widget: LineEdit = get_settings_window().get_node("Host/Address") var address_widget: LineEdit = get_settings_window().get_node("Connect/VBoxContainer/Join/Address")
var default_address: String = address_widget.placeholder_text var default_address: String = address_widget.placeholder_text
var actual_address := default_address if address.is_empty() else address var actual_address := default_address if address.is_empty() else address
@ -127,16 +134,11 @@ func on_disconnect_pressed() -> void:
multiplayer.multiplayer_peer.close() multiplayer.multiplayer_peer.close()
var sync_controller_scene: PackedScene = load("res://Mods/copyMultiplayer/Scenes/sync_controller.tscn");
func on_peer_connected(id: int) -> void: func on_peer_connected(id: int) -> void:
var model_controller := ModelController.new() var controller := sync_controller_scene.instantiate()
model_controller.name = str(id) controller.name = str(id)
var sync_controller := SyncController.new() add_child(controller)
sync_controller.name = "SyncController"
model_controller.add_child(sync_controller)
add_child(model_controller)
player_order.append(id)
update_model_transforms()
# Send information to the newly connected player about ourselves. # Send information to the newly connected player about ourselves.
# (Technically this doesn't need to be relayed through the server, but oh well.) # (Technically this doesn't need to be relayed through the server, but oh well.)
@ -145,17 +147,14 @@ func on_peer_connected(id: int) -> void:
if filename.is_valid_filename(): change_model.rpc_id(id, version, bone_lookup, blendshape_lookup, filename) if filename.is_valid_filename(): change_model.rpc_id(id, version, bone_lookup, blendshape_lookup, filename)
update_status() update_status()
print_log("%s connected" % sync_controller.get_display_name()) print_log("%s connected" % controller.get_display_name())
func on_peer_disconnected(id: int) -> void: func on_peer_disconnected(id: int) -> void:
var controller = get_sync_controller(id) var controller = get_sync_controller(id)
if not controller: return if not controller: return
remove_child(controller.model_controller) remove_child(controller)
controller.model_controller.queue_free() controller.queue_free()
player_order.remove_at(player_order.find(id))
update_model_transforms()
update_status() update_status()
print_log("Player %s disconnected" % id) print_log("Player %s disconnected" % id)
@ -177,12 +176,12 @@ func on_server_disconnected() -> void:
func update_enabled_state(is_online: bool) -> void: func update_enabled_state(is_online: bool) -> void:
var window = get_settings_window() var window = get_settings_window()
window.get_node("Name/LineEdit").editable = !is_online window.get_node("Settings/VBoxContainer/Name/LineEdit").editable = !is_online
window.get_node("Host/Address" ).editable = !is_online window.get_node("Connect/VBoxContainer/Join/Address" ).editable = !is_online
window.get_node("Host/Port" ).editable = !is_online window.get_node("Connect/VBoxContainer/Join/Button" ).disabled = is_online
window.get_node("Buttons/Join" ).disabled = is_online window.get_node("Connect/VBoxContainer/Host/Port" ).editable = !is_online
window.get_node("Buttons/Host" ).disabled = is_online window.get_node("Connect/VBoxContainer/Host/Button" ).disabled = is_online
window.get_node("Buttons/Disconnect").disabled = !is_online window.get_node("Connect/VBoxContainer/Disconnect" ).disabled = !is_online
func update_status() -> void: func update_status() -> void:
var num_players := 1 + multiplayer.get_peers().size() var num_players := 1 + multiplayer.get_peers().size()
@ -191,34 +190,19 @@ func update_status() -> void:
set_status("%s: %d player%s" % [side, num_players, s]) set_status("%s: %d player%s" % [side, num_players, 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:
var controller: ModelController = get_node(str(id))
controller.position = offset
offset += accumulative_offset
update_model_rotation(controller)
## Rotates the model a little bit towards the camera
## so it doesn't appear to be staring into nowhere.
func update_model_rotation(controller: ModelController) -> void:
var camera := get_viewport().get_camera_3d()
var from_2d := Vector2(controller.position.x, controller.position.z)
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.
## Removes all networked player models. ## Removes all networked player models.
func clear_player_models() -> void: func clear_player_models() -> void:
for controller in get_children(): for controller in get_children():
if controller is ModelController: if controller is SyncController:
remove_child(controller) remove_child(controller)
controller.queue_free() controller.queue_free()
player_order.clear()
func _process(_delta: float) -> void: func _process(_delta: float) -> void:
SyncController.send_model_animation(self) SyncController.send_model_animation(self)
var settings: copyMultiplayerSettings = get_settings_window()
# TODO: Add a setting to allow moving players even if "Players" tab is not active.
can_move_players = settings.is_visible_in_tree() && settings.is_tab_selected("Players")
# Called when module is initialized or load_vrm is called. # Called when module is initialized or load_vrm is called.
func scene_init() -> void: func scene_init() -> void:
@ -254,9 +238,7 @@ func scene_init() -> void:
## Gets the SyncController for the player with the specified peer id. ## Gets the SyncController for the player with the specified peer id.
func get_sync_controller(peer_id: int) -> SyncController: func get_sync_controller(peer_id: int) -> SyncController:
var model_controller := get_node_or_null(str(peer_id)) as ModelController return get_node_or_null(str(peer_id)) as SyncController
if not model_controller: return null
return model_controller.get_node_or_null("SyncController") as SyncController
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func change_nickname(new_nickname: String) -> void: func change_nickname(new_nickname: String) -> void:

Loading…
Cancel
Save