Roles are chosen in the bootstrap dev panel before Host/Client. NGO
ConnectionApproval is enabled at runtime and maps the role payload to
the matching prefab variant per connection (Hunter/Hider), so each peer
spawns the correct avatar including the host.
- PlayerRole.cs (new): single source of truth enum { Hider, Hunter }.
- NetworkBootstrap.cs: role toggle, ConnectionApproval + role payload,
server ApprovalCheck mapping role -> PrefabIdHash.
- NetworkPlayerSetup.cs: serialized PlayerRole field + Role property.
- GameModeManager.cs: BuildModes(role) - Hunter -> [First Person, Paint],
Hider -> [Third Person, Paint, Free Cam]; binds role from spawned avatar.
- Hunter.prefab: role = Hunter override.
- SampleScene.unity: NetworkBootstrap prefab references wired.
Also includes user inspector scale tuning (Hunter 2.5->2.4, Hider 0.5->0.7).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
95 lines
4.7 KiB
C#
95 lines
4.7 KiB
C#
using Unity.Netcode;
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// Per-avatar network glue on the spawned player. The local owner binds the scene singletons
|
|
/// (GameModeManager, cameras) to itself via <see cref="GameModeManager.BindLocalPlayer"/>; 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.
|
|
/// </summary>
|
|
[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;
|
|
|
|
/// <summary>The side this avatar plays (Hunter/Hider), baked into the prefab variant.</summary>
|
|
public PlayerRole Role => role;
|
|
|
|
public override void OnNetworkSpawn()
|
|
{
|
|
var controller = GetComponent<PlayerCharacterController>();
|
|
|
|
if (IsOwner)
|
|
{
|
|
PlaceOnSpawnRing();
|
|
|
|
var manager = FindFirstObjectByType<GameModeManager>(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<CharacterController>();
|
|
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<ConcealmentMeter>(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.
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<CharacterController>();
|
|
bool reenable = cc && cc.enabled;
|
|
if (cc) cc.enabled = false;
|
|
transform.position += offset;
|
|
if (cc) cc.enabled = reenable;
|
|
}
|
|
}
|