Merge thrust-allocator-wire: Ship.Update hands intent and live columns to ThrustAllocator
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # tests/Aetheria.Shared.Tests/RestoredHullsTests.cs
This commit is contained in:
commit
230aeccdd3
15 changed files with 728 additions and 369 deletions
|
|
@ -93,11 +93,6 @@ public interface IActivatedBehavior
|
|||
void Deactivate();
|
||||
}
|
||||
|
||||
public interface IAnalogBehavior
|
||||
{
|
||||
float Axis { get; set; }
|
||||
}
|
||||
|
||||
public interface IEventBehavior
|
||||
{
|
||||
void ResetEvents();
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
using MessagePack;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
// Presence marks the lock: Entity.ThrottleLocked is true while an active effect carries one, and Ship.Update
|
||||
// then reads full forward intent in place of MovementDirection. The behaviour holds no state and does nothing
|
||||
// itself; turning stays with the pilot.
|
||||
// Presence marks the lock: Entity.ThrottleLocked is true while an active effect carries one, and the thrust
|
||||
// allocator then bounds every forward-pushing column at full throttle from below in its solve. The behaviour
|
||||
// holds no state and does nothing itself; turning stays with the pilot.
|
||||
[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn), RuntimeInspectable]
|
||||
public class ThrottleLockData : BehaviorData
|
||||
{
|
||||
|
|
|
|||
|
|
@ -37,12 +37,24 @@ public class ThrusterData : BehaviorData
|
|||
}
|
||||
}
|
||||
|
||||
public class Thruster : Behavior, IAnalogBehavior, IPowerConsumer
|
||||
public class Thruster : Behavior, IPowerConsumer
|
||||
{
|
||||
// Live: heat, quality, durability and power move this stat after construction, so it is read, never cached.
|
||||
public float Thrust => Evaluate(_data.Thrust);
|
||||
// Thrust at a full grant, the same stat read without the power-supply term: what a column promises. Live, like Thrust.
|
||||
public float NominalThrust => EvaluateNominalPower(_data.Thrust);
|
||||
public float Torque { get; }
|
||||
|
||||
// This thruster's effect per unit throttle at the given thrust, in the body frame: xy is the push (x starboard, y
|
||||
// forward), z the clockwise yaw rate in rad/s. Zero when the item is absent or offline. The one geometry owner for
|
||||
// the allocator and the envelope.
|
||||
public float3 Column(float thrust)
|
||||
{
|
||||
if (Item == null || !Item.Active.Value) return default;
|
||||
var push = -float2(0, 1).Rotate(Item.EquippableItem.Rotation) * thrust / Entity.Mass;
|
||||
return float3(push.x, push.y, Torque * thrust * ItemManager.GameplaySettings.TorqueMultiplier / Entity.Mass);
|
||||
}
|
||||
|
||||
public float Axis
|
||||
{
|
||||
get => _input;
|
||||
|
|
@ -84,7 +96,7 @@ public class Thruster : Behavior, IAnalogBehavior, IPowerConsumer
|
|||
// registered request field (StatValidation.PowerRequestFields) -- read nominally, same reasoning as every
|
||||
// other IPowerConsumer in this cut, though EnergyUsage carries no PowerSupply term in Thruster's own shipped
|
||||
// catalog today; Thrust (the field Cut 7 curves) is a separate stat Execute reads with the real Evaluate.
|
||||
public float PowerRequest(float dt) => _input > .01f ? _input * EvaluateNominalPower(_data.EnergyUsage) : 0f;
|
||||
public float PowerRequest(float dt) => _input > 0f ? _input * EvaluateNominalPower(_data.EnergyUsage) : 0f;
|
||||
|
||||
// Cut 5 (docs/stats-and-power-cut.md §1.3, PowerTiers.cs): Medium -- mobility. Losing thrust for a tick
|
||||
// under brownout is an inconvenience, not the cascading failure a starved radiator or shield causes.
|
||||
|
|
@ -121,7 +133,7 @@ public class Thruster : Behavior, IAnalogBehavior, IPowerConsumer
|
|||
// Thrust stat carries a PowerSupply term, a partial grant already comes back reduced -- this gate no
|
||||
// longer demands a full grant, only that the thruster is being asked to do anything (_input) and that it
|
||||
// has not been cut to true zero supply (the epsilon PowerBus itself already treats as "nothing granted").
|
||||
if(_input > .01f && Item.PowerSupply > 1e-4f)
|
||||
if(_input > 0f && Item.PowerSupply > 1e-4f)
|
||||
{
|
||||
var thrust = Thrust;
|
||||
Entity.Velocity -= Direction.xz * _input * thrust / Entity.Mass * dt;
|
||||
|
|
|
|||
|
|
@ -1373,6 +1373,10 @@ public abstract class Entity
|
|||
yield return b;
|
||||
}
|
||||
|
||||
// The phase between the items' performance update and the power bus: what an entity commands its items to do this
|
||||
// tick, read from the same liveness and stats Execute then acts on. Ship's allocation lives here.
|
||||
protected virtual void UpdateThrottles() { }
|
||||
|
||||
public virtual void Update(float delta)
|
||||
{
|
||||
TurnRate = 0;
|
||||
|
|
@ -1402,6 +1406,7 @@ public abstract class Entity
|
|||
UpdateTemperature(delta);
|
||||
|
||||
foreach (var item in _orderedEquipment) item.UpdatePerformance();
|
||||
UpdateThrottles();
|
||||
|
||||
if (_active)
|
||||
{
|
||||
|
|
@ -2356,6 +2361,4 @@ public class BehaviorGroup
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
//public IAnalogBehavior Axis;
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ using System.Linq;
|
|||
using System.Reflection;
|
||||
using MessagePack;
|
||||
using Newtonsoft.Json;
|
||||
using UniRx;
|
||||
using CultMath;
|
||||
using static CultMath.math;
|
||||
using quaternion = CultMath.quaternion;
|
||||
|
|
@ -30,14 +29,11 @@ public class Ship : Entity
|
|||
public float Turn;
|
||||
public bool IsPlayerShip;
|
||||
|
||||
private HashSet<EquippedItem> _thrusterItems;
|
||||
private Thruster[] _allThrusters;
|
||||
private HashSet<Thruster> _forwardThrusters;
|
||||
private HashSet<Thruster> _reverseThrusters;
|
||||
private HashSet<Thruster> _rightThrusters;
|
||||
private HashSet<Thruster> _leftThrusters;
|
||||
private HashSet<Thruster> _clockwiseThrusters;
|
||||
private HashSet<Thruster> _counterClockwiseThrusters;
|
||||
// One column per thruster (its effect per unit throttle, in the body frame) and the throttles the allocator answers with.
|
||||
private float3[] _columns;
|
||||
private float[] _throttles;
|
||||
private readonly ThrustAllocator _allocator = new ThrustAllocator();
|
||||
|
||||
private bool _exitingWormhole = false;
|
||||
private bool _enteringWormhole = false;
|
||||
|
|
@ -48,23 +44,6 @@ public class Ship : Entity
|
|||
private float2 _wormholeExitVelocity;
|
||||
|
||||
public bool WormholeAnimationInProgress => _enteringWormhole || _exitingWormhole;
|
||||
public float ForwardThrust { get; private set; }
|
||||
public float ReverseThrust { get; private set; }
|
||||
public float LeftStrafeThrust { get; private set; }
|
||||
public float RightStrafeThrust { get; private set; }
|
||||
public float ClockwiseTorque { get; private set; }
|
||||
public float CounterClockwiseTorque { get; private set; }
|
||||
public float LeftStrafeTotalTorque { get; private set; }
|
||||
private List<Thruster> LeftStrafeTorqueThrusters = new List<Thruster>();
|
||||
public float RightStrafeTotalTorque { get; private set; }
|
||||
private List<Thruster> RightStrafeTorqueThrusters = new List<Thruster>();
|
||||
|
||||
public float TurnTime(float2 direction)
|
||||
{
|
||||
var angleDiff = Direction.Angle(normalize(direction));
|
||||
var clockwise = dot(direction, Direction.Rotate(ItemRotation.Clockwise)) > 0;
|
||||
return angleDiff / ((clockwise ? ClockwiseTorque : CounterClockwiseTorque) / Mass);
|
||||
}
|
||||
|
||||
public event Action OnExitedWormhole;
|
||||
public event Action OnEnteredWormhole;
|
||||
|
|
@ -95,49 +74,18 @@ public class Ship : Entity
|
|||
base.Activate();
|
||||
|
||||
_allThrusters = GetBehaviors<Thruster>().ToArray();
|
||||
_thrusterItems = new HashSet<EquippedItem>(_allThrusters.Select(x=>x.Item));
|
||||
|
||||
_forwardThrusters = new HashSet<Thruster>(_allThrusters
|
||||
.Where(x => x.Item.EquippableItem.Rotation == ItemRotation.Reversed));
|
||||
|
||||
_reverseThrusters = new HashSet<Thruster>(_allThrusters
|
||||
.Where(x => x.Item.EquippableItem.Rotation == ItemRotation.None));
|
||||
|
||||
_rightThrusters = new HashSet<Thruster>(_allThrusters
|
||||
.Where(x => x.Item.EquippableItem.Rotation == ItemRotation.CounterClockwise));
|
||||
|
||||
_leftThrusters = new HashSet<Thruster>(_allThrusters
|
||||
.Where(x => x.Item.EquippableItem.Rotation == ItemRotation.Clockwise));
|
||||
|
||||
_counterClockwiseThrusters = new HashSet<Thruster>(_allThrusters
|
||||
.Where(x => x.Torque < -ItemManager.GameplaySettings.TorqueFloor));
|
||||
|
||||
_clockwiseThrusters = new HashSet<Thruster>(_allThrusters
|
||||
.Where(x => x.Torque > ItemManager.GameplaySettings.TorqueFloor));
|
||||
_columns = new float3[_allThrusters.Length];
|
||||
_throttles = new float[_allThrusters.Length];
|
||||
}
|
||||
|
||||
public Ship(ItemManager itemManager, Zone zone, EquippableItem hull, EntitySettings settings) : base(itemManager, zone, hull, settings)
|
||||
{
|
||||
ItemDestroyed.Where(item=>_thrusterItems.Contains(item)).Subscribe(RemoveThruster);
|
||||
}
|
||||
|
||||
private void RemoveThruster(EquippedItem item)
|
||||
{
|
||||
_thrusterItems.Remove(item);
|
||||
var thruster = item.GetBehavior<Thruster>();
|
||||
if (_forwardThrusters.Contains(thruster)) _forwardThrusters.Remove(thruster);
|
||||
if (_reverseThrusters.Contains(thruster)) _reverseThrusters.Remove(thruster);
|
||||
if (_rightThrusters.Contains(thruster)) _rightThrusters.Remove(thruster);
|
||||
if (_leftThrusters.Contains(thruster)) _leftThrusters.Remove(thruster);
|
||||
if (_clockwiseThrusters.Contains(thruster)) _clockwiseThrusters.Remove(thruster);
|
||||
if (_counterClockwiseThrusters.Contains(thruster)) _counterClockwiseThrusters.Remove(thruster);
|
||||
}
|
||||
|
||||
#region ThrustCalculation
|
||||
|
||||
// What this ship can do at this instant (see ManoeuvreEnvelope): the sum of what each live propulsor reports with
|
||||
// its own Execute arithmetic; thrusters are its only propulsors. Derived each update, never saved. The aggregates above are not an envelope (they
|
||||
// mix units and cancel a lone off-axis strafer), so nothing in it reads them.
|
||||
// its own Execute arithmetic; thrusters are its only propulsors. Derived each update, never saved.
|
||||
public ManoeuvreEnvelope Envelope { get; private set; }
|
||||
|
||||
private void RecalculateEnvelope()
|
||||
|
|
@ -147,120 +95,23 @@ public class Ship : Entity
|
|||
Envelope = envelope;
|
||||
}
|
||||
|
||||
private void RecalculateThrust()
|
||||
{
|
||||
RecalculateEnvelope();
|
||||
RecalculateForwardThrust();
|
||||
RecalculateReverseThrust();
|
||||
RecalculateLeftStrafeThrust();
|
||||
RecalculateRightStrafeThrust();
|
||||
RecalculateClockwiseTorque();
|
||||
RecalculateCounterClockwiseTorque();
|
||||
}
|
||||
|
||||
private void RecalculateForwardThrust()
|
||||
{
|
||||
ForwardThrust = 0;
|
||||
foreach (var thruster in _forwardThrusters)
|
||||
if (thruster.Item.Active.Value)
|
||||
ForwardThrust += thruster.Thrust;
|
||||
}
|
||||
|
||||
private void RecalculateReverseThrust()
|
||||
{
|
||||
ReverseThrust = 0;
|
||||
foreach (var thruster in _reverseThrusters)
|
||||
if (thruster.Item.Active.Value)
|
||||
ReverseThrust += thruster.Thrust;
|
||||
}
|
||||
|
||||
private void RecalculateLeftStrafeThrust()
|
||||
{
|
||||
LeftStrafeThrust = 0;
|
||||
LeftStrafeTotalTorque = 0;
|
||||
foreach (var thruster in _leftThrusters)
|
||||
{
|
||||
if(thruster.Item.Active.Value)
|
||||
{
|
||||
LeftStrafeThrust += thruster.Thrust;
|
||||
LeftStrafeTotalTorque += thruster.Torque * thruster.Thrust;
|
||||
}
|
||||
}
|
||||
LeftStrafeTorqueThrusters.Clear();
|
||||
foreach(var thruster in _leftThrusters)
|
||||
if (abs(sign(thruster.Torque) - sign(LeftStrafeTotalTorque)) < .01f)
|
||||
LeftStrafeTorqueThrusters.Add(thruster);
|
||||
}
|
||||
|
||||
private void RecalculateRightStrafeThrust()
|
||||
{
|
||||
RightStrafeThrust = 0;
|
||||
RightStrafeTotalTorque = 0;
|
||||
foreach (var thruster in _rightThrusters)
|
||||
{
|
||||
if(thruster.Item.Active.Value)
|
||||
{
|
||||
RightStrafeThrust += thruster.Thrust;
|
||||
RightStrafeTotalTorque += thruster.Torque * thruster.Thrust;
|
||||
}
|
||||
}
|
||||
RightStrafeTorqueThrusters.Clear();
|
||||
foreach(var thruster in _rightThrusters)
|
||||
if (abs(sign(thruster.Torque) - sign(RightStrafeTotalTorque)) < .01f)
|
||||
RightStrafeTorqueThrusters.Add(thruster);
|
||||
}
|
||||
|
||||
private void RecalculateClockwiseTorque()
|
||||
{
|
||||
ClockwiseTorque = 0;
|
||||
foreach (var thruster in _clockwiseThrusters)
|
||||
if (thruster.Item.Active.Value)
|
||||
ClockwiseTorque += thruster.Torque;
|
||||
}
|
||||
|
||||
private void RecalculateCounterClockwiseTorque()
|
||||
{
|
||||
CounterClockwiseTorque = 0;
|
||||
foreach (var thruster in _counterClockwiseThrusters)
|
||||
if (thruster.Item.Active.Value)
|
||||
CounterClockwiseTorque -= thruster.Torque;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Called by Entity.Update once the items' performance is current and before the power bus is stepped and the items
|
||||
// execute: liveness, thrust and the throttles they decide are one phase, the one Execute acts on. The lock is a
|
||||
// bound on the allocator's solve; intent reaches the allocator here and nowhere else.
|
||||
protected override void UpdateThrottles()
|
||||
{
|
||||
if (_exitingWormhole || _enteringWormhole) return;
|
||||
RecalculateEnvelope();
|
||||
for (var i = 0; i < _allThrusters.Length; i++)
|
||||
_columns[i] = _allThrusters[i].Column(_allThrusters[i].NominalThrust);
|
||||
_allocator.Allocate(_columns, MovementDirection, Turn, _throttles, ThrottleLocked);
|
||||
for (var i = 0; i < _allThrusters.Length; i++) _allThrusters[i].Axis = _throttles[i];
|
||||
}
|
||||
|
||||
public override void Update(float delta)
|
||||
{
|
||||
if (_active && !_exitingWormhole && !_enteringWormhole)
|
||||
{
|
||||
RecalculateThrust();
|
||||
// The lock replaces intent here, once; player and agent intent both pass through this one read.
|
||||
var move = ThrottleLocked ? float2(0, 1) : MovementDirection;
|
||||
foreach (var thruster in _allThrusters) thruster.Axis = 0;
|
||||
var rightThrusterTorqueCompensation = abs(RightStrafeTotalTorque) / RightStrafeTorqueThrusters.Count;
|
||||
foreach (var thruster in _rightThrusters)
|
||||
{
|
||||
var thrust = 0f;
|
||||
thrust += move.x;
|
||||
if (RightStrafeTorqueThrusters.Contains(thruster))
|
||||
thrust -= move.x * (rightThrusterTorqueCompensation / (abs(thruster.Torque) * thruster.Thrust));
|
||||
thruster.Axis = thrust;
|
||||
}
|
||||
var leftThrusterTorqueCompensation = abs(LeftStrafeTotalTorque) / LeftStrafeTorqueThrusters.Count;
|
||||
foreach (var thruster in _leftThrusters)
|
||||
{
|
||||
var thrust = 0f;
|
||||
thrust += -move.x;
|
||||
if (LeftStrafeTorqueThrusters.Contains(thruster))
|
||||
thrust += move.x * (leftThrusterTorqueCompensation / (abs(thruster.Torque) * thruster.Thrust));
|
||||
thruster.Axis = thrust;
|
||||
}
|
||||
foreach (var thruster in _forwardThrusters) thruster.Axis += move.y;
|
||||
foreach (var thruster in _reverseThrusters) thruster.Axis += -move.y;
|
||||
|
||||
foreach (var thruster in _clockwiseThrusters) thruster.Axis += Turn;
|
||||
foreach (var thruster in _counterClockwiseThrusters) thruster.Axis += -Turn;
|
||||
}
|
||||
|
||||
var velocityMagnitude = length(Velocity);
|
||||
if(velocityMagnitude > .01f)
|
||||
Velocity = normalize(Velocity) * decay(velocityMagnitude, HullData.Drag, delta);
|
||||
|
|
|
|||
|
|
@ -31,11 +31,18 @@ public sealed class ThrustAllocator
|
|||
int _columns = -1;
|
||||
float[] _a, _b, _lo, _hi, _x, _workspace;
|
||||
|
||||
public void Allocate(ReadOnlySpan<float3> columns, float2 move, float turn, Span<float> throttle)
|
||||
/// <summary>
|
||||
/// Solves the throttles for the intent. <paramref name="forwardFloor"/> is the throttle lock: every thruster whose
|
||||
/// column pushes forward is bounded below at full, the stick is ignored (forward is the only translation asked, so
|
||||
/// nothing fires against the floored drives), and the turn is served by what remains.
|
||||
/// </summary>
|
||||
public void Allocate(ReadOnlySpan<float3> columns, float2 move, float turn, Span<float> throttle, bool forwardFloor = false)
|
||||
{
|
||||
var n = columns.Length;
|
||||
if (n != _columns) Resize(n);
|
||||
for (var i = 0; i < n; i++) throttle[i] = 0;
|
||||
if (forwardFloor) move = new float2(0, 1);
|
||||
for (var i = 0; i < n; i++) _lo[i] = forwardFloor && columns[i].y > 0 ? 1f : 0f;
|
||||
|
||||
// Nothing asked, nothing fired: that is a fact about the intent, decided before the solve. The solver's warm
|
||||
// start is left as the last solved point, which is where the next non-zero call resumes.
|
||||
|
|
|
|||
Loading…
Reference in a new issue