Pick role-specific spawn areas with a shared fallback
Choose the spawn area by player role: Hunter avatars look for a HunterSpawnArea-tagged collider, Hiders for HiderSpawnArea. When the role-specific area is missing, fall back to the shared SpawnArea tag, and when that is absent too, keep the existing client-id ring placement. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
1e17b90358
commit
c1a13f6338
1 file changed
+105
-1
@@ -21,6 +21,30 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
[Tooltip("Slots per ring before the radius steps outward, so spacing stays roughly constant as more join.")]
|
||||
[SerializeField] private int spawnRingSlots = 8;
|
||||
|
||||
[Tooltip("Role-specific spawn areas, tried FIRST by an owner based on its Role (Hunter uses " +
|
||||
"hunterSpawnAreaTag, Hider uses hiderSpawnAreaTag). If a loaded scene holds a GameObject with the " +
|
||||
"matching tag, the owner spawns at a random point inside its collider. If this avatar's role area " +
|
||||
"is absent it falls back to spawnAreaTag, then to the client-id ring. Empty disables the role lookup.")]
|
||||
[SerializeField] private string hunterSpawnAreaTag = "HunterSpawnArea";
|
||||
[SerializeField] private string hiderSpawnAreaTag = "HiderSpawnArea";
|
||||
|
||||
[Tooltip("Shared fallback spawn area, used when this avatar's role-specific area (hunter/hiderSpawnAreaTag) " +
|
||||
"is not present in any loaded scene. If a loaded scene holds a GameObject with this tag, owners " +
|
||||
"spawn at a RANDOM point inside its collider instead of the client-id ring (random scatter already " +
|
||||
"prevents the stack-up the ring guards against). Empty disables the lookup. The area collider " +
|
||||
"should be convex (a convex MeshCollider or a primitive) so the point is constrained to its actual " +
|
||||
"shape, not just its bounding box; a non-convex collider falls back to the bounding box.")]
|
||||
[SerializeField] private string spawnAreaTag = "SpawnArea";
|
||||
|
||||
[Tooltip("Metres the spawn is lifted above the spawn area's floor (collider bounds bottom) so the " +
|
||||
"CharacterController doesn't start embedded; gravity then settles it. Mirrors the lobby/match " +
|
||||
"spawn-centre lift.")]
|
||||
[SerializeField] private float spawnAreaGroundLift = 1f;
|
||||
|
||||
// Tries to land a random sample inside the convex footprint before falling back to the bounds centre.
|
||||
// The footprint is typically a chunk of its AABB, so a handful of tries hits the inside with near-certainty.
|
||||
private const int SpawnAreaSampleAttempts = 24;
|
||||
|
||||
[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.")]
|
||||
@@ -44,7 +68,7 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
|
||||
if (IsOwner)
|
||||
{
|
||||
PlaceOnSpawnRing();
|
||||
PlaceAtSpawn();
|
||||
|
||||
var manager = FindFirstObjectByType<GameModeManager>(FindObjectsInactive.Include);
|
||||
if (manager)
|
||||
@@ -70,6 +94,86 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
// non-rest pose. Its bake is the necessary cost of correct remote paint, not wasted work.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Places the freshly-spawned owner. Prefers this avatar's role-specific spawn area (Hunter/Hider tag),
|
||||
/// then the shared <see cref="spawnAreaTag"/> area; if a matching area is loaded the owner drops at a
|
||||
/// random point inside that collider (designer-authored spawn region). With no area present it falls back
|
||||
/// to the deterministic client-id ring. Owner-authoritative either way - the NetworkTransform replicates
|
||||
/// the result, and PlayerCharacterController captures it on its first Update as the kill-plane rescue target.
|
||||
/// </summary>
|
||||
private void PlaceAtSpawn()
|
||||
{
|
||||
if (TryGetSpawnArea(out Collider area))
|
||||
PlaceRandomlyInArea(area);
|
||||
else
|
||||
PlaceOnSpawnRing();
|
||||
}
|
||||
|
||||
// Resolves the spawn-area collider for this avatar: the Role-specific area (Hunter/Hider) takes priority,
|
||||
// then the shared spawnAreaTag area; a miss on both leaves it to the caller's client-id ring fallback.
|
||||
private bool TryGetSpawnArea(out Collider area)
|
||||
{
|
||||
string roleTag = role == PlayerRole.Hunter ? hunterSpawnAreaTag : hiderSpawnAreaTag;
|
||||
return TryFindAreaByTag(roleTag, out area) || TryFindAreaByTag(spawnAreaTag, out area);
|
||||
}
|
||||
|
||||
// Finds a collider tagged 'tag' in any loaded scene, if present. FindWithTag throws when the tag is
|
||||
// undefined (e.g. stripped from a build), so it is guarded - a missing/undefined tag just means "not here".
|
||||
private static bool TryFindAreaByTag(string tag, out Collider area)
|
||||
{
|
||||
area = null;
|
||||
if (string.IsNullOrEmpty(tag)) return false;
|
||||
|
||||
GameObject go;
|
||||
try { go = GameObject.FindWithTag(tag); }
|
||||
catch (UnityException) { return false; }
|
||||
|
||||
if (!go) return false;
|
||||
area = go.GetComponent<Collider>();
|
||||
return area;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops the owner at a random point inside the spawn-area collider. X/Z are sampled within the
|
||||
/// collider's footprint (rejection-sampled against the convex shape so the irregular outline is honoured,
|
||||
/// not just its bounding box); Y sits a touch above the area floor so gravity settles the avatar onto the
|
||||
/// ground. The CharacterController is toggled around the move because it otherwise overrides direct
|
||||
/// transform writes (same pattern as <see cref="PlaceOnSpawnRing"/>).
|
||||
/// </summary>
|
||||
private void PlaceRandomlyInArea(Collider area)
|
||||
{
|
||||
Bounds b = area.bounds;
|
||||
bool convex = SupportsClosestPoint(area);
|
||||
|
||||
Vector3 pick = b.center;
|
||||
for (int i = 0; i < SpawnAreaSampleAttempts; i++)
|
||||
{
|
||||
Vector3 p = new Vector3(Random.Range(b.min.x, b.max.x), b.center.y, Random.Range(b.min.z, b.max.z));
|
||||
// ClosestPoint returns the input unchanged when it lies inside a convex collider, so an unchanged
|
||||
// sample is a point within the real (possibly non-box) footprint.
|
||||
if (!convex || (area.ClosestPoint(p) - p).sqrMagnitude <= 1e-6f)
|
||||
{
|
||||
pick = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Vector3 target = new Vector3(pick.x, b.min.y + spawnAreaGroundLift, pick.z);
|
||||
|
||||
var cc = GetComponent<CharacterController>();
|
||||
bool reenable = cc && cc.enabled;
|
||||
if (cc) cc.enabled = false;
|
||||
transform.position = target;
|
||||
if (cc) cc.enabled = reenable;
|
||||
}
|
||||
|
||||
// Collider.ClosestPoint only constrains to the real shape for primitives and CONVEX mesh colliders;
|
||||
// a non-convex mesh returns the input unchanged (no containment info), so we fall back to its AABB.
|
||||
private static bool SupportsClosestPoint(Collider c)
|
||||
{
|
||||
return !(c is MeshCollider mc) || mc.convex;
|
||||
}
|
||||
|
||||
/// <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
|
||||
|
||||
Reference in new issue
Block a user