From d8cc1d08593eefefa243f42d0890ef587b259317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20St=C4=99pie=C5=84?= Date: Wed, 26 Aug 2026 22:59:29 +0200 Subject: [PATCH] feat: replicate players across multiplayer peers --- NightShift.csproj | 2 + .../Contracts/IPlayerInteractionController.cs | 16 ++++ .../IPlayerInteractionController.cs.uid | 1 + .../Godot/PlayerInteractionController.cs | 73 +++++++++++++------ .../Multiplayer/Contracts/IMultiplayerPeer.cs | 1 + .../Godot/MultiplayerController.cs | 6 +- .../Players/Contracts/IPlayerController.cs | 4 + .../Contracts/IPlayerController.cs.uid | 1 + .../Players/Godot/LocalPlayerController.cs | 24 ++++++ .../Players/Godot/PlayerController.cs | 66 ++++++++++++++--- src/Features/Players/Godot/PlayerNode.tscn | 20 ++++- .../Players/Godot/PlayerSpawnerController.cs | 58 +++++++++++++++ .../Godot/PlayerSpawnerController.cs.uid | 1 + .../Attributes/RequireInterfaceAttribute.cs | 13 ++++ .../RequireInterfaceAttribute.cs.uid | 1 + .../NodeExportValidationExtensions.cs | 32 +++++++- src/Godot/GameBootstrap.cs | 69 +++++++++++++----- src/Godot/Scenes/GameWorld.tscn | 31 +++++--- 18 files changed, 344 insertions(+), 75 deletions(-) create mode 100644 src/Features/Interactions/Contracts/IPlayerInteractionController.cs create mode 100644 src/Features/Interactions/Contracts/IPlayerInteractionController.cs.uid create mode 100644 src/Features/Players/Contracts/IPlayerController.cs create mode 100644 src/Features/Players/Contracts/IPlayerController.cs.uid create mode 100644 src/Features/Players/Godot/LocalPlayerController.cs create mode 100644 src/Features/Players/Godot/PlayerSpawnerController.cs create mode 100644 src/Features/Players/Godot/PlayerSpawnerController.cs.uid create mode 100644 src/Godot/Attributes/RequireInterfaceAttribute.cs create mode 100644 src/Godot/Attributes/RequireInterfaceAttribute.cs.uid diff --git a/NightShift.csproj b/NightShift.csproj index 32f49ce..a3da75c 100644 --- a/NightShift.csproj +++ b/NightShift.csproj @@ -3,5 +3,7 @@ net8.0 true Game + true + $(NoWarn);CS1591 \ No newline at end of file diff --git a/src/Features/Interactions/Contracts/IPlayerInteractionController.cs b/src/Features/Interactions/Contracts/IPlayerInteractionController.cs new file mode 100644 index 0000000..d8962e5 --- /dev/null +++ b/src/Features/Interactions/Contracts/IPlayerInteractionController.cs @@ -0,0 +1,16 @@ +using Game.Features.Interactions.Contracts; +using Game.Features.Players.Contracts; +using Godot; + +namespace Game.Features.Items.Contracts; + +public interface IPlayerInteractionController : IItemSpawnPoint +{ + void SetActive(bool isActive); + + void InitializeController( + PlayerId playerId, + IItemInteractionService items, + IInteractionService interactions + ); +} diff --git a/src/Features/Interactions/Contracts/IPlayerInteractionController.cs.uid b/src/Features/Interactions/Contracts/IPlayerInteractionController.cs.uid new file mode 100644 index 0000000..fc67ef3 --- /dev/null +++ b/src/Features/Interactions/Contracts/IPlayerInteractionController.cs.uid @@ -0,0 +1 @@ +uid://cvfdyxpc3xkfd diff --git a/src/Features/Interactions/Godot/PlayerInteractionController.cs b/src/Features/Interactions/Godot/PlayerInteractionController.cs index 0cbe48d..1779815 100644 --- a/src/Features/Interactions/Godot/PlayerInteractionController.cs +++ b/src/Features/Interactions/Godot/PlayerInteractionController.cs @@ -9,15 +9,22 @@ using Godot; namespace Game.Features.Interactions.Godot; -public partial class PlayerInteractionController : Node, IItemSpawnPoint +public partial class PlayerInteractionController : Node, IPlayerInteractionController { - [Export] public Camera3D Camera; - [Export] public CharacterBody3D Body; - [Export] public InteractionRaycaster InteractionRaycaster; - [Export] public RayCast3D DropRaycaster; - [Export] public InteractionPromptController PromptController; - [Export] public Node3D LeftHandAnchor; - [Export] public Node3D RightHandAnchor; + [Export] + public Camera3D Camera; + + [Export] + public CharacterBody3D Body; + + [Export] + public InteractionRaycaster InteractionRaycaster; + + [Export] + public RayCast3D DropRaycaster; + + [Export] + public InteractionPromptController PromptController; private IItemInteractionService _items; private IInteractionService _interactions; @@ -30,7 +37,13 @@ public partial class PlayerInteractionController : Node, IItemSpawnPoint this.AssertRequiredExports(); } - public void Initialize( + public void SetActive(bool isActive) + { + SetProcess(isActive); + SetProcessUnhandledInput(isActive); + } + + public void InitializeController( PlayerId playerId, IItemInteractionService items, IInteractionService interactions @@ -43,7 +56,12 @@ public partial class PlayerInteractionController : Node, IItemSpawnPoint public override void _Process(double delta) { - if (_items is null || _interactions is null || GamePause.IsPaused || !MouseCapture.IsCaptured) + if ( + _items is null + || _interactions is null + || GamePause.IsPaused + || !MouseCapture.IsCaptured + ) { PromptController.Clear(); return; @@ -54,7 +72,12 @@ public partial class PlayerInteractionController : Node, IItemSpawnPoint public override void _UnhandledInput(InputEvent @event) { - if (_items is null || _interactions is null || GamePause.IsPaused || !MouseCapture.IsCaptured) + if ( + _items is null + || _interactions is null + || GamePause.IsPaused + || !MouseCapture.IsCaptured + ) { return; } @@ -107,12 +130,9 @@ public partial class PlayerInteractionController : Node, IItemSpawnPoint { if (_activeUseTargets.Remove(hand, out InteractionTargetId? activeTargetId)) { - _items.TryUse(new ItemUseRequest( - _playerId, - hand, - ItemActionPhase.Cancelled, - activeTargetId - )); + _items.TryUse( + new ItemUseRequest(_playerId, hand, ItemActionPhase.Cancelled, activeTargetId) + ); } _items.TryDrop(hand, CalculateDropPosition()); @@ -168,7 +188,10 @@ public partial class PlayerInteractionController : Node, IItemSpawnPoint if (DropRaycaster.IsColliding()) { - distance = Mathf.Max(0.25f, origin.DistanceTo(DropRaycaster.GetCollisionPoint()) - 0.2f); + distance = Mathf.Max( + 0.25f, + origin.DistanceTo(DropRaycaster.GetCollisionPoint()) - 0.2f + ); } return origin + direction * distance; @@ -198,9 +221,11 @@ public partial class PlayerInteractionController : Node, IItemSpawnPoint if (itemInfo.HandRequirement == HandRequirement.Both) { bool bothHandsFree = _items.IsHandFree(Hand.Left) && _items.IsHandFree(Hand.Right); - PromptController.ShowPrompt(bothHandsFree - ? $"[LPM lub PPM] Podnieś {itemInfo.DisplayName} — obie ręce" - : "Potrzebujesz obu wolnych rąk"); + PromptController.ShowPrompt( + bothHandsFree + ? $"[LPM lub PPM] Podnieś {itemInfo.DisplayName} — obie ręce" + : "Potrzebujesz obu wolnych rąk" + ); return; } @@ -216,8 +241,8 @@ public partial class PlayerInteractionController : Node, IItemSpawnPoint actions.Add($"[PPM] Podnieś {itemInfo.DisplayName} — prawa ręka"); } - PromptController.ShowPrompt(actions.Count > 0 - ? string.Join("\n", actions) - : "Brak wolnej ręki"); + PromptController.ShowPrompt( + actions.Count > 0 ? string.Join("\n", actions) : "Brak wolnej ręki" + ); } } diff --git a/src/Features/Multiplayer/Contracts/IMultiplayerPeer.cs b/src/Features/Multiplayer/Contracts/IMultiplayerPeer.cs index 32edbdf..9ccbbc5 100644 --- a/src/Features/Multiplayer/Contracts/IMultiplayerPeer.cs +++ b/src/Features/Multiplayer/Contracts/IMultiplayerPeer.cs @@ -7,6 +7,7 @@ public interface IMultiplayerPeer event Action Connected; event Action ConnectionFailed; event Action Disconnected; + event Action HostingStarted; event Action PeerJoined; event Action PeerLeft; diff --git a/src/Features/Multiplayer/Godot/MultiplayerController.cs b/src/Features/Multiplayer/Godot/MultiplayerController.cs index 2c81630..e689849 100644 --- a/src/Features/Multiplayer/Godot/MultiplayerController.cs +++ b/src/Features/Multiplayer/Godot/MultiplayerController.cs @@ -17,6 +17,7 @@ public partial class MultiplayerController : Node, IMultiplayerPeer public event Action Connected; public event Action ConnectionFailed; public event Action Disconnected; + public event Action HostingStarted; public event Action PeerJoined; public event Action PeerLeft; @@ -91,7 +92,10 @@ public partial class MultiplayerController : Node, IMultiplayerPeer AssertNoActiveSession(); Multiplayer.MultiplayerPeer = _multiplayerPeerFactory.CreateHost(port, MultiplayerSession.MaxPlayers - 1); - _session.StartHosting(new PeerId(Multiplayer.GetUniqueId())); + + PeerId hostPeerId = new(Multiplayer.GetUniqueId()); + _session.StartHosting(hostPeerId); + HostingStarted?.Invoke(hostPeerId); } public void Join(string address, int port) diff --git a/src/Features/Players/Contracts/IPlayerController.cs b/src/Features/Players/Contracts/IPlayerController.cs new file mode 100644 index 0000000..0a361c0 --- /dev/null +++ b/src/Features/Players/Contracts/IPlayerController.cs @@ -0,0 +1,4 @@ +using Game.Features.Items.Contracts; +using Game.Features.Players.Contracts; + +public interface IPlayerController : IMovementModeSwitcher, IItemSpawnPoint { } diff --git a/src/Features/Players/Contracts/IPlayerController.cs.uid b/src/Features/Players/Contracts/IPlayerController.cs.uid new file mode 100644 index 0000000..070ea32 --- /dev/null +++ b/src/Features/Players/Contracts/IPlayerController.cs.uid @@ -0,0 +1 @@ +uid://1i0wvlumjjiu diff --git a/src/Features/Players/Godot/LocalPlayerController.cs b/src/Features/Players/Godot/LocalPlayerController.cs new file mode 100644 index 0000000..58b700f --- /dev/null +++ b/src/Features/Players/Godot/LocalPlayerController.cs @@ -0,0 +1,24 @@ +using Game.Features.Items.Contracts; +using Game.Features.Players.Contracts; +using Godot; + +namespace Game.Features.Players.Godot; + +public sealed class LocalPlayerController : IPlayerController +{ + public PlayerController Current { get; private set; } + + public void SetCurrent(PlayerController player) + { + Current = player; + } + + public MovementMode ActiveMovementMode => Current?.ActiveMovementMode ?? MovementMode.Ground; + + public void SetMovementMode(MovementMode movementMode) + { + Current?.SetMovementMode(movementMode); + } + + public Vector3 GetItemSpawnPosition() => Current?.GetItemSpawnPosition() ?? Vector3.Zero; +} diff --git a/src/Features/Players/Godot/PlayerController.cs b/src/Features/Players/Godot/PlayerController.cs index a3a285e..7c64f24 100644 --- a/src/Features/Players/Godot/PlayerController.cs +++ b/src/Features/Players/Godot/PlayerController.cs @@ -1,25 +1,53 @@ using System.Collections.Generic; +using Game.Features.Items.Contracts; using Game.Features.Players.Contracts; using Game.Features.Players.Domain; using Game.Features.Players.Domain.Movement; using Game.Godot; +using Game.Godot.Attributes; using Game.Godot.Extensions; using Game.Godot.Mappings; using Godot; namespace Game.Features.Players.Godot; -public partial class PlayerController : Node3D, IMovementModeSwitcher +public partial class PlayerController : Node3D, IPlayerController { - [Export] public CharacterBody3D Body; - [Export] public CollisionShape3D Collision; - [Export] public Camera3D Camera; + [Export] + public CharacterBody3D Body; - [Export] public float MoveSpeed = 4.0f; - [Export] public float JumpVelocity = 4.5f; - [Export] public float NoclipFlySpeed = 8.0f; - [Export] public float MouseSensitivity = 0.003f; - [Export] public float MaxLookAngleDegrees = 89.0f; + [Export] + public CollisionShape3D Collision; + + [Export] + public Camera3D Camera; + + [Export] + public Node3D LeftHandAnchor; + + [Export] + public Node3D RightHandAnchor; + + [Export] + public float MoveSpeed = 4.0f; + + [Export] + public float JumpVelocity = 4.5f; + + [Export] + public float NoclipFlySpeed = 8.0f; + + [Export] + public float MouseSensitivity = 0.003f; + + [Export] + public float MaxLookAngleDegrees = 89.0f; + + [Export] + [RequireInterface(typeof(IPlayerInteractionController))] + private Node InteractionController; + public IPlayerInteractionController Interaction => + (IPlayerInteractionController)InteractionController; private Dictionary _movementStrategies; private IMovementStrategy _currentMovementStrategy; @@ -34,24 +62,38 @@ public partial class PlayerController : Node3D, IMovementModeSwitcher Collision.Disabled = movementMode == MovementMode.Noclip; } + public Vector3 GetItemSpawnPosition() => Interaction.GetItemSpawnPosition(); + + public override void _EnterTree() + { + SetMultiplayerAuthority(int.Parse(Name.ToString())); + } + public override void _Ready() { this.AssertRequiredExports(); _look = new PlayerLook(MouseSensitivity, MaxLookAngleDegrees); - + float gravity = (float)ProjectSettings.GetSetting(ProjectSetting.Gravity.SettingPath()); _movementStrategies = new Dictionary { [MovementMode.Ground] = new GroundMovement(MoveSpeed, JumpVelocity, gravity), - [MovementMode.Noclip] = new NoclipMovement(NoclipFlySpeed) + [MovementMode.Noclip] = new NoclipMovement(NoclipFlySpeed), }; _currentMovementStrategy = _movementStrategies[ActiveMovementMode]; + + bool isLocal = IsMultiplayerAuthority(); + Camera.Current = isLocal; + SetPhysicsProcess(isLocal); + SetProcessUnhandledInput(isLocal); + Interaction.SetActive(isLocal); } public override void _UnhandledInput(InputEvent @event) { - if (@event is not InputEventMouseMotion mouseMotion || !MouseCapture.IsCaptured) return; + if (@event is not InputEventMouseMotion mouseMotion || !MouseCapture.IsCaptured) + return; Body.RotateY(_look.CalculateYawDelta(mouseMotion.Relative.X)); _look.UpdatePitch(mouseMotion.Relative.Y); diff --git a/src/Features/Players/Godot/PlayerNode.tscn b/src/Features/Players/Godot/PlayerNode.tscn index 3c472d6..512a1ae 100644 --- a/src/Features/Players/Godot/PlayerNode.tscn +++ b/src/Features/Players/Godot/PlayerNode.tscn @@ -293,14 +293,28 @@ _surfaces = [{ }] blend_shape_mode = 0 -[node name="Player" type="Node3D" unique_id=26823676 node_paths=PackedStringArray("Body", "Collision", "Camera")] +[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_player_transform"] +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 + +[node name="Player" type="Node3D" unique_id=26823676 node_paths=PackedStringArray("Body", "Collision", "Camera", "LeftHandAnchor", "RightHandAnchor", "InteractionController")] script = ExtResource("1_gkqfc") Body = NodePath("CharacterBody3D") Collision = NodePath("CharacterBody3D/CollisionShape3D") Camera = NodePath("CharacterBody3D/Camera3D") +LeftHandAnchor = NodePath("CharacterBody3D/Camera3D/LeftHandAnchor") +RightHandAnchor = NodePath("CharacterBody3D/Camera3D/RightHandAnchor") +InteractionController = NodePath("PlayerInteraction") [node name="CharacterBody3D" type="CharacterBody3D" parent="." unique_id=1327672780] +[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="CharacterBody3D"] +replication_config = SubResource("SceneReplicationConfig_player_transform") + [node name="CollisionShape3D" type="CollisionShape3D" parent="CharacterBody3D" unique_id=1006347064] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) shape = SubResource("CapsuleShape3D_130ks") @@ -796,15 +810,13 @@ enabled = true target_position = Vector3(0, 0, -1) collide_with_areas = true -[node name="PlayerInteraction" type="Node" parent="." node_paths=PackedStringArray("Camera", "Body", "InteractionRaycaster", "DropRaycaster", "PromptController", "LeftHandAnchor", "RightHandAnchor")] +[node name="PlayerInteraction" type="Node" parent="." node_paths=PackedStringArray("Camera", "Body", "InteractionRaycaster", "DropRaycaster", "PromptController")] script = ExtResource("3_interaction") Camera = NodePath("../CharacterBody3D/Camera3D") Body = NodePath("../CharacterBody3D") InteractionRaycaster = NodePath("../CharacterBody3D/Camera3D/InteractionRaycaster") DropRaycaster = NodePath("../CharacterBody3D/Camera3D/DropRaycaster") PromptController = NodePath("InteractionUi") -LeftHandAnchor = NodePath("../CharacterBody3D/Camera3D/LeftHandAnchor") -RightHandAnchor = NodePath("../CharacterBody3D/Camera3D/RightHandAnchor") [node name="InteractionUi" type="CanvasLayer" parent="PlayerInteraction" node_paths=PackedStringArray("PromptLabel")] layer = 50 diff --git a/src/Features/Players/Godot/PlayerSpawnerController.cs b/src/Features/Players/Godot/PlayerSpawnerController.cs new file mode 100644 index 0000000..e100a96 --- /dev/null +++ b/src/Features/Players/Godot/PlayerSpawnerController.cs @@ -0,0 +1,58 @@ +using System; +using Game.Features.Multiplayer.Contracts; +using Game.Godot.Extensions; +using Godot; + +namespace Game.Features.Players.Godot; + +public partial class PlayerSpawnerController : MultiplayerSpawner +{ + /// Container that spawned players are added to. + [Export] + public Node3D PlayersRoot; + + /// Scene instantiated for each peer. + [Export] + public PackedScene PlayerScene; + + public event Action LocalPlayerSpawned; + + private void Spawn(PeerId peerId) + { + if (!IsMultiplayerAuthority()) + { + return; + } + + PlayerController player = PlayerScene.Instantiate(); + player.Name = peerId.ToString(); + PlayersRoot.AddChild(player); + + if (peerId.Value == Multiplayer.GetUniqueId()) + { + LocalPlayerSpawned?.Invoke(player); + } + } + + private void Despawn(PeerId peerId) + { + if (!IsMultiplayerAuthority()) + { + return; + } + + PlayersRoot.GetNodeOrNull(peerId.ToString())?.QueueFree(); + } + + public void UseMultiplayerPeer(IMultiplayerPeer multiplayer) + { + multiplayer.HostingStarted += Spawn; + multiplayer.PeerJoined += Spawn; + multiplayer.PeerLeft += Despawn; + } + + public override void _Ready() + { + this.AssertRequiredExports(); + } +} diff --git a/src/Features/Players/Godot/PlayerSpawnerController.cs.uid b/src/Features/Players/Godot/PlayerSpawnerController.cs.uid new file mode 100644 index 0000000..8a51938 --- /dev/null +++ b/src/Features/Players/Godot/PlayerSpawnerController.cs.uid @@ -0,0 +1 @@ +uid://5j5el8n12uxm diff --git a/src/Godot/Attributes/RequireInterfaceAttribute.cs b/src/Godot/Attributes/RequireInterfaceAttribute.cs new file mode 100644 index 0000000..cee6508 --- /dev/null +++ b/src/Godot/Attributes/RequireInterfaceAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace Game.Godot.Attributes; + +/// +/// Marks an [Export]ed Node field/property as required to implement the given interface. +/// Validated by . +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] +public sealed class RequireInterfaceAttribute(Type interfaceType) : Attribute +{ + public Type InterfaceType { get; } = interfaceType; +} diff --git a/src/Godot/Attributes/RequireInterfaceAttribute.cs.uid b/src/Godot/Attributes/RequireInterfaceAttribute.cs.uid new file mode 100644 index 0000000..73e303b --- /dev/null +++ b/src/Godot/Attributes/RequireInterfaceAttribute.cs.uid @@ -0,0 +1 @@ +uid://g0fcf6gcpkcm diff --git a/src/Godot/Extensions/NodeExportValidationExtensions.cs b/src/Godot/Extensions/NodeExportValidationExtensions.cs index ad3286f..40c8ad8 100644 --- a/src/Godot/Extensions/NodeExportValidationExtensions.cs +++ b/src/Godot/Extensions/NodeExportValidationExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using Game.Godot.Attributes; using Game.Godot.Exceptions; using Godot; @@ -44,11 +45,33 @@ public static class NodeExportValidationExtensions return missingFields.Concat(missingProperties).ToArray(); } + private static string[] GetInterfaceMismatchWarnings(Node node) + { + Type nodeType = node.GetType(); + + IEnumerable fieldWarnings = nodeType + .GetFields(MemberFlags) + .SelectMany(field => field.GetCustomAttributes() + .Select(require => (field.Name, Require: require, Value: field.GetValue(node)))) + .Where(member => member.Value is not null && !member.Require.InterfaceType.IsInstanceOfType(member.Value)) + .Select(member => $"{member.Name} ({member.Value.GetType().Name}) does not implement {member.Require.InterfaceType.Name}."); + + IEnumerable propertyWarnings = nodeType + .GetProperties(MemberFlags) + .SelectMany(property => property.GetCustomAttributes() + .Select(require => (property.Name, Require: require, Value: property.GetValue(node)))) + .Where(member => member.Value is not null && !member.Require.InterfaceType.IsInstanceOfType(member.Value)) + .Select(member => $"{member.Name} ({member.Value.GetType().Name}) does not implement {member.Require.InterfaceType.Name}."); + + return fieldWarnings.Concat(propertyWarnings).ToArray(); + } + /// - /// Throws if any [Export]ed Node field on this node is unassigned. + /// Throws if any [Export]ed Node field on this node is unassigned, or assigned to a Node + /// that does not implement the interface declared via [RequireInterface]. /// /// Node to check. - /// One or more exports are missing. + /// One or more exports are missing or invalid. /// /// /// public override void _Ready() @@ -60,7 +83,10 @@ public static class NodeExportValidationExtensions /// public static void AssertRequiredExports(this Node node) { - List missing = GetMissingExportWarnings(node).ToList(); + List missing = GetMissingExportWarnings(node) + .Concat(GetInterfaceMismatchWarnings(node)) + .ToList(); + if (missing.Count == 0) { return; diff --git a/src/Godot/GameBootstrap.cs b/src/Godot/GameBootstrap.cs index 2320d01..0cc0db7 100644 --- a/src/Godot/GameBootstrap.cs +++ b/src/Godot/GameBootstrap.cs @@ -2,13 +2,14 @@ using System; using Game.Features.Console.Application.Commands; using Game.Features.Console.Godot; using Game.Features.Console.Infrastructure; -using Game.Features.Logging.Godot; -using Game.Features.Logging.Infrastructure; using Game.Features.Interactions.Application; using Game.Features.Interactions.Godot; using Game.Features.Items.Application; +using Game.Features.Items.Contracts; using Game.Features.Items.Domain; using Game.Features.Items.Godot; +using Game.Features.Logging.Godot; +using Game.Features.Logging.Infrastructure; using Game.Features.Multiplayer.Godot; using Game.Features.Players.Contracts; using Game.Features.Players.Godot; @@ -20,13 +21,25 @@ namespace Game.Godot; public partial class GameBootstrap : Node { - [Export] public ConsoleController Console; - [Export] public PlayerController Player; - [Export] public PlayerInteractionController PlayerInteraction; - [Export] public ItemWorldController ItemWorld; - [Export] public ItemSpawnerController ItemSpawner; - [Export] public InteractionWorldController InteractionWorld; - [Export] public MultiplayerController MultiplayerController; + [Export] + public ConsoleController Console; + + [Export] + public PlayerSpawnerController PlayerSpawner; + + [Export] + public ItemWorldController ItemWorld; + + [Export] + public ItemSpawnerController ItemSpawner; + + [Export] + public InteractionWorldController InteractionWorld; + + [Export] + public MultiplayerController MultiplayerController; + + private readonly LocalPlayerController _localPlayerController = new(); private FileLogger _fileLogger; private CompositeLogger _logger; @@ -37,7 +50,7 @@ public partial class GameBootstrap : Node _logger = new CompositeLogger([ new GodotConsoleLogger(), new LogTargetLogger(Console), - _fileLogger + _fileLogger, ]); } @@ -47,8 +60,8 @@ public partial class GameBootstrap : Node registry.Register(new HelpCommand(registry)); registry.Register(new EchoCommand()); registry.Register(new ClearCommand()); - registry.Register(new NoclipCommand(Player)); - registry.Register(new SpawnItemCommand(ItemSpawner, PlayerInteraction)); + registry.Register(new NoclipCommand(_localPlayerController)); + registry.Register(new SpawnItemCommand(ItemSpawner, _localPlayerController)); registry.Register(new HostCommand(MultiplayerController)); registry.Register(new JoinCommand(MultiplayerController)); @@ -58,11 +71,17 @@ public partial class GameBootstrap : Node private void InitializeMultiplayer() { MultiplayerController.UsePeerFactory(new EnetMultiplayerPeerFactory()); - MultiplayerController.Connected += () => _logger.Info("Multiplayer", "Connected successfully."); - MultiplayerController.ConnectionFailed += () => _logger.Warning("Multiplayer", "Connection failed."); - MultiplayerController.Disconnected += () => _logger.Warning("Multiplayer", "Server disconnected."); + MultiplayerController.Connected += () => + _logger.Info("Multiplayer", "Connected successfully."); + MultiplayerController.ConnectionFailed += () => + _logger.Warning("Multiplayer", "Connection failed."); + MultiplayerController.Disconnected += () => + _logger.Warning("Multiplayer", "Server disconnected."); MultiplayerController.PeerJoined += id => _logger.Info("Multiplayer", $"Peer {id} joined."); MultiplayerController.PeerLeft += id => _logger.Info("Multiplayer", $"Peer {id} left."); + + PlayerSpawner.UseMultiplayerPeer(MultiplayerController); + PlayerSpawner.LocalPlayerSpawned += OnLocalPlayerSpawned; } private void InitializeItems() @@ -70,15 +89,27 @@ public partial class GameBootstrap : Node ItemWorld.RegisterSceneItems(); ItemSpawner.Initialize(); InteractionWorld.RegisterSceneTargets(); + } + + private void OnLocalPlayerSpawned(PlayerController player) + { + _localPlayerController.SetCurrent(player); + + IPlayerInteractionController interaction = player.Interaction; PlayerHeldItemPresenter presenter = new( ItemWorld, - PlayerInteraction.LeftHandAnchor, - PlayerInteraction.RightHandAnchor + player.LeftHandAnchor, + player.RightHandAnchor ); ItemInteractionService itemInteractions = new(new HandsInventory(), ItemWorld, presenter); InteractionService interactions = new(InteractionWorld); - PlayerInteraction.Initialize(new PlayerId(Guid.NewGuid()), itemInteractions, interactions); + + interaction.InitializeController( + new PlayerId(Guid.NewGuid()), + itemInteractions, + interactions + ); } public override void _Ready() @@ -86,7 +117,7 @@ public partial class GameBootstrap : Node this.AssertRequiredExports(); MouseCapture.Capture(); - + InitializeLogging(); InitializeItems(); InitializeMultiplayer(); diff --git a/src/Godot/Scenes/GameWorld.tscn b/src/Godot/Scenes/GameWorld.tscn index 87f71f7..1c64e7b 100644 --- a/src/Godot/Scenes/GameWorld.tscn +++ b/src/Godot/Scenes/GameWorld.tscn @@ -6,10 +6,11 @@ [ext_resource type="Script" uid="uid://drctu5y21ey2y" path="res://src/Godot/GameBootstrap.cs" id="3_bootstrap"] [ext_resource type="PackedScene" uid="uid://bk0771dvonxh8" path="res://src/Features/Console/Godot/ConsoleController.tscn" id="4_console"] [ext_resource type="Script" uid="uid://drqx0wiw0sbj1" path="res://src/Features/Multiplayer/Godot/MultiplayerController.cs" id="5_multiplayer"] -[ext_resource type="Script" uid="uid://f8n3b6w1k5r9m" path="res://src/Features/Items/Godot/ItemWorldController.cs" id="6_items"] +[ext_resource type="Script" uid="uid://cd87p40r0xwiu" path="res://src/Features/Items/Godot/ItemWorldController.cs" id="6_items"] [ext_resource type="PackedScene" uid="uid://cv8m3q1y6r4nf" path="res://src/Features/Items/Godot/Flashlight/FlashlightItem.tscn" id="7_flashlight"] -[ext_resource type="Script" uid="uid://c3x9m6w2q8r1b" path="res://src/Features/Items/Godot/ItemSpawnerController.cs" id="8_spawner"] -[ext_resource type="Script" uid="uid://f6q1c9m4w7v2n" path="res://src/Features/Interactions/Godot/InteractionWorldController.cs" id="9_interactions"] +[ext_resource type="Script" uid="uid://c3yam6w2q8r1b" path="res://src/Features/Items/Godot/ItemSpawnerController.cs" id="8_spawner"] +[ext_resource type="Script" uid="uid://ccc5q7pvd01bv" path="res://src/Features/Interactions/Godot/InteractionWorldController.cs" id="9_interactions"] +[ext_resource type="Script" uid="uid://5j5el8n12uxm" path="res://src/Features/Players/Godot/PlayerSpawnerController.cs" id="10_player_spawner"] [sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_epykj"] panorama = ExtResource("1_g4n7o") @@ -22,11 +23,10 @@ background_mode = 2 sky = SubResource("Sky_1kpcd") glow_enabled = true -[node name="GameWorld" type="Node" unique_id=222644234 node_paths=PackedStringArray("Console", "Player", "PlayerInteraction", "ItemWorld", "ItemSpawner", "InteractionWorld", "MultiplayerController")] +[node name="GameWorld" type="Node" unique_id=222644234 node_paths=PackedStringArray("Console", "PlayerSpawner", "ItemWorld", "ItemSpawner", "InteractionWorld", "MultiplayerController")] script = ExtResource("3_bootstrap") Console = NodePath("Console") -Player = NodePath("Player") -PlayerInteraction = NodePath("Player/PlayerInteraction") +PlayerSpawner = NodePath("PlayerSpawner") ItemWorld = NodePath("ItemWorld") ItemSpawner = NodePath("ItemSpawner") InteractionWorld = NodePath("InteractionWorld") @@ -39,24 +39,31 @@ shadow_enabled = true [node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=847581298] environment = SubResource("Environment_mvtit") -[node name="Player" parent="." unique_id=26823676 instance=ExtResource("2_mvtit")] +[node name="PlayersRoot" type="Node3D" parent="." unique_id=26823676] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) +[node name="PlayerSpawner" type="MultiplayerSpawner" parent="." unique_id=1573506152 node_paths=PackedStringArray("PlayersRoot")] +_spawnable_scenes = PackedStringArray("uid://bka1dmgdi5btd") +spawn_path = NodePath("../PlayersRoot") +script = ExtResource("10_player_spawner") +PlayersRoot = NodePath("../PlayersRoot") +PlayerScene = ExtResource("2_mvtit") + [node name="Console" parent="." unique_id=1947043894 instance=ExtResource("4_console")] [node name="Multiplayer" type="Node" parent="." unique_id=1209131013] script = ExtResource("5_multiplayer") -[node name="ItemWorld" type="Node" parent="."] +[node name="ItemWorld" type="Node" parent="." unique_id=1821063866] script = ExtResource("6_items") -[node name="ItemSpawner" type="Node" parent="." node_paths=PackedStringArray("ItemsRoot", "ItemWorld")] +[node name="ItemSpawner" type="Node" parent="." unique_id=483552217 node_paths=PackedStringArray("ItemsRoot", "ItemWorld")] script = ExtResource("8_spawner") ItemsRoot = NodePath("../Entities") ItemWorld = NodePath("../ItemWorld") ItemScenes = Array[PackedScene]([ExtResource("7_flashlight")]) -[node name="InteractionWorld" type="Node" parent="."] +[node name="InteractionWorld" type="Node" parent="." unique_id=556371192] script = ExtResource("9_interactions") [node name="Entities" type="Node3D" parent="." unique_id=851492709] @@ -87,10 +94,10 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4, 0, -4) [node name="floor_ceiling_hr_50" parent="Entities" unique_id=1804933431 instance=ExtResource("2_1kpcd")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4, 0, 0) -[node name="FlashlightLeft" parent="Entities" instance=ExtResource("7_flashlight")] +[node name="FlashlightLeft" parent="Entities" unique_id=2018294910 instance=ExtResource("7_flashlight")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.25, 0.65, -0.7) InstanceId = "c08790de-b2de-44e6-a73c-a7502ca27e34" -[node name="FlashlightRight" parent="Entities" instance=ExtResource("7_flashlight")] +[node name="FlashlightRight" parent="Entities" unique_id=2042120202 instance=ExtResource("7_flashlight")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.25, 0.65, -0.7) InstanceId = "f71e67cb-786b-49eb-bd56-cb42ad19a754"