Sensor Ping

Added ping feature to sensors, pulsing a wave of revealing light into the level
Added IEventBehavior interface, used by sensors and weapons, for behaviors containing event subscriptions which need to be reset when EntityInstance is disposed
Implemented sensor impact for locked weapons (lock speed varies with sensor acquisition)
Added ping cooldown bar to HUD
Added spline curve implementations to AetheriaMath, applied better mapping to wormhole transition animation
Tweaked loadout generator to prefer larger items to a controllable degree (e.g. very important for cargo bays, less for shop inventory), shop inventory bans non-ship hulls and manages internal space more effectively
This commit is contained in:
Reinier Vens 2021-03-21 01:55:25 +00:00
commit 645fcf4b09
62 changed files with 115821 additions and 79 deletions

View file

@ -112,4 +112,81 @@ public static class AetheriaMath
return sqrt(dx * dx + dy * dy);
}
//Returns a position between 4 float3s with Catmull-Rom spline algorithm
//http://www.iquilezles.org/www/articles/minispline/minispline.htm
public static float GetCatmullRomPosition(float p0, float p1, float p2, float p3, float t)
{
//The coefficients of the cubic polynomial (except the 0.5f * which I added later for performance)
var a = 2f * p1;
var b = p2 - p0;
var c = 2f * p0 - 5f * p1 + 4f * p2 - p3;
var d = -p0 + 3f * p1 - 3f * p2 + p3;
//The cubic polynomial: a + b * t + c * t^2 + d * t^3
var pos = 0.5f * (a + (b * t) + (c * t * t) + (d * t * t * t));
return pos;
}
public static float3 GetCatmullRomPosition(float3 p0, float3 p1, float3 p2, float3 p3, float t)
{
//The coefficients of the cubic polynomial (except the 0.5f * which I added later for performance)
var a = 2f * p1;
var b = p2 - p0;
var c = 2f * p0 - 5f * p1 + 4f * p2 - p3;
var d = -p0 + 3f * p1 - 3f * p2 + p3;
//The cubic polynomial: a + b * t + c * t^2 + d * t^3
var pos = 0.5f * (a + (b * t) + (c * t * t) + (d * t * t * t));
return pos;
}
// https://catlikecoding.com/unity/tutorials/curves-and-splines/
public static float GetQuadraticSplinePosition(float p0, float p1, float p2, float t)
{
t = saturate(t);
var oneMinusT = 1f - t;
return
oneMinusT * oneMinusT * p0 +
2f * oneMinusT * t * p1 +
t * t * p2;
}
public static float3 GetQuadraticSplinePosition(float3 p0, float3 p1, float3 p2, float t)
{
t = saturate(t);
var oneMinusT = 1f - t;
return
oneMinusT * oneMinusT * p0 +
2f * oneMinusT * t * p1 +
t * t * p2;
}
public static float3 GetCubicSplinePosition (float p0, float p1, float p2, float p3, float t) {
t = saturate(t);
var oneMinusT = 1f - t;
return
oneMinusT * oneMinusT * oneMinusT * p0 +
3f * oneMinusT * oneMinusT * t * p1 +
3f * oneMinusT * t * t * p2 +
t * t * t * p3;
}
public static float3 GetCubicSplinePosition (float3 p0, float3 p1, float3 p2, float3 p3, float t) {
t = saturate(t);
var oneMinusT = 1f - t;
return
oneMinusT * oneMinusT * oneMinusT * p0 +
3f * oneMinusT * oneMinusT * t * p1 +
3f * oneMinusT * t * t * p2 +
t * t * t * p3;
}
public static float Smoothstep(float x)
{
x = saturate(x);
return x * x * (3 - 2 * x);
}
}

View file

