using System.Collections.Generic; using UnityEngine; // Procedural "assets" for the shooting prototype: tinted/emissive materials cloned from the // render pipeline's default material (so they work in URP without shipping .mat files), and // synthesized sound effects built with AudioClip.Create (no audio files needed). public static class ShootingFx { static Material baseMaterial; static readonly Dictionary<(Color, float), Material> materialCache = new Dictionary<(Color, float), Material>(); public static Material GetMaterial(Color color, float emission = 0f) { if (materialCache.TryGetValue((color, emission), out var cached) && cached != null) return cached; if (baseMaterial == null) { var probe = GameObject.CreatePrimitive(PrimitiveType.Cube); baseMaterial = probe.GetComponent().sharedMaterial; Object.Destroy(probe); } var mat = new Material(baseMaterial) { color = color }; if (emission > 0f) { mat.EnableKeyword("_EMISSION"); mat.SetColor("_EmissionColor", color * emission); mat.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive; } materialCache[(color, emission)] = mat; return mat; } // Primitive with its collider stripped — for visuals that must never block raycasts. public static GameObject CreateVisual(PrimitiveType type, string name, Transform parent, Vector3 localPosition, Vector3 localScale, Material material, Vector3 localEuler = default) { var go = GameObject.CreatePrimitive(type); go.name = name; Object.Destroy(go.GetComponent()); go.transform.SetParent(parent, false); go.transform.localPosition = localPosition; go.transform.localRotation = Quaternion.Euler(localEuler); go.transform.localScale = localScale; var renderer = go.GetComponent(); renderer.sharedMaterial = material; renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; return go; } // ---- Effects ------------------------------------------------------------------------- public static void SpawnTracer(Vector3 from, Vector3 to) { Vector3 dir = to - from; float length = dir.magnitude; if (length < 0.05f) return; var tracer = CreateVisual(PrimitiveType.Cube, "Tracer", null, from + dir * 0.5f, new Vector3(0.015f, 0.015f, length), GetMaterial(new Color(1f, 0.85f, 0.4f), 4f)); tracer.transform.rotation = Quaternion.LookRotation(dir); Object.Destroy(tracer, 0.05f); } public static void SpawnImpact(Vector3 point, Vector3 normal, bool fleshHit) { var decal = CreateVisual(PrimitiveType.Cube, "BulletHole", null, point + normal * 0.005f, new Vector3(0.07f, 0.07f, 0.01f), GetMaterial(new Color(0.05f, 0.05f, 0.05f))); decal.transform.rotation = Quaternion.LookRotation(normal); Object.Destroy(decal, fleshHit ? 0.1f : 4f); Color sparkColor = fleshHit ? new Color(1f, 0.2f, 0.15f) : new Color(1f, 0.75f, 0.3f); var sparkMat = GetMaterial(sparkColor, 3f); for (int i = 0; i < 5; i++) { var spark = CreateVisual(PrimitiveType.Cube, "Spark", null, point + normal * 0.03f, Vector3.one * 0.025f, sparkMat, Random.insideUnitSphere * 180f); var body = spark.AddComponent(); body.useGravity = true; body.linearVelocity = (normal + Random.insideUnitSphere * 0.8f).normalized * Random.Range(2f, 4.5f); Object.Destroy(spark, Random.Range(0.25f, 0.5f)); } } // Vertical light column that squeezes away — "target materialized" effect. public static void SpawnBeam(Vector3 groundPoint) { var beam = CreateVisual(PrimitiveType.Cylinder, "SpawnBeam", null, groundPoint + Vector3.up * 1.5f, new Vector3(0.9f, 1.5f, 0.9f), GetMaterial(new Color(0.35f, 0.9f, 1f), 3f)); beam.AddComponent(); } // ---- Sounds -------------------------------------------------------------------------- const int SampleRate = 44100; static AudioClip gunshot, emptyClick, magOut, magIn, hitBody, hitHead, killConfirm; public static AudioClip Gunshot => gunshot ??= Synth("Gunshot", 0.28f, (t, n) => n * Mathf.Exp(-t * 28f) * 0.9f + Mathf.Sin(2f * Mathf.PI * 95f * t) * Mathf.Exp(-t * 18f) * 0.8f + Mathf.Sin(2f * Mathf.PI * 190f * t) * Mathf.Exp(-t * 40f) * 0.3f); public static AudioClip EmptyClick => emptyClick ??= Synth("EmptyClick", 0.05f, (t, n) => (n * 0.5f + Mathf.Sin(2f * Mathf.PI * 3200f * t)) * Mathf.Exp(-t * 120f) * 0.5f); public static AudioClip MagOut => magOut ??= Synth("MagOut", 0.12f, (t, n) => (n * 0.6f + Mathf.Sin(2f * Mathf.PI * 700f * t) * 0.4f) * Mathf.Exp(-t * 45f) * 0.6f); public static AudioClip MagIn => magIn ??= Synth("MagIn", 0.15f, (t, n) => (n * 0.5f + Mathf.Sin(2f * Mathf.PI * 1100f * t) * 0.5f) * Mathf.Exp(-t * 35f) * 0.7f + (t > 0.07f ? (n * 0.6f + Mathf.Sin(2f * Mathf.PI * 1500f * t) * 0.4f) * Mathf.Exp(-(t - 0.07f) * 50f) * 0.7f : 0f)); public static AudioClip HitBody => hitBody ??= Synth("HitBody", 0.06f, (t, n) => Mathf.Sin(2f * Mathf.PI * 1700f * t) * Mathf.Exp(-t * 60f) * 0.35f); public static AudioClip HitHead => hitHead ??= Synth("HitHead", 0.18f, (t, n) => (Mathf.Sin(2f * Mathf.PI * 2600f * t) + Mathf.Sin(2f * Mathf.PI * 3900f * t) * 0.5f) * Mathf.Exp(-t * 22f) * 0.35f); public static AudioClip KillConfirm => killConfirm ??= Synth("Kill", 0.22f, (t, n) => Mathf.Sin(2f * Mathf.PI * (t < 0.08f ? 880f : 1320f) * t) * Mathf.Exp(-(t % 0.08f) * 18f) * 0.35f); static AudioClip whoosh, boom, blip, goStinger; // Filtered noise that swells and fades, with the filter opening up (camera fly-by). public static AudioClip Whoosh => whoosh ??= SynthWhoosh(); static AudioClip SynthWhoosh() { float lowpass = 0f; return Synth("Whoosh", 0.9f, (t, n) => { float cutoff = Mathf.Lerp(0.02f, 0.25f, Mathf.Sin(Mathf.PI * Mathf.Clamp01(t / 0.9f))); lowpass += (n - lowpass) * cutoff; float env = Mathf.Sin(Mathf.PI * Mathf.Clamp01(t / 0.9f)); return lowpass * env * env * 1.8f; }); } // Sub-bass drop with a noisy transient (title hit). public static AudioClip Boom => boom ??= Synth("Boom", 1.6f, (t, n) => { float freq = Mathf.Lerp(90f, 38f, Mathf.Clamp01(t / 1.2f)); return Mathf.Sin(2f * Mathf.PI * freq * t) * Mathf.Exp(-t * 2.2f) * 0.9f + n * Mathf.Exp(-t * 30f) * 0.5f; }); // Short digital blip — pitch it up per target for a rising "boot sequence". public static AudioClip Blip => blip ??= Synth("Blip", 0.12f, (t, n) => (Mathf.Sin(2f * Mathf.PI * 880f * t) + Mathf.Sin(2f * Mathf.PI * 1760f * t) * 0.3f) * Mathf.Exp(-t * 30f) * 0.35f); // Bright major chord stab for "GO". public static AudioClip GoStinger => goStinger ??= Synth("Go", 0.8f, (t, n) => { float env = Mathf.Min(1f, t * 60f) * Mathf.Exp(-t * 4f); return (Mathf.Sin(2f * Mathf.PI * 523.25f * t) + Mathf.Sin(2f * Mathf.PI * 659.25f * t) + Mathf.Sin(2f * Mathf.PI * 783.99f * t) + Mathf.Sin(2f * Mathf.PI * 1046.5f * t) * 0.5f) * env * 0.18f + n * Mathf.Exp(-t * 40f) * 0.2f; }); delegate float SampleFunc(float time, float noise); // Samples are generated in order, so a SampleFunc may keep state (e.g. a filter) in a closure. static AudioClip Synth(string name, float duration, SampleFunc func) { int count = Mathf.CeilToInt(duration * SampleRate); var data = new float[count]; var rng = new System.Random(name.GetHashCode()); for (int i = 0; i < count; i++) { float noise = (float)(rng.NextDouble() * 2.0 - 1.0); data[i] = Mathf.Clamp(func(i / (float)SampleRate, noise), -1f, 1f); } var clip = AudioClip.Create(name, count, 1, SampleRate, false); clip.SetData(data, 0); return clip; } }