Refactor scripts: dedup remainder, remove dead code, perf hardening

Iteration 4 over Assets/Scripts (no component/file splits, no Unity assets touched):

Dedup:
- Route inline cursor-release, material guards, Ctrl-held and undo/redo
  input through existing InputUtil / SetMatFloat helpers
- Add ungated InputUtil.KeyPressedThisFrame; GameplayKeyPressed builds on it
- Unify FindDeep/FindDeepChild into GeometryUtil.FindDeep

Dead code / stale docs:
- Remove unused code-gen bake path (HsvColorPicker.Build, PaintHud factory)
- Drop unreferenced symbols (SubPhaseChanged event, LocalPhaseDuration,
  IsLocalSceneMain, MouseSensitivity.Changed, IsPaused, InSession,
  NetworkControlled, PreviousPose, JointCount, voxel-count accessors)
- Fix incorrect PlayerRole / PaintHud / Pick / PlayerPose docs

Perf / robustness (behaviour-neutral):
- ConcealmentMeter.Evaluate: reuse vertex/topology buffers, two-pointer
  crossing count
- BuildMeshCaches: alloc-free vertex copy, cached topology/UVs
- Guard all PlayerPaintSync LogSync sites behind logSync
- Cache LocalViewerIsHunter / controller / CanShoot / TypingGuard lookups;
  dedup LocalIsHunter with MatchHud
- Change-guard RefreshEmissionColor and HeroCutout.Disarm
- Cache scene/countdown string comparisons per frame
- Grow NonAlloc buffers and log-once on saturation

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-07-02 13:33:18 +02:00
1 parent 2243cc823e
commit 382bfb36df
32 files changed
+292 -348

No files matched your search

