Add the hide/seek/resolution match state machine
Drive the match through three server-authoritative phases (hide, seek, resolution) with per-phase durations, broadcasting the active phase and countdown to clients over the existing named-message infrastructure so MatchManager stays a plain MonoBehaviour and Main keeps zero in-scene network objects. During hide, hider meshes are culled on hunter clients only (each client computes visibility locally from role, phase and found state). During seek, hunters shoot a single raycast from the first-person camera centre (left click, cooldown-gated); a validated hit marks the hider found, which hides its mesh for everyone and binds the owner to spectator freecam. Seek ends early once every hider is found, then resolution runs before returning to the lobby. Force AlwaysAnimate on remote replicas so the animator keeps writing bone transforms while the body is hidden or off-screen. The Paintman animator is authored CullUpdateTransforms, so a culled renderer froze the pose on the rest pose and the skinned collider baked that frozen pose, landing replicated paint stamps on the wrong body location and leaving the painting desynced after hiders were shown again. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
95effb91da
commit
5b3fe1d3d1
5 files changed
+555
-3
No files matched your search
@@ -0,0 +1,106 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder on-screen match HUD: the current sub-phase + countdown (top centre) and a hunter crosshair
|
||||
/// during the seek phase. Drawn with IMGUI (OnGUI) on purpose - it needs no Canvas / font asset authoring,
|
||||
/// so the match-state logic can be exercised immediately; swap it for a proper uGUI canvas once the look is
|
||||
/// finalised. Added at runtime by <see cref="MatchManager"/> (AddComponent), so it rides the persistent
|
||||
/// manager object and reads <see cref="MatchManager.Instance"/> on every machine.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class MatchHud : MonoBehaviour
|
||||
{
|
||||
private GUIStyle _phaseStyle;
|
||||
private GUIStyle _timeStyle;
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
var match = MatchManager.Instance;
|
||||
if (!match || !match.MatchActive) return;
|
||||
|
||||
EnsureStyles();
|
||||
|
||||
const float w = 360f;
|
||||
var phaseRect = new Rect((Screen.width - w) * 0.5f, 12f, w, 30f);
|
||||
var timeRect = new Rect(phaseRect.x, phaseRect.y + 28f, w, 36f);
|
||||
DrawShadowed(phaseRect, PhaseLabel(match.LocalSubPhase), _phaseStyle);
|
||||
DrawShadowed(timeRect, FormatTime(match.LocalPhaseRemaining), _timeStyle);
|
||||
|
||||
if (match.LocalSubPhase == MatchManager.MatchSubPhase.Seek && LocalIsHunter())
|
||||
DrawCrosshair();
|
||||
}
|
||||
|
||||
private static string PhaseLabel(MatchManager.MatchSubPhase phase)
|
||||
{
|
||||
switch (phase)
|
||||
{
|
||||
case MatchManager.MatchSubPhase.Hide: return "Verstecken";
|
||||
case MatchManager.MatchSubPhase.Seek: return "Suchen";
|
||||
case MatchManager.MatchSubPhase.Resolution: return "Auflösung";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatTime(float seconds)
|
||||
{
|
||||
int s = Mathf.CeilToInt(Mathf.Max(0f, seconds));
|
||||
return $"{s / 60:0}:{s % 60:00}";
|
||||
}
|
||||
|
||||
private static bool LocalIsHunter()
|
||||
{
|
||||
var nm = Unity.Netcode.NetworkManager.Singleton;
|
||||
if (!nm || nm.LocalClient == null) return false;
|
||||
var po = nm.LocalClient.PlayerObject;
|
||||
if (!po) return false;
|
||||
var setup = po.GetComponent<NetworkPlayerSetup>();
|
||||
return setup && setup.ScopedModes && setup.Role == PlayerRole.Hunter;
|
||||
}
|
||||
|
||||
private static void DrawCrosshair()
|
||||
{
|
||||
float cx = Screen.width * 0.5f;
|
||||
float cy = Screen.height * 0.5f;
|
||||
const float r = 8f, t = 2f;
|
||||
Color prev = GUI.color;
|
||||
GUI.color = new Color(1f, 1f, 1f, 0.85f);
|
||||
GUI.DrawTexture(new Rect(cx - r, cy - t * 0.5f, r * 2f, t), Texture2D.whiteTexture);
|
||||
GUI.DrawTexture(new Rect(cx - t * 0.5f, cy - r, t, r * 2f), Texture2D.whiteTexture);
|
||||
GUI.color = prev;
|
||||
}
|
||||
|
||||
// One label drawn twice (dark offset behind, then the real colour) so it stays readable on any backdrop.
|
||||
private static void DrawShadowed(Rect r, string text, GUIStyle style)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return;
|
||||
Color real = style.normal.textColor;
|
||||
style.normal.textColor = new Color(0f, 0f, 0f, 0.7f);
|
||||
GUI.Label(new Rect(r.x + 1f, r.y + 1f, r.width, r.height), text, style);
|
||||
style.normal.textColor = real;
|
||||
GUI.Label(r, text, style);
|
||||
}
|
||||
|
||||
private void EnsureStyles()
|
||||
{
|
||||
if (_phaseStyle == null)
|
||||
{
|
||||
_phaseStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontSize = 22,
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
_phaseStyle.normal.textColor = Color.white;
|
||||
}
|
||||
if (_timeStyle == null)
|
||||
{
|
||||
_timeStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontSize = 28,
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
_timeStyle.normal.textColor = Color.white;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user