From de9e1ba2f91430e92b20ca101b4c534dc593d3c4 Mon Sep 17 00:00:00 2001 From: soul Date: Thu, 22 Oct 2026 23:49:52 +0200 Subject: [PATCH] Projectile: a round is told of its shot by its manager's one subscription, and first seen where its barrel was when the sim fired it inside the step Co-Authored-By: Claude Sonnet 5.5 --- .../Scripts/Editor/DrawnPoseWiringVerify.cs | 2 +- Assets/Scripts/Gameplay/Weapons/Projectile.cs | 19 ++++------- .../Gameplay/Weapons/ProjectileManager.cs | 33 +++++++++++++++++-- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/Assets/Scripts/Editor/DrawnPoseWiringVerify.cs b/Assets/Scripts/Editor/DrawnPoseWiringVerify.cs index 85223b510..69109a622 100644 --- a/Assets/Scripts/Editor/DrawnPoseWiringVerify.cs +++ b/Assets/Scripts/Editor/DrawnPoseWiringVerify.cs @@ -159,7 +159,7 @@ namespace Aetheria.EditorTools MaxRange = 1e6f, FireRange = 1e6f, CommitTime = 1e5f, ArrivalTime = 2e5f }; var projectile = new GameObject("Wiring Round").AddComponent(); - projectile.Launch(zone, shot, new Vector3(0, 0, 0)); + projectile.Launch(null, zone, shot, new Vector3(0, 0, 0)); yield return null; zone.Update(.5f); diff --git a/Assets/Scripts/Gameplay/Weapons/Projectile.cs b/Assets/Scripts/Gameplay/Weapons/Projectile.cs index e5f0b1e86..a585e92d0 100644 --- a/Assets/Scripts/Gameplay/Weapons/Projectile.cs +++ b/Assets/Scripts/Gameplay/Weapons/Projectile.cs @@ -24,19 +24,16 @@ public class Projectile : MonoBehaviour private CultMath.float3 _barrel; private ShotOutcome _known; private bool _alive; - private CompositeDisposable _binding; + private ProjectileManager _owner; // Takes the shot as the sim holds it, the zone whose sim time it is drawn at, and where the barrel is as this round is first seen. - public void Launch(Zone zone, PendingShot shot, Vector3 barrel) + public void Launch(ProjectileManager owner, Zone zone, PendingShot shot, Vector3 barrel) { + _owner = owner; _zone = zone; _shot = shot; _barrel = new CultMath.float3(barrel.x, barrel.y, barrel.z); _known = null; - _binding?.Dispose(); - _binding = new CompositeDisposable( - zone.ShotCommitted.Where(o => o.ShotId == shot.ShotId).Subscribe(Committed), - zone.ShotResolved.Where(o => o.ShotId == shot.ShotId).Subscribe(Resolve)); _alive = true; transform.forward = new Vector3(shot.TravelDirection.x, 0, shot.TravelDirection.y); Draw(); @@ -56,14 +53,14 @@ public class Projectile : MonoBehaviour } // The sim's commit: the record it holds now (a contact burst's arrival is shortened here) and what it decided. - private void Committed(ShotOutcome outcome) + internal void Committed(ShotOutcome outcome) { if (_zone.TryGetShot(outcome.ShotId, out var record)) _shot = record; _known = outcome; } // The simulation's verdict. A round that hit or burst is drawn at its end and stops; a miss keeps flying. - private void Resolve(ShotOutcome outcome) + internal void Resolve(ShotOutcome outcome) { if (!_alive) return; _known = outcome; @@ -78,16 +75,14 @@ public class Projectile : MonoBehaviour private void Finish() { _alive = false; - _binding?.Dispose(); - _binding = null; + _owner?.Forget(_shot.ShotId); StartCoroutine(Fade()); } void OnDisable() { _alive = false; - _binding?.Dispose(); - _binding = null; + _owner?.Forget(_shot.ShotId); } IEnumerator Fade() diff --git a/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs b/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs index 216756cf9..3254d3177 100644 --- a/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs +++ b/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs @@ -1,17 +1,46 @@ -using UnityEngine; +using System.Collections.Generic; +using UniRx; +using UnityEngine; public class ProjectileManager : InstantWeaponEffectManager { public Prototype ProjectilePrototype; + // The rounds this manager has in the air by shot id, told of the sim's commit and verdict by one subscription per zone: a + // gun that fires hundreds of rounds in a step costs the dictionary an entry per round and nothing else (no subscription, + // closure or disposable per round). + private readonly Dictionary _live = new Dictionary(); + private readonly CompositeDisposable _listening = new CompositeDisposable(); + private Zone _zone; + // The round is drawn by DrawAhead from what the simulation publishes, so there is nothing to aim, jitter // or inherit here: spread is priced by FireControl, and the sim's round inherits nothing from its shooter. + // A round the sim fired inside the step (rounds per step) is first seen where its shooter's barrel was at that moment: + // the barrel as it is now, carried back along the shooter's motion for the time since. public override void Fire(InstantWeapon weapon, EquippedItem item, EntityInstance source, EntityInstance target, int shotId) { var zone = source.Entity.Zone; if (!zone.TryGetShot(shotId, out var shot)) return; + Listen(zone); + var age = (float) (zone.ExactTime - shot.FireTime); + var velocity = source.Entity.Velocity; var p = ProjectilePrototype.Instantiate(); - p.Launch(zone, shot, source.GetBarrel(item.Hardpoint).position); + _live[shotId] = p; + p.Launch(this, zone, shot, source.GetBarrel(item.Hardpoint).position - new Vector3(velocity.x, 0, velocity.y) * age); p.Trail.Clear(); } + + internal void Forget(int shotId) => _live.Remove(shotId); + + private void Listen(Zone zone) + { + if (_zone == zone) return; + _listening.Clear(); + _live.Clear(); + _zone = zone; + zone.ShotCommitted.Subscribe(outcome => { if (_live.TryGetValue(outcome.ShotId, out var p)) p.Committed(outcome); }).AddTo(_listening); + zone.ShotResolved.Subscribe(outcome => { if (_live.TryGetValue(outcome.ShotId, out var p)) p.Resolve(outcome); }).AddTo(_listening); + } + + void OnDestroy() => _listening.Dispose(); }