using UnityEngine; using UnityEngine.Rendering; /// /// 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 to trigger; the beam /// recycles itself (disables the renderer) once it expires. /// [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(); _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; } /// Fires a one-shot beam along the given world-space segment; restarts the animation if already running. 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); } }