Wall/ceiling/ground climb probes now skip any player collider (own and remote) instead of only the local hierarchy, so running into another chameleon no longer triggers the climb mechanic. New players are placed on a per-clientId ring at spawn (host stays at origin) to avoid stacking into one collision pile. Also persists the serialized defaults of the recent network fields into the Player prefab (incl. the spawnRingRadius inspector tuning) and a TMP dynamic-atlas reset artifact.
87 lines
4.2 KiB
C#
87 lines
4.2 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;
|
|
|
|
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;
|
|
}
|
|
}
|