Co-Op Layer
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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<PlayerSpawnerAuthoring>
|
||||
{
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ namespace ProjectM.Client
|
||||
SystemAPI.GetSingletonEntity<GhostCollection>(),
|
||||
SystemAPI.GetSingletonEntity<SpawnedGhostEntityMap>());
|
||||
|
||||
m_PredictedGhostSpawnLookup = state.GetBufferLookup<PredictedGhostSpawn>(true);
|
||||
m_PredictedGhostSpawnLookup = state.GetBufferLookup<PredictedGhostSpawn>(false);
|
||||
m_ProjectileLookup = state.GetComponentLookup<Projectile>(true);
|
||||
|
||||
state.RequireForUpdate<GhostSpawnQueue>();
|
||||
@@ -134,7 +134,7 @@ namespace ProjectM.Client
|
||||
// is NOT marked [ReadOnly].
|
||||
public Entity PredictedSpawnListEntity;
|
||||
|
||||
[ReadOnly] public BufferLookup<PredictedGhostSpawn> PredictedGhostSpawnLookup;
|
||||
public BufferLookup<PredictedGhostSpawn> PredictedGhostSpawnLookup;
|
||||
[ReadOnly] public ComponentLookup<Projectile> ProjectileLookup;
|
||||
|
||||
// 'data' is taken by value (NOT 'in') because TryGetComponentDataFromSnapshotHistory needs a
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Client/thin-client half of the M4 LAN join flow. When the per-world <see cref="ConnectionConfig"/>
|
||||
/// requests <see cref="ConnectionMode.Join"/>, creates a <see cref="NetworkStreamRequestConnect"/>
|
||||
/// 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: <c>NetworkEndpoint.TryParse</c>
|
||||
/// takes a managed string and this is a cold path.
|
||||
/// </summary>
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation)]
|
||||
public partial struct ClientConnectionControlSystem : ISystem
|
||||
{
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<ConnectionConfig>();
|
||||
}
|
||||
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var cfgRef = SystemAPI.GetSingletonRW<ConnectionConfig>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c274f15ad41814f8d8337f786590313d
|
||||
@@ -0,0 +1,79 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <see cref="ConnectionConfig"/> request into the
|
||||
/// relevant netcode world(s): <b>Host</b> makes the server world listen and the local client join over
|
||||
/// loopback; <b>Join</b> 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 <c>EditorAutoHostSystem</c> usually connects first, so this panel just shows "Connected".
|
||||
/// </summary>
|
||||
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<NetworkId>());
|
||||
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<ConnectionConfig>());
|
||||
Entity e = q.IsEmpty ? em.CreateEntity(typeof(ConnectionConfig)) : q.GetSingletonEntity();
|
||||
em.SetComponentData(e, new ConnectionConfig
|
||||
{
|
||||
Mode = mode,
|
||||
Address = address,
|
||||
Port = port,
|
||||
Requested = true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 977a7574cf6ff4898b7e9db6c21368f9
|
||||
@@ -10,7 +10,8 @@
|
||||
"Unity.Burst",
|
||||
"Unity.NetCode",
|
||||
"Unity.Entities.Graphics",
|
||||
"Unity.InputSystem"
|
||||
"Unity.InputSystem",
|
||||
"Unity.Networking.Transport"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace ProjectM.Server
|
||||
var networkId = SystemAPI.GetComponent<NetworkId>(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.
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
using Unity.Networking.Transport;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-world half of the M4 LAN host flow. When the per-world <see cref="ConnectionConfig"/>
|
||||
/// requests <see cref="ConnectionMode.Host"/>, creates a <see cref="NetworkStreamRequestListen"/>
|
||||
/// entity (netcode binds the server driver and starts listening) and clears the request. Replaces the
|
||||
/// old <c>AutoConnectPort</c> auto-listen. Not Burst-compiled: a cold path that fires only on an
|
||||
/// explicit host request.
|
||||
/// </summary>
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct ServerConnectionControlSystem : ISystem
|
||||
{
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<ConnectionConfig>();
|
||||
}
|
||||
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var cfgRef = SystemAPI.GetSingletonRW<ConnectionConfig>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5530186288473432f8edcc47c1844e07
|
||||
@@ -8,7 +8,8 @@
|
||||
"Unity.Collections",
|
||||
"Unity.Mathematics",
|
||||
"Unity.Burst",
|
||||
"Unity.NetCode"
|
||||
"Unity.NetCode",
|
||||
"Unity.Networking.Transport"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Connection intent for the M4 LAN host/join flow. Lives in Simulation so both the server and
|
||||
/// client worlds can read it. A UI (<c>ConnectionUI</c>) — or, in the editor, the auto-host system —
|
||||
/// writes the desired <see cref="Mode"/> + endpoint and sets <see cref="Requested"/> true; the
|
||||
/// per-world ConnectionControlSystem turns it into a netcode <c>NetworkStreamRequestListen</c> /
|
||||
/// <c>NetworkStreamRequestConnect</c> and clears <see cref="Requested"/>. Direct IP/LAN only for now —
|
||||
/// Unity Relay is deferred to a later pass. Created per-world as a singleton.
|
||||
/// </summary>
|
||||
public enum ConnectionMode : byte
|
||||
{
|
||||
None,
|
||||
Host,
|
||||
Join,
|
||||
}
|
||||
|
||||
public struct ConnectionConfig : IComponentData
|
||||
{
|
||||
/// <summary>What to do with this world's network stream.</summary>
|
||||
public ConnectionMode Mode;
|
||||
|
||||
/// <summary>Dotted IPv4 to connect to (Join). Ignored for Host (binds AnyIpv4).</summary>
|
||||
public FixedString64Bytes Address;
|
||||
|
||||
/// <summary>Listen/connect port.</summary>
|
||||
public ushort Port;
|
||||
|
||||
/// <summary>Set true to request the control system act on Mode this frame; it clears the flag.</summary>
|
||||
public bool Requested;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85356726c8f524aebb6cc93336fa09dd
|
||||
@@ -0,0 +1,35 @@
|
||||
#if UNITY_EDITOR
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// EDITOR-ONLY dev convenience for the M4 LAN flow. With auto-connect disabled in <c>GameBootstrap</c>,
|
||||
/// this seeds a <see cref="ConnectionConfig"/> 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 <see cref="ConnectionConfig"/> already exists (e.g. set by
|
||||
/// ConnectionUI). Does not exist in player builds.
|
||||
/// </summary>
|
||||
[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<ConnectionConfig>())
|
||||
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
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 642fc5c4d71654becba10814a2d08e92
|
||||
@@ -5,21 +5,24 @@ using UnityEngine.Scripting;
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom Netcode for Entities bootstrap. Subclassing <see cref="ClientServerBootstrap"/>
|
||||
/// 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 <see cref="ClientServerBootstrap"/> gives an
|
||||
/// explicit hook to customize world creation, tick rate, and connection.
|
||||
/// <para>
|
||||
/// M4 (LAN co-op): auto-connect is DISABLED (<see cref="ClientServerBootstrap.AutoConnectPort"/> = 0).
|
||||
/// Listening/connecting is driven explicitly via the <see cref="ConnectionConfig"/> singleton and the
|
||||
/// per-world ConnectionControlSystems — from <c>ConnectionUI</c> (Host / Join + IP) in player builds,
|
||||
/// or from the editor-only <c>EditorAutoHostSystem</c>, 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.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public static class PlayerSpawnMath
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <paramref name="radius"/>;
|
||||
/// once a ring's slots are full, further players spill onto an outer ring (radius * (ring + 1)),
|
||||
/// keeping every position distinct. Returns zero when <paramref name="radius"/> <= 0 (degenerate
|
||||
/// or unbaked spawner) so behaviour falls back to the single shared spawn point.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cabd6efb0aaf14e3a95c47c95bc4160c
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
using NUnit.Framework;
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace ProjectM.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Pure-function tests for <see cref="PlayerSpawnMath.SpawnOffset"/> (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.
|
||||
/// </summary>
|
||||
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<float3>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6578b95ff4c4c40c780ec25b56e28eff
|
||||
@@ -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]].
|
||||
|
||||
@@ -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) | ⬜ |
|
||||
|
||||
@@ -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<PredictedGhostSpawn>(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<PredictedGhostSpawn>(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.
|
||||
@@ -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]].
|
||||
@@ -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",
|
||||
|
||||
Generated
+105
@@ -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,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"PlayerTags": [],
|
||||
"version": "6000.4.7f1"
|
||||
}
|
||||
Reference in New Issue
Block a user