@ -35,6 +35,11 @@ public interface IDisposableBehavior
void Dispose();
}
public interface IEventBehavior
{
void ResetEvents();
}
public interface IInitializableBehavior
{
void Initialize();

View file

@ -51,7 +51,7 @@ public class ChargedWeaponData : InstantWeaponData
}
}
public class ChargedWeapon : InstantWeapon
public class ChargedWeapon : InstantWeapon, IEventBehavior
{
private ChargedWeaponData _data;
private bool _charging;
@ -79,6 +79,7 @@ public class ChargedWeapon : InstantWeapon
public override void ResetEvents()
{
base.ResetEvents();
OnStartCharging = null;
OnStopCharging = null;
OnCharged = null;

View file

@ -20,7 +20,7 @@ public class ConstantWeaponData : WeaponData
}
}
public class ConstantWeapon : Weapon, IProgressBehavior
public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior
{
private ConstantWeaponData _data;
private int _ammo = 1;
@ -49,7 +49,7 @@ public class ConstantWeapon : Weapon, IProgressBehavior
public event Action OnStartFiring;
public event Action OnStopFiring;
public override void ResetEvents()
public void ResetEvents()
{
OnReloadBegin = null;
OnReloadComplete = null;

View file

@ -27,7 +27,7 @@ public class InstantWeaponData : WeaponData
}
}
public class InstantWeapon : Weapon, IProgressBehavior
public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior
{
private InstantWeaponData _data;
@ -63,7 +63,7 @@ public class InstantWeapon : Weapon, IProgressBehavior
public event Action OnCooldownComplete;
public event Action OnFire;
public override void ResetEvents()
public virtual void ResetEvents()
{
OnReloadBegin = null;
OnReloadComplete = null;

View file

@ -81,7 +81,7 @@ public class LockWeapon : InstantWeapon
if (degrees < LockAngle)
{
var lerp = 1 - unlerp(0, 90, degrees);
_lock = saturate(_lock + pow(lerp, DirectionImpact) * delta * LockSpeed);
_lock = saturate(_lock + pow(lerp, DirectionImpact) * delta * LockSpeed * pow(Entity.EntityInfoGathered[Entity.Target.Value], SensorImpact));
}
else _lock = saturate(_lock - delta * Decay);
}

View file

@ -2,6 +2,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using System;
using System.Collections.Generic;
using MessagePack;
using Newtonsoft.Json;
using Unity.Mathematics;
@ -10,11 +12,32 @@ using static Unity.Mathematics.math;
[InspectableField, MessagePackObject, JsonObject(MemberSerialization.OptIn), RuntimeInspectable]
public class SensorData : BehaviorData
{
[InspectableField, JsonProperty("sensitivity"), Key(3), RuntimeInspectable]
[InspectableField, JsonProperty("sensitivity"), Key(3), RuntimeInspectable]
public PerformanceStat Sensitivity = new PerformanceStat();
[InspectableAnimationCurve, JsonProperty("sensitivityCurve"), Key(4), RuntimeInspectable]
[InspectableAnimationCurve, JsonProperty("sensitivityCurve"), Key(4), RuntimeInspectable]
public float4[] SensitivityCurve;
[InspectableField, JsonProperty("pingBoost"), Key(5), RuntimeInspectable]
public PerformanceStat PingBoost;
[InspectableField, JsonProperty("pingEnergy"), Key(6), RuntimeInspectable]
public PerformanceStat PingEnergy;
[InspectableField, JsonProperty("pingVisibility"), Key(7), RuntimeInspectable]
public PerformanceStat PingVisibility;
[InspectableField, JsonProperty("pingRange"), Key(8), RuntimeInspectable]
public PerformanceStat PingRange;
[InspectableField, JsonProperty("pingCooldown"), Key(9), RuntimeInspectable]
public PerformanceStat PingCooldown;
[InspectableField, JsonProperty("pingDuration"), Key(10)]
public float PingDuration = 2;
[InspectableField, JsonProperty("pingRadiusExponent"), Key(11)]
public float PingRadiusExponent = .5f;
public override IBehavior CreateInstance(ItemManager context, Entity entity, EquippedItem item)
{
@ -22,15 +45,55 @@ public class SensorData : BehaviorData
}
}
public class Sensor : IBehavior
public class Sensor : IBehavior, IEventBehavior
{
private SensorData _data;
private float _pingCooldown;
private float _pingLerp;
private bool _pinging;
private float _pingRadius;
private HashSet<Entity> _pingedEntities = new HashSet<Entity>();
private Entity Entity { get; }
private EquippedItem Item { get; }
private ItemManager Context { get; }
public BehaviorData Data => _data;
public float Cooldown
{
get => saturate(_pingCooldown);
}
public float PingRadius
{
get => _pingRadius;
}
public float PingBrightness => pow(1 - _pingLerp, _data.PingRadiusExponent);
public event Action OnPingStart;
public event Action OnPingEnd;
public void ResetEvents()
{
OnPingStart = null;
OnPingEnd = null;
}
public void Ping()
{
if(_pingCooldown < 0 && Item.Entity.TryConsumeEnergy(Item.Evaluate(_data.PingEnergy)))
{
Item.Entity.VisibilitySources[this] = Item.Evaluate(_data.PingVisibility);
_pinging = true;
_pingCooldown = 1;
_pingLerp = 0;
_pingRadius = 0;
_pingedEntities.Clear();
OnPingStart?.Invoke();
}
}
public Sensor(ItemManager context, SensorData data, Entity entity, EquippedItem item)
{
@ -42,6 +105,19 @@ public class Sensor : IBehavior
public bool Execute(float delta)
{
if (_pinging)
{
_pingLerp += delta / _data.PingDuration;
_pingRadius = lerp(0, Item.Evaluate(_data.PingRange), pow(_pingLerp, _data.PingRadiusExponent));
if (_pingLerp > 1)
{
_pinging = false;
OnPingEnd?.Invoke();
}
}
_pingCooldown -= delta / Item.Evaluate(_data.PingCooldown);
// TODO: Handle Active Detection / Visibility From Reflected Radiance
var hardpoint = Entity.Hardpoints[Item.Position.x, Item.Position.y];
var forward = hardpoint != null && Entity.HardpointTransforms.ContainsKey(hardpoint) ?
@ -54,35 +130,32 @@ public class Sensor : IBehavior
var diff = entity.Position.xz - Entity.Position.xz;
var angle = acos(dot(forward, normalize(diff)));
var dist = length(diff);
float previous;
float previous, next;
Entity.EntityInfoGathered.TryGetValue(entity, out previous);
var next = saturate(
previous +
entity.Visibility *
Item.Evaluate(_data.Sensitivity) *
_data.SensitivityCurve.Evaluate(angle / PI) *
delta / dist);
if (!_pingedEntities.Contains(entity) && dist < _pingRadius)
{
_pingedEntities.Add(entity);
next = saturate(
previous +
entity.Visibility *
Item.Evaluate(_data.Sensitivity) *
Item.Evaluate(_data.PingBoost) *
dist);
}
else
{
next = saturate(
previous +
entity.Visibility *
Item.Evaluate(_data.Sensitivity) *
_data.SensitivityCurve.Evaluate(angle / PI) *
delta / dist);
}
next *= 1 - Context.GameplaySettings.TargetInfoDecay * delta;
//Context.Log($"{entity.Name} visibility {(int)(previous * 100)}% -> {(int)(next * 100)}%");
Entity.EntityInfoGathered[entity] = next;
}
}
return true;
// var ship = Hardpoint.Ship.Ship.transform;
// var contacts =
// Physics.OverlapSphere(ship.position, _data.Range.Evaluate(Hardpoint)).Where(c=>c.attachedRigidbody?.GetComponent<Targetable>()!=null).Select(c=>c.attachedRigidbody.GetComponent<Targetable>());
// foreach (var contact in contacts)
// {
// var diff = (contact.transform.position - ship.position).Flatland();
// var angle = acos(dot(ship.forward.Flatland().normalized,
// diff.normalized)) / (float)PI;
// var sens = _data.Sensitivity.Evaluate(Hardpoint) *
// Hardpoint.Ship.HullData.VisibilityCurve.Evaluate(saturate(angle));
// var vis = contact.Visibility / diff.sqrMagnitude;
// if(vis > 1/sens)
// Hardpoint.Ship.Contacts[contact] = Time.time;
// }
//
// Hardpoint.Ship.VisibilitySources[this] = _data.Radiance.Evaluate(Hardpoint) / _data.RadianceMasking.Evaluate(Hardpoint);
}
}

