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; } } }