diff --git a/Game.cs b/Game.cs new file mode 100644 index 0000000..778cb8d --- /dev/null +++ b/Game.cs @@ -0,0 +1,9 @@ +public partial class Game : Node +{ + [Export] public Player LocalPlayer { get; set; } + + public MultiplayerManager MultiplayerManager { get; private set; } + + public override void _EnterTree() + => MultiplayerManager = GetNode("MultiplayerManager"); +} diff --git a/scenes/game.tscn b/game.tscn similarity index 60% rename from scenes/game.tscn rename to game.tscn index e13c94a..94badc6 100644 --- a/scenes/game.tscn +++ b/game.tscn @@ -1,20 +1,34 @@ -[gd_scene load_steps=8 format=3 uid="uid://puuk72ficqhu"] +[gd_scene load_steps=11 format=3 uid="uid://puuk72ficqhu"] -[ext_resource type="PackedScene" uid="uid://bwfuet1irfi17" path="res://scenes/workshop.tscn" id="1_m8m3w"] -[ext_resource type="Shader" path="res://assets/shaders/outline.gdshader" id="2_yvnqw"] -[ext_resource type="Script" path="res://scripts/OutlineCamera.cs" id="3_wd8hf"] -[ext_resource type="Texture2D" uid="uid://lxxfestfg2dt" path="res://assets/crosshair.png" id="4_06ang"] -[ext_resource type="PackedScene" uid="uid://c5ooi36ibspfo" path="res://ui/menu.tscn" id="4_77nbu"] -[ext_resource type="Script" path="res://scripts/Crosshair.cs" id="5_i8gkf"] +[ext_resource type="Script" path="res://scripts/MultiplayerManager.cs" id="1_7shyh"] +[ext_resource type="Script" path="res://Game.cs" id="1_uywdd"] +[ext_resource type="PackedScene" uid="uid://dmd7w2r8s0x6y" path="res://player/player.tscn" id="2_iv2f7"] +[ext_resource type="PackedScene" uid="uid://bwfuet1irfi17" path="res://scenes/workshop.tscn" id="3_4u5ql"] +[ext_resource type="Shader" path="res://assets/shaders/outline.gdshader" id="4_gacvj"] +[ext_resource type="Script" path="res://scripts/OutlineCamera.cs" id="5_qpc14"] +[ext_resource type="PackedScene" uid="uid://c5ooi36ibspfo" path="res://ui/menu.tscn" id="6_ol0j5"] +[ext_resource type="Texture2D" uid="uid://lxxfestfg2dt" path="res://assets/crosshair.png" id="7_0l5tv"] +[ext_resource type="Script" path="res://scripts/Crosshair.cs" id="8_mfhgr"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_ke1l3"] -shader = ExtResource("2_yvnqw") +shader = ExtResource("4_gacvj") shader_parameter/line_color = Color(1, 1, 1, 0.75) shader_parameter/line_thickness = 2.0 [node name="Game" type="Node"] +script = ExtResource("1_uywdd") -[node name="Workshop" parent="." instance=ExtResource("1_m8m3w")] +[node name="MultiplayerManager" type="Node" parent="." node_paths=PackedStringArray("LocalPlayer", "Players")] +script = ExtResource("1_7shyh") +LocalPlayer = NodePath("../Players/Local") +Players = NodePath("../Players") +PlayerScene = ExtResource("2_iv2f7") + +[node name="Workshop" parent="." instance=ExtResource("3_4u5ql")] + +[node name="Players" type="Node3D" parent="."] + +[node name="Local" parent="Players" instance=ExtResource("2_iv2f7")] [node name="OutlineViewportContainer" type="SubViewportContainer" parent="."] material = SubResource("ShaderMaterial_ke1l3") @@ -34,7 +48,7 @@ render_target_update_mode = 4 [node name="OutlineCamera" type="Camera3D" parent="OutlineViewportContainer/OutlineViewport"] cull_mask = 2 current = true -script = ExtResource("3_wd8hf") +script = ExtResource("5_qpc14") [node name="HUD" type="Control" parent="."] layout_mode = 3 @@ -45,7 +59,7 @@ grow_horizontal = 2 grow_vertical = 2 mouse_filter = 2 -[node name="Menu" parent="HUD" instance=ExtResource("4_77nbu")] +[node name="Menu" parent="HUD" instance=ExtResource("6_ol0j5")] visible = false layout_mode = 1 anchors_preset = 15 @@ -69,5 +83,5 @@ offset_right = 20.0 offset_bottom = 20.0 grow_horizontal = 2 grow_vertical = 2 -texture = ExtResource("4_06ang") -script = ExtResource("5_i8gkf") +texture = ExtResource("7_0l5tv") +script = ExtResource("8_mfhgr") diff --git a/player/AnimationController.cs b/player/AnimationController.cs index 9cc1d05..a03e09f 100644 --- a/player/AnimationController.cs +++ b/player/AnimationController.cs @@ -22,11 +22,8 @@ public partial class AnimationController : Node3D _walkBackwardAnim = _animTree.GetAnimation("walk_backward"); _rootBone = GetNode("Root"); - foreach (var child in FindChildren("*", "BoneAttachment3D")) { - var bone = (BoneAttachment3D)child; - bone.OverridePose = true; - _bones[bone.Name] = bone; - } + foreach (var bone in FindChildren("*").OfType()) + { bone.OverridePose = true; _bones[bone.Name] = bone; } // We disable the AnimationTree while in the editor so our // BoneAttackment3D nodes don't get updated, resulting in diff --git a/player/CameraController.cs b/player/CameraController.cs index 08709e4..b6de835 100644 --- a/player/CameraController.cs +++ b/player/CameraController.cs @@ -7,8 +7,8 @@ public partial class CameraController : Node public Transform3D DefaultTransform { get; private set; } - public float CurrentPitch { get; set; } - public float CurrentYaw { get; set; } + [Export] public float CurrentPitch { get; set; } + [Export] public float CurrentYaw { get; set; } public static bool IsMouseCaptured => Input.MouseMode == Input.MouseModeEnum.Captured; diff --git a/player/MovementController.cs b/player/MovementController.cs index 8bbc4d0..d3ebd1f 100644 --- a/player/MovementController.cs +++ b/player/MovementController.cs @@ -16,7 +16,7 @@ public partial class MovementController : Node public bool IsSprinting { get; private set; } /// The raw input movement vector with a maximum length of 1. - public Vector2 InputVector { get; private set; } + [Export] public Vector2 InputVector { get; private set; } public bool IsMoving => LocalMoveVector.Length() > 0.01f; /// The actual amount the player is moving, relative to the world. Y is always 0. diff --git a/player/Player.cs b/player/Player.cs index 5cc7abb..dfb9b4e 100644 --- a/player/Player.cs +++ b/player/Player.cs @@ -1,6 +1,7 @@ public partial class Player : CharacterBody3D { - [Export] public bool IsLocal { get; set; } = true; + public bool IsLocal { get; set; } = true; + public int PeerId { get; set; } public MovementController Movement { get; private set; } public CameraController Camera { get; private set; } diff --git a/player/Synchronizer.cs b/player/Synchronizer.cs new file mode 100644 index 0000000..8b16e51 --- /dev/null +++ b/player/Synchronizer.cs @@ -0,0 +1,12 @@ +public partial class Synchronizer : MultiplayerSynchronizer +{ + // Required because `Velocity` can't be synchronized automatically. + [Export] public Vector3 PlayerVelocity { + get => _player.Velocity; + set => _player.Velocity = value; + } + + Player _player; + public override void _Ready() + => _player = GetParent(); +} diff --git a/player/player.tscn b/player/player.tscn index 8e02694..8e684db 100644 --- a/player/player.tscn +++ b/player/player.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=24 format=3 uid="uid://dmd7w2r8s0x6y"] +[gd_scene load_steps=26 format=3 uid="uid://dmd7w2r8s0x6y"] [ext_resource type="PackedScene" uid="uid://bfh3eqgywr0ul" path="res://assets/models/character.blend" id="1_3qh37"] [ext_resource type="Script" path="res://player/Player.cs" id="1_a0mas"] @@ -7,15 +7,36 @@ [ext_resource type="Script" path="res://player/PickupController.cs" id="2_ns2pe"] [ext_resource type="Script" path="res://player/CameraController.cs" id="2_r3gna"] [ext_resource type="Script" path="res://player/AnimationController.cs" id="3_5rlwc"] +[ext_resource type="Script" path="res://player/Synchronizer.cs" id="4_h8li4"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_h1mfd"] radius = 0.24 height = 1.5 [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_l8s0f"] -radius = 0.28 +radius = 0.24 height = 1.5 +[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_dpppx"] +properties/0/path = NodePath(".:position") +properties/0/spawn = true +properties/0/replication_mode = 1 +properties/1/path = NodePath(".:rotation") +properties/1/spawn = true +properties/1/replication_mode = 1 +properties/2/path = NodePath("MultiplayerSynchronizer:PlayerVelocity") +properties/2/spawn = true +properties/2/replication_mode = 1 +properties/3/path = NodePath("MovementController:InputVector") +properties/3/spawn = true +properties/3/replication_mode = 1 +properties/4/path = NodePath("CameraController:CurrentPitch") +properties/4/spawn = true +properties/4/replication_mode = 1 +properties/5/path = NodePath("CameraController:CurrentYaw") +properties/5/spawn = true +properties/5/replication_mode = 1 + [sub_resource type="Animation" id="Animation_5wsog"] resource_name = "idle_loop" length = 2.5 @@ -784,7 +805,6 @@ input_1/auto_advance = false input_1/reset = true [sub_resource type="AnimationNodeBlendTree" id="AnimationNodeBlendTree_cstk8"] -graph_offset = Vector2(30, 1.30002) nodes/Animation/node = SubResource("AnimationNodeAnimation_ra5r1") nodes/Animation/position = Vector2(600, 60) nodes/not_moving/node = SubResource("AnimationNodeAnimation_cun17") @@ -822,6 +842,10 @@ shape = SubResource("CapsuleShape3D_l8s0f") [node name="Model" parent="." instance=ExtResource("1_3qh37")] transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, -0.75, 0) +[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."] +replication_config = SubResource("SceneReplicationConfig_dpppx") +script = ExtResource("4_h8li4") + [node name="MovementController" type="Node" parent="."] script = ExtResource("2_1pst4") @@ -864,14 +888,14 @@ use_external_skeleton = true external_skeleton = NodePath("../../../Model/Skeleton/Skeleton3D") [node name="UpperBody" type="BoneAttachment3D" parent="AnimationController/Root/LowerBody"] -transform = Transform3D(1, -1.08108e-08, 8.67517e-08, 0, 0.992324, 0.123662, -8.74228e-08, -0.123662, 0.992324, 8.88178e-16, 0.154362, 0) +transform = Transform3D(1, -1.08108e-08, 8.67517e-08, 0, 0.992324, 0.123662, -8.74228e-08, -0.123662, 0.992324, 1.77636e-15, 0.154362, 0) bone_name = "UpperBody" bone_idx = 4 use_external_skeleton = true external_skeleton = NodePath("../../../../Model/Skeleton/Skeleton3D") [node name="Neck" type="BoneAttachment3D" parent="AnimationController/Root/LowerBody/UpperBody"] -transform = Transform3D(1, -4.4409e-16, -7.10543e-15, 0, 0.998891, -0.0470728, 0, 0.0470728, 0.998892, -4.44089e-16, 0.251888, 1.49012e-08) +transform = Transform3D(1, -4.4409e-16, -7.10543e-15, 0, 0.998891, -0.0470728, 0, 0.0470728, 0.998892, -1.11022e-15, 0.251888, 1.49012e-08) bone_name = "Neck" bone_idx = 5 use_external_skeleton = true diff --git a/project.godot b/project.godot index 99622ea..5b8c3c7 100644 --- a/project.godot +++ b/project.godot @@ -11,7 +11,7 @@ config_version=5 [application] config/name="Inventory2" -run/main_scene="res://scenes/game.tscn" +run/main_scene="res://game.tscn" config/features=PackedStringArray("4.2", "C#", "GL Compatibility") config/icon="res://assets/icon.svg" diff --git a/scenes/workshop.tscn b/scenes/workshop.tscn index b886d51..f731224 100644 --- a/scenes/workshop.tscn +++ b/scenes/workshop.tscn @@ -1,6 +1,5 @@ -[gd_scene load_steps=14 format=3 uid="uid://bwfuet1irfi17"] +[gd_scene load_steps=13 format=3 uid="uid://bwfuet1irfi17"] -[ext_resource type="PackedScene" uid="uid://dmd7w2r8s0x6y" path="res://player/player.tscn" id="1_cxvln"] [ext_resource type="Script" path="res://objects/Grid.cs" id="2_gstd0"] [ext_resource type="PackedScene" uid="uid://yvy5vvaqgxy8" path="res://objects/crate.tscn" id="2_j6a20"] [ext_resource type="Script" path="res://objects/Item.cs" id="3_01pgc"] @@ -34,12 +33,6 @@ size = Vector2(10, 10) [node name="Sun" type="DirectionalLight3D" parent="."] transform = Transform3D(0.866025, 0, -0.5, 0.25, 0.866025, 0.433013, 0.433013, -0.5, 0.75, 0, 5, 0) -[node name="Player" parent="." instance=ExtResource("1_cxvln")] - -[node name="OtherPlayer" parent="." instance=ExtResource("1_cxvln")] -transform = Transform3D(-1, 0, 8.74228e-08, 0, 1, 0, -8.74228e-08, 0, -1, 2, 0.75, -4) -IsLocal = false - [node name="Crates" type="Node3D" parent="."] [node name="Crate1" parent="Crates" instance=ExtResource("2_j6a20")] diff --git a/scripts/MultiplayerManager.cs b/scripts/MultiplayerManager.cs new file mode 100644 index 0000000..eddfe18 --- /dev/null +++ b/scripts/MultiplayerManager.cs @@ -0,0 +1,81 @@ +public partial class MultiplayerManager : Node +{ + [Export] public Player LocalPlayer { get; set; } + [Export] public Node3D Players { get; set; } + [Export] public PackedScene PlayerScene { get; set; } + + public event Action PlayerJoined; + public event Action PlayerLeft; + + public override void _Ready() + { + Multiplayer.ConnectedToServer += OnMultiplayerReady; + Multiplayer.ServerDisconnected += OnMultiplayerDisconnected; + Multiplayer.PeerConnected += OnPeerConnected; + Multiplayer.PeerDisconnected += OnPeerDisconnected; + } + + + public void Connect(string address, ushort port) + { + var peer = new ENetMultiplayerPeer(); + peer.CreateClient(address, port); + Multiplayer.MultiplayerPeer = peer; + } + + public bool CreateServer(ushort port) + { + var peer = new ENetMultiplayerPeer(); + if (peer.CreateServer(port) == Error.Ok) { + Multiplayer.MultiplayerPeer = peer; + OnMultiplayerReady(); + return true; + } else + return false; + } + + public void Disconnect() + => OnMultiplayerDisconnected(); + + + void OnMultiplayerReady() + { + var localId = Multiplayer.GetUniqueId(); + LocalPlayer.Name = localId.ToString(); + LocalPlayer.SetMultiplayerAuthority(localId); + + if (!Multiplayer.IsServer()) + // Spawn players for all the other peers. This excludes the server, + // since `OnPeerConnected` will already be called for it on connecting. + foreach (var peerId in Multiplayer.GetPeers()) + if (peerId != 1) OnPeerConnected(peerId); + } + + void OnMultiplayerDisconnected() + { + LocalPlayer.Name = "Local"; + foreach (var player in Players.GetChildren().Cast()) + OnPeerDisconnected(player.PeerId); + Multiplayer.MultiplayerPeer.Close(); + Multiplayer.MultiplayerPeer = null; + } + + + void OnPeerConnected(long peerId) + { + var player = PlayerScene.Instantiate(); + player.SetMultiplayerAuthority((int)peerId); + player.Name = peerId.ToString(); + player.IsLocal = false; + player.PeerId = (int)peerId; + Players.AddChild(player); + PlayerJoined?.Invoke(player); + } + + void OnPeerDisconnected(long peerId) + { + var player = Players.GetNode(peerId.ToString()); + PlayerLeft?.Invoke(player); + player.QueueFree(); + } +} diff --git a/scripts/globals/GlobalUsings.cs b/scripts/globals/GlobalUsings.cs index 08ba83f..e276fe3 100644 --- a/scripts/globals/GlobalUsings.cs +++ b/scripts/globals/GlobalUsings.cs @@ -1,4 +1,5 @@ global using System; +global using System.Linq; global using Godot; global using Godot.Collections; global using static Godot.GD; diff --git a/ui/MultiplayerMenu.cs b/ui/MultiplayerMenu.cs index adf8c86..4fff271 100644 --- a/ui/MultiplayerMenu.cs +++ b/ui/MultiplayerMenu.cs @@ -30,16 +30,18 @@ public partial class MultiplayerMenu : MarginContainer [Export] public Label PlayersLabel { get; set; } [Export] public Button DisconnectButton { get; set; } + Game _game; public override void _Ready() { + _game = GetNode("/root/Game"); PortDisplay.AddThemeColorOverride("font_uneditable_color", PortDisplay.GetThemeColor("font_color")); Multiplayer.ConnectedToServer += () => UpdateStatus(Status.Connected); Multiplayer.ConnectionFailed += () => UpdateStatus(Status.ConnectionFailed); Multiplayer.ServerDisconnected += () => UpdateStatus(Status.Disconnected); - Multiplayer.PeerConnected += (_) => UpdatePlayerCount(); - Multiplayer.PeerDisconnected += (_) => UpdatePlayerCount(); + _game.MultiplayerManager.PlayerJoined += (_) => UpdatePlayerCount(); + _game.MultiplayerManager.PlayerLeft += (_) => UpdatePlayerCount(); } void UpdateStatus(Status status) @@ -79,7 +81,7 @@ public partial class MultiplayerMenu : MarginContainer < Status.Connecting => "Singleplayer", Status.Connecting => "??? Players", > Status.Connecting => ((Func)(() => { - var players = Multiplayer.GetPeers().Length + 1; + var players = _game.MultiplayerManager.Players.GetChildCount(); return $"{players} {(players != 1 ? "Players" : "Player")}"; }))(), }; @@ -97,32 +99,24 @@ public partial class MultiplayerMenu : MarginContainer { var address = AddressInput.Text; if (address == "") address = AddressInput.PlaceholderText; - var port = RoundToInt(PortInput.Value); - - var peer = new ENetMultiplayerPeer(); - peer.CreateClient(address, port); - Multiplayer.MultiplayerPeer = peer; + var port = (ushort)RoundToInt(PortInput.Value); + _game.MultiplayerManager.Connect(address, port); UpdateStatus(Status.Connecting); } public void OnHostPressed() { - var port = RoundToInt(PortInput.Value); - PortDisplay.Text = port.ToString(); - - var peer = new ENetMultiplayerPeer(); - if (peer.CreateServer(port) == Error.Ok) { - Multiplayer.MultiplayerPeer = peer; + var port = (ushort)RoundToInt(PortInput.Value); + if (_game.MultiplayerManager.CreateServer(port)) { + PortDisplay.Text = port.ToString(); UpdateStatus(Status.Hosting); - } else { + } else UpdateStatus(Status.HostingFailed); - } } public void OnDisconnectPressed() { - Multiplayer.MultiplayerPeer.Close(); - Multiplayer.MultiplayerPeer = null; + _game.MultiplayerManager.Disconnect(); UpdateStatus(Status.Disconnected); } }