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"