Loot 3 group A: the grab is presented, not simulated
IGrabPresenter.Grab(GrabEvent) only; the binder's Transform and velocity resolvers go. TractorBeam presents a running grab (aims at Started's TargetPosition, emits at Power, dark otherwise). FieldDriver draws its tendril from GrabEvents alone with a real-time phase clock over each event's PhaseDuration and writes no transform and destroys nothing; its grab times move to FieldTester, now the scene's stand-in sim that owns its pickup's timeline and pose. ItemPickup keeps its label lit while held. ImpactPresenterSmoke becomes CapabilityPresentationSmoke with a grab section; its two untracked-ship expectations follow DrawsAt since contact-hud. Builds (Unity 6000.3.24f1 batchmode: smoke ok, scene opens, no missing script). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
24ab95af8d
commit
48b42992ee
9 changed files with 232 additions and 166 deletions
|
|
@ -4818,6 +4818,3 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
ParticleSystem: {fileID: 5087162364086924483}
|
||||
Radius: 25
|
||||
Traction: 25
|
||||
Distance: 75
|
||||
|
|
|
|||
|
|
@ -811,6 +811,9 @@ MonoBehaviour:
|
|||
Camera: {fileID: 1153356615}
|
||||
TestField: {fileID: 909026488}
|
||||
Properties: {fileID: 401779935}
|
||||
GrabExtendTime: 1.5
|
||||
GrabEnvelopTime: 0.75
|
||||
GrabPullTime: 1.5
|
||||
--- !u!4 &699508827
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -1116,9 +1119,6 @@ MonoBehaviour:
|
|||
TendrilBaseDamping: 2
|
||||
TendrilTipRadiusAnimationExponent: 2
|
||||
TendrilFadeExponent: 2
|
||||
GrabExtendTime: 1.5
|
||||
GrabEnvelopTime: 0.75
|
||||
GrabPullTime: 1.5
|
||||
RefractionAxis: 1
|
||||
--- !u!114 &909026489
|
||||
MonoBehaviour:
|
||||
|
|
|
|||
|
|
@ -5,24 +5,27 @@ using System.Reflection;
|
|||
using GameCult.Caching;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using CultMath;
|
||||
using static CultMath.math;
|
||||
|
||||
// Batch proof of ImpactPresenter's wiring, without a scene: a zone ship is bound to an EntityInstance through the game's
|
||||
// own SetEntity, and impacts are published on the ship's Capabilities exactly as FireControl does. A hull absorb spawns
|
||||
// Batch proof of the capability presenters' wiring, without a scene: a zone ship is bound to an EntityInstance through the
|
||||
// game's own SetEntity, and impacts and grabs are published on the ship's Capabilities exactly as the simulation does. A hull absorb spawns
|
||||
// one hull effect under the instance and, for the player's ship at ScreenShake 1, raises one camera impulse; ScreenShake 0
|
||||
// or another ship raises none; a shield absorb spawns no hull effect. The shield ripple itself (ShieldManager.ShowHit
|
||||
// needs a shield material and a rig) is looked at in the editor, not here.
|
||||
// needs a shield material and a rig) is looked at in the editor, not here. Grabs: a Tractor Beam prefab under the instance
|
||||
// is bound by the same SetEntity and presents a grab from events alone (emission at Power and the aim at the held point
|
||||
// from Started to Completed or Cancelled, nothing otherwise, nothing for another entity's grab).
|
||||
//
|
||||
// Unity -batchmode -nographics -projectPath <project> -executeMethod ImpactPresenterSmoke.Run -logFile <log>
|
||||
// Unity -batchmode -nographics -projectPath <project> -executeMethod CapabilityPresentationSmoke.Run -logFile <log>
|
||||
//
|
||||
// Do not pass -quit: Run exits the Editor itself, 0 on success and 1 on any failure.
|
||||
public static class ImpactPresenterSmoke
|
||||
public static class CapabilityPresentationSmoke
|
||||
{
|
||||
private const BindingFlags Any = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
|
||||
|
||||
public static void Run()
|
||||
{
|
||||
var work = Path.Combine(Path.GetTempPath(), $"impact-presenter-smoke-{Guid.NewGuid():N}");
|
||||
var work = Path.Combine(Path.GetTempPath(), $"capability-presentation-smoke-{Guid.NewGuid():N}");
|
||||
GameObject[] objects = null;
|
||||
try
|
||||
{
|
||||
|
|
@ -86,6 +89,12 @@ public static class ImpactPresenterSmoke
|
|||
instance.ThrusterHardpoints = new ThrusterHardpoint[0];
|
||||
instance.WeaponHardpoints = new WeaponHardpoint[0];
|
||||
instance.ArticulationPoints = new ArticulationPoint[0];
|
||||
// The beam is a child before SetEntity on both ship paths, so the binder finds it as the game does.
|
||||
var beamPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Content/Prefabs/Tractor Beam.prefab")
|
||||
?? throw new InvalidOperationException("No Tractor Beam prefab.");
|
||||
var beam = ((GameObject)PrefabUtility.InstantiatePrefab(beamPrefab, instanceObject.transform)).GetComponent<TractorBeam>();
|
||||
beam.Power = .8f;
|
||||
beam.Direction = Vector3.forward;
|
||||
instance.SetEntity(null, ship);
|
||||
|
||||
int Effects() => instance.GetComponentsInChildren<Prototype>(true).Count(p => p != null && !p.isOriginalPrototype);
|
||||
|
|
@ -111,13 +120,47 @@ public static class ImpactPresenterSmoke
|
|||
ActionGameManager.PlayerSettings.GraphicsSettings.ScreenShake = 1f;
|
||||
manager.CurrentEntity = other;
|
||||
ship.Capabilities.PublishAbsorb(Hit(AbsorbLayer.Hull));
|
||||
Expect("hull hit on another ship", 3, 1);
|
||||
// An untracked ship is not drawn at (ActionGameManager.DrawsAt, since contact-hud): no hull effect, no impulse.
|
||||
Expect("hull hit on an untracked ship", 2, 1);
|
||||
|
||||
manager.CurrentEntity = ship;
|
||||
ship.Capabilities.PublishAbsorb(Hit(AbsorbLayer.Shield));
|
||||
Expect("player's shield hit", 3, 2);
|
||||
Expect("player's shield hit", 2, 2);
|
||||
|
||||
Debug.Log("IMPACT_PRESENTER_SMOKE ok effects=3 impulses=2");
|
||||
// Grabs: the beam presents the simulation's events and nothing else.
|
||||
float Emission() => beam.ParticleSystem.emission.rateOverTimeMultiplier;
|
||||
void ExpectBeam(string what, float emission, bool towardHeld)
|
||||
{
|
||||
beam.Update();
|
||||
var forward = beam.transform.forward;
|
||||
var aimed = towardHeld ? forward.x > .999f : forward.z > .999f;
|
||||
if (abs(Emission() - emission) > 1e-4f || !aimed)
|
||||
throw new InvalidOperationException($"{what}: emission {Emission()} (want {emission}), forward {forward} (want {(towardHeld ? "+x" : "+z")}).");
|
||||
}
|
||||
GrabEvent Grab(GrabPhase phase, float3 at) => new GrabEvent(new FloatingBodyId(7), phase, at, 1f);
|
||||
|
||||
ExpectBeam("idle beam", 0f, false);
|
||||
ship.Capabilities.PublishGrab(Grab(GrabPhase.Started, float3(20f, 0f, 0f)));
|
||||
ExpectBeam("grab started", .8f, true);
|
||||
foreach (var phase in new[] { GrabPhase.Extend, GrabPhase.Envelop, GrabPhase.Pull })
|
||||
{
|
||||
ship.Capabilities.PublishGrab(Grab(phase, float3(20f, 0f, 0f)));
|
||||
ExpectBeam($"grab {phase}", .8f, true);
|
||||
}
|
||||
ship.Capabilities.PublishGrab(Grab(GrabPhase.Completed, float3(20f, 0f, 0f)));
|
||||
ExpectBeam("grab completed", 0f, false);
|
||||
|
||||
// A grab cancelled after its item vanished carries no position: the beam goes dark and never turns toward it.
|
||||
ship.Capabilities.PublishGrab(Grab(GrabPhase.Started, float3(20f, 0f, 0f)));
|
||||
ExpectBeam("second grab started", .8f, true);
|
||||
ship.Capabilities.PublishGrab(Grab(GrabPhase.Cancelled, float3(0f, 0f, 0f)));
|
||||
ExpectBeam("grab cancelled", 0f, false);
|
||||
|
||||
// The beam belongs to the ship's entity: another entity's grab is no business of its own.
|
||||
other.Capabilities.PublishGrab(Grab(GrabPhase.Started, float3(20f, 0f, 0f)));
|
||||
ExpectBeam("another entity's grab", 0f, false);
|
||||
|
||||
Debug.Log("CAPABILITY_PRESENTATION_SMOKE ok effects=2 impulses=2 grabs=ok");
|
||||
return new[] { feedbackObject, effectObject, managerObject, instanceObject };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,27 +8,18 @@ using UnityEngine;
|
|||
// playground-cut.md fork L: "presentation is chosen per item, independently of capability... one
|
||||
// presentation... can visualize any subset of an item's capabilities"). This binder has no knowledge
|
||||
// of FieldDriver or any other concrete presentation, and no knowledge of what publishes events --
|
||||
// today the FieldShieldTest scene calls Bind() directly; a real capability owner will do the same
|
||||
// later without this binder changing.
|
||||
// EntityInstance binds every entity's events, and the FieldShieldTest scene binds its stand-in sim's the same way.
|
||||
public class CapabilityPresentationBinder : MonoBehaviour
|
||||
{
|
||||
private readonly List<IDisposable> _subscriptions = new List<IDisposable>();
|
||||
private Func<FloatingBodyId, Transform> _grabTargetResolver;
|
||||
private Func<FloatingBodyId, Vector3> _grabInitialVelocityResolver;
|
||||
|
||||
// Subscribes every presenter interface implemented by components on this object or its children to
|
||||
// the matching stream of `events`. `grabTargetResolver`/`grabInitialVelocityResolver` resolve a
|
||||
// GrabEvent's target id to the live Transform/velocity a grab presenter needs; the owner
|
||||
// of `events` supplies them (the test scene today; the real pickup capability's caller later).
|
||||
public void Bind(CapabilityEvents events, Func<FloatingBodyId, Transform> grabTargetResolver = null,
|
||||
Func<FloatingBodyId, Vector3> grabInitialVelocityResolver = null)
|
||||
// the matching stream of `events`.
|
||||
public void Bind(CapabilityEvents events)
|
||||
{
|
||||
Unbind();
|
||||
if (events == null) return;
|
||||
|
||||
_grabTargetResolver = grabTargetResolver;
|
||||
_grabInitialVelocityResolver = grabInitialVelocityResolver;
|
||||
|
||||
foreach (var presenter in GetComponentsInChildren<IAbsorbPresenter>(true))
|
||||
{
|
||||
var p = presenter;
|
||||
|
|
@ -38,12 +29,7 @@ public class CapabilityPresentationBinder : MonoBehaviour
|
|||
foreach (var presenter in GetComponentsInChildren<IGrabPresenter>(true))
|
||||
{
|
||||
var p = presenter;
|
||||
_subscriptions.Add(events.Grab.Subscribe(e =>
|
||||
{
|
||||
var target = _grabTargetResolver?.Invoke(e.Target);
|
||||
var velocity = _grabInitialVelocityResolver?.Invoke(e.Target) ?? Vector3.zero;
|
||||
p.Grab(e, target, velocity);
|
||||
}));
|
||||
_subscriptions.Add(events.Grab.Subscribe(e => p.Grab(e)));
|
||||
}
|
||||
|
||||
foreach (var presenter in GetComponentsInChildren<IMeleePresenter>(true))
|
||||
|
|
@ -63,8 +49,6 @@ public class CapabilityPresentationBinder : MonoBehaviour
|
|||
{
|
||||
foreach (var s in _subscriptions) s.Dispose();
|
||||
_subscriptions.Clear();
|
||||
_grabTargetResolver = null;
|
||||
_grabInitialVelocityResolver = null;
|
||||
}
|
||||
|
||||
private void OnDisable() => Unbind();
|
||||
|
|
|
|||
|
|
@ -10,13 +10,10 @@ public interface IAbsorbPresenter
|
|||
void Absorb(AbsorbEvent e);
|
||||
}
|
||||
|
||||
// Grab needs a live Transform to animate, which the engine-free GrabEvent cannot carry (its
|
||||
// Target is a plain identity, not a Unity reference). The binder resolves the id through a
|
||||
// delegate supplied by the event source's owner and hands the presenter the result, along with the
|
||||
// initial velocity the target was already moving with (also owner-supplied, for the same reason).
|
||||
// Presenters read the event and the simulated pose, never a Transform the sim does not own.
|
||||
public interface IGrabPresenter
|
||||
{
|
||||
void Grab(GrabEvent e, Transform target, Vector3 initialVelocity);
|
||||
void Grab(GrabEvent e);
|
||||
}
|
||||
|
||||
public interface IMeleePresenter
|
||||
|
|
|
|||
|
|
@ -63,11 +63,7 @@ public class FieldDriver : MonoBehaviour, IAbsorbPresenter, IGrabPresenter, IMel
|
|||
// public float GrabScaleAnimationExponent = .5f;
|
||||
|
||||
[Inspectable]
|
||||
public float GrabExtendTime;
|
||||
[Inspectable]
|
||||
public float GrabEnvelopTime;
|
||||
[Inspectable]
|
||||
public float GrabPullTime;
|
||||
public float TendrilTipScale = 1;
|
||||
|
||||
[Inspectable]
|
||||
public RectTransform.Axis RefractionAxis = RectTransform.Axis.Horizontal;
|
||||
|
|
@ -81,29 +77,17 @@ public class FieldDriver : MonoBehaviour, IAbsorbPresenter, IGrabPresenter, IMel
|
|||
private float _meleeTime;
|
||||
private bool _meleeActive;
|
||||
private MeshCollider _collider;
|
||||
private Transform _grabObject;
|
||||
private Vector3 _grabObjectStartPos;
|
||||
private Vector3 _grabObjectStartTendrilBasePos;
|
||||
private Vector3 _grabObjectEndPos;
|
||||
private Vector3 _grabObjectVelocity;
|
||||
private float _grabObjectScale;
|
||||
private bool _grabbing;
|
||||
private GrabPhase _grabPhase;
|
||||
private float _grabTime;
|
||||
private TendrilPhase _grabPhase;
|
||||
private float _phaseDuration;
|
||||
private Vector3 _grabStart;
|
||||
private Vector3 _grabStartTendrilBase;
|
||||
private Vector3 _tendrilBasePos;
|
||||
private Vector3 _tendrilBendTarget;
|
||||
private Vector3 _tendrilTargetPos;
|
||||
private float _fadePoint;
|
||||
|
||||
// Named distinctly from the global GrabPhase (CapabilityEvents.cs): this is FieldDriver's own
|
||||
// internal tendril-animation timing, still owned here until a real pickup capability exists to
|
||||
// publish per-phase GrabEvent transitions (see Grab(GrabEvent, Transform, Vector3) below).
|
||||
private enum TendrilPhase
|
||||
{
|
||||
Extend,
|
||||
Envelop,
|
||||
Pull
|
||||
}
|
||||
|
||||
private struct FieldHit
|
||||
{
|
||||
public float3 Position;
|
||||
|
|
@ -181,32 +165,41 @@ public class FieldDriver : MonoBehaviour, IAbsorbPresenter, IGrabPresenter, IMel
|
|||
_meleeTime = 0;
|
||||
}
|
||||
|
||||
public bool CanGrab => _grabObject == null;
|
||||
|
||||
public void GrabObject(Transform t, Vector3 v)
|
||||
{
|
||||
_grabPhase = TendrilPhase.Extend;
|
||||
_grabObject = t;
|
||||
_grabObjectStartPos = t.position;
|
||||
_grabObjectVelocity = v;
|
||||
_grabObjectScale = t.localScale.x;
|
||||
_tendrilBasePos = _grabObjectStartTendrilBasePos = _collider.ClosestPoint(_grabObjectStartPos);
|
||||
_fadePoint = .999f;
|
||||
}
|
||||
|
||||
// IAbsorbPresenter: delegates straight to the existing hit-buffer mechanism. e.DamageType is not
|
||||
// read -- the field shield has no per-damage-type visual today; it rides along in the event for
|
||||
// presentations that do vary by type (the mask itself is absorb-capability data, out of scope here).
|
||||
public void Absorb(AbsorbEvent e) => AddHit(e.Position, e.Direction, e.Magnitude);
|
||||
|
||||
// IGrabPresenter: only the Started phase does anything today. FieldDriver still times the
|
||||
// extend/envelop/pull animation itself (TendrilPhase above, driven by GrabExtendTime/
|
||||
// GrabEnvelopTime/GrabPullTime) because no capability yet publishes those phase transitions;
|
||||
// once one does, this is where FieldDriver would react to Envelop/Pull/Completed/Cancelled too.
|
||||
public void Grab(GrabEvent e, Transform target, Vector3 initialVelocity)
|
||||
// IGrabPresenter: the simulation owns the grab; this draws the tendril from the events alone. Started records
|
||||
// where the item is held and where the tendril leaves the field; Extend, Envelop and Pull set the phase and
|
||||
// restart the real-time phase clock over the event's duration (it clamps at 1 until the next event, so a
|
||||
// slowed simulation stretches the wait, not the animation); Completed and Cancelled end it.
|
||||
public void Grab(GrabEvent e)
|
||||
{
|
||||
if (e.Phase == GrabPhase.Started && target != null)
|
||||
GrabObject(target, initialVelocity);
|
||||
switch (e.Phase)
|
||||
{
|
||||
case GrabPhase.Started:
|
||||
_grabbing = true;
|
||||
_grabPhase = GrabPhase.Started;
|
||||
_grabStart = e.TargetPosition.ToUnity();
|
||||
_tendrilBasePos = _grabStartTendrilBase = _collider.ClosestPoint(_grabStart);
|
||||
_tendrilTargetPos = _grabStart;
|
||||
_fadePoint = .999f;
|
||||
break;
|
||||
case GrabPhase.Extend:
|
||||
case GrabPhase.Envelop:
|
||||
case GrabPhase.Pull:
|
||||
if (!_grabbing) break;
|
||||
_grabPhase = e.Phase;
|
||||
_grabTime = 0;
|
||||
_phaseDuration = e.PhaseDuration;
|
||||
break;
|
||||
case GrabPhase.Completed:
|
||||
case GrabPhase.Cancelled:
|
||||
_grabbing = false;
|
||||
_field.SetFloat("_TendrilInfluence", 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// IMeleePresenter: the field shield's swing shape comes from its own authored curves
|
||||
|
|
@ -277,74 +270,47 @@ public class FieldDriver : MonoBehaviour, IAbsorbPresenter, IGrabPresenter, IMel
|
|||
}
|
||||
|
||||
|
||||
if(_grabObject != null)
|
||||
if (_grabbing && _grabPhase != GrabPhase.Started)
|
||||
{
|
||||
_grabTime += Time.deltaTime / _grabPhase switch
|
||||
_grabTime = min(1, _grabTime + Time.deltaTime / max(_phaseDuration, 1e-4f));
|
||||
_tendrilBasePos = damp(_tendrilBasePos.ToCultMath(), _tendrilTargetPos.ToCultMath(), TendrilBaseDamping, Time.deltaTime).ToUnity();
|
||||
switch (_grabPhase)
|
||||
{
|
||||
TendrilPhase.Extend => GrabExtendTime,
|
||||
TendrilPhase.Envelop => GrabEnvelopTime,
|
||||
TendrilPhase.Pull => GrabPullTime,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
if (_grabTime > 1)
|
||||
{
|
||||
_grabTime = 0;
|
||||
if (_grabPhase == TendrilPhase.Envelop)
|
||||
{
|
||||
_grabObjectEndPos = _grabObject.position;
|
||||
}
|
||||
if (_grabPhase == TendrilPhase.Pull)
|
||||
{
|
||||
Destroy(_grabObject.gameObject);
|
||||
_grabObject = null;
|
||||
_field.SetFloat("_TendrilInfluence", 0);
|
||||
}
|
||||
else _grabPhase++;
|
||||
}
|
||||
|
||||
if (_grabObject != null)
|
||||
{
|
||||
_tendrilBasePos = damp(_tendrilBasePos.ToCultMath(), _grabObject.position.ToCultMath(), TendrilBaseDamping, Time.deltaTime).ToUnity();
|
||||
switch (_grabPhase)
|
||||
{
|
||||
case TendrilPhase.Extend:
|
||||
_tendrilBendTarget = _grabObjectStartPos;
|
||||
_grabObject.position += _grabObjectVelocity * Time.deltaTime;
|
||||
_tendrilTargetPos = lerp(
|
||||
lerp(_grabObjectStartTendrilBasePos.ToCultMath(), _grabObjectStartPos.ToCultMath(), _grabTime),
|
||||
lerp(_grabObjectStartPos.ToCultMath(), _grabObject.position.ToCultMath(), _grabTime),
|
||||
pow(_grabTime, TendrilExtensionExponent)).ToUnity();
|
||||
_field.SetFloat("_TendrilInfluence", pow(_grabTime, TendrilExtendBaseAnimationExponent));
|
||||
_field.SetFloat("_TendrilSize", lerp(TendrilBaseRadius/2,TendrilBaseRadius,pow(_grabTime, TendrilExtendBaseAnimationExponent)));
|
||||
_field.SetFloat("_TendrilRadius", _grabObjectScale * pow(_grabTime, TendrilTipRadiusAnimationExponent) * TendrilTipRadius);
|
||||
break;
|
||||
case TendrilPhase.Envelop:
|
||||
_tendrilBendTarget = _grabObjectStartPos;
|
||||
_tendrilTargetPos = _grabObject.position += _grabObjectVelocity * Time.deltaTime * (1-_grabTime);
|
||||
_field.SetFloat("_TendrilInfluence", 1);
|
||||
_field.SetFloat("_TendrilSize", TendrilBaseRadius);
|
||||
_field.SetFloat("_TendrilRadius", _grabObjectScale * TendrilTipRadius);
|
||||
break;
|
||||
case TendrilPhase.Pull:
|
||||
_tendrilTargetPos = _grabObject.position = lerp(_grabObjectEndPos.ToCultMath(), transform.position.ToCultMath(), _grabTime*_grabTime).ToUnity();
|
||||
_tendrilBendTarget = lerp(_grabObjectStartPos.ToCultMath(), _grabObjectEndPos.ToCultMath(), _grabTime*_grabTime).ToUnity();
|
||||
if (_fadePoint > .99 && transform.InverseTransformPoint(_grabObject.position).sqrMagnitude < 1)
|
||||
_fadePoint = _grabTime;
|
||||
_field.SetFloat("_TendrilInfluence", pow(smoothstep(1, _fadePoint, _grabTime), TendrilFadeExponent));
|
||||
_field.SetFloat("_TendrilSize", TendrilBaseRadius);
|
||||
_field.SetFloat("_TendrilRadius", _grabObjectScale * TendrilTipRadius);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
var tendrilRadii = EnvelopeRadiiOrFallback();
|
||||
_field.SetVector("_TendrilBase", Vector3.Scale(transform.InverseTransformPoint(_tendrilBasePos).normalized, tendrilRadii));
|
||||
_field.SetVector("_TendrilBend", Vector3.Scale(transform.InverseTransformPoint(_tendrilBendTarget), tendrilRadii));
|
||||
_field.SetVector("_TendrilTarget", Vector3.Scale(transform.InverseTransformPoint(_tendrilTargetPos), tendrilRadii));
|
||||
case GrabPhase.Extend:
|
||||
_tendrilBendTarget = _grabStart;
|
||||
_tendrilTargetPos = lerp(
|
||||
lerp(_grabStartTendrilBase.ToCultMath(), _grabStart.ToCultMath(), _grabTime),
|
||||
_grabStart.ToCultMath(),
|
||||
pow(_grabTime, TendrilExtensionExponent)).ToUnity();
|
||||
_field.SetFloat("_TendrilInfluence", pow(_grabTime, TendrilExtendBaseAnimationExponent));
|
||||
_field.SetFloat("_TendrilSize", lerp(TendrilBaseRadius/2,TendrilBaseRadius,pow(_grabTime, TendrilExtendBaseAnimationExponent)));
|
||||
_field.SetFloat("_TendrilRadius", TendrilTipScale * pow(_grabTime, TendrilTipRadiusAnimationExponent) * TendrilTipRadius);
|
||||
break;
|
||||
case GrabPhase.Envelop:
|
||||
_tendrilBendTarget = _grabStart;
|
||||
_tendrilTargetPos = _grabStart;
|
||||
_field.SetFloat("_TendrilInfluence", 1);
|
||||
_field.SetFloat("_TendrilSize", TendrilBaseRadius);
|
||||
_field.SetFloat("_TendrilRadius", TendrilTipScale * TendrilTipRadius);
|
||||
break;
|
||||
case GrabPhase.Pull:
|
||||
// The same curve as Zone's held pose (Zone.cs): the tip rides the item the simulation pulls in.
|
||||
_tendrilTargetPos = lerp(_grabStart.ToCultMath(), transform.position.ToCultMath(), smoothstep(0, 1, _grabTime)).ToUnity();
|
||||
_tendrilBendTarget = _grabStart;
|
||||
if (_fadePoint > .99 && transform.InverseTransformPoint(_tendrilTargetPos).sqrMagnitude < 1)
|
||||
_fadePoint = _grabTime;
|
||||
_field.SetFloat("_TendrilInfluence", pow(smoothstep(1, _fadePoint, _grabTime), TendrilFadeExponent));
|
||||
_field.SetFloat("_TendrilSize", TendrilBaseRadius);
|
||||
_field.SetFloat("_TendrilRadius", TendrilTipScale * TendrilTipRadius);
|
||||
break;
|
||||
}
|
||||
var tendrilRadii = EnvelopeRadiiOrFallback();
|
||||
_field.SetVector("_TendrilBase", Vector3.Scale(transform.InverseTransformPoint(_tendrilBasePos).normalized, tendrilRadii));
|
||||
_field.SetVector("_TendrilBend", Vector3.Scale(transform.InverseTransformPoint(_tendrilBendTarget), tendrilRadii));
|
||||
_field.SetVector("_TendrilTarget", Vector3.Scale(transform.InverseTransformPoint(_tendrilTargetPos), tendrilRadii));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private float almostIdentity( float x )
|
||||
{
|
||||
return x*x*(2.0f-x);
|
||||
|
|
@ -358,7 +324,7 @@ public class FieldDriver : MonoBehaviour, IAbsorbPresenter, IGrabPresenter, IMel
|
|||
private const int GIZMO_STEPS = 16;
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (_grabObject != null)
|
||||
if (_grabbing)
|
||||
{
|
||||
Vector3 previous = _tendrilBasePos;
|
||||
for (int i = 1; i <= GIZMO_STEPS; i++)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ public class ItemPickup : MonoBehaviour
|
|||
var viewAngle = acos(dot(toThis, ViewDirection)) * Mathf.Rad2Deg;
|
||||
if (length(diff) < LabelDisplayMaxDistance && viewAngle < LabelDisplayAngle)
|
||||
_displayTime = Time.time;
|
||||
if (_floating?.Holder != null) _displayTime = Time.time;
|
||||
var targetAlpha = Time.time - _displayTime < LabelPersistDuration ? 1 : 0;
|
||||
var c = ScanLabel.color;
|
||||
c.a = c.a + sign(targetAlpha - c.a) * (Time.deltaTime / LabelFadeDuration);
|
||||
|
|
|
|||
|
|
@ -1,22 +1,51 @@
|
|||
using System;
|
||||
using UnityEngine;
|
||||
using CultMath.UnityBridge;
|
||||
|
||||
public class TractorBeam : MonoBehaviour
|
||||
// Presents the ship's grab: while the simulation holds an item, the beam looks along the ground-plane vector to the
|
||||
// point the item was held from and emits at Power. Zone pulls the held item along the segment from the ship to that
|
||||
// point (docs/aetheria-release-map.md L11), which is why GrabEvent.TargetPosition on Started is the exact aim and no
|
||||
// pose lookup is needed. Idle, the beam turns with Direction and emits nothing. The simulation decides every grab fact.
|
||||
public class TractorBeam : MonoBehaviour, IGrabPresenter
|
||||
{
|
||||
public ParticleSystem ParticleSystem;
|
||||
public float Radius;
|
||||
public float Traction;
|
||||
public float Distance;
|
||||
|
||||
|
||||
public float Power { get; set; }
|
||||
public Vector3 Direction { get; set; }
|
||||
|
||||
private bool _grabbing;
|
||||
private Vector3 _heldPoint;
|
||||
private Vector3 _heldDirection;
|
||||
|
||||
// Cancelled can carry no position (a vanished item), so only Started is read for one.
|
||||
public void Grab(GrabEvent e)
|
||||
{
|
||||
switch (e.Phase)
|
||||
{
|
||||
case GrabPhase.Started:
|
||||
_grabbing = true;
|
||||
_heldPoint = e.TargetPosition.ToUnity();
|
||||
_heldDirection = Direction;
|
||||
break;
|
||||
case GrabPhase.Completed:
|
||||
case GrabPhase.Cancelled:
|
||||
_grabbing = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
transform.rotation = Quaternion.LookRotation(Direction);
|
||||
// The beam draws nothing until it presents a sim grab (loot-3): no particles, and no pull; the simulation
|
||||
// decides every floating item's motion.
|
||||
var look = Direction;
|
||||
if (_grabbing)
|
||||
{
|
||||
var toHeld = _heldPoint - transform.position;
|
||||
toHeld.y = 0;
|
||||
if (toHeld.sqrMagnitude > 1e-6f) _heldDirection = toHeld;
|
||||
look = _heldDirection;
|
||||
}
|
||||
transform.rotation = Quaternion.LookRotation(look);
|
||||
var emission = ParticleSystem.emission;
|
||||
emission.rateOverTimeMultiplier = 0;
|
||||
emission.rateOverTimeMultiplier = _grabbing ? Power : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ public class FieldTester : MonoBehaviour
|
|||
public Camera Camera;
|
||||
public FieldDriver TestField;
|
||||
public PropertiesPanel Properties;
|
||||
// The scene's slow viewing timings for a grab, published as the events' phase durations.
|
||||
public float GrabExtendTime;
|
||||
public float GrabEnvelopTime;
|
||||
public float GrabPullTime;
|
||||
|
||||
private AetheriaInput _input;
|
||||
private bool _forceThrust;
|
||||
|
|
@ -49,6 +53,14 @@ public class FieldTester : MonoBehaviour
|
|||
|
||||
private List<(float lerp, float time, Transform transform)> _pickups = new List<(float lerp, float time, Transform transform)>();
|
||||
|
||||
// This scene stands in for the simulation, so it owns its pickup's grab timeline and pose: one grab at a time.
|
||||
private bool _grabbing;
|
||||
private int _grabIndex;
|
||||
private float _grabElapsed;
|
||||
private float3 _grabStart;
|
||||
private bool _envelopPublished;
|
||||
private bool _pullPublished;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_input = new AetheriaInput();
|
||||
|
|
@ -59,10 +71,7 @@ public class FieldTester : MonoBehaviour
|
|||
if (binder == null)
|
||||
Debug.LogError("FieldTester expects a CapabilityPresentationBinder on TestField's GameObject; no capability events will reach the field.");
|
||||
else
|
||||
binder.Bind(_capabilityEvents,
|
||||
grabTargetResolver: id => (int) id.Value >= 0 && (int) id.Value < _pickups.Count ? _pickups[(int) id.Value].transform : null,
|
||||
grabInitialVelocityResolver: id => Vector3.forward *
|
||||
(PickupTravelDistance / lerp(PickupTravelTimeMin, PickupTravelTimeMax, _pickups[(int) id.Value].lerp)));
|
||||
binder.Bind(_capabilityEvents);
|
||||
|
||||
// Impact absorption (fork L): what used to be FieldDriver's own click handler is now this
|
||||
// scene's stand-in for whatever will eventually detect a hit and publish AbsorbEvent.
|
||||
|
|
@ -99,12 +108,16 @@ public class FieldTester : MonoBehaviour
|
|||
var click = pickup.GetComponent<ClickableCollider>();
|
||||
click.OnClick += (collider, data, ray, hit) =>
|
||||
{
|
||||
if (!TestField.CanGrab) return;
|
||||
if (_grabbing) return;
|
||||
click.Clear();
|
||||
var p = _pickups[i1];
|
||||
_capabilityEvents.PublishGrab(new GrabEvent(new FloatingBodyId((uint) i1), GrabPhase.Started, p.transform.position.ToCultMath(), 0));
|
||||
p.transform = null;
|
||||
_pickups[i1] = p;
|
||||
_grabbing = true;
|
||||
_grabIndex = i1;
|
||||
_grabElapsed = 0;
|
||||
_envelopPublished = _pullPublished = false;
|
||||
_grabStart = _pickups[i1].transform.position.ToCultMath();
|
||||
var id = new FloatingBodyId((uint) i1);
|
||||
_capabilityEvents.PublishGrab(new GrabEvent(id, GrabPhase.Started, _grabStart, 0));
|
||||
_capabilityEvents.PublishGrab(new GrabEvent(id, GrabPhase.Extend, _grabStart, GrabExtendTime));
|
||||
};
|
||||
|
||||
pickup.ScanLabelContainer.gameObject.SetActive(false);
|
||||
|
|
@ -117,6 +130,40 @@ public class FieldTester : MonoBehaviour
|
|||
}
|
||||
}
|
||||
|
||||
// Publishes Envelop and Pull once each at their cumulative boundaries, holds the pickup still until the pull and
|
||||
// then moves it along the same smoothstep as Zone's held pose, and ends with Completed and the pickup's removal.
|
||||
private void AdvanceGrab()
|
||||
{
|
||||
_grabElapsed += Time.deltaTime;
|
||||
var id = new FloatingBodyId((uint) _grabIndex);
|
||||
var pullStart = GrabExtendTime + GrabEnvelopTime;
|
||||
if (!_envelopPublished && _grabElapsed >= GrabExtendTime)
|
||||
{
|
||||
_envelopPublished = true;
|
||||
_capabilityEvents.PublishGrab(new GrabEvent(id, GrabPhase.Envelop, _grabStart, GrabEnvelopTime));
|
||||
}
|
||||
if (!_pullPublished && _grabElapsed >= pullStart)
|
||||
{
|
||||
_pullPublished = true;
|
||||
_capabilityEvents.PublishGrab(new GrabEvent(id, GrabPhase.Pull, _grabStart, GrabPullTime));
|
||||
}
|
||||
var pickup = _pickups[_grabIndex].transform;
|
||||
if (_pullPublished)
|
||||
{
|
||||
var progress = saturate((_grabElapsed - pullStart) / max(GrabPullTime, 1e-4f));
|
||||
pickup.position = lerp(_grabStart, TestField.transform.position.ToCultMath(), smoothstep(0, 1, progress)).ToUnity();
|
||||
if (progress >= 1)
|
||||
{
|
||||
_capabilityEvents.PublishGrab(new GrabEvent(id, GrabPhase.Completed, pickup.position.ToCultMath(), 0));
|
||||
Destroy(pickup.gameObject);
|
||||
var p = _pickups[_grabIndex];
|
||||
p.transform = null;
|
||||
_pickups[_grabIndex] = p;
|
||||
_grabbing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
var move = _forceThrust ? float2(0,1) : _input.Player.Move.ReadValue<Vector2>().ToCultMath();
|
||||
|
|
@ -144,10 +191,12 @@ public class FieldTester : MonoBehaviour
|
|||
|
||||
_capabilityEvents.PublishThrust(new ThrustEvent(_push, min(length(_push), 1), float2(_frontTwist, _rearTwist)));
|
||||
|
||||
if (_grabbing) AdvanceGrab();
|
||||
|
||||
for (var i = 0; i < _pickups.Count; i++)
|
||||
{
|
||||
var (l, time, t) = _pickups[i];
|
||||
if (t == null) continue;
|
||||
if (t == null || (_grabbing && i == _grabIndex)) continue;
|
||||
time = frac(time + Time.deltaTime / lerp(PickupTravelTimeMin, PickupTravelTimeMax, l));
|
||||
t.localScale = Vector3.one * lerp(PickupSizeMin, PickupSizeMax, l) * Zone.PowerPulse(time - .5f, ScaleExponent);
|
||||
t.position = new Vector3(t.position.x, t.position.y, (time - .5f) * PickupTravelDistance);
|
||||
|
|
|
|||
Loading…
Reference in a new issue