+1 -1
View File
@@ -274,7 +274,7 @@ public class ConcealmentColliderBaker : MonoBehaviour
}
/// <summary>
/// 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.
+25 -19
View File
@@ -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. <see cref="EmbeddedPercent"/> is the headline value,
/// <see cref="VisiblePercent"/> its complement.
/// <see cref="visiblePercent"/> its complement.
///
/// Pipeline:
/// 1. <see cref="SkinnedMeshRenderer.BakeMesh(Mesh)"/> -> mesh in the renderer's LOCAL space, then the
@@ -66,15 +66,15 @@ public class ConcealmentMeter : MonoBehaviour
/// <summary>Percentage [0..100] of the character's solid volume embedded inside environment colliders.</summary>
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<Vector3> _verts = new List<Vector3>(); // 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<Vector3> _vbuf = new List<Vector3>(); // 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++)
{
-1
View File
@@ -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<ConcealmentVolume> Active = new List<ConcealmentVolume>();
public static IReadOnlyList<ConcealmentVolume> All => Active;
-1
View File
@@ -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;
+19 -4
View File
@@ -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<NetworkPlayerSetup>();
return setup && setup.ScopedModes && setup.Role == PlayerRole.Hunter;
if (player != _setupSource)
{
_setupSource = player;
_setup = player.GetComponent<NetworkPlayerSetup>();
}
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
+2 -7
View File
@@ -62,9 +62,6 @@ public class GameModeManager : MonoBehaviour
private bool _paused;
private int _resumeIndex = -1;
/// <summary>True while local gameplay is suspended for a modal overlay (see <see cref="SetPaused"/>).</summary>
public bool IsPaused => _paused;
/// <summary>The currently active mode (null before startup).</summary>
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);
}
/// <summary>
@@ -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
}
+13 -1
View File
@@ -1,9 +1,21 @@
using System.Collections.Generic;
using UnityEngine;
/// <summary>Shared mesh/bounds geometry helpers.</summary>
/// <summary>Shared mesh/bounds/hierarchy helpers.</summary>
public static class GeometryUtil
{
/// <summary>Depth-first hierarchy search by name; checks <paramref name="root"/> itself first.</summary>
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;
}
/// <summary>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;
/// <paramref name="scratch"/> is reused so nothing is allocated per bake.</summary>
+2 -14
View File
@@ -180,7 +180,7 @@ public class HandIK : MonoBehaviour
var chain = new List<Transform>(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;
+9 -1
View File
@@ -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
/// <summary>Clears the "enabled" flag so cutout materials render as plain URP/Lit.</summary>
private void Disarm() => Shader.SetGlobalVector(TargetId, Vector4.zero);
private void Disarm()
{
if (!_armed) return;
_armed = false;
Shader.SetGlobalVector(TargetId, Vector4.zero);
}
/// <summary>
/// Lazily resolves the orbit vcam and its Cinemachine Deoccluder (they may live in a scene that
+4 -57
View File
@@ -10,11 +10,10 @@ using UnityEngine.UI;
/// edited separately by a standalone slider in the HUD, not by this picker; <see cref="SetColor"/>
/// still stores the incoming alpha so <see cref="Value"/> round-trips it.
///
/// <see cref="Build"/> creates the child structure (stable names, no sprites or hooks); it can
/// be baked into a prefab. <see cref="Rebind"/> 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 <see cref="Rebind"/>, so no re-bake is required.
/// The child structure (stable names) comes from the baked HUD prefab. <see cref="Rebind"/>
/// (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 <see cref="Rebind"/>, so no
/// re-bake is required.
/// </summary>
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();
}
/// <summary>
/// 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 <see cref="Rebind"/> afterwards (at runtime) to make it
/// functional.
/// </summary>
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<UiPointerSurface>();
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<Image>();
hueImg.type = Image.Type.Simple;
hue.gameObject.AddComponent<UiPointerSurface>();
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;
}
/// <summary>
/// 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);
}
/// <summary>Anchors a child to this rect's top-left corner with the given offset and size.</summary>
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;
}
}
+14 -16
View File
@@ -4,9 +4,7 @@ using UnityEngine.InputSystem;
#endif
/// <summary>
/// Shared low-level input reads that were previously copy-pasted verbatim across the camera rigs
/// (<see cref="FirstPersonController"/>, <see cref="FreeCamController"/>,
/// <see cref="ThirdPersonOrbitInput"/>), 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.
/// </summary>
@@ -105,20 +103,20 @@ public static class InputUtil
#endif
}
/// <summary>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.</summary>
/// <summary>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.</summary>
#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
/// <summary>Gameplay key pressed on this exact frame (edge), suppressed while a UI text field is
/// focused so typing never triggers gameplay bindings.</summary>
#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
}
+11 -18
View File
@@ -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<NetworkPlayerSetup>();
return setup && setup.ScopedModes && setup.Role == PlayerRole.Hunter;
}
private static void DrawCrosshair()
{
float cx = Screen.width * 0.5f;
+24 -17
View File
@@ -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;
/// <summary>Seconds left in the current sub-phase for the HUD (clamped to 0; cosmetic).</summary>
public float LocalPhaseRemaining => Mathf.Max(0f, _phaseEndTime - Time.time);
/// <summary>Total length of the current sub-phase (for a HUD progress bar, if wanted).</summary>
public float LocalPhaseDuration => _phaseDuration;
/// <summary>Raised after the local sub-phase changes (argument = the new sub-phase). HUD/visibility follow it.</summary>
public event System.Action<MatchSubPhase> 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
/// <summary>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.</summary>
/// the authority or no match is running.</summary>
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).
+9 -14
View File
@@ -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);
}
}
}
+1 -6
View File
@@ -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 " +
+38 -14
View File
@@ -39,25 +39,41 @@ public partial class MeshPainter
}
}
/// <summary>Reads vertices/triangles/UVs of the collider mesh and computes culling data.</summary>
// 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<Vector3> _vertScratch = new System.Collections.Generic.List<Vector3>();
private int _meshCacheId; // mesh instance the topology/UV caches were built for (0 = none)
/// <summary>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.</summary>
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);
}
}
/// <summary>Pushes the global emission intensity to the material's _EmissionColor (live-tunable).</summary>
private float _lastEmissionIntensity = float.NaN;
/// <summary>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.</summary>
private void RefreshEmissionColor()
{
if (emissionIntensity == _lastEmissionIntensity) return;
if (_materialInstance && _materialInstance.HasProperty(EmissionColorId))
{
_materialInstance.SetColor(EmissionColorId, Color.white * Mathf.Max(0f, emissionIntensity));
_lastEmissionIntensity = emissionIntensity;
}
}
/// <summary>Eases the see-through X-ray glow's alpha toward its on/off target (<see cref="XRayRevealed"/>)
+5 -10
View File
@@ -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
}
}
+7 -1
View File
@@ -339,7 +339,8 @@ public partial class MeshPainter : MonoBehaviour
// --- backends ---
private CpuMap[] _cpu;
private readonly List<int> _dilateScratch = new List<int>(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++)
-5
View File
@@ -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<float> 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);
}
}
}
+17 -9
View File
@@ -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<PlayerCharacterController>();
var controller = _controller = GetComponent<PlayerCharacterController>();
if (IsOwner)
{
@@ -236,11 +237,8 @@ public class NetworkPlayerSetup : NetworkBehaviour
{
bool freeze = ComputeResolutionFreeze();
if (freeze && IsOwner)
{
var controller = GetComponent<PlayerCharacterController>();
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<NetworkPlayerSetup>();
return setup && setup.ScopedModes && setup.Role == PlayerRole.Hunter;
if (po != _localViewerPo)
{
_localViewerPo = po;
var setup = po.GetComponent<NetworkPlayerSetup>();
_localViewerIsHunter = setup && setup.ScopedModes && setup.Role == PlayerRole.Hunter;
}
return _localViewerIsHunter;
}
/// <summary>
+9 -7
View File
@@ -11,8 +11,8 @@ using UnityEngine.InputSystem;
/// uGUI binding for <see cref="OnlineSessionManager"/>. Drives the server-browser canvas - Host / Refresh /
/// pick a row / Join / Disconnect plus a status line - and mirrors the manager's state through its
/// <see cref="OnlineSessionManager.Changed"/> / <see cref="OnlineSessionManager.SessionsChanged"/> events.
/// Replaces the old IMGUI browser. Lives on the browser canvas; the session logic stays on the
/// NetworkManager's <see cref="OnlineSessionManager"/>.
/// Lives on the browser canvas; the session logic stays on the NetworkManager's
/// <see cref="OnlineSessionManager"/>.
///
/// Selection model: Refresh fills the list with clones of <see cref="entryTemplate"/> (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<TMP_Text>() : 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
}
}
-1
View File
@@ -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<ISessionInfo> Sessions => _sessions;
private void Awake()
+4 -4
View File
@@ -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<TMP_InputField>(root, LibNameInput);
_saveInfo = Find<TMP_Text>(root, LibSaveInfo);
@@ -92,7 +92,7 @@ public partial class PaintHud
Hook(Find<Button>(root, LibSaveCancel), () => { if (_saveDialog) _saveDialog.SetActive(false); });
if (_nameInput) _nameInput.onValueChanged.AddListener(_ => ValidateSaveName());
var cd = FindDeep(root, LibConfirmDialog);
var cd = GeometryUtil.FindDeep(root, LibConfirmDialog);
_confirmDialog = cd ? cd.gameObject : null;
_confirmText = Find<TMP_Text>(root, LibConfirmText);
_confirmButton = Find<Button>(root, LibConfirmOk);
+4 -4
View File
@@ -125,7 +125,7 @@ public partial class PaintHud
foreach (var name in DraggablePanels)
{
if (FindDeep(panel, name) is RectTransform rt)
if (GeometryUtil.FindDeep(panel, name) is RectTransform rt)
state.panels.Add(new PanelPos { name = name, pos = rt.anchoredPosition });
}
@@ -201,7 +201,7 @@ public partial class PaintHud
var root = _panel.transform;
if (state.panels != null)
foreach (var p in state.panels)
if (FindDeep(root, p.name) is RectTransform rt)
if (GeometryUtil.FindDeep(root, p.name) is RectTransform rt)
rt.anchoredPosition = p.pos;
}
@@ -441,7 +441,7 @@ public partial class PaintHud
if (_hasAuthoredLayout || !_panel) return;
var root = _panel.transform;
foreach (var name in DraggablePanels)
if (FindDeep(root, name) is RectTransform rt)
if (GeometryUtil.FindDeep(root, name) is RectTransform rt)
_authoredPanelPos[name] = rt.anchoredPosition;
_hasAuthoredLayout = true;
}
@@ -483,7 +483,7 @@ public partial class PaintHud
{
var root = _panel.transform;
foreach (var kv in _authoredPanelPos)
if (FindDeep(root, kv.Key) is RectTransform rt)
if (GeometryUtil.FindDeep(root, kv.Key) is RectTransform rt)
rt.anchoredPosition = kv.Value;
}
+14 -56
View File
@@ -11,10 +11,10 @@ using UnityEngine.InputSystem.UI;
/// <see cref="PaintMode"/> (follows the manager's ModeChanged) and binds its widgets directly to the
/// live <see cref="MeshPainter"/> fields, so edits apply immediately.
///
/// The layout (independently draggable sub-panels per channel + a global brush card) comes from an
/// authored prefab (<see cref="layoutPrefab"/>) or is generated in code as the fallback. Either way
/// the BINDINGS live in code and are (re)applied at runtime by <see cref="WirePanel"/> against the
/// same stable widget names - a prefab stores only the visual layout, never behaviour.
/// The layout (independently draggable sub-panels per channel + a global brush card) comes from the
/// authored prefab (<see cref="layoutPrefab"/>). The BINDINGS live in code and are (re)applied at
/// runtime by <see cref="WirePanel"/> against stable widget names - the prefab stores only the
/// visual layout, never behaviour.
/// </summary>
[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
/// </summary>
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<Image>();
if (frame) frame.color = i == painter.activeBrushIndex ? MaskFrameSelected : MaskFrameUnselected;
@@ -477,7 +477,7 @@ public partial class PaintHud : MonoBehaviour
/// </summary>
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<DraggablePanel>();
if (!drag) drag = panel.gameObject.AddComponent<DraggablePanel>();
@@ -507,7 +507,7 @@ public partial class PaintHud : MonoBehaviour
/// </summary>
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<CanvasGroup>();
if (!group) group = panel.gameObject.AddComponent<CanvasGroup>();
@@ -522,7 +522,7 @@ public partial class PaintHud : MonoBehaviour
/// <summary>Finds a slider cell by name and the Slider inside it (null when either is missing).</summary>
private static Slider FindSlider(Transform panel, string name, out Transform cell)
{
cell = FindDeep(panel, name);
cell = GeometryUtil.FindDeep(panel, name);
return cell ? cell.GetComponentInChildren<Slider>() : null;
}
@@ -560,52 +560,10 @@ public partial class PaintHud : MonoBehaviour
private static T Find<T>(Transform panel, string name) where T : Component
{
var child = FindDeep(panel, name);
var child = GeometryUtil.FindDeep(panel, name);
return child ? child.GetComponent<T>() : null;
}
/// <summary>Depth-first search by name (widgets now live nested inside per-channel sub-panels).</summary>
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<Image>();
img.color = color;
return img;
}
/// <summary>Adds a full-rect, non-raycasting overlay image (sprite assigned by the caller).</summary>
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<Image>();
img.type = Image.Type.Simple;
img.raycastTarget = false;
return img;
}
// Cached gradient sprites for the colour picker.
private static Sprite _satSprite, _valSprite, _hueSprite;
+16 -5
View File
@@ -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);
}
/// <summary>
/// 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;
+17 -15
View File
@@ -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));
}
+2 -26
View File
@@ -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;
/// <summary>Idle clip held while the player stands still in this pose (may be null).</summary>
// Read-only accessors; semantics are documented on the serialized fields' tooltips above.
public AnimationClip Clip => clip;
/// <summary>Playback speed multiplier for the idle clip (1 = authored speed).</summary>
public float ClipSpeed => clipSpeed;
/// <summary>Optional clip played while walking on the ground; null = hold the idle pose.</summary>
public AnimationClip WalkClip => walkClip;
/// <summary>Playback speed multiplier for the walk clip (1 = authored speed).</summary>
public float WalkClipSpeed => walkClipSpeed;
/// <summary>Optional clip played while sprinting on the ground; null = hold the idle pose.</summary>
public AnimationClip RunClip => runClip;
/// <summary>Playback speed multiplier for the run clip (1 = authored speed).</summary>
public float RunClipSpeed => runClipSpeed;
/// <summary>Optional clip played while falling; null = hold the idle pose.</summary>
public AnimationClip FallClip => fallClip;
/// <summary>Playback speed multiplier for the fall clip (1 = authored speed).</summary>
public float FallClipSpeed => fallClipSpeed;
/// <summary>CharacterController capsule center Y applied while this pose is active.</summary>
public float ControllerCenterY => controllerCenterY;
/// <summary>CharacterController capsule radius applied while this pose is active.</summary>
public float ControllerRadius => controllerRadius;
/// <summary>CharacterController capsule height applied while this pose is active.</summary>
public float ControllerHeight => controllerHeight;
/// <summary>Local-up offset applied to the camera target while this pose is active (any state).</summary>
public float CameraTargetYOffset => cameraTargetYOffset;
/// <summary>Additional local-up offset for the first-person eye, on top of the camera target (first person only).</summary>
public float FirstPersonCameraTargetYOffset => firstPersonCameraTargetYOffset;
}
-6
View File
@@ -121,9 +121,6 @@ public class PlayerPoseController : MonoBehaviour
/// <summary>Locomotion category resolved this frame (which clip the active pose is playing). The owner publishes it for network sync.</summary>
public Locomotion CurrentLocomotion => _currentLocomotion;
/// <summary>True once this avatar is driven by replicated network state instead of local input/physics.</summary>
public bool NetworkControlled => _networkControlled;
/// <summary>
/// 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
/// <summary>Advances to the next pose, wrapping at the end of the list.</summary>
public void NextPose() => SetPoseIndex(_currentIndex + 1);
/// <summary>Steps back to the previous pose, wrapping at the start of the list.</summary>
public void PreviousPose() => SetPoseIndex(_currentIndex - 1);
/// <summary>Selects a pose by index, wrapping into range so cycling never goes out of bounds.</summary>
public void SetPoseIndex(int index) => _currentIndex = WrapIndex(index);
+3 -3
View File
@@ -1,7 +1,7 @@
/// <summary>
/// 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.
/// </summary>
public enum PlayerRole
{
+7 -1
View File
@@ -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<Collider>();
@@ -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<NetworkObject>();
+15 -4
View File
@@ -1,3 +1,4 @@
using UnityEngine;
using UnityEngine.EventSystems;
using TMPro;
@@ -9,6 +10,12 @@ using TMPro;
/// </summary>
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;
/// <summary>A text input field is currently focused and capturing keystrokes.</summary>
public static bool IsTyping
{
@@ -20,11 +27,15 @@ public static class TypingGuard
var go = es.currentSelectedGameObject;
if (!go) return false;
var tmp = go.GetComponent<TMP_InputField>();
if (tmp) return tmp.isFocused;
if (go != _cachedGo)
{
_cachedGo = go;
_cachedTmp = go.GetComponent<TMP_InputField>();
_cachedLegacy = _cachedTmp ? null : go.GetComponent<UnityEngine.UI.InputField>();
}
var legacy = go.GetComponent<UnityEngine.UI.InputField>();
return legacy && legacy.isFocused;
if (_cachedTmp) return _cachedTmp.isFocused;
return _cachedLegacy && _cachedLegacy.isFocused;
}
}
}