Asset Dump
This commit is contained in:
@@ -16,8 +16,14 @@ namespace ProjectM.Client
|
||||
/// spawns a death burst + death SFX; the local player crossing to 0 HP does the same. A local-player ability
|
||||
/// fire (AbilityCooldown advancing) spawns a muzzle flash + zap. Everything derives from already-replicated
|
||||
/// state, so it is correct without touching the prediction loop, and it lives only in the client world so the
|
||||
/// server never instantiates GameObjects. SFX are generated procedurally; VFX use a small runtime pool — the
|
||||
/// slice ships with NO binary audio/particle assets.
|
||||
/// server never instantiates GameObjects.
|
||||
/// <para>
|
||||
/// VFX prefer authored GabrielAguiar Shuriken prefabs supplied by <see cref="VFXConfig"/> (muzzle / hit /
|
||||
/// death + a projectile-following trail); each hook falls back to a procedural particle burst when no prefab
|
||||
/// is assigned, so the slice still runs asset-free. Spawned VFX are stripped to particles only
|
||||
/// (<see cref="StripCosmetic"/>) — GA "projectile" prefabs ship a Rigidbody + collider + mover that would
|
||||
/// otherwise self-propel and spawn secondary effects. SFX remain procedural.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Per-entity last Health + position + isEnemy are cached in a managed dictionary (Entity is a stable client
|
||||
/// key for a ghost's lifetime); stale keys are pruned each frame (a pruned Husk = a kill → death VFX at its
|
||||
@@ -36,6 +42,12 @@ namespace ProjectM.Client
|
||||
readonly List<Entity> _stale = new();
|
||||
readonly List<FloatingNumber> _numbers = new();
|
||||
|
||||
// Authored-VFX lifetime tracking (GabrielAguiar prefabs spawned via VFXConfig).
|
||||
readonly List<TimedVfx> _activeVfx = new();
|
||||
readonly Dictionary<Entity, GameObject> _projTrails = new();
|
||||
readonly HashSet<Entity> _projSeen = new();
|
||||
readonly List<Entity> _projStale = new();
|
||||
|
||||
Transform _fxRoot;
|
||||
ParticleSystem _hitFx;
|
||||
ParticleSystem _deathFx;
|
||||
@@ -49,6 +61,9 @@ namespace ProjectM.Client
|
||||
bool _fireTickInit;
|
||||
|
||||
const int NumberPoolSize = 32;
|
||||
const int MaxActiveVfx = 40; // bound one-shot VFX GameObject churn under sustained combat
|
||||
|
||||
struct TimedVfx { public GameObject Go; public double Kill; }
|
||||
|
||||
protected override void OnCreate()
|
||||
{
|
||||
@@ -81,6 +96,7 @@ namespace ProjectM.Client
|
||||
{
|
||||
float dt = SystemAPI.Time.DeltaTime;
|
||||
var cam = Camera.main;
|
||||
var cfg = VFXConfig.Instance;
|
||||
|
||||
// Make sure predicted/physics jobs writing these are done before this main-thread read.
|
||||
EntityManager.CompleteDependencyBeforeRO<Health>();
|
||||
@@ -112,7 +128,7 @@ namespace ProjectM.Client
|
||||
if (cur < prev.Hp - 0.001f)
|
||||
{
|
||||
SpawnNumber(prev.Hp - cur, (Vector3)p, isLocalPlayer, cam);
|
||||
EmitAt(_hitFx, (Vector3)p + Vector3.up * 0.8f, 10);
|
||||
Burst(_hitFx, cfg != null ? cfg.Hit : null, (Vector3)p + Vector3.up * 0.8f, 10);
|
||||
PlayClip(_hitClip, (Vector3)p, 0.7f);
|
||||
PrototypeCameraRig.AddShake(isLocalPlayer ? 0.32f : 0.10f);
|
||||
}
|
||||
@@ -120,7 +136,7 @@ namespace ProjectM.Client
|
||||
// Player death (players don't despawn — they respawn; Husk death is handled on prune).
|
||||
if (!isEnemy && cur <= 0f && prev.Hp > 0f)
|
||||
{
|
||||
EmitAt(_deathFx, (Vector3)p + Vector3.up * 0.5f, 28);
|
||||
Burst(_deathFx, PlayerDeathPrefab(cfg), (Vector3)p + Vector3.up * 0.5f, 28);
|
||||
PlayClip(_deathClip, (Vector3)p, 0.7f);
|
||||
PrototypeCameraRig.AddShake(isLocalPlayer ? 0.5f : 0.25f);
|
||||
}
|
||||
@@ -141,7 +157,7 @@ namespace ProjectM.Client
|
||||
var c = _cache[_stale[i]];
|
||||
if (c.IsEnemy)
|
||||
{
|
||||
EmitAt(_deathFx, (Vector3)c.Pos + Vector3.up * 0.5f, 28);
|
||||
Burst(_deathFx, cfg != null ? cfg.EnemyDeath : null, (Vector3)c.Pos + Vector3.up * 0.5f, 28);
|
||||
PlayClip(_deathClip, (Vector3)c.Pos, 0.65f);
|
||||
PrototypeCameraRig.AddShake(0.16f);
|
||||
}
|
||||
@@ -150,21 +166,139 @@ namespace ProjectM.Client
|
||||
}
|
||||
|
||||
// Local-player fire feedback: AbilityCooldown.NextFireTick advances on each shot.
|
||||
// Raw uint inequality is intentional here: only the edge (a new shot) matters and the worst case
|
||||
// of a tick wrap is a single dropped/duplicated muzzle flash — purely cosmetic, never the sim.
|
||||
if (_localPlayer != Entity.Null && EntityManager.HasComponent<AbilityCooldown>(_localPlayer))
|
||||
{
|
||||
uint nextFire = EntityManager.GetComponentData<AbilityCooldown>(_localPlayer).NextFireTick;
|
||||
if (_fireTickInit && nextFire != 0 && nextFire != _lastLocalFireTick)
|
||||
{
|
||||
EmitAt(_muzzleFx, (Vector3)localPos + Vector3.up * 0.9f, 8);
|
||||
Burst(_muzzleFx, cfg != null ? cfg.Muzzle : null, (Vector3)localPos + Vector3.up * 0.9f, 8);
|
||||
PlayClip(_fireClip, (Vector3)localPos, 0.5f);
|
||||
}
|
||||
_lastLocalFireTick = nextFire;
|
||||
_fireTickInit = true;
|
||||
}
|
||||
|
||||
UpdateProjectileTrails(cfg);
|
||||
PruneVfx();
|
||||
AnimateNumbers(dt, cam);
|
||||
}
|
||||
|
||||
// ---- Authored VFX (GabrielAguiar prefabs via VFXConfig); fall back to the procedural burst ----
|
||||
|
||||
static GameObject PlayerDeathPrefab(VFXConfig cfg)
|
||||
{
|
||||
if (cfg == null) return null;
|
||||
return cfg.PlayerDeath != null ? cfg.PlayerDeath : cfg.EnemyDeath;
|
||||
}
|
||||
|
||||
void Burst(ParticleSystem fallback, GameObject prefab, Vector3 pos, int count)
|
||||
{
|
||||
if (prefab != null) SpawnVfx(prefab, pos, Quaternion.identity);
|
||||
else EmitAt(fallback, pos, count);
|
||||
}
|
||||
|
||||
void SpawnVfx(GameObject prefab, Vector3 pos, Quaternion rot)
|
||||
{
|
||||
if (prefab == null || _fxRoot == null) return;
|
||||
if (_activeVfx.Count >= MaxActiveVfx) return; // saturated: drop (cheap) rather than thrash GC
|
||||
var go = Object.Instantiate(prefab, pos, rot, _fxRoot);
|
||||
go.transform.position = pos;
|
||||
StripCosmetic(go);
|
||||
var systems = go.GetComponentsInChildren<ParticleSystem>();
|
||||
for (int i = 0; i < systems.Length; i++) systems[i].Play();
|
||||
_activeVfx.Add(new TimedVfx { Go = go, Kill = SystemAPI.Time.ElapsedTime + VfxLifetime(go) });
|
||||
}
|
||||
|
||||
void PruneVfx()
|
||||
{
|
||||
double now = SystemAPI.Time.ElapsedTime;
|
||||
for (int i = _activeVfx.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (now < _activeVfx[i].Kill) continue;
|
||||
if (_activeVfx[i].Go != null) Object.Destroy(_activeVfx[i].Go);
|
||||
_activeVfx.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
// A looping trail prefab follows each in-flight projectile ghost; destroyed when it despawns.
|
||||
void UpdateProjectileTrails(VFXConfig cfg)
|
||||
{
|
||||
if (cfg == null || cfg.ProjectileTrail == null || _fxRoot == null)
|
||||
{
|
||||
// Config cleared mid-run: drop any orphaned trails so they don't linger.
|
||||
if (_projTrails.Count > 0)
|
||||
{
|
||||
foreach (var kv in _projTrails) if (kv.Value != null) Object.Destroy(kv.Value);
|
||||
_projTrails.Clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_projSeen.Clear();
|
||||
foreach (var (xf, entity) in
|
||||
SystemAPI.Query<RefRO<LocalTransform>>().WithAll<Projectile>().WithEntityAccess())
|
||||
{
|
||||
_projSeen.Add(entity);
|
||||
Vector3 wp = (Vector3)xf.ValueRO.Position;
|
||||
if (_projTrails.TryGetValue(entity, out var trail))
|
||||
{
|
||||
if (trail != null) trail.transform.position = wp;
|
||||
}
|
||||
else
|
||||
{
|
||||
var go = Object.Instantiate(cfg.ProjectileTrail, wp, Quaternion.identity, _fxRoot);
|
||||
StripCosmetic(go); // GA "projectile" prefabs ship a Rigidbody + mover; keep particles only
|
||||
var systems = go.GetComponentsInChildren<ParticleSystem>();
|
||||
for (int i = 0; i < systems.Length; i++) systems[i].Play();
|
||||
_projTrails[entity] = go;
|
||||
}
|
||||
}
|
||||
|
||||
if (_projTrails.Count == _projSeen.Count) return;
|
||||
_projStale.Clear();
|
||||
foreach (var kv in _projTrails)
|
||||
if (!_projSeen.Contains(kv.Key)) _projStale.Add(kv.Key);
|
||||
for (int i = 0; i < _projStale.Count; i++)
|
||||
{
|
||||
if (_projTrails[_projStale[i]] != null) Object.Destroy(_projTrails[_projStale[i]]);
|
||||
_projTrails.Remove(_projStale[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Cosmetic VFX must be particles only. GA demo "projectile" prefabs ship a non-kinematic Rigidbody,
|
||||
// a solid collider, and a mover (ProjectileMoveScript) that self-propels and spawns secondary muzzle/hit
|
||||
// effects on contact — strip all of that so our per-frame reposition is authoritative and nothing leaks.
|
||||
static void StripCosmetic(GameObject go)
|
||||
{
|
||||
foreach (var rb in go.GetComponentsInChildren<Rigidbody>(true)) Object.Destroy(rb);
|
||||
foreach (var col in go.GetComponentsInChildren<Collider>(true)) Object.Destroy(col);
|
||||
foreach (var mb in go.GetComponentsInChildren<MonoBehaviour>(true))
|
||||
{
|
||||
if (mb == null) continue;
|
||||
string n = mb.GetType().Name;
|
||||
// Disable (not destroy) BEFORE Start runs so the mover's Start-spawned muzzle never fires.
|
||||
if (n.IndexOf("Projectile", System.StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
n.IndexOf("Move", System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
mb.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Real effect duration from the longest child ParticleSystem (clamped), so we don't force-kill early
|
||||
// or hold a finished GameObject around on a blanket TTL.
|
||||
static double VfxLifetime(GameObject go)
|
||||
{
|
||||
float longest = 0f;
|
||||
foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(true))
|
||||
{
|
||||
var main = ps.main;
|
||||
float d = main.duration + main.startLifetime.constantMax;
|
||||
if (d > longest) longest = d;
|
||||
}
|
||||
return Mathf.Clamp(longest, 1f, 6f);
|
||||
}
|
||||
|
||||
// ---- Floating damage numbers (pooled, billboarded TextMesh) ----
|
||||
|
||||
class FloatingNumber
|
||||
@@ -236,7 +370,7 @@ namespace ProjectM.Client
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Procedural SFX + pooled particle bursts ----
|
||||
// ---- Procedural SFX + pooled particle bursts (fallback when no authored prefab) ----
|
||||
|
||||
static AudioClip MakeClip(string name, float f0, float f1, float dur, float vol, bool noise)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user