perf(paint): stronger X-ray hit flash + kill spawn/respawn freeze
X-ray hit flash: drive a dedicated flash mode instead of the normal reveal - pulse off (steady), intensity boost (xrayFlashBoost, default 3x), snap-on, then restore authored color/pulse exactly. Fixes the weak/dark flicker where the 0.35s hit window often caught only the pulse's near-off plateau. Spawn/respawn freeze: pool GPU RenderTextures across despawn->respawn (TeardownGpu recycles, CreateRT pops) so a hider->hunter conversion no longer churns ~70MB of RT alloc/free on every observer; reuse staging textures to cut per-map GC. Defer the blank base-seed on reveal-gated remote replicas (DeferInitialSeed) - the real pixels arrive with the snapshot apply anyway; gate the FurShellRenderer alongside the body so a deferred replica never shows the pooled RT's stale fur; EnsureBackendSeeded + TryCaptureSnapshot guard against capture-before-reseed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
4ceaf85c8f
commit
48975c0f20
9 files changed
+196
-24
No files matched your search
@@ -46,6 +46,7 @@ public partial class MeshPainter
|
||||
/// </summary>
|
||||
private void ReseedCanonical(Color32[][] canonical)
|
||||
{
|
||||
_backendSeeded = true; // the backend is about to hold real content (fast-path reseed or full rebuild)
|
||||
if (_setupValid)
|
||||
{
|
||||
if (useGpu && _gpuActive && _gpu != null) { ReseedGpuInPlace(canonical); _hasLastScreen = false; return; }
|
||||
@@ -54,6 +55,17 @@ public partial class MeshPainter
|
||||
RebuildBackend(useGpu, canonical);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Materializes a DEFERRED backend's base seed on demand: a remote replica that skipped its Start seed
|
||||
/// (see <see cref="DeferInitialSeed"/>) must fill the base BEFORE a replicated stamp lands on it or it is
|
||||
/// captured, so nothing paints over / serves uninitialized pixels. No-op once seeded (the normal case: the
|
||||
/// snapshot apply already re-seeded it). A null canonical resolves each map to the material seed.
|
||||
/// </summary>
|
||||
private void EnsureBackendSeeded()
|
||||
{
|
||||
if (!_backendSeeded) ReseedCanonical(null);
|
||||
}
|
||||
|
||||
/// <summary>Tears down the active backend and sets up the requested one on the given maps.</summary>
|
||||
private void RebuildBackend(bool gpu, Color32[][] canonical)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ public partial class MeshPainter
|
||||
|
||||
private void SetupCpu(Color32[][] canonical)
|
||||
{
|
||||
int n = textureSize * textureSize;
|
||||
_cpu = new CpuMap[MAP_COUNT];
|
||||
for (int mi = 0; mi < MAP_COUNT; mi++)
|
||||
{
|
||||
@@ -17,15 +18,20 @@ public partial class MeshPainter
|
||||
wrapMode = TextureWrapMode.Clamp,
|
||||
filterMode = FilterMode.Bilinear
|
||||
},
|
||||
pixels = SeedCpuPixels(mi, canonical),
|
||||
// UV-island seed protects loaded/switched-in content from edge padding right away
|
||||
// (the gutter stays paddable). Clone: painted[] is mutated per stroke.
|
||||
painted = (bool[])GetIslandMask().Clone(),
|
||||
fillStamp = new int[textureSize * textureSize],
|
||||
// Deferred (remote replica): allocate empty buffers and skip the seed + upload; the snapshot
|
||||
// re-seed fills them before the reveal gate shows the body. Otherwise seed now. The UV-island
|
||||
// seed protects loaded/switched-in content from edge padding right away (the gutter stays
|
||||
// paddable). Clone: painted[] is mutated per stroke.
|
||||
pixels = DeferInitialSeed ? new Color32[n] : SeedCpuPixels(mi, canonical),
|
||||
painted = DeferInitialSeed ? new bool[n] : (bool[])GetIslandMask().Clone(),
|
||||
fillStamp = new int[n],
|
||||
dilateStamp = 0
|
||||
};
|
||||
cm.tex.SetPixels32(cm.pixels);
|
||||
cm.tex.Apply(false);
|
||||
if (!DeferInitialSeed)
|
||||
{
|
||||
cm.tex.SetPixels32(cm.pixels);
|
||||
cm.tex.Apply(false);
|
||||
}
|
||||
_cpu[mi] = cm;
|
||||
AssignMapTexture(mi, cm.tex);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,26 @@ public partial class MeshPainter
|
||||
private Material _packMat;
|
||||
private bool _prevSRGB;
|
||||
|
||||
// --- Shared backend resource pools ---------------------------------------------------------------
|
||||
// Every paint RT has the identical descriptor (textureSize² ARGBHalf linear), so a despawned avatar's
|
||||
// freed RTs can be reused by the next spawn instead of re-allocating ~70 MB. This closes the gap the
|
||||
// in-place reseed left: reseed reuses an EXISTING backend, but a Conversion hit (hider despawns ->
|
||||
// hunter respawns) tears one down and builds another - the observer stalled on that full free+alloc.
|
||||
// Pooling reclaims the just-freed set immediately. Bounded so a mass despawn (match end) does not park
|
||||
// every avatar's RTs; extras beyond the cap are released normally. Static, to survive the despawn->
|
||||
// respawn gap; flushed on quit. Unity's null-check safely skips any entry a fast-enter-playmode reload
|
||||
// left dangling.
|
||||
private static readonly System.Collections.Generic.Stack<RenderTexture> s_rtPool =
|
||||
new System.Collections.Generic.Stack<RenderTexture>();
|
||||
private const int RtPoolCap = 18; // ~2 avatars' worth (9 RTs each)
|
||||
private static bool s_poolQuitHooked;
|
||||
|
||||
// Reused staging textures for seed uploads (one per colour space), so re-seeding on every spawn/apply
|
||||
// does not churn a 4 MB Texture2D per map. The linear flag must match the data (colour maps decode sRGB
|
||||
// on the blit), so the two spaces are kept apart; rebuilt if the paint size ever changes.
|
||||
private static Texture2D s_stageLinear;
|
||||
private static Texture2D s_stageSRGB;
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the GPU backend (RenderTexture set + materials per map). Returns true on
|
||||
/// success; on missing shaders it falls back to the CPU backend and returns false so
|
||||
@@ -62,7 +82,7 @@ public partial class MeshPainter
|
||||
};
|
||||
_gpu[mi] = gm;
|
||||
|
||||
SeedGpuMap(mi, canonical);
|
||||
if (!DeferInitialSeed) SeedGpuMap(mi, canonical); // deferred: RTs stay unseeded until the snapshot re-seed
|
||||
|
||||
// Color maps render to the display RT; the MS map exposes its packed final RT.
|
||||
AssignMapTexture(mi, gm.final != null ? gm.final : gm.display);
|
||||
@@ -149,9 +169,9 @@ public partial class MeshPainter
|
||||
foreach (var gm in _gpu)
|
||||
{
|
||||
if (gm == null) continue;
|
||||
if (gm.display) { gm.display.Release(); Destroy(gm.display); }
|
||||
if (gm.temp) { gm.temp.Release(); Destroy(gm.temp); }
|
||||
if (gm.final) { gm.final.Release(); Destroy(gm.final); }
|
||||
RecycleRT(gm.display);
|
||||
RecycleRT(gm.temp);
|
||||
RecycleRT(gm.final);
|
||||
}
|
||||
_gpu = null;
|
||||
}
|
||||
@@ -160,8 +180,45 @@ public partial class MeshPainter
|
||||
if (_packMat) { Destroy(_packMat); _packMat = null; }
|
||||
}
|
||||
|
||||
// Returns an RT to the shared pool for the next spawn to reuse, or releases it if the pool is full.
|
||||
// Deliberately NOT Released before pooling - Release frees the GPU memory the reuse is meant to keep.
|
||||
private static void RecycleRT(RenderTexture rt)
|
||||
{
|
||||
if (!rt) return;
|
||||
if (s_rtPool.Count < RtPoolCap) s_rtPool.Push(rt);
|
||||
else { rt.Release(); Object.Destroy(rt); }
|
||||
}
|
||||
|
||||
// Flushes the pooled RTs + staging textures. Hooked to Application.quitting (does not fire on editor
|
||||
// play-mode stop, so the capped set lingers until the next domain reload there - a bounded ~150 MB).
|
||||
private static void ClearBackendPool()
|
||||
{
|
||||
while (s_rtPool.Count > 0)
|
||||
{
|
||||
var rt = s_rtPool.Pop();
|
||||
if (rt) { rt.Release(); Object.Destroy(rt); }
|
||||
}
|
||||
if (s_stageLinear) { Object.Destroy(s_stageLinear); s_stageLinear = null; }
|
||||
if (s_stageSRGB) { Object.Destroy(s_stageSRGB); s_stageSRGB = null; }
|
||||
}
|
||||
|
||||
private RenderTexture CreateRT(string rtName)
|
||||
{
|
||||
// Reuse a pooled RT of the matching size before allocating (see s_rtPool). A pooled RT is still
|
||||
// Created (never Released while pooled); its stale content is fully overwritten by SeedGpuMap.
|
||||
while (s_rtPool.Count > 0)
|
||||
{
|
||||
var pooled = s_rtPool.Pop();
|
||||
if (pooled && pooled.width == textureSize && pooled.height == textureSize)
|
||||
{
|
||||
pooled.name = rtName;
|
||||
return pooled;
|
||||
}
|
||||
if (pooled) { pooled.Release(); Destroy(pooled); } // size mismatch / stale -> drop it
|
||||
}
|
||||
|
||||
if (!s_poolQuitHooked) { Application.quitting += ClearBackendPool; s_poolQuitHooked = true; }
|
||||
|
||||
// 16-bit half-float, NOT ARGB32: repeated low-opacity source-over only converges to the
|
||||
// target colour if the accumulation buffer has sub-8-bit precision. At 8 bpp the per-stroke
|
||||
// decrement dst*alpha rounds back to dst once it drops below half an LSB, so painting e.g.
|
||||
@@ -189,7 +246,7 @@ public partial class MeshPainter
|
||||
/// zero alpha is correct there too.</summary>
|
||||
private void UploadColorsToRT(Color32[] colors, RenderTexture rt, bool linear)
|
||||
{
|
||||
var tmp = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false, linear);
|
||||
var tmp = AcquireStage(linear); // reused per colour space, not allocated per call
|
||||
for (int i = 0; i < colors.Length; i++) colors[i].a = 0;
|
||||
tmp.SetPixels32(colors);
|
||||
tmp.Apply(false);
|
||||
@@ -200,7 +257,25 @@ public partial class MeshPainter
|
||||
GL.sRGBWrite = false;
|
||||
Graphics.Blit(tmp, rt);
|
||||
GL.sRGBWrite = prev;
|
||||
Destroy(tmp);
|
||||
}
|
||||
|
||||
/// <summary>A reusable staging Texture2D per colour space (see s_stageLinear/s_stageSRGB): seeding uploads
|
||||
/// through it instead of churning a 4 MB texture per map. The linear flag must match the data (colour maps
|
||||
/// decode sRGB when the blit samples the staging tex), so the two spaces are kept separate; rebuilt if the
|
||||
/// paint size ever changes. Main-thread only, so the shared instances never overlap.</summary>
|
||||
private Texture2D AcquireStage(bool linear)
|
||||
{
|
||||
ref Texture2D slot = ref linear ? ref s_stageLinear : ref s_stageSRGB;
|
||||
if (!slot || slot.width != textureSize || slot.height != textureSize)
|
||||
{
|
||||
if (slot) Destroy(slot);
|
||||
slot = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false, linear)
|
||||
{
|
||||
name = linear ? "MeshPainter_StageLinear" : "MeshPainter_StageSRGB",
|
||||
hideFlags = HideFlags.HideAndDontSave
|
||||
};
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -125,6 +125,7 @@ public partial class MeshPainter
|
||||
public void ApplyNetworkStamp(in PaintStampNet s)
|
||||
{
|
||||
if (!_setupValid) return;
|
||||
EnsureBackendSeeded(); // a stamp arriving before the first snapshot re-seed must land on the base seed
|
||||
|
||||
paintAlbedo = (s.Channels & PaintStampNet.FlagAlbedo) != 0;
|
||||
paintMetallicSmoothness = (s.Channels & PaintStampNet.FlagMetallicSmoothness) != 0;
|
||||
|
||||
@@ -350,7 +350,9 @@ public partial class MeshPainter
|
||||
var bytes = new byte[n * 4];
|
||||
for (int mi = 0; mi < MAP_COUNT; mi++)
|
||||
{
|
||||
Color32[] px = ReadCanonical(mi);
|
||||
// A deferred backend not yet re-seeded holds uninitialized RTs; serve the material seed instead
|
||||
// so a snapshot captured in that window is the base look (consistent with its cutoff), not garbage.
|
||||
Color32[] px = _backendSeeded ? ReadCanonical(mi) : _seedCanonical[mi];
|
||||
int count = Mathf.Min(n, px.Length);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
|
||||
@@ -293,4 +293,22 @@ public partial class MeshPainter
|
||||
c.a = a;
|
||||
_materialInstance.SetColor(XRayColorId, c);
|
||||
}
|
||||
|
||||
/// <summary>Applies the current hit-flash state to the X-ray material: flash mode boosts the authored glow
|
||||
/// rgb by <c>xrayFlashBoost</c>, turns the breathing pulse off (steady), and snaps the reveal alpha on for an
|
||||
/// immediate pop; leaving flash restores the authored rgb + pulse (alpha keeps easing via TickXRayFade). The
|
||||
/// pulse plateau otherwise leaves a short hit window looking dim, so a steady boosted glow reads far stronger.</summary>
|
||||
private void ApplyXRayFlashState()
|
||||
{
|
||||
if (!_materialInstance || !_materialInstance.HasProperty(XRayColorId)) return;
|
||||
float boost = _xrayFlash ? Mathf.Max(1f, xrayFlashBoost) : 1f;
|
||||
Color c = _materialInstance.GetColor(XRayColorId);
|
||||
c.r = _xrayBaseColor.r * boost;
|
||||
c.g = _xrayBaseColor.g * boost;
|
||||
c.b = _xrayBaseColor.b * boost;
|
||||
if (_xrayFlash) c.a = _xrayTarget; // snap the glow on so the hit reads instantly (no ramp-in)
|
||||
_materialInstance.SetColor(XRayColorId, c);
|
||||
if (_materialInstance.HasProperty(XRayPulseStrengthId))
|
||||
_materialInstance.SetFloat(XRayPulseStrengthId, _xrayFlash ? 0f : _xrayBasePulseStrength);
|
||||
}
|
||||
}
|
||||
@@ -126,13 +126,30 @@ public partial class MeshPainter : MonoBehaviour
|
||||
public bool XRayRevealed
|
||||
{
|
||||
get => _xrayTarget > 0f;
|
||||
set => _xrayTarget = value ? 1f : 0f;
|
||||
set => SetXRayReveal(value, false);
|
||||
}
|
||||
|
||||
/// <summary>Drives the X-ray reveal with an optional hunter-hit <paramref name="flash"/> mode: the flash
|
||||
/// multiplies the authored glow by <see cref="xrayFlashBoost"/> and disables the breathing pulse, so a hit
|
||||
/// reads as a strong, solid pop (the subtle breathing plateau otherwise leaves a short hit window looking
|
||||
/// dim). Leaving flash mode restores the authored colour + pulse. Only writes the material on a change.</summary>
|
||||
public void SetXRayReveal(bool revealed, bool flash)
|
||||
{
|
||||
_xrayTarget = revealed ? 1f : 0f;
|
||||
if (flash != _xrayFlash) { _xrayFlash = flash; ApplyXRayFlashState(); }
|
||||
}
|
||||
|
||||
[Tooltip("Seconds for the see-through X-ray reveal glow to fade fully in or out when toggled (0 = instant snap).")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float xrayFadeDuration = 0.3f;
|
||||
|
||||
[Tooltip("Hunter-hit flash strength: multiplies the authored _XRayColor and turns off its breathing pulse for the flash window, so a hit is a strong solid pop instead of the subtle breathing reveal used elsewhere.")]
|
||||
[Min(1f)]
|
||||
[SerializeField] private float xrayFlashBoost = 3f;
|
||||
private float _xrayTarget; // desired _XRayColor alpha (0/1); the material eases toward it each frame
|
||||
private bool _xrayFlash; // hit-flash mode: boosted rgb + pulse off, applied on toggle
|
||||
private Color _xrayBaseColor = Color.white; // authored _XRayColor rgb, captured at setup (alpha ignored)
|
||||
private float _xrayBasePulseStrength; // authored _XRayPulseStrength, restored when the flash ends
|
||||
|
||||
[Header("Backend")]
|
||||
[Tooltip("ON = GPU painting (fast), OFF = CPU painting. Can be toggled live in Play mode.")]
|
||||
@@ -349,6 +366,19 @@ public partial class MeshPainter : MonoBehaviour
|
||||
private bool _gpuActive;
|
||||
private bool _setupValid;
|
||||
|
||||
// Networked remote replicas set this (before Start) to SKIP the base seed during the initial backend
|
||||
// build: the RTs/materials are still created, but the grey/white base pixels are not uploaded. The body
|
||||
// is reveal-gated hidden until the painting snapshot is applied (which re-seeds the backend), so seeding
|
||||
// the base at Start is wasted work that stalls every observer's spawn frame (SetPixels/Apply/coverage-
|
||||
// raster per map). One-shot: consumed at the end of Start, so any later (re)build seeds normally. The
|
||||
// owner never defers (it shows immediately).
|
||||
public bool DeferInitialSeed { get; set; }
|
||||
|
||||
// True once the active backend holds valid content. False only between a DEFERRED setup and the first
|
||||
// re-seed/stamp: during that window the reveal gate hides the body and TryCaptureSnapshot serves the
|
||||
// material seed instead of the still-uninitialized RTs (see EnsureBackendSeeded).
|
||||
private bool _backendSeeded;
|
||||
|
||||
// Seed colors per map in canonical (material) layout, built once at Start:
|
||||
// albedo -> RGB color
|
||||
// MSO -> R = metallic, G = occlusion/height (0.5 = neutral: no relief, AO = 1), A = smoothness
|
||||
@@ -401,6 +431,7 @@ public partial class MeshPainter : MonoBehaviour
|
||||
private static readonly int HalfLambertBrightnessId = Shader.PropertyToID("_HalfLambertBrightness");
|
||||
private static readonly int SpecularDampenId = Shader.PropertyToID("_SpecularDampen");
|
||||
private static readonly int XRayColorId = Shader.PropertyToID("_XRayColor");
|
||||
private static readonly int XRayPulseStrengthId = Shader.PropertyToID("_XRayPulseStrength");
|
||||
private static readonly int BrushColorId = Shader.PropertyToID("_BrushColor");
|
||||
private static readonly int ColorWriteMaskId = Shader.PropertyToID("_ColorWriteMask");
|
||||
private static readonly int BrushCenterId = Shader.PropertyToID("_BrushCenter");
|
||||
@@ -446,12 +477,22 @@ public partial class MeshPainter : MonoBehaviour
|
||||
|
||||
// Seed the X-ray reveal target from the authored alpha so the fade baseline matches the material
|
||||
// (authored 0 = off). NetworkPlayerSetup drives this target at runtime; Update eases the alpha to it.
|
||||
if (_materialInstance.HasProperty(XRayColorId)) _xrayTarget = _materialInstance.GetColor(XRayColorId).a;
|
||||
// Also cache the authored glow rgb + pulse strength so the hit flash can boost/restore them.
|
||||
if (_materialInstance.HasProperty(XRayColorId))
|
||||
{
|
||||
Color xc = _materialInstance.GetColor(XRayColorId);
|
||||
_xrayBaseColor = new Color(xc.r, xc.g, xc.b, 1f);
|
||||
_xrayTarget = xc.a;
|
||||
}
|
||||
if (_materialInstance.HasProperty(XRayPulseStrengthId))
|
||||
_xrayBasePulseStrength = _materialInstance.GetFloat(XRayPulseStrengthId);
|
||||
|
||||
BuildSeedColors();
|
||||
// Always start fresh from the material seed. Saved paintings are loaded explicitly from the
|
||||
// HUD library (MeshPainter.Persistence), never auto-loaded on start.
|
||||
SetupBackend(useGpu, _seedCanonical);
|
||||
_backendSeeded = !DeferInitialSeed;
|
||||
DeferInitialSeed = false; // one-shot: only the initial build defers; later (re)builds seed normally
|
||||
ConfigureMaterialPbr();
|
||||
InitUndoHistory(); // baseline = material seed, captured before any stroke
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using Chameleon.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Per-avatar network glue on the spawned player. The local owner binds the scene singletons
|
||||
@@ -102,6 +103,7 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
public void MarkConversionSpawn(Vector3 worldPos) => _conversionSpawnPos = worldPos;
|
||||
|
||||
private Renderer[] _bodyRenderers; // skinned body mesh(es), toggled by the per-client visibility pass
|
||||
private FurShellRenderer[] _furRenderers; // fur draws via a custom GPU-instanced pass (NOT a Renderer); gated with the body so the avatar reveals atomically and never shows stale pooled fur
|
||||
private bool _visibilityApplied;
|
||||
private bool _lastVisible = true;
|
||||
private bool _renderGate; // conversion: body force-hidden until the fresh hunter has settled into place
|
||||
@@ -113,6 +115,7 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
private PlayerCharacterController _controller; // sibling, cached (the freeze re-asserts per frame)
|
||||
private bool _xrayApplied;
|
||||
private bool _lastXRay;
|
||||
private bool _lastFlash;
|
||||
private bool _frozenApplied;
|
||||
private bool _lastFrozen;
|
||||
private float _xrayFlashUntil; // Time.time deadline for the just-shot X-ray flash (local per client)
|
||||
@@ -122,6 +125,7 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
{
|
||||
_active.Add(this); // scoreboard registry (paired with OnNetworkDespawn)
|
||||
_bodyRenderers = GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
||||
_furRenderers = GetComponentsInChildren<FurShellRenderer>(true);
|
||||
_painter = GetComponentInChildren<MeshPainter>(true);
|
||||
_paintSync = GetComponentInChildren<PlayerPaintSync>(true);
|
||||
_concealment = GetComponentInChildren<ConcealmentMeter>(true);
|
||||
@@ -297,6 +301,12 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
if (_bodyRenderers != null)
|
||||
foreach (var r in _bodyRenderers)
|
||||
if (r) r.enabled = visible;
|
||||
// Fur is drawn by a custom GPU-instanced pass, NOT a Renderer, so the skinned-mesh toggle above
|
||||
// misses it; gate it too (enabled=false unsubscribes its camera hook), or a deferred/reveal-gated
|
||||
// body would show stale pooled fur strands while its own mesh is hidden.
|
||||
if (_furRenderers != null)
|
||||
foreach (var f in _furRenderers)
|
||||
if (f) f.enabled = visible;
|
||||
_lastVisible = visible;
|
||||
_visibilityApplied = true;
|
||||
}
|
||||
@@ -353,20 +363,21 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
private void UpdateXRayReveal()
|
||||
{
|
||||
if (!_painter) return;
|
||||
bool reveal = ComputeXRay();
|
||||
if (_xrayApplied && reveal == _lastXRay) return;
|
||||
_painter.XRayRevealed = reveal;
|
||||
// Just-shot flash: a hunter's hit lights this hider up for a brief window (server-broadcast). It drives
|
||||
// a distinct, much stronger flash mode (steady + boosted), so it is tracked apart from the normal reveal.
|
||||
bool flash = Time.time < _xrayFlashUntil;
|
||||
bool reveal = flash || ComputeXRay();
|
||||
if (_xrayApplied && reveal == _lastXRay && flash == _lastFlash) return;
|
||||
_painter.SetXRayReveal(reveal, flash);
|
||||
_lastXRay = reveal;
|
||||
_lastFlash = flash;
|
||||
_xrayApplied = true;
|
||||
}
|
||||
|
||||
// The X-ray reveal predicate for this avatar on the local machine.
|
||||
// The normal (non-flash) X-ray reveal predicate for this avatar on the local machine. The just-shot flash
|
||||
// is OR-combined by the caller so it can select the stronger flash mode.
|
||||
private bool ComputeXRay()
|
||||
{
|
||||
// Just-shot flash: a hunter's hit lights this hider up for a brief window (server-broadcast), on top
|
||||
// of the normal reveal triggers, so the moment of the hit reads before the hider leaves play.
|
||||
if (Time.time < _xrayFlashUntil) return true;
|
||||
|
||||
// Buried-hider reveal: already gated to hider + running match on the owner before it is published.
|
||||
if (_embeddedReveal.Value) return true;
|
||||
|
||||
|
||||
@@ -158,6 +158,12 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
// Remote replica: never paint from this machine's mouse; paint only from replicated data.
|
||||
_painter.PaintingEnabled = false;
|
||||
|
||||
// Skip the wasted base seed on the initial backend build: this reveal-gated body stays hidden
|
||||
// until the painting snapshot is applied (which re-seeds the backend), so seeding grey/white at
|
||||
// Start only stalls this observer's spawn frame. Set BEFORE the painter's Start (OnNetworkSpawn
|
||||
// runs first), so the deferral takes effect.
|
||||
_painter.DeferInitialSeed = true;
|
||||
|
||||
// Track the owner's emission intensity; seed the current (late-join) value right away.
|
||||
_emissionIntensity.OnValueChanged += OnEmissionChanged;
|
||||
_painter.emissionIntensity = _emissionIntensity.Value;
|
||||
|
||||
Reference in new issue
Block a user