- PortalTestDestination practice range: Valorant-style hitscan rifle (recoil, spray/bloom, reload, controller support), debug HUD, practice-dummy targets with head/body hitboxes, hit/death/respawn animations, procedural SFX/FX - AK47 model (Quaternius, CC0) held via Animator IK, with fire/reload/equip anims - Intro cinematic (camera shots, title card, target beam-in, GO hand-off) - Scene dressing editor pass: CC0 textures, props, warehouse roof, lighting and post-processing; custom stylized skybox shader - CharacterMovement: camera recoil, shoulder offset, cinematic control lock - Unity CLI pipeline package; plus in-progress chat/Rive/player changes Asset credits: Assets/Models/Props/PROPS_CREDITS.txt (3 CC-BY 3.0 props), Assets/Textures/Range/TEXTURES_LICENSE.txt, Assets/Resources/*/..._LICENSE.txt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
181 lines
8.1 KiB
C#
181 lines
8.1 KiB
C#
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<Renderer>().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<Collider>());
|
|
go.transform.SetParent(parent, false);
|
|
go.transform.localPosition = localPosition;
|
|
go.transform.localRotation = Quaternion.Euler(localEuler);
|
|
go.transform.localScale = localScale;
|
|
var renderer = go.GetComponent<Renderer>();
|
|
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<Rigidbody>();
|
|
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<FxShrink>();
|
|
}
|
|
|
|
// ---- 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;
|
|
}
|
|
}
|