feat: replicate players across multiplayer peers

This commit is contained in:
2026-08-26 22:59:29 +02:00
parent 00627a7e91
commit d8cc1d0859
18 changed files with 344 additions and 75 deletions
+2
View File
@@ -3,5 +3,7 @@
<TargetFramework>net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
<RootNamespace>Game</RootNamespace>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
</Project>
@@ -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
);
}
@@ -0,0 +1 @@
uid://cvfdyxpc3xkfd
@@ -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"
);
}
}
@@ -7,6 +7,7 @@ public interface IMultiplayerPeer
event Action Connected;
event Action ConnectionFailed;
event Action Disconnected;
event Action<PeerId> HostingStarted;
event Action<PeerId> PeerJoined;
event Action<PeerId> PeerLeft;
@@ -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<PeerId> HostingStarted;
public event Action<PeerId> PeerJoined;
public event Action<PeerId> 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)
@@ -0,0 +1,4 @@
using Game.Features.Items.Contracts;
using Game.Features.Players.Contracts;
public interface IPlayerController : IMovementModeSwitcher, IItemSpawnPoint { }
@@ -0,0 +1 @@
uid://1i0wvlumjjiu
@@ -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;
}
+53 -11
View File
@@ -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<MovementMode, IMovementStrategy> _movementStrategies;
private IMovementStrategy _currentMovementStrategy;
@@ -34,6 +62,13 @@ 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();
@@ -44,14 +79,21 @@ public partial class PlayerController : Node3D, IMovementModeSwitcher
_movementStrategies = new Dictionary<MovementMode, IMovementStrategy>
{
[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);
+16 -4
View File
@@ -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
@@ -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
{
/// <summary>Container that spawned players are added to.</summary>
[Export]
public Node3D PlayersRoot;
/// <summary>Scene instantiated for each peer.</summary>
[Export]
public PackedScene PlayerScene;
public event Action<PlayerController> LocalPlayerSpawned;
private void Spawn(PeerId peerId)
{
if (!IsMultiplayerAuthority())
{
return;
}
PlayerController player = PlayerScene.Instantiate<PlayerController>();
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();
}
}
@@ -0,0 +1 @@
uid://5j5el8n12uxm
@@ -0,0 +1,13 @@
using System;
namespace Game.Godot.Attributes;
/// <summary>
/// Marks an [Export]ed Node field/property as required to implement the given interface.
/// Validated by <see cref="Game.Godot.Extensions.NodeExportValidationExtensions.AssertRequiredExports"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)]
public sealed class RequireInterfaceAttribute(Type interfaceType) : Attribute
{
public Type InterfaceType { get; } = interfaceType;
}
@@ -0,0 +1 @@
uid://g0fcf6gcpkcm
@@ -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<string> fieldWarnings = nodeType
.GetFields(MemberFlags)
.SelectMany(field => field.GetCustomAttributes<RequireInterfaceAttribute>()
.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<string> propertyWarnings = nodeType
.GetProperties(MemberFlags)
.SelectMany(property => property.GetCustomAttributes<RequireInterfaceAttribute>()
.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();
}
/// <summary>
/// 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].
/// </summary>
/// <param name="node">Node to check.</param>
/// <exception cref="MissingRequiredExportException">One or more exports are missing.</exception>
/// <exception cref="MissingRequiredExportException">One or more exports are missing or invalid.</exception>
/// <example>
/// <code>
/// public override void _Ready()
@@ -60,7 +83,10 @@ public static class NodeExportValidationExtensions
/// </example>
public static void AssertRequiredExports(this Node node)
{
List<string> missing = GetMissingExportWarnings(node).ToList();
List<string> missing = GetMissingExportWarnings(node)
.Concat(GetInterfaceMismatchWarnings(node))
.ToList();
if (missing.Count == 0)
{
return;
+49 -18
View File
@@ -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()
+19 -12
View File
@@ -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"