using Unity.Netcode; using UnityEngine; /// /// Per-avatar network glue on the spawned player. The local owner binds the scene singletons /// (GameModeManager, cameras) to itself via ; remote /// avatars stop simulating so their transform is driven purely by the NetworkTransform replication. /// Position-sync milestone: remote avatars still show their mesh/idle pose, just no local control. /// [RequireComponent(typeof(PlayerCharacterController))] [DisallowMultipleComponent] public class NetworkPlayerSetup : NetworkBehaviour { [Header("Spawn placement")] [Tooltip("All avatars spawn from the same prefab position, so without an offset they stack and shove " + "each other on connect. The owner places itself on a ring keyed by its client id (owner-" + "authoritative NetworkTransform replicates it). Id 0 (host) stays at the origin; later joiners " + "fan out. Spacing between ring slots, in world units. 0 disables placement.")] [SerializeField] private float spawnRingRadius = 2.5f; [Tooltip("Slots per ring before the radius steps outward, so spacing stays roughly constant as more join.")] [SerializeField] private int spawnRingSlots = 8; [Header("Role")] [Tooltip("Which side this avatar plays. Baked per prefab variant (Hider on the base/Hider prefab, " + "Hunter on the Hunter prefab); GameModeManager reads it on spawn to build this role's mode set.")] [SerializeField] private PlayerRole role = PlayerRole.Hider; [Tooltip("False = lobby/neutral avatar (the base Player prefab) gets the FULL mode set; true = match " + "avatar (Hunter/Hider variant) gets only its role's modes. Baked per prefab variant, so the " + "mode set follows the spawned avatar with no replicated-phase timing race.")] [SerializeField] private bool scopedModes = false; /// The side this avatar plays (Hunter/Hider), baked into the prefab variant. public PlayerRole Role => role; /// True on the match role variants (scope the mode set to ); false on the /// neutral lobby Player prefab (full mode set). Baked per prefab variant. public bool ScopedModes => scopedModes; public override void OnNetworkSpawn() { var controller = GetComponent(); if (IsOwner) { PlaceOnSpawnRing(); var manager = FindFirstObjectByType(FindObjectsInactive.Include); if (manager) manager.BindLocalPlayer(controller); return; } // Remote avatar: NetworkTransform owns the transform here, so disable the local // simulation (input/movement) and the CharacterController's own depenetration. if (controller) controller.enabled = false; var cc = GetComponent(); if (cc) cc.enabled = false; // Owner cleanup: the concealment meter is a per-frame skinned-mesh bake + voxelization + // physics overlap whose result only feeds the LOCAL player (HUD / gameplay). A remote avatar's // concealment is computed on its own owner's machine, so running it here is pure wasted CPU. var concealment = GetComponentInChildren(true); if (concealment) concealment.enabled = false; // NOTE: SkinnedColliderBaker deliberately stays ENABLED on remotes. MeshPainter's stamp replay // (ApplyNetworkStamp -> DrawBrushGpu) rasterizes the baker's live, per-frame BakedMesh to map a // 3D brush placement into UV space; freezing the bake would mis-map replicated paint onto any // non-rest pose. Its bake is the necessary cost of correct remote paint, not wasted work. } /// /// Offsets the freshly-spawned owner onto a deterministic ring keyed by its client id so avatars /// don't pile up at the shared prefab spawn point. Id 0 (the host) keeps the origin so single-player /// "host alone" is unchanged; every later joiner takes the next ring slot, stepping the radius out /// once a ring fills. Only X/Z are shifted (gravity settles Y), and the CharacterController is toggled /// around the move because it otherwise overrides direct transform writes. Owner-authoritative, so the /// NetworkTransform replicates the placement outward. /// private void PlaceOnSpawnRing() { if (spawnRingRadius <= 0f || spawnRingSlots < 1) return; ulong id = OwnerClientId; if (id == 0) return; // host stays centered ulong k = id - 1; int ring = (int)(k / (ulong)spawnRingSlots); int slot = (int)(k % (ulong)spawnRingSlots); float radius = spawnRingRadius * (ring + 1); // Half-slot stagger per ring so outer slots don't line up radially with inner ones. float angle = (slot / (float)spawnRingSlots + ring * 0.5f / spawnRingSlots) * Mathf.PI * 2f; Vector3 offset = new Vector3(Mathf.Cos(angle), 0f, Mathf.Sin(angle)) * radius; var cc = GetComponent(); bool reenable = cc && cc.enabled; if (cc) cc.enabled = false; transform.position += offset; if (cc) cc.enabled = reenable; } }