diff --git a/Assets/Scripts/ConcealmentColliderBaker.cs b/Assets/Scripts/ConcealmentColliderBaker.cs
index ae05d1a..9b3eb3c 100644
--- a/Assets/Scripts/ConcealmentColliderBaker.cs
+++ b/Assets/Scripts/ConcealmentColliderBaker.cs
@@ -274,7 +274,7 @@ public class ConcealmentColliderBaker : MonoBehaviour
}
///
- /// Readable, deterministic base name for the generated collider-mesh assets (replaces the old GUID):
+ /// Readable, deterministic base name for the generated collider-mesh assets:
/// - Prefab stage: "{assetPath without 'Assets/' and '.prefab'}/{path below the prefab root}"
/// - Scene: "{sceneName}/{full hierarchy path from the scene root}"
/// Sanitized to a filesystem-safe token; on collision with a DIFFERENT baker's assets, "_2", "_3", … is appended.
diff --git a/Assets/Scripts/ConcealmentMeter.cs b/Assets/Scripts/ConcealmentMeter.cs
index efe6339..3039492 100644
--- a/Assets/Scripts/ConcealmentMeter.cs
+++ b/Assets/Scripts/ConcealmentMeter.cs
@@ -7,7 +7,7 @@ using UnityEngine;
/// Each evaluation bakes the character's current (deformed) skinned mesh, voxelizes its
/// SOLID volume, and reports what fraction of that volume sits INSIDE environment colliders
/// (= embedded / hidden) versus exposed. is the headline value,
-/// its complement.
+/// its complement.
///
/// Pipeline:
/// 1. -> mesh in the renderer's LOCAL space, then the
@@ -66,15 +66,15 @@ public class ConcealmentMeter : MonoBehaviour
/// Percentage [0..100] of the character's solid volume embedded inside environment colliders.
public float EmbeddedPercent { get; private set; }
- public float visiblePercent;
+ public float visiblePercent; // Inspector debug readout (complement of EmbeddedPercent)
- public int InteriorVoxelCount { get; private set; }
- public int EmbeddedVoxelCount { get; private set; }
public bool HasResult { get; private set; }
private Mesh _baked;
- private Vector3[] _verts;
- private int[] _tris;
+ private readonly List _verts = new List(); // refilled per eval without allocating
+ private int[] _tris; // topology is invariant across bakes of the same renderer -> fetched once
+ private int _trisVertexCount = -1; // vertex count _tris was fetched for (detects a renderer/mesh swap)
+ private bool _overlapSaturated; // log-once guard for a saturated broadphase buffer
private float _timer;
private readonly List _vbuf = new List(); // reused scale-fixup scratch; nothing allocated per eval
@@ -133,9 +133,16 @@ public class ConcealmentMeter : MonoBehaviour
// player's uniform scale guarantees. Mirrors SkinnedColliderBaker's identical fixup.
GeometryUtil.RescaleToUnitScale(_baked, targetRenderer.transform.lossyScale, _vbuf);
- _verts = _baked.vertices;
- _tris = _baked.triangles;
- if (_verts.Length == 0 || _tris.Length < 3) return;
+ // mesh.vertices/.triangles return fresh arrays per call - at 10 evals/s that is steady GC
+ // pressure. Vertices go into a reused list; the (bake-invariant) index buffer is fetched only
+ // when the source mesh actually changed.
+ _baked.GetVertices(_verts);
+ if (_tris == null || _trisVertexCount != _baked.vertexCount)
+ {
+ _tris = _baked.triangles;
+ _trisVertexCount = _baked.vertexCount;
+ }
+ if (_verts.Count == 0 || _tris.Length < 3) return;
Bounds lb = _baked.bounds; // local-space AABB of the baked pose
Vector3 size = lb.size;
@@ -175,16 +182,13 @@ public class ConcealmentMeter : MonoBehaviour
if (col == null || col.Count < 2) continue;
col.Sort();
+ // col is sorted and cy grows monotonically with iy, so a single running index
+ // replaces a from-scratch scan per voxel: O(ny + crossings) per column.
+ int below = 0;
for (int iy = 0; iy < ny; iy++)
{
float cy = origin.y + (iy + 0.5f) * voxel;
-
- int below = 0;
- for (int k = 0; k < col.Count; k++)
- {
- if (col[k] <= cy) below++;
- else break; // sorted ascending
- }
+ while (below < col.Count && col[below] <= cy) below++;
if ((below & 1) == 0) continue; // even crossings below -> outside the solid
interior++;
@@ -203,8 +207,6 @@ public class ConcealmentMeter : MonoBehaviour
}
}
- InteriorVoxelCount = interior;
- EmbeddedVoxelCount = embedded;
EmbeddedPercent = interior > 0 ? 100f * embedded / interior : 0f;
visiblePercent = 100 - EmbeddedPercent;
HasResult = true;
@@ -285,7 +287,11 @@ public class ConcealmentMeter : MonoBehaviour
int n = Physics.OverlapBoxNonAlloc(wb.center, wb.extents, _overlap, Quaternion.identity,
environmentMask, QueryTriggerInteraction.Ignore);
- if (n > _overlap.Length) n = _overlap.Length;
+ if (n == _overlap.Length && !_overlapSaturated)
+ {
+ _overlapSaturated = true; // silently dropped colliders would skew EmbeddedPercent low
+ Debug.LogWarning($"ConcealmentMeter: broadphase buffer saturated ({n}); environment colliders may be missed.", this);
+ }
for (int i = 0; i < n; i++)
{
diff --git a/Assets/Scripts/ConcealmentVolume.cs b/Assets/Scripts/ConcealmentVolume.cs
index 4a95df8..e07cf2b 100644
--- a/Assets/Scripts/ConcealmentVolume.cs
+++ b/Assets/Scripts/ConcealmentVolume.cs
@@ -45,7 +45,6 @@ public class ConcealmentVolume : MonoBehaviour
[SerializeField, HideInInspector] private int insideCount;
public bool HasBake => bits != null && bits.Length > 0 && nx > 0;
- public int InsideVoxelCount => insideCount;
private static readonly List Active = new List();
public static IReadOnlyList All => Active;
diff --git a/Assets/Scripts/FingerIK.cs b/Assets/Scripts/FingerIK.cs
index 7273366..004a4e0 100644
--- a/Assets/Scripts/FingerIK.cs
+++ b/Assets/Scripts/FingerIK.cs
@@ -80,7 +80,6 @@ public class FingerIK : MonoBehaviour
private static readonly Vector3 SpreadAxis = Vector3.forward; // local Z
- public int JointCount => joints != null ? joints.Count : 0;
public Transform Tip => tip;
public Transform Target => target;
public float Curl => curl;
diff --git a/Assets/Scripts/FirstPersonMode.cs b/Assets/Scripts/FirstPersonMode.cs
index 4fe1855..a017196 100644
--- a/Assets/Scripts/FirstPersonMode.cs
+++ b/Assets/Scripts/FirstPersonMode.cs
@@ -46,7 +46,12 @@ public class FirstPersonMode : GameMode
// so the legacy lobby first-person view never shoots.
private float _nextShootTime;
- private static readonly RaycastHit[] _hits = new RaycastHit[16];
+ private static readonly RaycastHit[] _hits = new RaycastHit[32];
+ private static bool _hitsSaturated; // log-once: a saturated buffer can silently drop the true blocker
+
+ // Cached per bound avatar: CanShoot runs every Tick during a match.
+ private PlayerCharacterController _setupSource;
+ private NetworkPlayerSetup _setup;
public override void Tick()
{
@@ -62,10 +67,15 @@ public class FirstPersonMode : GameMode
{
var match = MatchManager.Instance;
if (!match || !match.MatchActive || match.LocalSubPhase != MatchManager.MatchSubPhase.Seek) return false;
- if (!Manager.Player || !Manager.FirstPerson) return false;
+ var player = Manager.Player;
+ if (!player || !Manager.FirstPerson) return false;
- var setup = Manager.Player.GetComponent();
- return setup && setup.ScopedModes && setup.Role == PlayerRole.Hunter;
+ if (player != _setupSource)
+ {
+ _setupSource = player;
+ _setup = player.GetComponent();
+ }
+ return _setup && _setup.ScopedModes && _setup.Role == PlayerRole.Hunter;
}
private void TryShoot()
@@ -75,6 +85,11 @@ public class FirstPersonMode : GameMode
float range = Mathf.Max(0f, match.ShootRange);
int count = Physics.RaycastNonAlloc(cam.position, cam.forward, _hits, range, ~0, QueryTriggerInteraction.Ignore);
+ if (count == _hits.Length && !_hitsSaturated)
+ {
+ _hitsSaturated = true;
+ Debug.LogWarning($"FirstPersonMode: shot raycast buffer saturated ({count}); the nearest blocker may be missed.");
+ }
if (count == 0) return;
// Nearest-first so the shot stops at the first solid thing it meets (a wall blocks; a nearer avatar
diff --git a/Assets/Scripts/GameModeManager.cs b/Assets/Scripts/GameModeManager.cs
index 4e83f5c..fe200eb 100644
--- a/Assets/Scripts/GameModeManager.cs
+++ b/Assets/Scripts/GameModeManager.cs
@@ -62,9 +62,6 @@ public class GameModeManager : MonoBehaviour
private bool _paused;
private int _resumeIndex = -1;
- /// True while local gameplay is suspended for a modal overlay (see ).
- public bool IsPaused => _paused;
-
/// The currently active mode (null before startup).
public GameMode ActiveMode =>
(_activeIndex >= 0 && _activeIndex < _modes.Count) ? _modes[_activeIndex] : null;
@@ -246,8 +243,7 @@ public class GameModeManager : MonoBehaviour
cameraInput.LookMode = ThirdPersonOrbitInput.OrbitLookMode.Disabled;
// Belt-and-braces explicit release (covers the orbit input being absent/disabled).
- Cursor.lockState = CursorLockMode.None;
- Cursor.visible = true;
+ InputUtil.SetCursorLocked(false);
}
///
@@ -279,8 +275,7 @@ public class GameModeManager : MonoBehaviour
cameraInput.enabled = true;
cameraInput.LookMode = ThirdPersonOrbitInput.OrbitLookMode.Disabled;
}
- Cursor.lockState = CursorLockMode.None;
- Cursor.visible = true;
+ InputUtil.SetCursorLocked(false);
ModeChanged?.Invoke(null); // hide mode-following UI (the paint HUD) beneath the overlay
}
diff --git a/Assets/Scripts/GeometryUtil.cs b/Assets/Scripts/GeometryUtil.cs
index 6ef3fd1..a774428 100644
--- a/Assets/Scripts/GeometryUtil.cs
+++ b/Assets/Scripts/GeometryUtil.cs
@@ -1,9 +1,21 @@
using System.Collections.Generic;
using UnityEngine;
-/// Shared mesh/bounds geometry helpers.
+/// Shared mesh/bounds/hierarchy helpers.
public static class GeometryUtil
{
+ /// Depth-first hierarchy search by name; checks itself first.
+ public static Transform FindDeep(Transform root, string name)
+ {
+ if (root.name == name) return root;
+ for (int i = 0; i < root.childCount; i++)
+ {
+ var found = FindDeep(root.GetChild(i), name);
+ if (found) return found;
+ }
+ return null;
+ }
+
/// Divides a world scale back out of a freshly baked mesh so it is true unit-scale local
/// geometry (BakeMesh freezes the transform's lossyScale into the vertices). No-op at scale 1;
/// is reused so nothing is allocated per bake.
diff --git a/Assets/Scripts/HandIK.cs b/Assets/Scripts/HandIK.cs
index 484c488..d259b8b 100644
--- a/Assets/Scripts/HandIK.cs
+++ b/Assets/Scripts/HandIK.cs
@@ -180,7 +180,7 @@ public class HandIK : MonoBehaviour
var chain = new List(8);
for (int f = 0; f < RootNames.Length; f++)
{
- Transform root = FindDeepChild(handRoot, RootNames[f]);
+ Transform root = GeometryUtil.FindDeep(handRoot, RootNames[f]);
if (!root)
{
Debug.LogWarning($"HandIK: finger root '{RootNames[f]}' not found under {handRoot.name}.", this);
@@ -241,22 +241,10 @@ public class HandIK : MonoBehaviour
private Transform FindHandRoot()
{
- Transform found = FindDeepChild(transform, "Hand");
+ Transform found = GeometryUtil.FindDeep(transform, "Hand");
return found ? found : transform;
}
- private static Transform FindDeepChild(Transform parent, string targetName)
- {
- for (int i = 0; i < parent.childCount; i++)
- {
- Transform c = parent.GetChild(i);
- if (c.name == targetName) return c;
- Transform deep = FindDeepChild(c, targetName);
- if (deep) return deep;
- }
- return null;
- }
-
#if UNITY_EDITOR
private double _lastSig = double.NaN;
diff --git a/Assets/Scripts/HeroCutout.cs b/Assets/Scripts/HeroCutout.cs
index 9696747..43585d1 100644
--- a/Assets/Scripts/HeroCutout.cs
+++ b/Assets/Scripts/HeroCutout.cs
@@ -137,6 +137,7 @@ public class HeroCutout : MonoBehaviour
if (Time.time - _lastOccludedTime > Mathf.Max(0f, occlusionHold)) { Disarm(); return; }
// Same hero point feeds the ray and the cone axis, so the gate and the cut volume agree.
+ _armed = true;
Shader.SetGlobalVector(TargetId, new Vector4(heroPos.x, heroPos.y, heroPos.z, 1f));
Shader.SetGlobalVector(ParamsId, new Vector4(Mathf.Max(0f, radius * bodyScale), Mathf.Max(1e-4f, feather * bodyScale), Mathf.Max(1e-4f, depthFeather), 0f));
}
@@ -181,8 +182,15 @@ public class HeroCutout : MonoBehaviour
return null;
}
+ private bool _armed; // change-guard: Disarm runs every non-armed frame, so it only writes the transition
+
/// Clears the "enabled" flag so cutout materials render as plain URP/Lit.
- private void Disarm() => Shader.SetGlobalVector(TargetId, Vector4.zero);
+ private void Disarm()
+ {
+ if (!_armed) return;
+ _armed = false;
+ Shader.SetGlobalVector(TargetId, Vector4.zero);
+ }
///
/// Lazily resolves the orbit vcam and its Cinemachine Deoccluder (they may live in a scene that
diff --git a/Assets/Scripts/HsvColorPicker.cs b/Assets/Scripts/HsvColorPicker.cs
index ac4104e..658133e 100644
--- a/Assets/Scripts/HsvColorPicker.cs
+++ b/Assets/Scripts/HsvColorPicker.cs
@@ -10,11 +10,10 @@ using UnityEngine.UI;
/// edited separately by a standalone slider in the HUD, not by this picker;
/// still stores the incoming alpha so round-trips it.
///
-/// creates the child structure (stable names, no sprites or hooks); it can
-/// be baked into a prefab. then (re)assigns the runtime gradient sprites
-/// (which a prefab cannot store) and (re)hooks the drag relays - it runs for both the code-built
-/// picker and one instantiated from a prefab, so the two are behaviourally identical. A leftover
-/// alpha bar from a pre-split prefab is hidden by , so no re-bake is required.
+/// The child structure (stable names) comes from the baked HUD prefab.
+/// (re)assigns the runtime gradient sprites (which a prefab cannot store) and (re)hooks the drag
+/// relays. A leftover alpha bar from a pre-split prefab is hidden by , so no
+/// re-bake is required.
///
public class HsvColorPicker : MonoBehaviour
{
@@ -24,9 +23,6 @@ public class HsvColorPicker : MonoBehaviour
private float _h, _s, _v, _a = 1f;
- // Gap between the stacked square / hue rows (matches the layout in Build).
- private const float RowGap = 6f;
-
private Image _hueBase; // pure-hue background of the SV square
private RectTransform _svHandle;
private RectTransform _hueHandle;
@@ -51,46 +47,6 @@ public class HsvColorPicker : MonoBehaviour
Refresh();
}
- ///
- /// Builds the picker structure (square + hue bar + swatch) under this object,
- /// top-left aligned. Layout only - no gradient sprites and no event hooks, so the result is
- /// safe to bake into a prefab. Call afterwards (at runtime) to make it
- /// functional.
- ///
- public void Build(float squareSize, float hueHeight)
- {
- // Saturation / value square: base hue + two named gradient overlays, top-left of this rect.
- var sv = PaintHud.NewRect("SV", transform);
- TopLeft(sv, new Vector2(0f, 0f), new Vector2(squareSize, squareSize));
- PaintHud.AddImage(sv.gameObject, Color.red); // raycast target for the drag relay
-
- PaintHud.AddOverlay(sv, "SatOverlay");
- PaintHud.AddOverlay(sv, "ValOverlay");
-
- sv.gameObject.AddComponent();
-
- var svHandle = PaintHud.NewRect("Handle", sv);
- svHandle.sizeDelta = new Vector2(12f, 12f);
- PaintHud.AddImage(svHandle.gameObject, Color.white).raycastTarget = false;
-
- // Hue bar below the square.
- var hue = PaintHud.NewRect("Hue", transform);
- TopLeft(hue, new Vector2(0f, -(squareSize + RowGap)), new Vector2(squareSize, hueHeight));
- var hueImg = hue.gameObject.AddComponent();
- hueImg.type = Image.Type.Simple;
-
- hue.gameObject.AddComponent();
-
- var hueHandle = PaintHud.NewRect("Handle", hue);
- hueHandle.sizeDelta = new Vector2(4f, hueHeight + 4f);
- PaintHud.AddImage(hueHandle.gameObject, Color.white).raycastTarget = false;
-
- // Swatch, to the right of the square.
- var sw = PaintHud.NewRect("Swatch", transform);
- TopLeft(sw, new Vector2(squareSize + 10f, 0f), new Vector2(34f, 34f));
- PaintHud.AddImage(sw.gameObject, Color.white).raycastTarget = false;
- }
-
///
/// Re-finds the child widgets, (re)assigns the runtime gradient sprites and (re)hooks the drag
/// relays. Idempotent (unsubscribe-then-subscribe), so it is safe whether the structure was just
@@ -173,13 +129,4 @@ public class HsvColorPicker : MonoBehaviour
}
if (_swatch) _swatch.color = Color.HSVToRGB(_h, _s, _v);
}
-
- /// Anchors a child to this rect's top-left corner with the given offset and size.
- private static void TopLeft(RectTransform rt, Vector2 offset, Vector2 size)
- {
- rt.anchorMin = rt.anchorMax = new Vector2(0f, 1f);
- rt.pivot = new Vector2(0f, 1f);
- rt.anchoredPosition = offset;
- rt.sizeDelta = size;
- }
}
diff --git a/Assets/Scripts/InputUtil.cs b/Assets/Scripts/InputUtil.cs
index ad720f3..e154b21 100644
--- a/Assets/Scripts/InputUtil.cs
+++ b/Assets/Scripts/InputUtil.cs
@@ -4,9 +4,7 @@ using UnityEngine.InputSystem;
#endif
///
-/// Shared low-level input reads that were previously copy-pasted verbatim across the camera rigs
-/// (, ,
-/// ), the paint controller and the first-person shoot mode. Each
+/// Shared low-level input reads used by the camera rigs, the paint controller and the modes. Each
/// method preserves its original per-backend behaviour (new Input System when present, legacy Input
/// Manager otherwise). Sensitivity scaling stays at the call sites.
///
@@ -105,20 +103,20 @@ public static class InputUtil
#endif
}
- /// Gameplay key pressed on this exact frame (edge), suppressed while a UI text field is
- /// focused so typing never triggers gameplay bindings. The parameter type follows the active input
- /// backend, matching how the callers' serialized key fields are declared.
+ /// Key pressed on this exact frame (edge), regardless of UI focus. The parameter type
+ /// follows the active input backend, matching how the callers' serialized key fields are declared.
#if ENABLE_INPUT_SYSTEM
- public static bool GameplayKeyPressed(Key key)
- {
- if (TypingGuard.IsTyping) return false;
- return Keyboard.current != null && Keyboard.current[key].wasPressedThisFrame;
- }
+ public static bool KeyPressedThisFrame(Key key)
+ => Keyboard.current != null && Keyboard.current[key].wasPressedThisFrame;
#else
- public static bool GameplayKeyPressed(KeyCode key)
- {
- if (TypingGuard.IsTyping) return false;
- return Input.GetKeyDown(key);
- }
+ public static bool KeyPressedThisFrame(KeyCode key) => Input.GetKeyDown(key);
+#endif
+
+ /// Gameplay key pressed on this exact frame (edge), suppressed while a UI text field is
+ /// focused so typing never triggers gameplay bindings.
+#if ENABLE_INPUT_SYSTEM
+ public static bool GameplayKeyPressed(Key key) => !TypingGuard.IsTyping && KeyPressedThisFrame(key);
+#else
+ public static bool GameplayKeyPressed(KeyCode key) => !TypingGuard.IsTyping && KeyPressedThisFrame(key);
#endif
}
diff --git a/Assets/Scripts/MatchHud.cs b/Assets/Scripts/MatchHud.cs
index ed748ce..78d5d50 100644
--- a/Assets/Scripts/MatchHud.cs
+++ b/Assets/Scripts/MatchHud.cs
@@ -12,6 +12,8 @@ public class MatchHud : MonoBehaviour
{
private GUIStyle _phaseStyle;
private GUIStyle _timeStyle;
+ private int _lastSecond = -1; // countdown text cache: OnGUI runs 2+ times per frame,
+ private string _timeText = ""; // so the string is only rebuilt when the displayed second changes
private void OnGUI()
{
@@ -20,13 +22,20 @@ public class MatchHud : MonoBehaviour
EnsureStyles();
+ int s = Mathf.CeilToInt(Mathf.Max(0f, match.LocalPhaseRemaining));
+ if (s != _lastSecond)
+ {
+ _lastSecond = s;
+ _timeText = $"{s / 60:0}:{s % 60:00}";
+ }
+
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);
+ DrawShadowed(timeRect, _timeText, _timeStyle);
- if (match.LocalSubPhase == MatchManager.MatchSubPhase.Seek && LocalIsHunter())
+ if (match.LocalSubPhase == MatchManager.MatchSubPhase.Seek && NetworkPlayerSetup.LocalViewerIsHunter())
DrawCrosshair();
}
@@ -41,22 +50,6 @@ public class MatchHud : MonoBehaviour
}
}
- 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();
- return setup && setup.ScopedModes && setup.Role == PlayerRole.Hunter;
- }
-
private static void DrawCrosshair()
{
float cx = Screen.width * 0.5f;
diff --git a/Assets/Scripts/MatchManager.cs b/Assets/Scripts/MatchManager.cs
index 5cce81a..c71cfd3 100644
--- a/Assets/Scripts/MatchManager.cs
+++ b/Assets/Scripts/MatchManager.cs
@@ -139,7 +139,6 @@ public class MatchManager : MonoBehaviour
// The countdown is cosmetic/local; the authoritative transition is server-driven, so drift self-corrects.
private MatchSubPhase _subPhase = MatchSubPhase.None;
private float _phaseEndTime; // Time.time when the current sub-phase ends (for the HUD countdown)
- private float _phaseDuration;
private MatchMode _localMode = MatchMode.Classic; // mirror of the server's matchMode, carried in the phase message
private const string PreMatchTag = "PreMatch"; // scene props destroyed when seek starts
@@ -152,10 +151,6 @@ public class MatchManager : MonoBehaviour
public MatchMode LocalMatchMode => _localMode;
/// Seconds left in the current sub-phase for the HUD (clamped to 0; cosmetic).
public float LocalPhaseRemaining => Mathf.Max(0f, _phaseEndTime - Time.time);
- /// Total length of the current sub-phase (for a HUD progress bar, if wanted).
- public float LocalPhaseDuration => _phaseDuration;
- /// Raised after the local sub-phase changes (argument = the new sub-phase). HUD/visibility follow it.
- public event System.Action SubPhaseChanged;
private NetworkManager _net;
private GameObject _playerPrefab; // captured from NetworkConfig.PlayerPrefab; auto-spawn is then disabled so we spawn manually into a loaded scene
@@ -508,21 +503,16 @@ public class MatchManager : MonoBehaviour
BroadcastPhase(phase, duration, matchMode, 0, true);
}
- // Sets this machine's sub-phase mirror + countdown + mode and notifies followers (HUD / visibility).
+ // Sets this machine's sub-phase mirror + countdown + mode (followers poll LocalSubPhase).
// Shared by the server's SetPhaseServer and a client's OnPhaseMessage handler.
private void ApplyPhaseLocal(MatchSubPhase phase, float duration, MatchMode mode)
{
bool changed = _subPhase != phase;
_subPhase = phase;
- _phaseDuration = duration;
_phaseEndTime = Time.time + duration;
_localMode = mode;
- if (changed)
- {
- SubPhaseChanged?.Invoke(phase);
- // Props staged only for the hide phase are gone the instant seek begins.
- if (phase == MatchSubPhase.Seek) DestroyPreMatchObjects();
- }
+ // Props staged only for the hide phase are gone the instant seek begins.
+ if (changed && phase == MatchSubPhase.Seek) DestroyPreMatchObjects();
}
// Runs on every machine (server directly, clients via the phase message); the tagged props are plain
@@ -801,7 +791,7 @@ public class MatchManager : MonoBehaviour
/// Ends the running match for everyone (back to the Lobby), routed through authority: the
/// host/server ends directly; a remote authority client asks the server. No-op if this machine is not
- /// the authority or no match is running. Replaces the old Esc handler - now driven by the panel button.
+ /// the authority or no match is running.
public void RequestEndMatch()
{
if (_net == null) return;
@@ -811,9 +801,26 @@ public class MatchManager : MonoBehaviour
// Local active-scene kind. Every peer sets its active scene to Lobby/Map as those load (see OnSceneEvent)
// and back to Main on teardown, so a local UI can tell where it is without the server-local Phase.
- public bool IsLocalSceneMain => SceneManager.GetActiveScene().name == mainSceneName;
- public bool IsLocalSceneLobby => SceneManager.GetActiveScene().name == lobbySceneName;
- public bool IsLocalSceneMap => SceneManager.GetActiveScene().name == matchSceneName;
+ // Scene.name allocates a fresh string per call and these are polled per frame -> cached per scene handle.
+ public bool IsLocalSceneLobby => ActiveSceneName == lobbySceneName;
+ public bool IsLocalSceneMap => ActiveSceneName == matchSceneName;
+
+ private int _activeSceneHandle;
+ private string _activeSceneName;
+
+ private string ActiveSceneName
+ {
+ get
+ {
+ Scene s = SceneManager.GetActiveScene();
+ if (_activeSceneName == null || s.handle != _activeSceneHandle)
+ {
+ _activeSceneHandle = s.handle;
+ _activeSceneName = s.name;
+ }
+ return _activeSceneName;
+ }
+ }
// Sends a named message whose payload carries no information (a named message still needs a buffer,
// so a single dummy byte is written).
diff --git a/Assets/Scripts/MeshPainter.Backend.cs b/Assets/Scripts/MeshPainter.Backend.cs
index d259c48..4de395b 100644
--- a/Assets/Scripts/MeshPainter.Backend.cs
+++ b/Assets/Scripts/MeshPainter.Backend.cs
@@ -90,25 +90,20 @@ public partial class MeshPainter
private static bool MapIsLinear(int mi) => mi == MAP_MSO;
- private string[] MapProps(int mi)
+ // Material slots per map, as pre-hashed IDs (this runs per stroke end - no per-call allocations).
+ // MSO: one mask map drives three URP slots: _MetallicGlossMap (R/A) + _OcclusionMap (G) +
+ // _ParallaxMap (G = occlusion reused as a 0.5-centred height for parallax/relief).
+ private static readonly int[][] MapPropIds =
{
- switch (mi)
- {
- case MAP_ALBEDO: return new[] { "_BaseMap", "_MainTex" };
- // One mask map drives three URP slots: _MetallicGlossMap (R/A) + _OcclusionMap (G) +
- // _ParallaxMap (G = occlusion reused as a 0.5-centred height for parallax/relief).
- case MAP_MSO: return new[] { "_MetallicGlossMap", "_OcclusionMap", "_ParallaxMap" };
- default: return new[] { "_EmissionMap" };
- }
- }
+ new[] { BaseMapId, MainTexId }, // MAP_ALBEDO
+ new[] { MetallicGlossMapId, OcclusionMapId, ParallaxMapId }, // MAP_MSO
+ new[] { EmissionMapId }, // MAP_EM
+ };
private void AssignMapTexture(int mi, Texture tex)
{
if (!_materialInstance) return;
- foreach (var p in MapProps(mi))
- {
- int id = Shader.PropertyToID(p);
+ foreach (int id in MapPropIds[mi])
if (_materialInstance.HasProperty(id)) _materialInstance.SetTexture(id, tex);
- }
}
}
diff --git a/Assets/Scripts/MeshPainter.Pick.cs b/Assets/Scripts/MeshPainter.Pick.cs
index 34976aa..f6d4e7f 100644
--- a/Assets/Scripts/MeshPainter.Pick.cs
+++ b/Assets/Scripts/MeshPainter.Pick.cs
@@ -3,12 +3,7 @@ using UnityEngine;
public partial class MeshPainter
{
- // --- Pipette (eyedropper) ---
- //
- // A sampling MODE rather than a paint channel: a click (Shift/Ctrl held, or the HUD toggle) reads the
- // channel values at the hit point into the brush, leaving the brush opacities/alphas untouched - a
- // pick changes WHAT is painted, not how strongly. Height is adopted only off our own paint mesh.
- // Per-surface routing: see PickAtScreen.
+ // --- Pipette (eyedropper) --- (semantics: see the pipette tooltip; per-surface routing: PickAtScreen)
[Header("Brush - pipette (eyedropper)")]
[Tooltip("Pipette mode: a click samples the channel values at the hit point on ANY surface " +
diff --git a/Assets/Scripts/MeshPainter.Setup.cs b/Assets/Scripts/MeshPainter.Setup.cs
index 32fc2ae..3eddd64 100644
--- a/Assets/Scripts/MeshPainter.Setup.cs
+++ b/Assets/Scripts/MeshPainter.Setup.cs
@@ -39,25 +39,41 @@ public partial class MeshPainter
}
}
- /// Reads vertices/triangles/UVs of the collider mesh and computes culling data.
+ // BuildMeshCaches scratch: refilled per bake without allocating (with updateColliderEveryFrame this
+ // runs per frame while painting; mesh.vertices/.triangles/.uv return fresh arrays per call).
+ private readonly System.Collections.Generic.List _vertScratch = new System.Collections.Generic.List();
+ private int _meshCacheId; // mesh instance the topology/UV caches were built for (0 = none)
+
+ /// Reads vertices/triangles/UVs of the collider mesh and computes culling data.
+ /// Only the vertices change across re-bakes of the same mesh; topology/UVs are fetched once.
private void BuildMeshCaches()
{
Mesh m = _collider.sharedMesh;
if (!m) return;
- _verts = m.vertices;
- _tris = m.triangles;
- _uvs = m.uv;
+ m.GetVertices(_vertScratch);
+ int vCount = _vertScratch.Count;
+ if (_verts == null || _verts.Length != vCount) _verts = new Vector3[vCount];
+ _vertScratch.CopyTo(_verts);
- // Fallback to the original mesh UVs if the baked mesh dropped them.
- if ((_uvs == null || _uvs.Length != _verts.Length) && _skinned && _skinned.sharedMesh)
- _uvs = _skinned.sharedMesh.uv;
+ if (_meshCacheId != m.GetInstanceID())
+ {
+ _meshCacheId = m.GetInstanceID();
+ _tris = m.triangles;
+ _uvs = m.uv;
+ // Fallback to the original mesh UVs if the baked mesh dropped them.
+ if ((_uvs == null || _uvs.Length != vCount) && _skinned && _skinned.sharedMesh)
+ _uvs = _skinned.sharedMesh.uv;
+ }
if (_uvs == null || _uvs.Length != _verts.Length) return;
int triCount = _tris.Length / 3;
- _triCenter = new Vector3[triCount];
- _triRadius = new float[triCount];
+ if (_triCenter == null || _triCenter.Length != triCount)
+ {
+ _triCenter = new Vector3[triCount];
+ _triRadius = new float[triCount];
+ }
double sumArea3D = 0.0, sumAreaUv = 0.0;
@@ -209,7 +225,7 @@ public partial class MeshPainter
{
_materialInstance.EnableKeyword("_METALLICSPECGLOSSMAP");
_materialInstance.DisableKeyword("_SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A");
- if (_materialInstance.HasProperty(SmoothnessId)) _materialInstance.SetFloat(SmoothnessId, 1f);
+ SetMatFloat(SmoothnessId, 1f);
}
// Emission map x _EmissionColor (white * intensity). Clear the "emissive is black"
@@ -218,6 +234,7 @@ public partial class MeshPainter
{
_materialInstance.EnableKeyword("_EMISSION");
_materialInstance.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive;
+ _lastEmissionIntensity = float.NaN; // fresh material instance -> force the first write
RefreshEmissionColor();
}
@@ -226,7 +243,7 @@ public partial class MeshPainter
if (_materialInstance.HasProperty(OcclusionMapId))
{
_materialInstance.EnableKeyword("_OCCLUSIONMAP");
- if (_materialInstance.HasProperty(OcclusionStrengthId)) _materialInstance.SetFloat(OcclusionStrengthId, 1f);
+ SetMatFloat(OcclusionStrengthId, 1f);
}
// Height: the same mask map is also bound to _ParallaxMap, so the painted occlusion (G) doubles
@@ -239,16 +256,23 @@ public partial class MeshPainter
{
_materialInstance.EnableKeyword("_PARALLAXMAP");
_materialInstance.EnableKeyword("_NORMALMAP");
- if (_materialInstance.HasProperty(ParallaxId) && _materialInstance.GetFloat(ParallaxId) < 0.02f)
- _materialInstance.SetFloat(ParallaxId, 0.02f);
+ if (GetMatFloat(ParallaxId, 0.02f) < 0.02f)
+ SetMatFloat(ParallaxId, 0.02f);
}
}
- /// Pushes the global emission intensity to the material's _EmissionColor (live-tunable).
+ private float _lastEmissionIntensity = float.NaN;
+
+ /// Pushes the global emission intensity to the material's _EmissionColor (live-tunable).
+ /// Runs per frame, so it only writes the material when the intensity actually changed.
private void RefreshEmissionColor()
{
+ if (emissionIntensity == _lastEmissionIntensity) return;
if (_materialInstance && _materialInstance.HasProperty(EmissionColorId))
+ {
_materialInstance.SetColor(EmissionColorId, Color.white * Mathf.Max(0f, emissionIntensity));
+ _lastEmissionIntensity = emissionIntensity;
+ }
}
/// Eases the see-through X-ray glow's alpha toward its on/off target ()
diff --git a/Assets/Scripts/MeshPainter.Undo.cs b/Assets/Scripts/MeshPainter.Undo.cs
index df91cb9..eb295d2 100644
--- a/Assets/Scripts/MeshPainter.Undo.cs
+++ b/Assets/Scripts/MeshPainter.Undo.cs
@@ -242,24 +242,19 @@ public partial class MeshPainter
undo = false;
redo = false;
if (TypingGuard.IsTyping) return; // let Ctrl+Z/Y edit text instead of the painting
+ if (!InputUtil.CtrlHeld()) return;
#if ENABLE_INPUT_SYSTEM
var kb = Keyboard.current;
if (kb != null)
{
- if (kb.leftCtrlKey.isPressed || kb.rightCtrlKey.isPressed)
- {
- undo = kb.zKey.wasPressedThisFrame;
- redo = kb.yKey.wasPressedThisFrame;
- }
+ undo = kb.zKey.wasPressedThisFrame;
+ redo = kb.yKey.wasPressedThisFrame;
return;
}
#endif
#if ENABLE_LEGACY_INPUT_MANAGER
- if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl))
- {
- undo = Input.GetKeyDown(KeyCode.Z);
- redo = Input.GetKeyDown(KeyCode.Y);
- }
+ undo = Input.GetKeyDown(KeyCode.Z);
+ redo = Input.GetKeyDown(KeyCode.Y);
#endif
}
}
diff --git a/Assets/Scripts/MeshPainter.cs b/Assets/Scripts/MeshPainter.cs
index 80c931f..18097e9 100644
--- a/Assets/Scripts/MeshPainter.cs
+++ b/Assets/Scripts/MeshPainter.cs
@@ -339,7 +339,8 @@ public partial class MeshPainter : MonoBehaviour
// --- backends ---
private CpuMap[] _cpu;
private readonly List _dilateScratch = new List(512);
- private readonly RaycastHit[] _rayHits = new RaycastHit[32];
+ private readonly RaycastHit[] _rayHits = new RaycastHit[64];
+ private bool _rayHitsSaturated; // log-once: a saturated buffer can silently drop our own collider
// --- preview overlay ---
public Shader previewShader; // optional; else Shader.Find("Hidden/MeshPaintPreview")
@@ -967,6 +968,11 @@ public partial class MeshPainter : MonoBehaviour
Ray ray = paintCamera.ScreenPointToRay(screenPos);
int hitCount = Physics.RaycastNonAlloc(ray, _rayHits, 1000f, ~0, QueryTriggerInteraction.Ignore);
+ if (hitCount == _rayHits.Length && !_rayHitsSaturated)
+ {
+ _rayHitsSaturated = true;
+ Debug.LogWarning($"MeshPainter: raycast buffer saturated ({hitCount}); the paint mesh may be missed.", this);
+ }
float nearest = float.MaxValue;
bool found = false;
for (int i = 0; i < hitCount; i++)
diff --git a/Assets/Scripts/MouseSensitivity.cs b/Assets/Scripts/MouseSensitivity.cs
index 98dadbe..acdb86f 100644
--- a/Assets/Scripts/MouseSensitivity.cs
+++ b/Assets/Scripts/MouseSensitivity.cs
@@ -1,4 +1,3 @@
-using System;
using UnityEngine;
/// Global mouse look-around sensitivity multiplier, shared by every camera
@@ -15,9 +14,6 @@ public static class MouseSensitivity
static bool _loaded;
static float _multiplier = Default;
- /// Fires whenever the multiplier changes (UI may run before the avatar spawns).
- public static event Action Changed;
-
public static float Multiplier
{
get
@@ -40,7 +36,6 @@ public static class MouseSensitivity
// only a clean quit. Settings changes are rare user actions, so the
// extra write is negligible.
PlayerPrefs.Save();
- Changed?.Invoke(v);
}
}
}
diff --git a/Assets/Scripts/NetworkPlayerSetup.cs b/Assets/Scripts/NetworkPlayerSetup.cs
index 06d1d84..81bee3a 100644
--- a/Assets/Scripts/NetworkPlayerSetup.cs
+++ b/Assets/Scripts/NetworkPlayerSetup.cs
@@ -108,6 +108,7 @@ public class NetworkPlayerSetup : NetworkBehaviour
private MeshPainter _painter; // owns the per-avatar material instance carrying _XRayColor
private ConcealmentMeter _concealment; // owner-only embedded-percent source (disabled on remotes)
+ private PlayerCharacterController _controller; // sibling, cached (the freeze re-asserts per frame)
private bool _xrayApplied;
private bool _lastXRay;
private bool _frozenApplied;
@@ -125,7 +126,7 @@ public class NetworkPlayerSetup : NetworkBehaviour
if (IsServer && _conversionSpawnPos.HasValue)
_spawnOverride.Value = _conversionSpawnPos.Value;
- var controller = GetComponent();
+ var controller = _controller = GetComponent();
if (IsOwner)
{
@@ -236,11 +237,8 @@ public class NetworkPlayerSetup : NetworkBehaviour
{
bool freeze = ComputeResolutionFreeze();
- if (freeze && IsOwner)
- {
- var controller = GetComponent();
- if (controller) controller.ControlEnabled = false;
- }
+ if (freeze && IsOwner && _controller)
+ _controller.ControlEnabled = false;
if (_frozenApplied && freeze == _lastFrozen) return;
_lastFrozen = freeze;
@@ -362,14 +360,24 @@ public class NetworkPlayerSetup : NetworkBehaviour
}
// True if THIS machine's local player is a Hunter role variant (so it must not see hiders during hide).
- private static bool LocalViewerIsHunter()
+ // Shared with MatchHud. Called per frame per avatar during hide/seek, so the GetComponent result is
+ // cached per local PlayerObject; a respawn/conversion swaps the object and refreshes the cache.
+ private static NetworkObject _localViewerPo;
+ private static bool _localViewerIsHunter;
+
+ internal static bool LocalViewerIsHunter()
{
var nm = NetworkManager.Singleton;
if (!nm || nm.LocalClient == null) return false;
var po = nm.LocalClient.PlayerObject;
if (!po) return false;
- var setup = po.GetComponent();
- return setup && setup.ScopedModes && setup.Role == PlayerRole.Hunter;
+ if (po != _localViewerPo)
+ {
+ _localViewerPo = po;
+ var setup = po.GetComponent();
+ _localViewerIsHunter = setup && setup.ScopedModes && setup.Role == PlayerRole.Hunter;
+ }
+ return _localViewerIsHunter;
}
///
diff --git a/Assets/Scripts/OnlineSessionBrowserUI.cs b/Assets/Scripts/OnlineSessionBrowserUI.cs
index 8ea320d..e9d32c1 100644
--- a/Assets/Scripts/OnlineSessionBrowserUI.cs
+++ b/Assets/Scripts/OnlineSessionBrowserUI.cs
@@ -11,8 +11,8 @@ using UnityEngine.InputSystem;
/// uGUI binding for . Drives the server-browser canvas - Host / Refresh /
/// pick a row / Join / Disconnect plus a status line - and mirrors the manager's state through its
/// / events.
-/// Replaces the old IMGUI browser. Lives on the browser canvas; the session logic stays on the
-/// NetworkManager's .
+/// Lives on the browser canvas; the session logic stays on the NetworkManager's
+/// .
///
/// Selection model: Refresh fills the list with clones of (each a clickable row
/// showing name + player count). Clicking a row selects it; Join connects to the selected session. While
@@ -66,7 +66,7 @@ public class OnlineSessionBrowserUI : MonoBehaviour
private bool _open; // Esc-toggled visibility while connected
private bool _connected; // last-seen connection state (to detect transitions)
- private string _lastScene; // last-seen active scene name (to auto-close on a scene change)
+ private Scene _lastScene; // last-seen active scene (to auto-close on a scene change)
private string _disconnectText; // last applied caption (avoids per-frame TMP churn)
private void Awake()
@@ -122,8 +122,9 @@ public class OnlineSessionBrowserUI : MonoBehaviour
// Auto-close the overlay if the scene changed underneath it (a remote ended/started the match, or
// we walked into Start Match) so it never lingers as a stale pause across a transition.
- string scene = SceneManager.GetActiveScene().name;
- if (connected && _open && _lastScene != null && scene != _lastScene)
+ // Compared as Scene structs (handles), not names, so nothing is allocated per frame.
+ Scene scene = SceneManager.GetActiveScene();
+ if (connected && _open && _lastScene.IsValid() && scene != _lastScene)
SetOpen(false);
_lastScene = scene;
@@ -301,12 +302,13 @@ public class OnlineSessionBrowserUI : MonoBehaviour
return t ? t.GetComponent() : null;
}
+ // NOT TypingGuard-gated (GameplayKeyPressed): Esc must close the panel even while a text field has focus.
private static bool EscapePressed()
{
#if ENABLE_INPUT_SYSTEM
- return Keyboard.current != null && Keyboard.current.escapeKey.wasPressedThisFrame;
+ return InputUtil.KeyPressedThisFrame(Key.Escape);
#else
- return Input.GetKeyDown(KeyCode.Escape);
+ return InputUtil.KeyPressedThisFrame(KeyCode.Escape);
#endif
}
}
diff --git a/Assets/Scripts/OnlineSessionManager.cs b/Assets/Scripts/OnlineSessionManager.cs
index 650d575..c4bda7a 100644
--- a/Assets/Scripts/OnlineSessionManager.cs
+++ b/Assets/Scripts/OnlineSessionManager.cs
@@ -52,7 +52,6 @@ public class OnlineSessionManager : MonoBehaviour
public string Status => _status;
public bool IsBusy => _busy;
public bool IsConnected => _net && _net.IsListening;
- public bool InSession => _session != null;
public IReadOnlyList Sessions => _sessions;
private void Awake()
diff --git a/Assets/Scripts/PaintHud.Library.cs b/Assets/Scripts/PaintHud.Library.cs
index 0ebfd04..183e5fd 100644
--- a/Assets/Scripts/PaintHud.Library.cs
+++ b/Assets/Scripts/PaintHud.Library.cs
@@ -68,8 +68,8 @@ public partial class PaintHud
{
if (_libraryBound || !root) return;
- _listContent = FindDeep(root, LibListContent) as RectTransform;
- var template = FindDeep(root, LibEntryTemplate);
+ _listContent = GeometryUtil.FindDeep(root, LibListContent) as RectTransform;
+ var template = GeometryUtil.FindDeep(root, LibEntryTemplate);
_entryTemplate = template ? template.gameObject : null;
if (!_listContent || !_entryTemplate) return; // panel not present in this layout
_entryTemplate.SetActive(false);
@@ -83,7 +83,7 @@ public partial class PaintHud
Hook(_deleteButton, OnDeleteClicked);
Hook(_resetButton, OnResetClicked);
- var sd = FindDeep(root, LibSaveDialog);
+ var sd = GeometryUtil.FindDeep(root, LibSaveDialog);
_saveDialog = sd ? sd.gameObject : null;
_nameInput = Find(root, LibNameInput);
_saveInfo = Find(root, LibSaveInfo);
@@ -92,7 +92,7 @@ public partial class PaintHud
Hook(Find
[DisallowMultipleComponent]
public partial class PaintHud : MonoBehaviour
@@ -25,10 +25,10 @@ public partial class PaintHud : MonoBehaviour
[Tooltip("Painter the HUD edits. Empty = the manager's painter, else found automatically.")]
public MeshPainter painter;
- [Tooltip("Authored panel layout. If set it is instantiated and bound; empty = build the panel in code.")]
+ [Tooltip("Authored panel layout. Instantiated and bound at startup.")]
public GameObject layoutPrefab;
- // Stable widget names shared by the code-gen builder, the baked prefab and the binder.
+ // Stable widget names shared by the baked prefab and the binder.
private const string PanelGeneral = "Panel_General";
private const string PanelSpray = "Panel_Spray";
private const string PanelAlbedo = "Panel_Albedo";
@@ -408,13 +408,13 @@ public partial class PaintHud : MonoBehaviour
///
private void WireMaskSelector(Transform panel)
{
- var selector = FindDeep(panel, MaskSelector);
+ var selector = GeometryUtil.FindDeep(panel, MaskSelector);
if (!selector) return;
var masks = painter ? painter.brushMasks : null;
for (int i = 0; ; i++)
{
- var swatch = FindDeep(selector, MaskSwatchPrefix + i);
+ var swatch = GeometryUtil.FindDeep(selector, MaskSwatchPrefix + i);
if (!swatch) break;
bool has = masks != null && i < masks.Count && masks[i];
@@ -445,12 +445,12 @@ public partial class PaintHud : MonoBehaviour
private void RefreshMaskHighlights()
{
if (!_panel || !painter) return;
- var selector = FindDeep(_panel.transform, MaskSelector);
+ var selector = GeometryUtil.FindDeep(_panel.transform, MaskSelector);
if (!selector) return;
for (int i = 0; ; i++)
{
- var swatch = FindDeep(selector, MaskSwatchPrefix + i);
+ var swatch = GeometryUtil.FindDeep(selector, MaskSwatchPrefix + i);
if (!swatch) break;
var frame = swatch.GetComponent();
if (frame) frame.color = i == painter.activeBrushIndex ? MaskFrameSelected : MaskFrameUnselected;
@@ -477,7 +477,7 @@ public partial class PaintHud : MonoBehaviour
///
private void MakeDraggable(Transform root, string panelName)
{
- var panel = FindDeep(root, panelName) as RectTransform;
+ var panel = GeometryUtil.FindDeep(root, panelName) as RectTransform;
if (!panel) return;
var drag = panel.GetComponent();
if (!drag) drag = panel.gameObject.AddComponent();
@@ -507,7 +507,7 @@ public partial class PaintHud : MonoBehaviour
///
private static void WirePanelFade(Transform root, string panelName, string toggleName)
{
- var panel = FindDeep(root, panelName);
+ var panel = GeometryUtil.FindDeep(root, panelName);
if (!panel) return;
var group = panel.GetComponent();
if (!group) group = panel.gameObject.AddComponent();
@@ -522,7 +522,7 @@ public partial class PaintHud : MonoBehaviour
/// Finds a slider cell by name and the Slider inside it (null when either is missing).
private static Slider FindSlider(Transform panel, string name, out Transform cell)
{
- cell = FindDeep(panel, name);
+ cell = GeometryUtil.FindDeep(panel, name);
return cell ? cell.GetComponentInChildren() : null;
}
@@ -560,52 +560,10 @@ public partial class PaintHud : MonoBehaviour
private static T Find(Transform panel, string name) where T : Component
{
- var child = FindDeep(panel, name);
+ var child = GeometryUtil.FindDeep(panel, name);
return child ? child.GetComponent() : null;
}
- /// Depth-first search by name (widgets now live nested inside per-channel sub-panels).
- private static Transform FindDeep(Transform root, string name)
- {
- if (root.name == name) return root;
- for (int i = 0; i < root.childCount; i++)
- {
- var found = FindDeep(root.GetChild(i), name);
- if (found) return found;
- }
- return null;
- }
-
- // --- Static UI factory (shared with HsvColorPicker) ---
-
- public static RectTransform NewRect(string name, Transform parent)
- {
- var go = new GameObject(name, typeof(RectTransform));
- go.transform.SetParent(parent, false);
- return (RectTransform)go.transform;
- }
-
- public static Image AddImage(GameObject go, Color color)
- {
- var img = go.AddComponent();
- img.color = color;
- return img;
- }
-
- /// Adds a full-rect, non-raycasting overlay image (sprite assigned by the caller).
- public static Image AddOverlay(RectTransform parent, string name)
- {
- var rt = NewRect(name, parent);
- rt.anchorMin = Vector2.zero;
- rt.anchorMax = Vector2.one;
- rt.offsetMin = Vector2.zero;
- rt.offsetMax = Vector2.zero;
- var img = rt.gameObject.AddComponent();
- img.type = Image.Type.Simple;
- img.raycastTarget = false;
- return img;
- }
-
// Cached gradient sprites for the colour picker.
private static Sprite _satSprite, _valSprite, _hueSprite;
diff --git a/Assets/Scripts/PlayerCharacterController.cs b/Assets/Scripts/PlayerCharacterController.cs
index dc66f80..48fe433 100644
--- a/Assets/Scripts/PlayerCharacterController.cs
+++ b/Assets/Scripts/PlayerCharacterController.cs
@@ -215,10 +215,10 @@ public class PlayerCharacterController : MonoBehaviour
private float _verticalVelocity;
private Vector3 _horizontalVelocity; // smoothed horizontal move velocity (accel/decel ramp)
private Vector3 _climbVelocity; // smoothed along-surface climb velocity (accel ramp for precise placement)
- private bool _sprinting; // sprint key state this frame (exposed via IsSprinting)
- private bool _isIdle; // snapshot: player essentially still this frame (exposed via IsIdle)
- private bool _movingOnGround; // snapshot: grounded & moving above the run threshold (exposed via IsMovingOnGround)
- private bool _isFalling; // snapshot: airborne & descending past the fall threshold (exposed via IsFalling)
+ private bool _sprinting; // exposed via IsSprinting
+ private bool _isIdle; // exposed via IsIdle
+ private bool _movingOnGround; // exposed via IsMovingOnGround
+ private bool _isFalling; // exposed via IsFalling
private float _groundOffset; // signed vertical mesh raise from the ground mouse pose-adjust (+ up / - down); folded into SetMeshLean
private Vector3 _spawnPosition; // world position this avatar spawned at; the rescue (kill-plane) teleport target
private bool _spawnCaptured; // false until the first Update captures the spawn position (after OnNetworkSpawn's ring offset has landed)
@@ -256,7 +256,8 @@ public class PlayerCharacterController : MonoBehaviour
private float _cameraYOffset; // last camera-target local-up offset written (cached so an unchanged value skips the transform write)
private float _meshOffsetResidual; // leftover mesh lean still easing back to 0 after leaving the wall (no longer climbing)
private Vector3 _meshOffsetExitNormal; // wall normal latched at exit; the residual eases along it (live _wallNormal may shift on stray contact)
- private readonly RaycastHit[] _wallProbe = new RaycastHit[8];
+ private readonly RaycastHit[] _wallProbe = new RaycastHit[16];
+ private bool _wallProbeSaturated; // log-once: a full buffer can hide the nearest valid wall/ground
// Mantle (top-out) state.
private const float TopOutRegrabCooldown = 0.3f; // blocks re-grab right after any top-out so holding W tops out cleanly instead of detach/re-grab buzzing
@@ -1038,6 +1039,7 @@ public class PlayerCharacterController : MonoBehaviour
int count = Physics.RaycastNonAlloc(
origin, dir, _wallProbe, maxDist, ~0, QueryTriggerInteraction.Ignore);
+ WarnIfProbeSaturated(count);
float best = float.MaxValue;
bool found = false;
@@ -1060,6 +1062,13 @@ public class PlayerCharacterController : MonoBehaviour
return found;
}
+ private void WarnIfProbeSaturated(int hitCount)
+ {
+ if (hitCount != _wallProbe.Length || _wallProbeSaturated) return;
+ _wallProbeSaturated = true;
+ Debug.LogWarning($"PlayerCharacterController: climb probe buffer saturated ({hitCount}); the nearest surface may be missed.", this);
+ }
+
///
/// Pulls the character up and over the wall's top edge: phase 1 rises until the
/// capsule clears the edge, phase 2 moves forward onto the surface, then exits.
@@ -1113,6 +1122,7 @@ public class PlayerCharacterController : MonoBehaviour
int count = Physics.RaycastNonAlloc(
origin, -_wallNormal, _wallProbe, maxDist, ~0, QueryTriggerInteraction.Ignore);
+ WarnIfProbeSaturated(count);
for (int i = 0; i < count; i++)
{
if (IsPlayerCollider(_wallProbe[i].collider) || IsNoClimb(_wallProbe[i].collider))
@@ -1135,6 +1145,7 @@ public class PlayerCharacterController : MonoBehaviour
int count = Physics.RaycastNonAlloc(
origin, Vector3.down, _wallProbe, maxDist, ~0, QueryTriggerInteraction.Ignore);
+ WarnIfProbeSaturated(count);
float best = float.MaxValue;
Vector3 normal = Vector3.up;
diff --git a/Assets/Scripts/PlayerPaintSync.cs b/Assets/Scripts/PlayerPaintSync.cs
index 9af2d54..1e648c3 100644
--- a/Assets/Scripts/PlayerPaintSync.cs
+++ b/Assets/Scripts/PlayerPaintSync.cs
@@ -156,7 +156,7 @@ public class PlayerPaintSync : NetworkBehaviour
if (!IsServer)
{
_awaitingSnapshot = true;
- LogSync("late joiner: requesting baseline snapshot");
+ if (logSync) LogSync("late joiner: requesting baseline snapshot");
RequestSnapshotRpc();
}
}
@@ -195,6 +195,8 @@ public class PlayerPaintSync : NetworkBehaviour
}
}
+ // Call sites are guarded with `if (logSync)` so the interpolated message string is never
+ // built when logging is off (some sites run per frame while stamps are flushing).
private void LogSync(string msg)
{
if (logSync) Debug.Log($"{SyncTag} {msg}", this);
@@ -223,7 +225,7 @@ public class PlayerPaintSync : NetworkBehaviour
{
_snapReadyToApply = false;
_painter.ApplyPaintSnapshot(_snapBuffer, _snapBaselineId);
- LogSync($"applied snapshot baseline {_snapBaselineId}; replaying buffered events");
+ if (logSync) LogSync($"applied snapshot baseline {_snapBaselineId}; replaying buffered events");
FlushBuffered();
_snapBuffer = null;
_awaitingSnapshot = false;
@@ -255,7 +257,7 @@ public class PlayerPaintSync : NetworkBehaviour
_pendingCarry = null;
if (_painter.LoadPaintFromSnapshot(carry))
{
- LogSync($"carried painting onto respawned avatar ({carry.Length} bytes); rebroadcasting to peers");
+ if (logSync) LogSync($"carried painting onto respawned avatar ({carry.Length} bytes); rebroadcasting to peers");
BroadcastLoadedPaint();
}
}
@@ -330,12 +332,12 @@ public class PlayerPaintSync : NetworkBehaviour
if (kind == OutKind.Restore)
{
SubmitRestoreServerRpc(seq, id);
- LogSync($"sent RESTORE to checkpoint {id} (seq {seq})");
+ if (logSync) LogSync($"sent RESTORE to checkpoint {id} (seq {seq})");
}
else
{
SubmitCheckpointServerRpc(seq, id);
- LogSync($"sent checkpoint {id} (seq {seq})");
+ if (logSync) LogSync($"sent checkpoint {id} (seq {seq})");
}
consumed++;
}
@@ -345,7 +347,7 @@ public class PlayerPaintSync : NetworkBehaviour
_outgoing.RemoveRange(0, consumed);
if (!unlimited && _outgoing.Count > 0)
- LogSync($"stamp cap ({cap}) reached; {_outgoing.Count} command(s) carried over to next frame");
+ if (logSync) LogSync($"stamp cap ({cap}) reached; {_outgoing.Count} command(s) carried over to next frame");
}
// ---- Stamp / checkpoint / restore commands (owner -> server -> remotes) ----
@@ -396,7 +398,7 @@ public class PlayerPaintSync : NetworkBehaviour
return;
_painter.RemoteCommitCheckpoint(id);
- LogSync($"committed checkpoint {id} (seq {seq})");
+ if (logSync) LogSync($"committed checkpoint {id} (seq {seq})");
}
[ClientRpc]
@@ -409,11 +411,11 @@ public class PlayerPaintSync : NetworkBehaviour
// undo past this peer's join point), fetch a fresh snapshot of the resulting state instead.
if (_painter.RemoteRestoreTo(id))
{
- LogSync($"reproduced RESTORE to checkpoint {id} (seq {seq})");
+ if (logSync) LogSync($"reproduced RESTORE to checkpoint {id} (seq {seq})");
}
else
{
- LogSync($"cannot reproduce checkpoint {id} (not in local history, seq {seq}) -> owner snapshot fallback");
+ if (logSync) LogSync($"cannot reproduce checkpoint {id} (not in local history, seq {seq}) -> owner snapshot fallback");
RequestSnapshotFallback();
}
}
@@ -441,7 +443,7 @@ public class PlayerPaintSync : NetworkBehaviour
if (!_painter.RemoteRestoreTo(ev.id))
{
// Can't reproduce -> the fresh snapshot we request next supersedes the rest.
- LogSync($"buffered RESTORE to checkpoint {ev.id} not reproducible -> owner snapshot fallback");
+ if (logSync) LogSync($"buffered RESTORE to checkpoint {ev.id} not reproducible -> owner snapshot fallback");
_buffered.Clear();
RequestSnapshotFallback();
return;
@@ -479,7 +481,7 @@ public class PlayerPaintSync : NetworkBehaviour
ulong requester = p.Receive.SenderClientId;
int baselineId = _painter.CurrentCheckpointId;
- LogSync($"serving fallback snapshot to client {requester} (cutoff {_seq}, baseline {baselineId}, {data.Length} bytes)");
+ if (logSync) LogSync($"serving fallback snapshot to client {requester} (cutoff {_seq}, baseline {baselineId}, {data.Length} bytes)");
StartCoroutine(UploadSnapshot(data, _seq, baselineId, broadcast: false, target: requester));
}
@@ -495,7 +497,7 @@ public class PlayerPaintSync : NetworkBehaviour
// last message this replica applied). The checkpoint id tags the baseline the joiner adopts.
int capturedSeq = IsOwner ? _seq : _seqHorizon;
int baselineId = _painter.CurrentCheckpointId;
- LogSync($"serving late-join snapshot to client {p.Receive.SenderClientId} (cutoff {capturedSeq}, baseline {baselineId}, {data.Length} bytes)");
+ if (logSync) LogSync($"serving late-join snapshot to client {p.Receive.SenderClientId} (cutoff {capturedSeq}, baseline {baselineId}, {data.Length} bytes)");
StartCoroutine(SendSnapshot(new[] { p.Receive.SenderClientId }, data, capturedSeq, baselineId));
}
@@ -539,7 +541,7 @@ public class PlayerPaintSync : NetworkBehaviour
// racing the carry). Equal cutoff is allowed so the owner-snapshot heal still lands.
if (capturedSeq < _acceptedCutoffFloor)
{
- LogSync($"ignoring stale snapshot #{xferId} (cutoff {capturedSeq} < accepted {_acceptedCutoffFloor})");
+ if (logSync) LogSync($"ignoring stale snapshot #{xferId} (cutoff {capturedSeq} < accepted {_acceptedCutoffFloor})");
return;
}
@@ -550,7 +552,7 @@ public class PlayerPaintSync : NetworkBehaviour
_snapBaselineId = baselineId;
_snapReadyToApply = false;
_awaitingSnapshot = true;
- LogSync($"receiving snapshot #{xferId} ({totalBytes} bytes, cutoff {capturedSeq}, baseline {baselineId})");
+ if (logSync) LogSync($"receiving snapshot #{xferId} ({totalBytes} bytes, cutoff {capturedSeq}, baseline {baselineId})");
}
[ClientRpc]
@@ -603,7 +605,7 @@ public class PlayerPaintSync : NetworkBehaviour
int cutoff = ++_seq; // every event up to here is now superseded by the loaded baseline
_seqHorizon = cutoff;
int baselineId = _painter.CurrentCheckpointId; // the load's fresh baseline id
- LogSync($"broadcasting loaded painting to all peers (cutoff {cutoff}, baseline {baselineId}, {data.Length} bytes)");
+ if (logSync) LogSync($"broadcasting loaded painting to all peers (cutoff {cutoff}, baseline {baselineId}, {data.Length} bytes)");
StartCoroutine(UploadSnapshot(data, cutoff, baselineId, broadcast: true, target: 0));
}
diff --git a/Assets/Scripts/PlayerPose.cs b/Assets/Scripts/PlayerPose.cs
index d319337..3eca126 100644
--- a/Assets/Scripts/PlayerPose.cs
+++ b/Assets/Scripts/PlayerPose.cs
@@ -5,7 +5,7 @@ using UnityEngine;
/// plus optional walk and run clips that the pose system plays itself (the Animator stays empty) while
/// moving on the ground, its own CharacterController capsule dimensions (so a low pose like a ball can
/// crawl under furniture the default standing capsule is too tall for), and a camera-target y-offset that
-/// frames the pose without moving the body. The first ScriptableObject in this project.
+/// frames the pose without moving the body.
///
/// Named PlayerPose (not Pose) to avoid shadowing the built-in UnityEngine.Pose struct, mirroring
/// why the controller is PlayerCharacterController and not just "PlayerController".
@@ -72,42 +72,18 @@ public class PlayerPose : ScriptableObject
"sensible height even though the camera target sits where third-person frames it. Meters.")]
[SerializeField] private float firstPersonCameraTargetYOffset;
- /// Idle clip held while the player stands still in this pose (may be null).
+ // Read-only accessors; semantics are documented on the serialized fields' tooltips above.
public AnimationClip Clip => clip;
-
- /// Playback speed multiplier for the idle clip (1 = authored speed).
public float ClipSpeed => clipSpeed;
-
- /// Optional clip played while walking on the ground; null = hold the idle pose.
public AnimationClip WalkClip => walkClip;
-
- /// Playback speed multiplier for the walk clip (1 = authored speed).
public float WalkClipSpeed => walkClipSpeed;
-
- /// Optional clip played while sprinting on the ground; null = hold the idle pose.
public AnimationClip RunClip => runClip;
-
- /// Playback speed multiplier for the run clip (1 = authored speed).
public float RunClipSpeed => runClipSpeed;
-
- /// Optional clip played while falling; null = hold the idle pose.
public AnimationClip FallClip => fallClip;
-
- /// Playback speed multiplier for the fall clip (1 = authored speed).
public float FallClipSpeed => fallClipSpeed;
-
- /// CharacterController capsule center Y applied while this pose is active.
public float ControllerCenterY => controllerCenterY;
-
- /// CharacterController capsule radius applied while this pose is active.
public float ControllerRadius => controllerRadius;
-
- /// CharacterController capsule height applied while this pose is active.
public float ControllerHeight => controllerHeight;
-
- /// Local-up offset applied to the camera target while this pose is active (any state).
public float CameraTargetYOffset => cameraTargetYOffset;
-
- /// Additional local-up offset for the first-person eye, on top of the camera target (first person only).
public float FirstPersonCameraTargetYOffset => firstPersonCameraTargetYOffset;
}
diff --git a/Assets/Scripts/PlayerPoseController.cs b/Assets/Scripts/PlayerPoseController.cs
index 25ae156..db819ab 100644
--- a/Assets/Scripts/PlayerPoseController.cs
+++ b/Assets/Scripts/PlayerPoseController.cs
@@ -121,9 +121,6 @@ public class PlayerPoseController : MonoBehaviour
/// Locomotion category resolved this frame (which clip the active pose is playing). The owner publishes it for network sync.
public Locomotion CurrentLocomotion => _currentLocomotion;
- /// True once this avatar is driven by replicated network state instead of local input/physics.
- public bool NetworkControlled => _networkControlled;
-
///
/// Switches this avatar to network-driven animation: local input, the sprint-reset, the per-pose
/// collision capsule and camera framing are all skipped; pose index and locomotion are supplied by
@@ -248,9 +245,6 @@ public class PlayerPoseController : MonoBehaviour
/// Advances to the next pose, wrapping at the end of the list.
public void NextPose() => SetPoseIndex(_currentIndex + 1);
- /// Steps back to the previous pose, wrapping at the start of the list.
- public void PreviousPose() => SetPoseIndex(_currentIndex - 1);
-
/// Selects a pose by index, wrapping into range so cycling never goes out of bounds.
public void SetPoseIndex(int index) => _currentIndex = WrapIndex(index);
diff --git a/Assets/Scripts/PlayerRole.cs b/Assets/Scripts/PlayerRole.cs
index 421c8b8..ad7d351 100644
--- a/Assets/Scripts/PlayerRole.cs
+++ b/Assets/Scripts/PlayerRole.cs
@@ -1,7 +1,7 @@
///
-/// Which side a spawned avatar plays. Picked at connect time (NetworkBootstrap) and sent as the
-/// connection-approval payload so the server spawns the matching prefab variant, then baked into
-/// that prefab and read back by GameModeManager to build the role's mode set.
+/// Which side a spawned avatar plays. Baked into each avatar prefab variant (the server rolls the
+/// roles at match start and spawns the matching variant); read back by GameModeManager to build the
+/// role's mode set.
///
public enum PlayerRole
{
diff --git a/Assets/Scripts/StartMatchTrigger.cs b/Assets/Scripts/StartMatchTrigger.cs
index 22e91c3..4f0024b 100644
--- a/Assets/Scripts/StartMatchTrigger.cs
+++ b/Assets/Scripts/StartMatchTrigger.cs
@@ -25,8 +25,9 @@ using UnityEngine;
public class StartMatchTrigger : MonoBehaviour
{
private Collider _collider;
- private readonly Collider[] _overlap = new Collider[16];
+ private readonly Collider[] _overlap = new Collider[32];
private bool _started;
+ private bool _overlapSaturated; // log-once: a full buffer could silently drop the authority's avatar
private void Awake() => _collider = GetComponent();
@@ -40,6 +41,11 @@ public class StartMatchTrigger : MonoBehaviour
if (!match || match.Phase != MatchManager.MatchPhase.Lobby) return;
int count = OverlapTrigger();
+ if (count == _overlap.Length && !_overlapSaturated)
+ {
+ _overlapSaturated = true;
+ Debug.LogWarning($"StartMatchTrigger: overlap buffer saturated ({count}); the authority avatar may be missed.", this);
+ }
for (int i = 0; i < count; i++)
{
var netObj = _overlap[i].GetComponentInParent();
diff --git a/Assets/Scripts/TypingGuard.cs b/Assets/Scripts/TypingGuard.cs
index e8c163e..b368bf4 100644
--- a/Assets/Scripts/TypingGuard.cs
+++ b/Assets/Scripts/TypingGuard.cs
@@ -1,3 +1,4 @@
+using UnityEngine;
using UnityEngine.EventSystems;
using TMPro;
@@ -9,6 +10,12 @@ using TMPro;
///
public static class TypingGuard
{
+ // Consulted several times per frame by the hotkey readers; the GetComponent lookups are cached
+ // per selected GameObject (a selection change refreshes them).
+ private static GameObject _cachedGo;
+ private static TMP_InputField _cachedTmp;
+ private static UnityEngine.UI.InputField _cachedLegacy;
+
/// A text input field is currently focused and capturing keystrokes.
public static bool IsTyping
{
@@ -20,11 +27,15 @@ public static class TypingGuard
var go = es.currentSelectedGameObject;
if (!go) return false;
- var tmp = go.GetComponent();
- if (tmp) return tmp.isFocused;
+ if (go != _cachedGo)
+ {
+ _cachedGo = go;
+ _cachedTmp = go.GetComponent();
+ _cachedLegacy = _cachedTmp ? null : go.GetComponent();
+ }
- var legacy = go.GetComponent();
- return legacy && legacy.isFocused;
+ if (_cachedTmp) return _cachedTmp.isFocused;
+ return _cachedLegacy && _cachedLegacy.isFocused;
}
}
}