Hunter shot: replicated laser beam + X-ray hit flash; snapshot apply in-place reseed
- Laser.shader + HunterLaserBeam: additive HDR beam, fade in/out over opacity and thickness, code-driven LineRenderer, tunable emission multiplier. - FirstPersonMode: TryShoot computes world endpoint, fires beam with lateral muzzle offset (right) so it isn't dead-center from hunter view. - NetworkPlayerSetup: shot relay RPCs (owner->server->all) + hit-flash RPC (server->all), flash OR-branch in ComputeXRay. - MatchManager: hit reveal broadcasts X-ray flash first, delays hider removal by hitRevealDelay so the glow is visible before the body vanishes. - MeshPainter reseed: in-place fast path avoids full GPU backend teardown on snapshot apply (no ~70MB RT churn), fixes spawn/paint-load hitch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
5b95cb7f56
commit
9290ef650b
10 files changed
+403
-23
No files matched your search
@@ -0,0 +1,135 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Momentary world-space laser beam for a hunter's shot. Purely visual (no networking): a LineRenderer drawn
|
||||
/// from the muzzle to the hit point that fades IN fast then OUT over its lifetime, animating both opacity
|
||||
/// (via per-vertex colour alpha, read by the Chameleon/Laser shader) and thickness (via widthMultiplier).
|
||||
/// Self-contained: it builds its own LineRenderer child + an additive material at runtime, so it can be added
|
||||
/// to a hunter avatar purely in code with no prefab authoring. Call <see cref="Fire"/> to trigger; the beam
|
||||
/// recycles itself (disables the renderer) once it expires.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class HunterLaserBeam : MonoBehaviour
|
||||
{
|
||||
[Header("Look")]
|
||||
[ColorUsage(true, true)]
|
||||
[Tooltip("HDR beam tint. Its alpha is ignored - the animated opacity envelope drives visibility.")]
|
||||
[SerializeField] private Color color = new Color(1f, 0.25f, 0.25f, 1f);
|
||||
|
||||
[Tooltip("HDR emission multiplier applied to the tint. >1 pushes the beam past LDR into bloom.")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float emission = 6f;
|
||||
|
||||
[Tooltip("Peak beam thickness in world units (the width envelope scales this).")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float maxWidth = 0.06f;
|
||||
|
||||
[Tooltip("Optional material override. Empty -> a built-in additive laser material (Chameleon/Laser) is created at runtime.")]
|
||||
[SerializeField] private Material materialOverride;
|
||||
|
||||
[Header("Timing")]
|
||||
[Tooltip("Total beam lifetime in seconds (fade-in + fade-out).")]
|
||||
[Min(0.01f)]
|
||||
[SerializeField] private float lifetime = 0.5f;
|
||||
|
||||
[Tooltip("Fraction of the lifetime spent fading IN (the rest fades out).")]
|
||||
[Range(0f, 1f)]
|
||||
[SerializeField] private float fadeInFraction = 0.2f;
|
||||
|
||||
[Tooltip("Thickness floor as a fraction of maxWidth at the envelope's ends, so the beam never pinches to a dot.")]
|
||||
[Range(0f, 1f)]
|
||||
[SerializeField] private float minWidthFraction = 0.35f;
|
||||
|
||||
private LineRenderer _line;
|
||||
private Material _material;
|
||||
private bool _ownsMaterial;
|
||||
private float _age;
|
||||
private bool _active;
|
||||
|
||||
private static readonly int ColorId = Shader.PropertyToID("_Color");
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
var go = new GameObject("LaserBeamLine");
|
||||
go.transform.SetParent(transform, false);
|
||||
|
||||
_line = go.AddComponent<LineRenderer>();
|
||||
_line.useWorldSpace = true; // endpoints are fixed world points; the beam ignores avatar motion
|
||||
_line.positionCount = 2;
|
||||
_line.numCapVertices = 2;
|
||||
_line.numCornerVertices = 0;
|
||||
_line.alignment = LineAlignment.View;
|
||||
_line.textureMode = LineTextureMode.Stretch;
|
||||
_line.shadowCastingMode = ShadowCastingMode.Off;
|
||||
_line.receiveShadows = false;
|
||||
_line.lightProbeUsage = LightProbeUsage.Off;
|
||||
_line.reflectionProbeUsage = ReflectionProbeUsage.Off;
|
||||
|
||||
if (materialOverride)
|
||||
{
|
||||
_material = materialOverride;
|
||||
}
|
||||
else
|
||||
{
|
||||
_material = new Material(Shader.Find("Chameleon/Laser"));
|
||||
_ownsMaterial = true;
|
||||
}
|
||||
_line.material = _material;
|
||||
if (_material && _material.HasProperty(ColorId))
|
||||
{
|
||||
Color hdr = color * emission; // push the tint into HDR so bloom picks it up
|
||||
hdr.a = 1f; // alpha is unused by the additive shader
|
||||
_material.SetColor(ColorId, hdr);
|
||||
}
|
||||
|
||||
_line.enabled = false;
|
||||
}
|
||||
|
||||
/// <summary>Fires a one-shot beam along the given world-space segment; restarts the animation if already running.</summary>
|
||||
public void Fire(Vector3 start, Vector3 end)
|
||||
{
|
||||
if (!_line) return;
|
||||
_line.SetPosition(0, start);
|
||||
_line.SetPosition(1, end);
|
||||
_age = 0f;
|
||||
_active = true;
|
||||
_line.enabled = true;
|
||||
ApplyEnvelope(0f);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!_active) return;
|
||||
_age += Time.deltaTime;
|
||||
float t = lifetime > 0f ? _age / lifetime : 1f;
|
||||
if (t >= 1f)
|
||||
{
|
||||
_active = false;
|
||||
_line.enabled = false;
|
||||
return;
|
||||
}
|
||||
ApplyEnvelope(t);
|
||||
}
|
||||
|
||||
// Maps normalized life t (0..1) to opacity + thickness: rises over fadeInFraction, then eases back down.
|
||||
private void ApplyEnvelope(float t)
|
||||
{
|
||||
float inFrac = Mathf.Clamp01(fadeInFraction);
|
||||
float e = t < inFrac
|
||||
? (inFrac > 0f ? t / inFrac : 1f)
|
||||
: (inFrac < 1f ? 1f - (t - inFrac) / (1f - inFrac) : 1f);
|
||||
e = Mathf.Clamp01(e);
|
||||
e = e * e * (3f - 2f * e); // smoothstep for a softer ramp
|
||||
|
||||
_line.widthMultiplier = maxWidth * Mathf.Lerp(minWidthFraction, 1f, e);
|
||||
var vc = new Color(1f, 1f, 1f, e); // hue from the material _Color; alpha carries the fade envelope
|
||||
_line.startColor = vc;
|
||||
_line.endColor = vc;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_ownsMaterial && _material) Destroy(_material);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3755898ae12d28e4585f6e55547c768d
|
||||
@@ -32,11 +32,27 @@ public partial class MeshPainter
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tears down the active backend and rebuilds it from externally supplied canonical maps (load /
|
||||
/// late-join snapshot / undo). Runs the island/dilation seeding path so edge padding is rebuilt for
|
||||
/// the new content. The input arrays are never mutated (SetupBackend clones its seed).
|
||||
/// Re-seeds the backend from externally supplied canonical maps (load / late-join snapshot / undo),
|
||||
/// rebuilding the island/dilation edge padding for the new content. The input arrays are never mutated
|
||||
/// (both seed paths clone before touching a map).
|
||||
///
|
||||
/// Fast path: when the backend is already built on this same type + size - the common runtime reseed
|
||||
/// (remote snapshot apply, carry/library load, undo/redo) - the existing RTs/textures are re-filled in
|
||||
/// place instead of a full teardown+rebuild. At 1024² a rebuild frees and re-creates nine ARGBHalf RTs
|
||||
/// (~70 MB of GPU allocation) every call, which stalls the frame and shows up as a hitch on every
|
||||
/// observer when a (freshly painted) avatar spawns. Re-seeding reuses the RTs (identical size/format)
|
||||
/// for a bit-identical result. A pending GPU<->CPU switch or an un-built backend falls through to the
|
||||
/// full rebuild.
|
||||
/// </summary>
|
||||
private void ReseedCanonical(Color32[][] canonical) => RebuildBackend(useGpu, canonical);
|
||||
private void ReseedCanonical(Color32[][] canonical)
|
||||
{
|
||||
if (_setupValid)
|
||||
{
|
||||
if (useGpu && _gpuActive && _gpu != null) { ReseedGpuInPlace(canonical); _hasLastScreen = false; return; }
|
||||
if (!useGpu && !_gpuActive && _cpu != null) { ReseedCpuInPlace(canonical); _hasLastScreen = false; return; }
|
||||
}
|
||||
RebuildBackend(useGpu, canonical);
|
||||
}
|
||||
|
||||
/// <summary>Tears down the active backend and sets up the requested one on the given maps.</summary>
|
||||
private void RebuildBackend(bool gpu, Color32[][] canonical)
|
||||
|
||||
@@ -43,6 +43,27 @@ public partial class MeshPainter
|
||||
return px;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-seeds the ALREADY-built CPU maps from canonical data without recreating the Texture2D mirrors:
|
||||
/// each map's pixel buffer + island guard are re-seeded and re-uploaded into the existing texture,
|
||||
/// mirroring SetupCpu but reusing the textures. The GPU counterpart is ReseedGpuInPlace. Callers ensure
|
||||
/// !_gpuActive && _cpu != null.
|
||||
/// </summary>
|
||||
private void ReseedCpuInPlace(Color32[][] canonical)
|
||||
{
|
||||
for (int mi = 0; mi < MAP_COUNT; mi++)
|
||||
{
|
||||
var cm = _cpu[mi];
|
||||
cm.pixels = SeedCpuPixels(mi, canonical);
|
||||
cm.painted = (bool[])GetIslandMask().Clone();
|
||||
if (cm.fillStamp != null) System.Array.Clear(cm.fillStamp, 0, cm.fillStamp.Length);
|
||||
cm.dilateStamp = 0;
|
||||
cm.dx0 = textureSize; cm.dy0 = textureSize; cm.dx1 = -1; cm.dy1 = -1; // no pending dirty region
|
||||
cm.tex.SetPixels32(cm.pixels);
|
||||
cm.tex.Apply(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void TeardownCpu()
|
||||
{
|
||||
if (_cpu == null) return;
|
||||
|
||||
@@ -100,6 +100,23 @@ public partial class MeshPainter
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-seeds the ALREADY-built GPU maps from canonical data without recreating the RenderTextures or
|
||||
/// materials: each map's display RT is re-uploaded + re-stamped (MSO re-packed) exactly as SetupGpu
|
||||
/// does, but the existing RTs (same size/format) are reused, so the fur/albedo RTs a sibling
|
||||
/// FurShellRenderer samples keep their references. Avoids the per-apply ~70 MB RT alloc/free churn a
|
||||
/// full RebuildBackend incurs at 1024². Callers ensure _gpuActive && _gpu != null.
|
||||
/// </summary>
|
||||
private void ReseedGpuInPlace(Color32[][] canonical)
|
||||
{
|
||||
for (int mi = 0; mi < MAP_COUNT; mi++)
|
||||
{
|
||||
SeedGpuMap(mi, canonical);
|
||||
var gm = _gpu[mi];
|
||||
AssignMapTexture(mi, gm.final != null ? gm.final : gm.display);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stamps the dilation coverage guard into a map's alpha: rasterizes the mesh with the brush
|
||||
/// shader's coverage pass (pass 1), writing alpha = 1 on exactly the texels the GPU brush can
|
||||
|
||||
Reference in new issue
Block a user