View file

@ -109,8 +109,6 @@ public abstract class Weapon : IActivatedBehavior
_itemData = context.GetData(item.EquippableItem);
}
public virtual void ResetEvents(){}
protected virtual void UpdateStats()
{
Damage = Item.Evaluate(_data.Damage);

View file

@ -96,6 +96,7 @@ public abstract class Entity
public Shield Shield { get; private set; }
public Cockpit Cockpit { get; private set; }
public Sensor Sensor { get; private set; }
public float Heatstroke { get; private set; }
public float TargetRange { get; private set; }
@ -125,6 +126,7 @@ public abstract class Entity
public virtual void Activate()
{
//ItemManager.Log($"Entity {Name} is activating!");
_active = true;
Heatstroke = 0;
foreach (var item in Equipment)
@ -134,6 +136,7 @@ public abstract class Entity
initializableBehavior.Initialize();
}
//foreach(var entity in Zone.Entities) EntityInfoGathered.Add(entity, 0);
_subscriptions.Add(Zone.Entities.ObserveAdd().Subscribe(add => EntityInfoGathered.Add(add.Value, 0)));
_subscriptions.Add(Zone.Entities.ObserveRemove().Subscribe(remove =>
{
if (Target.Value == remove.Value) Target.Value = null;
@ -150,6 +153,7 @@ public abstract class Entity
public virtual void Deactivate()
{
//ItemManager.Log($"Entity {Name} is deactivating!");
foreach(var s in _subscriptions) s.Dispose();
_subscriptions.Clear();
_active = false;
@ -379,6 +383,8 @@ public abstract class Entity
Shield = null;
if (b is Cockpit)
Cockpit = null;
if (b is Sensor)
Sensor = null;
}
return item.EquippableItem;
@ -559,6 +565,8 @@ public abstract class Entity
Shield = shield;
if (b is Cockpit cockpit)
Cockpit = cockpit;
if (b is Sensor sensor)
Sensor = sensor;
}
// equippedItem.OnOnline += () => ItemOnline.OnNext(equippedItem);
@ -585,7 +593,7 @@ public abstract class Entity
bay.DockedShip = ship;
ship.SetParent(this);
Zone.Entities.Remove(ship);
Deactivate();
ship.Deactivate();
ship.Docked.OnNext(this);
}

View file

@ -58,7 +58,7 @@ public class LoadoutGenerator
var emptyShape = entity.UnoccupiedSpace;
var dockingBayData = RandomItem<DockingBayData>(item => item.Shape.FitsWithin(emptyShape, out _, out _));
var dockingBayData = RandomItem<DockingBayData>(2, item => item.Shape.FitsWithin(emptyShape, out _, out _));
if (dockingBayData == null) throw new InvalidLoadoutException("No compatible docking bay found for station!");
dockingBayData.Shape.FitsWithin(emptyShape, out var cargoRotation, out var cargoPosition);
@ -70,7 +70,9 @@ public class LoadoutGenerator
}
var cargo = entity.CargoBays.First();
var inventory = RandomItems<EquippableItemData>(16, data => !(data is CargoBayData));
var inventory = RandomItems<EquippableItemData>(16, 1,
data => !(data is HullData hull && hull.HullType != HullType.Ship) && !(data is CargoBayData))
.OrderByDescending(item=>item.Shape.Coordinates.Length);
foreach (var item in inventory)
{
var instance = ItemManager.CreateInstance(item);
@ -93,7 +95,7 @@ public class LoadoutGenerator
).FirstOrDefault();
}
public T[] RandomItems<T>(int count, Predicate<T> filter = null) where T : EquippableItemData
public T[] RandomItems<T>(int count, float sizeExponent, Predicate<T> filter = null) where T : EquippableItemData
{
return ItemManager.ItemData.GetAll<T>()
.Where(item => Sector.ContainsFaction(item.Manufacturer) &&
@ -101,21 +103,21 @@ public class LoadoutGenerator
(filter?.Invoke(item) ?? true))
.WeightedRandomElements(ref Random, item =>
(Faction == null || item.Manufacturer == Faction.ID ? 1 : Faction.Allegiance[item.Manufacturer]) * // Prioritize items from allied manufacturers
item.Shape.Coordinates.Length * item.Shape.Coordinates.Length / // Prioritize larger items
pow(item.Shape.Coordinates.Length, sizeExponent) / // Prioritize larger items
Zone.Distance[Sector.HomeZones[ItemManager.ItemData.Get<Faction>(item.Manufacturer)]] / // Penalize distance to manufacturer headquarters
pow(item.Price, PriceExponent), // Penalize item price to a controllable degree
count
);
}
public T RandomItem<T>(Predicate<T> filter = null) where T : EquippableItemData
public T RandomItem<T>(float sizeExponent, Predicate<T> filter = null) where T : EquippableItemData
{
return RandomItems(1, filter).FirstOrDefault();
return RandomItems(1, sizeExponent, filter).FirstOrDefault();
}
public T RandomItem<T>(HardpointData hardpoint, Predicate<T> filter = null) where T : EquippableItemData
public T RandomItem<T>(HardpointData hardpoint, float sizeExponent, Predicate<T> filter = null) where T : EquippableItemData
{
return RandomItem<T>(item => item.HardpointType == hardpoint.Type &&
return RandomItem<T>(sizeExponent, item => item.HardpointType == hardpoint.Type &&
(filter?.Invoke(item) ?? true) &&
item.Shape.FitsWithin(hardpoint.Shape, hardpoint.Rotation, out _));
}
@ -129,7 +131,7 @@ public class LoadoutGenerator
{
if (hardpoint.Type == HardpointType.ControlModule)
{
var controllerData = RandomItem<GearData>(hardpoint,
var controllerData = RandomItem<GearData>(hardpoint, 2,
item => item.Behaviors.Any(b => entity is Ship && b is CockpitData || entity is OrbitalEntity && b is TurretControllerData));
if (controllerData == null) throw new InvalidLoadoutException("No compatible controller found for entity!");
var controller = ItemManager.CreateInstance(controllerData) as EquippableItem;
@ -143,7 +145,7 @@ public class LoadoutGenerator
// If a previously selected item fits, use that one (this is why we must process larger hardpoints first)
var itemData = previousItems
.FirstOrDefault(i => i.HardpointType == hardpoint.Type && i.Shape.FitsWithin(hardpoint.Shape, hardpoint.Rotation, out _));
itemData ??= RandomItem<GearData>(hardpoint);
itemData ??= RandomItem<GearData>(hardpoint, 2);
if (itemData == null) ItemManager.Log($"No compatible item found for entity {Enum.GetName(typeof(HardpointType), hardpoint.Type)} hardpoint!");
else
{
@ -160,7 +162,7 @@ public class LoadoutGenerator
var emptyShape = entity.UnoccupiedSpace;
var cargoData = RandomItem<CargoBayData>(item =>
var cargoData = RandomItem<CargoBayData>(3, item =>
!(item is DockingBayData) &&
item.Shape.FitsWithin(emptyShape, out _, out _));
if (cargoData == null) throw new InvalidLoadoutException("No compatible cargo bay found for entity!");
@ -173,7 +175,7 @@ public class LoadoutGenerator
emptyShape = entity.UnoccupiedSpace;
var capacitorData = RandomItem<GearData>(item =>
var capacitorData = RandomItem<GearData>(2, item =>
item.Behaviors.Any(b => b is CapacitorData) &&
item.Shape.FitsWithin(emptyShape, out _, out _));
if (capacitorData == null) throw new InvalidLoadoutException("No compatible capacitor found for entity!");

View file

@ -300,7 +300,7 @@ public class Ship : Entity
{
var exitLerp = (_wormholeAnimationProgress - ItemManager.GameplaySettings.WormholeExitCurveStart) /
(1 - ItemManager.GameplaySettings.WormholeExitCurveStart);
exitLerp *= exitLerp; // Square the interpolation variable to produce curve with zero slope at start
exitLerp = AetheriaMath.Smoothstep(exitLerp); // Square the interpolation variable to produce curve with zero slope at start
Position.xz = _wormholePosition + normalize(_wormholeExitVelocity) * exitLerp * ItemManager.GameplaySettings.WormholeExitRadius;
Rotation = quaternion.LookRotation(
lerp(float3(0, 1, 0), forward, exitLerp),
@ -326,7 +326,7 @@ public class Ship : Entity
if (_wormholeAnimationProgress < 1 - ItemManager.GameplaySettings.WormholeExitCurveStart)
{
var enterLerp = _wormholeAnimationProgress / (1 - ItemManager.GameplaySettings.WormholeExitCurveStart);
enterLerp *= enterLerp; // Square the interpolation variable to produce curve with zero slope at vertical
enterLerp = AetheriaMath.Smoothstep(enterLerp); // Square the interpolation variable to produce curve with zero slope at vertical
Position.xz = lerp(_wormholeEntryPosition, _wormholePosition, enterLerp);
Rotation = quaternion.LookRotation(
lerp(forward, float3(0, -1, 0), enterLerp),