Files
chameleon/Assets/Scripts/NetworkPlayerSetup.cs
T
adminandClaude Opus 4.8 bd70f3b8b9 Add lobby/match-start matchmaking flow with additive scene management
Replace the connection-approval role selection with a server-authoritative
match flow built on NGO additive scene management:

- Scenes: Main is the persistent foundation (NetworkManager, GameModeManager,
  cameras, HUD); Lobby and Test Map load/unload additively. Add Lobby + Test Map
  to the build settings; remove the obsolete SampleScene.
- MatchManager (plain MonoBehaviour singleton): loads Lobby on server start,
  StartMatchTrigger lets the host start the match, rolls one random Hunter (rest
  Hider), swaps Lobby -> Test Map (strictly serialized per NGO), respawns each
  player as its role variant. Forces additive client sync mode so clients keep
  Main instead of single-loading it (fixes "Destination scene is not valid").
- Server-driven spawning: null out NetworkConfig.PlayerPrefab at runtime to
  disable auto-spawn, then spawn players after the lobby floor exists (fixes
  falling through the floor) with double-spawn and transition guards.
- StartMatchTrigger: server-gated OnTriggerEnter, only the host's own player
  starts the match.
- NetworkBootstrap: drop role selection / connection approval; everyone spawns
  as the neutral Player prefab. Host/Client/Server/Shutdown buttons stay.
- NetworkPlayerSetup.ScopedModes flag (baked on Hunter/Hider prefabs): lobby
  avatar gets the full mode set, match variants get the role-scoped set.
- GameModeManager.BindLocalPlayer reads ScopedModes and rebinds the painter
  unconditionally (fixes stale painter binding on respawn -> brush painting in
  every mode).
- MeshPainter.PaintingEnabled defaults to false (unbound/pre-spawn/remote never
  paints); PaintHud visibility now also gates on a bound painter (fixes the HUD
  showing at startup before any avatar exists).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 22:44:18 +02:00

104 lines
5.3 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;
[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;
/// <summary>The side this avatar plays (Hunter/Hider), baked into the prefab variant.</summary>
public PlayerRole Role => role;
/// <summary>True on the match role variants (scope the mode set to <see cref="Role"/>); false on the
/// neutral lobby Player prefab (full mode set). Baked per prefab variant.</summary>
public bool ScopedModes => scopedModes;
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;
}
}