Projectile is drawn on FireControl.RoundAt and integrates nothing

Deletes the Time.deltaTime gravity/drag/position integration and the range-distance kill from Projectile and the
UnityEngine.Random jitter, barrel direction and inherited velocity from ProjectileManager. A round holds a copy of
its PendingShot and draws at RoundAt(Zone.Time + Clock.Lead) with a barrel offset decaying over BlendTime; ShotResolved
ends a hit or burst at the outcome's BurstPoint, a miss flies to the FireRange clamp. Unity compile not yet run at
this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-09 20:19:23 +02:00
commit aa9288d9de
2 changed files with 70 additions and 57 deletions

View file

@ -1,59 +1,87 @@
using System.Collections;
using System;
using System.Collections;
using UniRx;
using UnityEngine;
using static CultMath.math;
// Cut 3 (docs/fire-control-cut.md): the raycast, shield branch and SendHit are deleted -- FireControl already
// decided this shot's fate before this object was ever spawned (R8). What is left is pure flight-path
// presentation: fly, and disappear at Range or ShotId's resolution, whichever comes first.
// Cut 6b, 6.2 (12.4(b): Apply's own switch on the frozen Fuse decides this now, not Step): the flak-round
// blast resolves in the simulation, through FireControl.Detonate -- a MonoBehaviour calling it would be
// exactly the authority Cut 3 spent itself deleting, so the blast-distance/blast-radius fields this object
// used to carry for that are gone. This object's job stays fly-and-disappear.
// A ballistic round, drawn on FireControl.RoundAt and deciding nothing. The simulation owns the round's whole flight
// (origin, direction, speed, fire time, arrival, range: the PendingShot copy taken at spawn), so this object stores no
// velocity or position of its own and integrates nothing: each frame it asks RoundAt where the round is at sim time
// plus the clock's Lead (the same draw-ahead ships use), and the simulation's ShotResolved ends a round that hit or
// burst. A round that missed flies on to the FireRange point RoundAt clamps at, and is done there. The barrel is only
// where the round is first seen: its offset from the sim line decays to zero over BlendTime sim seconds. The trail's
// fade after the end stays real time (pure presentation, ruling sim-speed-presentation).
public class Projectile : MonoBehaviour
{
public TrailRenderer Trail;
public float Gravity;
public float Drag = .1f;
public Prototype HitEffect;
// Sim seconds over which the barrel's offset from the sim line decays to nothing.
public float BlendTime = .1f;
private PendingShot _shot;
private Zone _zone;
private Vector3 _barrelOffset;
private bool _alive;
private IDisposable _binding;
// Cut 3: FireControl.Fire's ShotId -- this projectile's handle onto Zone.ShotCommitted/ShotResolved.
public int ShotId { get; set; }
public Zone Zone { get; set; }
public Vector3 StartPosition { get; set; }
public Vector3 Velocity { get; set; }
public Entity SourceEntity { get; set; }
public float Range { get; set; }
private void OnEnable()
// Takes the frozen shot, 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)
{
_zone = zone;
_shot = shot;
var origin = shot.FireOrigin;
_barrelOffset = barrel - new Vector3(origin.x, origin.y, origin.z);
_binding?.Dispose();
_binding = zone.ShotResolved.Where(o => o.ShotId == shot.ShotId).Subscribe(Resolve);
_alive = true;
transform.forward = new Vector3(shot.TravelDirection.x, 0, shot.TravelDirection.y);
transform.position = barrel;
}
private Vector3 At(CultMath.float2 planar, float time)
{
var blend = BlendTime > 0 ? saturate(1 - (time - _shot.FireTime) / BlendTime) : 0f;
return new Vector3(planar.x, _shot.FireOrigin.y, planar.y) + _barrelOffset * blend;
}
// Update is called once per frame
void Update()
{
if (SourceEntity == null) return;
if(_alive)
{
var t = transform;
Velocity -= Vector3.up * (Gravity * Time.deltaTime);
Velocity *= max(0, 1 - Drag * Time.deltaTime);
t.forward = Velocity.normalized;
transform.position += Velocity * Time.deltaTime;
var distanceTraveled = (transform.position - StartPosition).magnitude;
if(distanceTraveled > Range)
StartCoroutine(Kill());
}
if (!_alive) return;
var time = _zone.Time + ActionGameManager.Clock.Lead;
transform.position = At(FireControl.RoundAt(_shot, time), time);
var end = _shot.Speed > .01f ? _shot.FireTime + _shot.FireRange / _shot.Speed : _shot.FireTime;
if (time >= end) Finish();
}
IEnumerator Kill()
// The simulation's verdict. A round that hit or burst stops where the sim put it, at the outcome's burst point,
// else where it arrives on its line; a miss keeps flying.
private void Resolve(ShotOutcome outcome)
{
if (!_alive || (outcome.Result != ShotResult.Hit && outcome.Result != ShotResult.Burst)) return;
var planar = outcome.HasBurstPoint ? outcome.BurstPoint : FireControl.RoundAt(_shot, _shot.ArrivalTime);
transform.position = At(planar, _shot.ArrivalTime);
if (HitEffect != null) HitEffect.Instantiate<Transform>().position = transform.position;
Finish();
}
private void Finish()
{
_alive = false;
_binding?.Dispose();
_binding = null;
StartCoroutine(Fade());
}
void OnDisable()
{
_alive = false;
_binding?.Dispose();
_binding = null;
}
IEnumerator Fade()
{
var startTime = Time.time;
var lifetime = Trail.time;
while (Time.time - startTime < lifetime)

View file

@ -1,32 +1,17 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;
using UnityEngine;
public class ProjectileManager : InstantWeaponEffectManager
{
public Prototype ProjectilePrototype;
public bool InheritVelocity;
// The round is drawn on the line the simulation flies it (FireControl.RoundAt), 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.
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;
var p = ProjectilePrototype.Instantiate<Projectile>();
var hp = item.Hardpoint;
var barrel = source.GetBarrel(hp);
var angle = weapon.Spread / 2;
p.ShotId = shotId;
p.SourceEntity = source.Entity;
p.Velocity = Quaternion.Euler(
Random.Range(-angle, angle),
Random.Range(-angle, angle),
Random.Range(-angle, angle)) *
barrel.forward *
weapon.Velocity;
p.StartPosition = p.transform.position = barrel.position + p.Velocity * (Random.value * Time.deltaTime);
if(InheritVelocity)
p.Velocity += new Vector3(source.Entity.Velocity.x, 0, source.Entity.Velocity.y);
p.Range = weapon.Range;
p.Zone = source.Entity.Zone;
p.Launch(zone, shot, source.GetBarrel(item.Hardpoint).position);
p.Trail.Clear();
}
}