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:
1 parent
2243cc823e
commit
382bfb36df
32 files changed
+292
-348
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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++)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 " +
|
||||
|
||||
@@ -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"/>)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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++)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user