diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity index 13e35cae1..d57490277 100644 --- a/Assets/Scenes/SampleScene.unity +++ b/Assets/Scenes/SampleScene.unity @@ -119,6 +119,51 @@ NavMeshSettings: debug: m_Flags: 0 m_NavMeshData: {fileID: 0} +--- !u!1 &121227139 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 121227141} + - component: {fileID: 121227140} + m_Layer: 0 + m_Name: NetConnectionUI + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &121227140 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 121227139} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 977a7574cf6ff4898b7e9db6c21368f9, type: 3} + m_Name: + m_EditorClassIdentifier: ProjectM.Client::ProjectM.Client.ConnectionUI + _port: 7979 +--- !u!4 &121227141 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 121227139} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &330585543 GameObject: m_ObjectHideFlags: 0 @@ -629,3 +674,4 @@ SceneRoots: - {fileID: 832575519} - {fileID: 1314640900} - {fileID: 833091047} + - {fileID: 121227141} diff --git a/Assets/_Project/Scripts/Authoring/Spawning/PlayerSpawnerAuthoring.cs b/Assets/_Project/Scripts/Authoring/Spawning/PlayerSpawnerAuthoring.cs index a89706caa..a7d1c41fc 100644 --- a/Assets/_Project/Scripts/Authoring/Spawning/PlayerSpawnerAuthoring.cs +++ b/Assets/_Project/Scripts/Authoring/Spawning/PlayerSpawnerAuthoring.cs @@ -16,6 +16,12 @@ namespace ProjectM.Authoring public Vector3 SpawnPoint = Vector3.zero; + [Tooltip("Radius (m) of the co-op spawn ring; players land on distinct slots so they don't stack.")] + public float SpawnRingRadius = 2.5f; + + [Tooltip("Evenly-spaced ring positions before players spill onto an outer ring.")] + public int RingSlots = 4; + private class PlayerSpawnerBaker : Baker { public override void Bake(PlayerSpawnerAuthoring authoring) @@ -26,7 +32,9 @@ namespace ProjectM.Authoring AddComponent(entity, new PlayerSpawner { PlayerPrefab = GetEntity(authoring.PlayerPrefab, TransformUsageFlags.Dynamic), - SpawnPoint = authoring.SpawnPoint + SpawnPoint = authoring.SpawnPoint, + SpawnRingRadius = authoring.SpawnRingRadius, + RingSlots = authoring.RingSlots }); } } diff --git a/Assets/_Project/Scripts/Client/Combat/ProjectileClassificationSystem.cs b/Assets/_Project/Scripts/Client/Combat/ProjectileClassificationSystem.cs index 89724e57b..001847ea8 100644 --- a/Assets/_Project/Scripts/Client/Combat/ProjectileClassificationSystem.cs +++ b/Assets/_Project/Scripts/Client/Combat/ProjectileClassificationSystem.cs @@ -63,7 +63,7 @@ namespace ProjectM.Client SystemAPI.GetSingletonEntity(), SystemAPI.GetSingletonEntity()); - m_PredictedGhostSpawnLookup = state.GetBufferLookup(true); + m_PredictedGhostSpawnLookup = state.GetBufferLookup(false); m_ProjectileLookup = state.GetComponentLookup(true); state.RequireForUpdate(); @@ -134,7 +134,7 @@ namespace ProjectM.Client // is NOT marked [ReadOnly]. public Entity PredictedSpawnListEntity; - [ReadOnly] public BufferLookup PredictedGhostSpawnLookup; + public BufferLookup PredictedGhostSpawnLookup; [ReadOnly] public ComponentLookup ProjectileLookup; // 'data' is taken by value (NOT 'in') because TryGetComponentDataFromSnapshotHistory needs a diff --git a/Assets/_Project/Scripts/Client/Connection/ClientConnectionControlSystem.cs b/Assets/_Project/Scripts/Client/Connection/ClientConnectionControlSystem.cs new file mode 100644 index 000000000..96423cc42 --- /dev/null +++ b/Assets/_Project/Scripts/Client/Connection/ClientConnectionControlSystem.cs @@ -0,0 +1,49 @@ +using ProjectM.Simulation; +using Unity.Collections; +using Unity.Entities; +using Unity.NetCode; +using Unity.Networking.Transport; +using UnityEngine; + +namespace ProjectM.Client +{ + /// + /// Client/thin-client half of the M4 LAN join flow. When the per-world + /// requests , creates a + /// entity for the parsed endpoint and clears the request. Runs in both full and thin client worlds so + /// the editor auto-host can connect thin clients too. Not Burst-compiled: NetworkEndpoint.TryParse + /// takes a managed string and this is a cold path. + /// + [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation)] + public partial struct ClientConnectionControlSystem : ISystem + { + public void OnCreate(ref SystemState state) + { + state.RequireForUpdate(); + } + + public void OnUpdate(ref SystemState state) + { + var cfgRef = SystemAPI.GetSingletonRW(); + if (!cfgRef.ValueRO.Requested || cfgRef.ValueRO.Mode != ConnectionMode.Join) + return; + + // Clear first so a malformed address does not retry-spam every frame. + cfgRef.ValueRW.Requested = false; + + var address = cfgRef.ValueRO.Address; + var port = cfgRef.ValueRO.Port; + if (!NetworkEndpoint.TryParse(address.ToString(), port, out var endpoint, NetworkFamily.Ipv4)) + { + Debug.LogError($"[ClientConnectionControlSystem] Invalid join address '{address}'."); + return; + } + + var ecb = new EntityCommandBuffer(Allocator.Temp); + var req = ecb.CreateEntity(); + ecb.AddComponent(req, new NetworkStreamRequestConnect { Endpoint = endpoint }); + ecb.Playback(state.EntityManager); + ecb.Dispose(); + } + } +} diff --git a/Assets/_Project/Scripts/Client/Connection/ClientConnectionControlSystem.cs.meta b/Assets/_Project/Scripts/Client/Connection/ClientConnectionControlSystem.cs.meta new file mode 100644 index 000000000..823cf25be --- /dev/null +++ b/Assets/_Project/Scripts/Client/Connection/ClientConnectionControlSystem.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c274f15ad41814f8d8337f786590313d \ No newline at end of file diff --git a/Assets/_Project/Scripts/Client/Connection/ConnectionUI.cs b/Assets/_Project/Scripts/Client/Connection/ConnectionUI.cs new file mode 100644 index 000000000..bad61207d --- /dev/null +++ b/Assets/_Project/Scripts/Client/Connection/ConnectionUI.cs @@ -0,0 +1,79 @@ +using ProjectM.Simulation; +using Unity.Entities; +using Unity.NetCode; +using UnityEngine; + +namespace ProjectM.Client +{ + /// + /// Minimal IMGUI Host / Join (+ IP) panel for the M4 LAN flow — the connection entry point for player + /// builds, where there is no editor auto-host. Writes a request into the + /// relevant netcode world(s): Host makes the server world listen and the local client join over + /// loopback; Join makes the client world connect to the typed IP. Hides itself once the client + /// world has a connection. Direct IP/LAN only — Unity Relay is deferred to a later pass. In the editor + /// the EditorAutoHostSystem usually connects first, so this panel just shows "Connected". + /// + public class ConnectionUI : MonoBehaviour + { + [SerializeField] ushort _port = 7979; + + string _ipField = "127.0.0.1"; + + void OnGUI() + { + if (IsClientConnected()) + { + GUILayout.BeginArea(new Rect(10, 10, 220, 26)); + GUILayout.Label("Net: Connected"); + GUILayout.EndArea(); + return; + } + + GUILayout.BeginArea(new Rect(10, 10, 240, 150), GUI.skin.box); + GUILayout.Label("Project M — LAN co-op"); + + // Host is only possible where a server world exists (editor or a ClientServer/host build). + if (ClientServerBootstrap.ServerWorld is { IsCreated: true }) + { + if (GUILayout.Button("Host")) + { + Seed(ClientServerBootstrap.ServerWorld, ConnectionMode.Host, "0.0.0.0", _port); + Seed(ClientServerBootstrap.ClientWorld, ConnectionMode.Join, "127.0.0.1", _port); + } + GUILayout.Space(6); + } + + GUILayout.Label("Join host IP:"); + _ipField = GUILayout.TextField(_ipField); + if (GUILayout.Button("Join")) + Seed(ClientServerBootstrap.ClientWorld, ConnectionMode.Join, _ipField, _port); + + GUILayout.EndArea(); + } + + static bool IsClientConnected() + { + var world = ClientServerBootstrap.ClientWorld; + if (world is not { IsCreated: true }) + return false; + using var q = world.EntityManager.CreateEntityQuery(ComponentType.ReadOnly()); + return !q.IsEmpty; + } + + static void Seed(World world, ConnectionMode mode, string address, ushort port) + { + if (world is not { IsCreated: true }) + return; + var em = world.EntityManager; + using var q = em.CreateEntityQuery(ComponentType.ReadWrite()); + Entity e = q.IsEmpty ? em.CreateEntity(typeof(ConnectionConfig)) : q.GetSingletonEntity(); + em.SetComponentData(e, new ConnectionConfig + { + Mode = mode, + Address = address, + Port = port, + Requested = true, + }); + } + } +} diff --git a/Assets/_Project/Scripts/Client/Connection/ConnectionUI.cs.meta b/Assets/_Project/Scripts/Client/Connection/ConnectionUI.cs.meta new file mode 100644 index 000000000..4c0089b0d --- /dev/null +++ b/Assets/_Project/Scripts/Client/Connection/ConnectionUI.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 977a7574cf6ff4898b7e9db6c21368f9 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Client/ProjectM.Client.asmdef b/Assets/_Project/Scripts/Client/ProjectM.Client.asmdef index ba274dd12..129669e82 100644 --- a/Assets/_Project/Scripts/Client/ProjectM.Client.asmdef +++ b/Assets/_Project/Scripts/Client/ProjectM.Client.asmdef @@ -10,7 +10,8 @@ "Unity.Burst", "Unity.NetCode", "Unity.Entities.Graphics", - "Unity.InputSystem" + "Unity.InputSystem", + "Unity.Networking.Transport" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Assets/_Project/Scripts/Server/Connection/GoInGameServerSystem.cs b/Assets/_Project/Scripts/Server/Connection/GoInGameServerSystem.cs index 00f7d241f..19a49d64d 100644 --- a/Assets/_Project/Scripts/Server/Connection/GoInGameServerSystem.cs +++ b/Assets/_Project/Scripts/Server/Connection/GoInGameServerSystem.cs @@ -45,7 +45,7 @@ namespace ProjectM.Server var networkId = SystemAPI.GetComponent(connection); var player = ecb.Instantiate(spawner.PlayerPrefab); - ecb.SetComponent(player, LocalTransform.FromPosition(spawner.SpawnPoint)); + ecb.SetComponent(player, LocalTransform.FromPosition(spawner.SpawnPoint + PlayerSpawnMath.SpawnOffset(networkId.Value, spawner.SpawnRingRadius, spawner.RingSlots))); ecb.SetComponent(player, new GhostOwner { NetworkId = networkId.Value }); // Auto-despawn the player when its owning connection is removed. diff --git a/Assets/_Project/Scripts/Server/Connection/ServerConnectionControlSystem.cs b/Assets/_Project/Scripts/Server/Connection/ServerConnectionControlSystem.cs new file mode 100644 index 000000000..e012c79a6 --- /dev/null +++ b/Assets/_Project/Scripts/Server/Connection/ServerConnectionControlSystem.cs @@ -0,0 +1,41 @@ +using ProjectM.Simulation; +using Unity.Collections; +using Unity.Entities; +using Unity.NetCode; +using Unity.Networking.Transport; + +namespace ProjectM.Server +{ + /// + /// Server-world half of the M4 LAN host flow. When the per-world + /// requests , creates a + /// entity (netcode binds the server driver and starts listening) and clears the request. Replaces the + /// old AutoConnectPort auto-listen. Not Burst-compiled: a cold path that fires only on an + /// explicit host request. + /// + [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] + public partial struct ServerConnectionControlSystem : ISystem + { + public void OnCreate(ref SystemState state) + { + state.RequireForUpdate(); + } + + public void OnUpdate(ref SystemState state) + { + var cfgRef = SystemAPI.GetSingletonRW(); + if (!cfgRef.ValueRO.Requested || cfgRef.ValueRO.Mode != ConnectionMode.Host) + return; + + cfgRef.ValueRW.Requested = false; + + var endpoint = NetworkEndpoint.AnyIpv4.WithPort(cfgRef.ValueRO.Port); + + var ecb = new EntityCommandBuffer(Allocator.Temp); + var req = ecb.CreateEntity(); + ecb.AddComponent(req, new NetworkStreamRequestListen { Endpoint = endpoint }); + ecb.Playback(state.EntityManager); + ecb.Dispose(); + } + } +} diff --git a/Assets/_Project/Scripts/Server/Connection/ServerConnectionControlSystem.cs.meta b/Assets/_Project/Scripts/Server/Connection/ServerConnectionControlSystem.cs.meta new file mode 100644 index 000000000..0a3e57006 --- /dev/null +++ b/Assets/_Project/Scripts/Server/Connection/ServerConnectionControlSystem.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5530186288473432f8edcc47c1844e07 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Server/ProjectM.Server.asmdef b/Assets/_Project/Scripts/Server/ProjectM.Server.asmdef index d04eea913..899ae21e2 100644 --- a/Assets/_Project/Scripts/Server/ProjectM.Server.asmdef +++ b/Assets/_Project/Scripts/Server/ProjectM.Server.asmdef @@ -8,7 +8,8 @@ "Unity.Collections", "Unity.Mathematics", "Unity.Burst", - "Unity.NetCode" + "Unity.NetCode", + "Unity.Networking.Transport" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Assets/_Project/Scripts/Simulation/Connection/ConnectionConfig.cs b/Assets/_Project/Scripts/Simulation/Connection/ConnectionConfig.cs new file mode 100644 index 000000000..efb6ad49c --- /dev/null +++ b/Assets/_Project/Scripts/Simulation/Connection/ConnectionConfig.cs @@ -0,0 +1,35 @@ +using Unity.Collections; +using Unity.Entities; + +namespace ProjectM.Simulation +{ + /// + /// Connection intent for the M4 LAN host/join flow. Lives in Simulation so both the server and + /// client worlds can read it. A UI (ConnectionUI) — or, in the editor, the auto-host system — + /// writes the desired + endpoint and sets true; the + /// per-world ConnectionControlSystem turns it into a netcode NetworkStreamRequestListen / + /// NetworkStreamRequestConnect and clears . Direct IP/LAN only for now — + /// Unity Relay is deferred to a later pass. Created per-world as a singleton. + /// + public enum ConnectionMode : byte + { + None, + Host, + Join, + } + + public struct ConnectionConfig : IComponentData + { + /// What to do with this world's network stream. + public ConnectionMode Mode; + + /// Dotted IPv4 to connect to (Join). Ignored for Host (binds AnyIpv4). + public FixedString64Bytes Address; + + /// Listen/connect port. + public ushort Port; + + /// Set true to request the control system act on Mode this frame; it clears the flag. + public bool Requested; + } +} diff --git a/Assets/_Project/Scripts/Simulation/Connection/ConnectionConfig.cs.meta b/Assets/_Project/Scripts/Simulation/Connection/ConnectionConfig.cs.meta new file mode 100644 index 000000000..199e60d97 --- /dev/null +++ b/Assets/_Project/Scripts/Simulation/Connection/ConnectionConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 85356726c8f524aebb6cc93336fa09dd \ No newline at end of file diff --git a/Assets/_Project/Scripts/Simulation/Connection/EditorAutoHostSystem.cs b/Assets/_Project/Scripts/Simulation/Connection/EditorAutoHostSystem.cs new file mode 100644 index 000000000..d5bd02300 --- /dev/null +++ b/Assets/_Project/Scripts/Simulation/Connection/EditorAutoHostSystem.cs @@ -0,0 +1,35 @@ +#if UNITY_EDITOR +using Unity.Entities; +using Unity.NetCode; + +namespace ProjectM.Simulation +{ + /// + /// EDITOR-ONLY dev convenience for the M4 LAN flow. With auto-connect disabled in GameBootstrap, + /// this seeds a in each world on its first update so entering Play "just + /// works" multi-client: the server world hosts on loopback and every client / thin-client world joins + /// it — reproducing the old single-key playflow but for N players. Runs once per world then disables + /// itself, and skips seeding if a already exists (e.g. set by + /// ConnectionUI). Does not exist in player builds. + /// + [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation | WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation)] + public partial struct EditorAutoHostSystem : ISystem + { + public void OnUpdate(ref SystemState state) + { + state.Enabled = false; // run exactly once per world + + if (SystemAPI.HasSingleton()) + return; + + const ushort port = 7979; + var cfg = state.WorldUnmanaged.IsServer() + ? new ConnectionConfig { Mode = ConnectionMode.Host, Address = "127.0.0.1", Port = port, Requested = true } + : new ConnectionConfig { Mode = ConnectionMode.Join, Address = "127.0.0.1", Port = port, Requested = true }; + + var e = state.EntityManager.CreateEntity(); + state.EntityManager.AddComponentData(e, cfg); + } + } +} +#endif diff --git a/Assets/_Project/Scripts/Simulation/Connection/EditorAutoHostSystem.cs.meta b/Assets/_Project/Scripts/Simulation/Connection/EditorAutoHostSystem.cs.meta new file mode 100644 index 000000000..2e72d1376 --- /dev/null +++ b/Assets/_Project/Scripts/Simulation/Connection/EditorAutoHostSystem.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 642fc5c4d71654becba10814a2d08e92 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Simulation/GameBootstrap.cs b/Assets/_Project/Scripts/Simulation/GameBootstrap.cs index edaf6edba..9783b8b26 100644 --- a/Assets/_Project/Scripts/Simulation/GameBootstrap.cs +++ b/Assets/_Project/Scripts/Simulation/GameBootstrap.cs @@ -5,21 +5,24 @@ using UnityEngine.Scripting; namespace ProjectM.Simulation { /// - /// Custom Netcode for Entities bootstrap. Subclassing - /// gives an explicit hook to customize world creation, tick rate, and auto-connect. - /// For now it reproduces the default behavior: create separate client and server worlds - /// based on the Multiplayer PlayMode Tools settings, without auto-connecting. + /// Custom Netcode for Entities bootstrap. Subclassing gives an + /// explicit hook to customize world creation, tick rate, and connection. + /// + /// M4 (LAN co-op): auto-connect is DISABLED ( = 0). + /// Listening/connecting is driven explicitly via the singleton and the + /// per-world ConnectionControlSystems — from ConnectionUI (Host / Join + IP) in player builds, + /// or from the editor-only EditorAutoHostSystem, which auto-hosts on loopback and connects the + /// in-proc client plus any Multiplayer-PlayMode-Tools thin clients. Direct IP/LAN only for now; Unity + /// Relay is deferred to a later pass. + /// /// [Preserve] public class GameBootstrap : ClientServerBootstrap { public override bool Initialize(string defaultWorldName) { - // Auto-connect in-editor: the server listens and the in-process client connects (over - // IPC) on the default BinaryWorlds host mode — one process hosts both worlds (M1 listen - // server). M3 replaces this with an explicit Unity Relay host/join flow. Set to 0 to - // disable auto-connect. - AutoConnectPort = 7979; + // No auto-connect: ConnectionConfig + the ConnectionControlSystems own listen/connect (M4). + AutoConnectPort = 0; CreateDefaultClientServerWorlds(); return true; } diff --git a/Assets/_Project/Scripts/Simulation/Spawning/PlayerSpawnMath.cs b/Assets/_Project/Scripts/Simulation/Spawning/PlayerSpawnMath.cs new file mode 100644 index 000000000..119180c3d --- /dev/null +++ b/Assets/_Project/Scripts/Simulation/Spawning/PlayerSpawnMath.cs @@ -0,0 +1,41 @@ +using Unity.Mathematics; + +namespace ProjectM.Simulation +{ + /// + /// Deterministic spawn-position math for co-op. Pure and side-effect-free — no RNG, no wall-clock — + /// so the same NetworkId always yields the same planar offset. The server places each connecting + /// player at a stable ring slot keyed by its NetworkId, so players never stack. Unit-tested in + /// EditMode (no netcode world required). + /// + public static class PlayerSpawnMath + { + /// + /// Planar (XZ) offset from the base spawn point for a connection's NetworkId. NetworkIds start at + /// 1, so id 1 maps to slot 0. Slots are evenly spaced around a ring of ; + /// once a ring's slots are full, further players spill onto an outer ring (radius * (ring + 1)), + /// keeping every position distinct. Returns zero when <= 0 (degenerate + /// or unbaked spawner) so behaviour falls back to the single shared spawn point. + /// + public static float3 SpawnOffset(int networkId, float radius, int slots) + { + if (radius <= 0f) + return float3.zero; + if (slots < 1) + slots = 1; + + int idx = networkId - 1; + if (idx < 0) + idx = 0; + + int ring = idx / slots; + int slotInRing = idx % slots; + + float angle = (2f * math.PI * slotInRing) / slots; + float r = radius * (ring + 1); + + math.sincos(angle, out float s, out float c); + return new float3(c * r, 0f, s * r); + } + } +} diff --git a/Assets/_Project/Scripts/Simulation/Spawning/PlayerSpawnMath.cs.meta b/Assets/_Project/Scripts/Simulation/Spawning/PlayerSpawnMath.cs.meta new file mode 100644 index 000000000..8942acc3c --- /dev/null +++ b/Assets/_Project/Scripts/Simulation/Spawning/PlayerSpawnMath.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: cabd6efb0aaf14e3a95c47c95bc4160c \ No newline at end of file diff --git a/Assets/_Project/Scripts/Simulation/Spawning/PlayerSpawner.cs b/Assets/_Project/Scripts/Simulation/Spawning/PlayerSpawner.cs index 9c7adeeb2..024848a24 100644 --- a/Assets/_Project/Scripts/Simulation/Spawning/PlayerSpawner.cs +++ b/Assets/_Project/Scripts/Simulation/Spawning/PlayerSpawner.cs @@ -11,5 +11,10 @@ namespace ProjectM.Simulation { public Entity PlayerPrefab; public float3 SpawnPoint; + + // M4 co-op: deterministic per-NetworkId ring spread so players don't stack on connect. + // Radius of the spawn ring (metres); RingSlots = evenly-spaced positions before rings expand. + public float SpawnRingRadius; + public int RingSlots; } } diff --git a/Assets/_Project/Tests/EditMode/PlayerSpawnRingTests.cs b/Assets/_Project/Tests/EditMode/PlayerSpawnRingTests.cs new file mode 100644 index 000000000..698dbc02c --- /dev/null +++ b/Assets/_Project/Tests/EditMode/PlayerSpawnRingTests.cs @@ -0,0 +1,87 @@ +using NUnit.Framework; +using ProjectM.Simulation; +using Unity.Mathematics; + +namespace ProjectM.Tests +{ + /// + /// Pure-function tests for (no ECS world), mirroring + /// StatMathTests. Pins the M4 co-op deterministic spawn spread: stable, distinct per-NetworkId ring + /// positions so players never stack on connect. + /// + public class PlayerSpawnRingTests + { + const float R = 2.5f; + const int Slots = 4; + const float Eps = 1e-4f; + + [Test] + public void Deterministic_SameInputs_SameOffset() + { + var a = PlayerSpawnMath.SpawnOffset(2, R, Slots); + var b = PlayerSpawnMath.SpawnOffset(2, R, Slots); + Assert.AreEqual(a.x, b.x, Eps); + Assert.AreEqual(a.y, b.y, Eps); + Assert.AreEqual(a.z, b.z, Eps); + } + + [Test] + public void FirstFour_Land_On_Cardinal_Ring_Slots() + { + // NetworkIds start at 1; slots=4 -> 0, 90, 180, 270 degrees on a ring of radius R. + AssertXz(PlayerSpawnMath.SpawnOffset(1, R, Slots), R, 0f); + AssertXz(PlayerSpawnMath.SpawnOffset(2, R, Slots), 0f, R); + AssertXz(PlayerSpawnMath.SpawnOffset(3, R, Slots), -R, 0f); + AssertXz(PlayerSpawnMath.SpawnOffset(4, R, Slots), 0f, -R); + } + + [Test] + public void Distinct_NetworkIds_Give_Distinct_Positions() + { + var seen = new System.Collections.Generic.List(); + for (int id = 1; id <= 8; id++) + { + var p = PlayerSpawnMath.SpawnOffset(id, R, Slots); + foreach (var q in seen) + Assert.Greater(math.distance(p, q), 1e-3f, $"id {id} collides with an earlier slot"); + seen.Add(p); + } + } + + [Test] + public void FifthPlayer_Spills_To_Outer_Ring() + { + // idx 4 with slots 4 -> ring 1, slot 0 -> radius doubled, angle 0. + AssertXz(PlayerSpawnMath.SpawnOffset(5, R, Slots), 2f * R, 0f); + } + + [Test] + public void NonPositiveRadius_Returns_Zero() + { + Assert.AreEqual(0f, math.length(PlayerSpawnMath.SpawnOffset(3, 0f, Slots)), Eps); + Assert.AreEqual(0f, math.length(PlayerSpawnMath.SpawnOffset(3, -1f, Slots)), Eps); + } + + [Test] + public void DegenerateSlots_DoNotThrow_AndStayDistinct() + { + // slots < 1 is clamped to 1 (every player on its own concentric ring). + var a = PlayerSpawnMath.SpawnOffset(1, R, 0); + var b = PlayerSpawnMath.SpawnOffset(2, R, 0); + Assert.Greater(math.distance(a, b), 1e-3f); + } + + [Test] + public void Offset_Is_Planar_NoVerticalComponent() + { + for (int id = 1; id <= 6; id++) + Assert.AreEqual(0f, PlayerSpawnMath.SpawnOffset(id, R, Slots).y, Eps); + } + + static void AssertXz(float3 p, float x, float z) + { + Assert.AreEqual(x, p.x, Eps); + Assert.AreEqual(z, p.z, Eps); + } + } +} diff --git a/Assets/_Project/Tests/EditMode/PlayerSpawnRingTests.cs.meta b/Assets/_Project/Tests/EditMode/PlayerSpawnRingTests.cs.meta new file mode 100644 index 000000000..896fbffff --- /dev/null +++ b/Assets/_Project/Tests/EditMode/PlayerSpawnRingTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6578b95ff4c4c40c780ec25b56e28eff \ No newline at end of file diff --git a/Docs/Vault/06_Roadmap/Backlog.md b/Docs/Vault/06_Roadmap/Backlog.md index a5e822603..4bc1623a9 100644 --- a/Docs/Vault/06_Roadmap/Backlog.md +++ b/Docs/Vault/06_Roadmap/Backlog.md @@ -2,7 +2,7 @@ tags: - roadmap - backlog -updated: 2026-05-29 +updated: 2026-06-01 permalink: gamevault/06-roadmap/backlog --- @@ -15,7 +15,7 @@ Unordered pool of candidate work. Promote to a [[Milestones|milestone]] when com - [ ] **Re-validate the M1 play-tick on a stable Unity 6.x** — live runtime blocked on the 6.6 alpha ([[DR-002_Unity66_Alpha_Netcode_Transport]]); optionally reproduce with the `networked-cube` sample to file a bug. - [ ] Replace template `SampleScene` with a dedicated bootstrap scene + gameplay subscene. - [ ] Optional template cleanup: remove `com.unity.visualscripting`, `Assets/TutorialInfo/`, `Assets/Readme.asset` (delete each asset **with** its `.meta`). -- [ ] Decide **relay provider** (default Unity Relay) before M4 (co-op). +- [x] Decide **relay provider** before M4 — resolved: **Direct IP/LAN now, Unity Relay later** ([[DR-005_M4_Connection_Model_Direct_IP]], [[2026-06-01_M4_LAN_CoOp_And_Classification_Fix]]). - [ ] Decide home-base **grid 2D vs 3D** before M6 (build/placement). - [ ] Decide **production replication** (predicted vs server-only) before M7 (automation). - [ ] **M2 follow-up — restart the editor to clear the corrupted Burst cache**, then confirm the console is clean on a warm play (no "not a known Burst entry point"). See [[2026-05-31_M2_Combat]] / [[DR-003_M2_Combat_Netcode_Architecture]]. diff --git a/Docs/Vault/06_Roadmap/Milestones.md b/Docs/Vault/06_Roadmap/Milestones.md index ccb88adce..fe8d25e1c 100644 --- a/Docs/Vault/06_Roadmap/Milestones.md +++ b/Docs/Vault/06_Roadmap/Milestones.md @@ -2,7 +2,7 @@ tags: - roadmap - milestones -updated: 2026-05-29 +updated: 2026-06-01 permalink: gamevault/06-roadmap/milestones --- @@ -12,9 +12,9 @@ permalink: gamevault/06-roadmap/milestones |---|---|---| | **M0 — Foundation** | DOTS + Netcode stack, asmdef split, bootstrap, smoke test green | ✅ Done 2026-05-29 — [[2026-05-29_Project_Setup]] | | **M1 — Player slice** | Server-spawned owner-predicted player; twin-stick WASD + directional aim | ✅ Done 2026-05-31 — runtime-validated on Unity 6.4.7 (connect→spawn→owner-predicted ghost→replication; EditMode 3/3). The 6.6 failure was environment-specific, see [[DR-002_Unity66_Alpha_Netcode_Transport]] — [[2026-05-30_M1_Player_Slice]] | -| **M2 — Combat** | Directional ability fire + deterministic soft auto-target; server-authoritative damage/health | ✅ Done 2026-05-31 — runtime-validated on 6.4.7: input→fire→**predicted projectile**→**swept hit**→server damage→`Health` `[GhostField]` replicated server→client; movement + fire confirmed live; EditMode 22/22. Predicted-projectile + server auto-target + non-Burst classifier — [[DR-003_M2_Combat_Netcode_Architecture]], [[2026-05-31_M2_Combat]]. (Projectile ghost-map errors appear only under server tick-batching from running two editors at once — close the reference editor for clean netcode.) | +| **M2 — Combat** | Directional ability fire + deterministic soft auto-target; server-authoritative damage/health | ✅ Done 2026-05-31 — runtime-validated on 6.4.7: input→fire→**predicted projectile**→**swept hit**→server damage→`Health` `[GhostField]` replicated server→client; movement + fire confirmed live; EditMode 22/22. Predicted-projectile + server auto-target + non-Burst classifier — [[DR-003_M2_Combat_Netcode_Architecture]], [[2026-05-31_M2_Combat]]. (Projectile ghost-map errors were later root-caused to a `[ReadOnly]`-write in `ProjectileClassificationSystem` — fixed 2026-06-01, see [[2026-06-01_M4_LAN_CoOp_And_Classification_Fix]] — NOT two-editor tick-batching as first thought.) | | **M3 — Data-driven abilities & modifiers** | Ability **and** character stats authored in ScriptableObjects, baked to DOTS **blob assets**; runtime **flat + % modifier** stacks (upgrades/buffs) → effective stats, server-authoritative + prediction-correct. Pattern slice: refactor the current projectile ability + 1–2 sample abilities onto the data model. | ✅ Done 2026-05-31 — runtime-validated on 6.4.7: blob DB baked into both worlds; data-driven base + replicated `StatModifier` ghost buffer → **identical effective stats on server & owner-predicted client** (held under tick-batching); data-only ability swap; real pickup grant; EditMode 38/38. Blob DB + replicated modifier buffer + every-tick effective recompute — [[DR-004_M3_DataDriven_Abilities_Modifiers]], [[2026-05-31_M3_Data_Driven_Abilities]]. | -| **M4 — Co-op** | 2–4 players; client-hosted listen-server over Unity Relay | ⬜ | +| **M4 — Co-op** | 2–4 players; client-hosted listen-server (Direct IP/LAN now, Unity Relay later) | 🚧 In progress 2026-06-01 — **LAN slice done + runtime-validated**: no-auto-connect `ConnectionConfig` + request-component host/join, editor auto-host + thin clients, deterministic ring spawn; 3 clients (1 real + 2 thin) connect→spawn (distinct slots)→replicate→clean disconnect; `ConnectionUI` for builds; EditMode 45/45. **Unity Relay + real two-build LAN join deferred** — [[DR-005_M4_Connection_Model_Direct_IP]], [[2026-06-01_M4_LAN_CoOp_And_Classification_Fix]]. | | **M5 — Home base + physics** | Persistent base subscene streaming + Unity Physics in the predicted loop | ⬜ | | **M6 — Build/placement** | Server-authoritative grid build placement via RPC | ⬜ | | **M7 — Automation** | Self-running tick-based production chains (deterministic offline catch-up) | ⬜ | diff --git a/Docs/Vault/07_Sessions/2026/2026-06-01_M4_LAN_CoOp_And_Classification_Fix.md b/Docs/Vault/07_Sessions/2026/2026-06-01_M4_LAN_CoOp_And_Classification_Fix.md new file mode 100644 index 000000000..5c42fc3d4 --- /dev/null +++ b/Docs/Vault/07_Sessions/2026/2026-06-01_M4_LAN_CoOp_And_Classification_Fix.md @@ -0,0 +1,53 @@ +--- +date: 2026-06-01 +type: session +tags: [session, dots, netcode, m4, co-op, lan, connection, bugfix, prediction] +permalink: gamevault/07-sessions/2026/2026-06-01-m4-lan-co-op-and-classification-fix +--- + +# Session 2026-06-01 — M4 LAN Co-op kickoff + projectile-classification cascade fix + +## Goal + +Two parts: (1) **fix the runtime console error cascade** that fired "when moving around and shooting" left by the M2/M3 work, and (2) start **M4 — Co-op**. Operator-scoped this pass to **playable LAN co-op** (multi-client correctness + a Host/Join connection flow with host-IP entry so a standalone build can join over LAN); transport **Direct IP/LAN now, Unity Relay deferred**. Architecture locked in [[DR-005_M4_Connection_Model_Direct_IP]]. + +## Part 1 — Projectile-classification cascade (root-caused + fixed) + +**The "ton of errors" was one bug.** `ProjectileClassificationSystem` declared its `PredictedGhostSpawn` buffer lookup `[ReadOnly]` (`GetBufferLookup(true)` + `[ReadOnly]` on the job field) but the job **writes** it via `predictedSpawnList.RemoveAtSwapBack(j)` on a match. So **every projectile spawn** threw `InvalidOperationException: …[ReadOnly]… writing to it` at `ProjectileClassificationSystem.cs:177`, which aborted classification → the predicted spawn was never paired → duplicate ghost → `Found a ghost … does not have an entity connected` → `Received baseline for a ghost we do not have` → `reset their entire ack history` → `Ghost ID n already added to the spawned ghost map` → **server tick batching**. One root cause, full cascade. + +**Fix (3 lines):** `GetBufferLookup(false)` + drop `[ReadOnly]` on the field (matches the default `DefaultGhostSpawnClassificationSystem`, which removes matched entries from the list; `ProjectileLookup` stays `[ReadOnly]`). The deliberate non-`[BurstCompile]` decision is unchanged. + +**This corrects the M2 misdiagnosis.** [[2026-05-31_M2_Combat]] / [[Milestones]] attributed these ghost-map errors to "server tick-batching from running two editors at once." That was wrong — the tick-batching was a *downstream symptom* of the `[ReadOnly]` write, not the cause. Validated: a single in-editor client, moving + firing, now produces **zero** of the five cascade signatures while projectiles spawn and classify (cooldown advances across fires). Residual `Server Tick Batching` warnings are the unfocused-background-editor perf artifact (sim ticks faster than it renders, 1.25–1.75 ticks/frame) — **not** Burst-cache corruption (no "not a known Burst entry point") and not the cascade; they clear when the Game view is focused / in a build. + +## Part 2 — M4 Playable LAN Host/Join (Direct IP) — see [[DR-005_M4_Connection_Model_Direct_IP]] + +Reused the existing per-connection spawn + `LinkedEntityGroup` auto-despawn (already N-player-ready). Added: + +- **Simulation:** `ConnectionConfig` singleton (`ConnectionMode {None,Host,Join}` + address/port/Requested); `EditorAutoHostSystem` (`#if UNITY_EDITOR`, once per world, seeds Host(loopback) on server + Join(loopback) on client/thin worlds, self-disables); `PlayerSpawnMath.SpawnOffset` (pure ring-slot math); `PlayerSpawner` gained `SpawnRingRadius`/`RingSlots`. +- **Server:** `ServerConnectionControlSystem` (Host → `NetworkStreamRequestListen{AnyIpv4:port}`); `GoInGameServerSystem` now applies the deterministic per-`NetworkId` ring offset to the spawn. +- **Client:** `ClientConnectionControlSystem` (Join → `NetworkStreamRequestConnect{Parse}`, runs in client **and thin** worlds); `ConnectionUI` IMGUI Host/Join+IP panel (build entry point; hides once connected). +- **Authoring/scene:** `PlayerSpawnerAuthoring` bakes radius/slots (default 2.5/4); `Gameplay.unity` re-baked; `NetConnectionUI` GameObject (with `ConnectionUI`) added to `SampleScene` + saved. +- **Bootstrap:** `GameBootstrap.AutoConnectPort 7979 → 0` (connection now explicit). +- **Asmdefs:** added `Unity.Networking.Transport` to `ProjectM.Client` + `ProjectM.Server` (needed to name `NetworkEndpoint`; transitive-via-NetCode doesn't satisfy the compiler — Unity.Transforms-class gotcha). + +### Validation +- **EditMode 45/45 green** (38 prior + 7 new `PlayerSpawnRingTests`); existing M1/M2/M3 suites unaffected. +- **Single-client (make-or-break):** no-auto-connect + `EditorAutoHostSystem` + request components → in-proc client connects to loopback, gets `NetworkId`, server spawns its player at `(2.5,0)` (NetworkId-1 ring slot, not origin). Subscene re-bake confirmed (`spawner.radius=2.5 slots=4`). +- **3-client co-op (1 real + 2 thin):** server `conns=3 inGame=3 players=3` at distinct ring slots `(2.5,0)/(0,2.5)/(-2.5,0)` — no stacking; the real `ClientWorld` sees all 3 (own owner-predicted + 2 interpolated); thin worlds connect (`conns=1` each, `players=0` locally as expected). Continuous movement replicates (server≈client with prediction lead). **Console clean of all five cascade signatures** under multi-client load + firing. +- **Disconnect:** `NetworkStreamRequestDisconnect` on a thin connection → server + client drop to `players=2` via `LinkedEntityGroup`. + +### Method +context7-led API confirm (`NetworkStreamRequestListen/Connect`, `NetworkEndpoint`, `ClientServerBootstrap.Create*World`, thin-client prefs) → plan-gated → compile-checkpointed clusters with `read_console` after each. Part 1 isolated + validated first so the fix couldn't be masked by the connection refactor. MCP `script_apply_edits` (anchor) for edits, `create_script` for new files, `Write` for the full `GameBootstrap` rewrite, `execute_code` for runtime world inspection + input injection. + +## Decisions +- [[DR-005_M4_Connection_Model_Direct_IP]] — no auto-connect; `ConnectionConfig` + request-component control systems; editor auto-host vs build UI; deterministic ring spawn; **Direct IP/LAN now, Unity Relay deferred** (closes the [[Backlog]] relay-provider blocker for this slice). + +## Open / deferred +- **Unity Relay transport** — layer onto the same `ConnectionConfig` flow (relay allocation + join code feeding the endpoint); needs Unity Gaming Services. Deferred. +- **Real two-build LAN join** — operator-side: build a ClientServer host + a Client player, run on one LAN, Join-by-IP. The in-editor path (incl. thin clients) is validated; the standalone build join is not yet exercised this session. +- **One-shot `Fire` under tick-batching** — continuous input replicates fine, but single-shot `Fire` events can drop in the unfocused editor; focus the Game view (or a build) for reliable fire validation. Pre-existing artifact, not introduced here. +- **`[ReadOnly]` regression lock** — a reflection-based EditMode guard would need `ProjectM.Client` in the test asmdef (pulls InputSystem/Graphics); skipped in favour of the runtime console-clean proof. Reconsider if the field regresses. +- **Spawn-point variety** — single ring around one `SpawnPoint`; fine for 2–4. Per-team/spread layouts later. + +## Next +Either (a) **Unity Relay transport** to make M4 remote-playable (layer on `ConnectionConfig`), or (b) advance to **M5 — Home base + physics** per [[Milestones]]. Recommend a quick real-LAN two-build smoke test first to confirm the `ConnectionUI` join path end-to-end. diff --git a/Docs/Vault/07_Sessions/_Decisions/DR-005_M4_Connection_Model_Direct_IP.md b/Docs/Vault/07_Sessions/_Decisions/DR-005_M4_Connection_Model_Direct_IP.md new file mode 100644 index 000000000..0aeab1cfa --- /dev/null +++ b/Docs/Vault/07_Sessions/_Decisions/DR-005_M4_Connection_Model_Direct_IP.md @@ -0,0 +1,37 @@ +--- +id: DR-005 +title: M4 connection model — explicit ConnectionConfig + netcode request components; Direct IP/LAN now, Unity Relay deferred +status: accepted +date: 2026-06-01 +tags: +- decision +- netcode +- connection +- co-op +- lan +permalink: gamevault/07-sessions/decisions/dr-005-m4-connection-model-direct-ip +--- + +# DR-005 — M4 Connection Model (Direct IP/LAN; Relay deferred) + +## Context + +M4 ([[Milestones]]) = co-op for 2–4 players. The roadmap framed it as "client-hosted listen-server over Unity Relay," and [[Backlog]] flagged **"decide relay provider"** as a blocker. The operator scoped this pass to **playable LAN co-op** with transport **Direct IP/LAN now, Unity Relay deferred**. Until now `GameBootstrap` hard-coded `AutoConnectPort = 7979` → a single in-proc client auto-connected over IPC; there was no host-vs-join choice, no multi-client path, and all players spawned on one point. The existing per-connection spawn (`GoInGameServerSystem` stamps `GhostOwner`, links the player to the connection's `LinkedEntityGroup` for auto-despawn) already generalised to N players — only connection establishment + spawn spread were missing. Settled at intake, validated against context7 (Netcode 1.13.2) + runtime. Extends [[DR-003_M2_Combat_Netcode_Architecture]]. + +## Decision + +1. **No auto-connect.** `GameBootstrap.AutoConnectPort = 0` (was 7979); worlds are created idle and wait for an explicit listen/connect. +2. **Connection driven by a `ConnectionConfig` singleton + per-world control systems.** `ConnectionConfig { ConnectionMode Mode; FixedString64Bytes Address; ushort Port; bool Requested }` (Simulation, created per world). `ServerConnectionControlSystem` (ServerSimulation) turns a Host request into a `NetworkStreamRequestListen { AnyIpv4:port }`; `ClientConnectionControlSystem` (ClientSimulation | ThinClientSimulation) turns a Join request into a `NetworkStreamRequestConnect { Parse(addr,port) }`. Both clear `Requested`. **Request components, NOT manual `NetworkStreamDriver.Listen/Connect`** — race-free (netcode's receive system acts once the driver store is ready) and identical across server / client / thin worlds. +3. **Two entry points.** Player builds: `ConnectionUI` (IMGUI Host / Join+IP, in `ProjectM.Client`) writes `ConnectionConfig` into `ClientServerBootstrap.ServerWorld` / `ClientWorld`; hides once connected; Host shown only where a server world exists. Editor: `EditorAutoHostSystem` (`#if UNITY_EDITOR`, runs once per world, then self-disables) seeds Host(loopback) in the server world and Join(loopback) in the client + **every** thin-client world — reproducing the old zero-config playflow, now multi-client. +4. **Deterministic spawn spread.** `PlayerSpawnMath.SpawnOffset(networkId, radius, slots)` (pure, in Simulation) places each connection on a ring slot keyed by `NetworkId` (slots evenly spaced; spills to concentric outer rings past `slots`); `GoInGameServerSystem` applies `SpawnPoint + offset`. Tunable via baked `PlayerSpawner.SpawnRingRadius` / `RingSlots` (default 2.5 / 4). Server-only, no RNG — unit-tested in `PlayerSpawnRingTests`. +5. **Transport: Direct IP/LAN now; Unity Relay deferred.** Closes the [[Backlog]] "decide relay provider" blocker for this slice: the eventual remote transport is **Unity Relay**, layered later on top of the same `ConnectionConfig` flow (swap the endpoint source for a relay allocation + join code). The request-component path is transport-agnostic, so the gameplay/connection code does not change. + +## Consequences + +- **Thin-client connect is the load-bearing surface, and it works.** With `AutoConnectPort=0` thin worlds connect only because `ClientConnectionControlSystem` carries `ThinClientSimulation` and `EditorAutoHostSystem` seeds each thin world. Runtime-validated: 3 clients (1 real + 2 thin) → server `conns=3 players=3` at distinct ring slots `(2.5,0)/(0,2.5)/(-2.5,0)`; the real client sees all three (own owner-predicted + two interpolated); a clean disconnect (`NetworkStreamRequestDisconnect`) drops server **and** client to 2 via the `LinkedEntityGroup` path. No ghost-classification errors under multi-client load + firing. +- **`Unity.Networking.Transport` is now a direct asmdef reference** on `ProjectM.Client` + `ProjectM.Server` (`NetworkEndpoint` lives there; transitive-via-`Unity.NetCode` visibility does NOT satisfy the compiler — same class of gotcha as `Unity.Transforms`). +- **Editor vs build divergence is intentional and small:** auto-host in editor, manual UI in builds. In-editor the request path is still exercised (auto-host uses it), so it is not an untested code path. +- **Relay deferral keeps this pass service-free** (no Unity Gaming Services dependency; fully validated in-session). Revisit when remote (non-LAN) play is needed: add a relay-allocation step feeding `ConnectionConfig` endpoints, then re-test connect + the connect-retry window. +- **Connect ordering relies on transport's connect-retry:** both server-listen and client-connect are seeded ~frame 1, so a client may issue connect before the server is listening; the transport retries until the listener is up (fine over loopback/LAN). Watch this if Relay introduces longer setup latency. + +Mirrors the server-authoritative + small-co-op (2–4, listen-server) pillars from [[Pillars]]. diff --git a/Packages/manifest.json b/Packages/manifest.json index 33f4dc6c5..4607143e2 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -10,10 +10,13 @@ "com.unity.ide.visualstudio": "2.0.27", "com.unity.inputsystem": "1.19.0", "com.unity.multiplayer.center": "2.0.0", + "com.unity.multiplayer.playmode": "2.0.2", + "com.unity.multiplayer.tools": "2.2.8", "com.unity.netcode": "1.13.2", "com.unity.physics": "1.4.6", "com.unity.probuilder": "6.0.9", "com.unity.render-pipelines.universal": "17.6.0", + "com.unity.services.multiplayer": "2.1.3", "com.unity.test-framework": "1.8.0", "com.unity.timeline": "1.8.12", "com.unity.ugui": "2.6.0", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 6e93b120b..2a0b3463b 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -145,6 +145,30 @@ "com.unity.modules.uielements": "1.0.0" } }, + "com.unity.multiplayer.playmode": { + "version": "2.0.2", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.nuget.newtonsoft-json": "2.0.2" + }, + "url": "https://packages.unity.com" + }, + "com.unity.multiplayer.tools": { + "version": "2.2.8", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.burst": "1.8.18", + "com.unity.collections": "2.5.1", + "com.unity.mathematics": "1.3.2", + "com.unity.profiling.core": "1.0.2", + "com.unity.nuget.mono-cecil": "1.11.4", + "com.unity.modules.uielements": "1.0.0", + "com.unity.nuget.newtonsoft-json": "3.2.1" + }, + "url": "https://packages.unity.com" + }, "com.unity.netcode": { "version": "1.13.2", "depth": 0, @@ -262,6 +286,87 @@ }, "url": "https://packages.unity.com" }, + "com.unity.services.authentication": { + "version": "3.6.1", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.ugui": "1.0.0", + "com.unity.services.core": "1.15.1", + "com.unity.nuget.newtonsoft-json": "3.2.1", + "com.unity.modules.unitywebrequest": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.services.core": { + "version": "1.16.0", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.modules.androidjni": "1.0.0", + "com.unity.nuget.newtonsoft-json": "3.2.1", + "com.unity.modules.unitywebrequest": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.services.deployment": { + "version": "1.7.2", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.services.core": "1.15.1", + "com.unity.services.deployment.api": "1.1.2" + }, + "url": "https://packages.unity.com" + }, + "com.unity.services.deployment.api": { + "version": "1.1.3", + "depth": 2, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.services.multiplayer": { + "version": "2.1.3", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.transport": "2.6.0", + "com.unity.collections": "2.2.1", + "com.unity.services.qos": "1.4.1", + "com.unity.services.core": "1.16.0", + "com.unity.services.wire": "1.4.1", + "com.unity.services.deployment": "1.7.1", + "com.unity.nuget.newtonsoft-json": "3.2.2", + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.services.authentication": "3.6.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.services.qos": { + "version": "1.4.1", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.collections": "1.2.4", + "com.unity.services.core": "1.12.5", + "com.unity.nuget.newtonsoft-json": "3.0.2", + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.services.authentication": "3.5.2" + }, + "url": "https://packages.unity.com" + }, + "com.unity.services.wire": { + "version": "1.4.2", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.services.core": "1.12.5", + "com.unity.nuget.newtonsoft-json": "3.2.1", + "com.unity.services.authentication": "2.7.4" + }, + "url": "https://packages.unity.com" + }, "com.unity.settings-manager": { "version": "2.1.1", "depth": 1, diff --git a/ProjectSettings/VirtualProjectsConfig.json b/ProjectSettings/VirtualProjectsConfig.json new file mode 100644 index 000000000..e76ea5ed1 --- /dev/null +++ b/ProjectSettings/VirtualProjectsConfig.json @@ -0,0 +1,4 @@ +{ + "PlayerTags": [], + "version": "6000.4.7f1" +} \ No newline at end of file