Aetheria/Assets/Scripts/Gameplay/Physics/AetheriaYmirPhysicsBridge.cs
2026-06-21 09:37:54 +01:00

865 lines
28 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using UnityEngine;
public sealed class AetheriaYmirPhysicsBridge : MonoBehaviour
{
private const string ProjectileBodyId = "aetheria.projectile";
private const string TargetBodyPrefix = "aetheria.target.";
private static AetheriaYmirPhysicsBridge _instance;
public string ServiceUrl = "http://127.0.0.1:8877/simulate/step";
public string OverlapSphereUrl = "http://127.0.0.1:8877/query/overlap-sphere";
public string OverlapCircleUrl = "http://127.0.0.1:8877/query/overlap-circle";
public string CastCircleUrl = "http://127.0.0.1:8877/query/cast-circle";
public string CastSphereUrl = "http://127.0.0.1:8877/query/cast-sphere";
public bool EnableProjectileCutover = true;
public float ProjectileRadius = 0.1f;
public float ProjectileMass = 1.0f;
public float TargetRadiusPadding = 0.1f;
public int TimeoutMilliseconds = 20;
public AetheriaDaemonObserver DaemonObserver;
private readonly List<YmirPhysicsBody> _projectileBodies = new List<YmirPhysicsBody>();
private readonly List<YmirPhysicsBody> _targetBodies = new List<YmirPhysicsBody>();
private readonly List<YmirPhysicsBody> _zoneBodies = new List<YmirPhysicsBody>();
private readonly List<YmirPhysicsBody> _daemonBodies = new List<YmirPhysicsBody>();
private readonly List<YmirSphereQueryBody> _clickableBodies = new List<YmirSphereQueryBody>();
private readonly Dictionary<string, HullCollider> _hullBodyMap = new Dictionary<string, HullCollider>(StringComparer.Ordinal);
private readonly Dictionary<string, ClickableCollider> _clickableBodyMap = new Dictionary<string, ClickableCollider>(StringComparer.Ordinal);
public static AetheriaYmirPhysicsBridge Instance
{
get
{
if (_instance != null) return _instance;
_instance = FindAnyObjectByType<AetheriaYmirPhysicsBridge>();
if (_instance != null) return _instance;
var bridge = new GameObject("Aetheria Ymir Physics Bridge");
DontDestroyOnLoad(bridge);
_instance = bridge.AddComponent<AetheriaYmirPhysicsBridge>();
return _instance;
}
}
public bool TryStepProjectile(Projectile projectile, float deltaTime, out AetheriaYmirProjectileStep step)
{
step = default;
if (!EnableProjectileCutover || projectile == null || deltaTime <= 0)
return false;
var request = BuildProjectileRequest(projectile, deltaTime);
if (request.world.bodies.Length == 0)
return false;
YmirStepResult result;
try
{
result = PostJson<YmirStepRequest, YmirStepResult>(ServiceUrl, request);
}
catch (Exception error)
{
Debug.LogWarning($"Ymir projectile step failed: {error.Message}");
return false;
}
if (result == null || result.world == null)
return false;
var projectileBody = FindBody(result.world, ProjectileBodyId);
if (projectileBody == null)
return false;
step.Position = new Vector3(projectileBody.position.x, projectile.transform.position.y, projectileBody.position.y);
step.Velocity = new Vector3(projectileBody.velocity.x, projectile.Velocity.y, projectileBody.velocity.y);
foreach (var contact in result.contacts ?? Array.Empty<YmirContactEvent>())
{
var otherBody = string.Equals(contact.bodyA, ProjectileBodyId, StringComparison.Ordinal)
? contact.bodyB
: string.Equals(contact.bodyB, ProjectileBodyId, StringComparison.Ordinal)
? contact.bodyA
: "";
if (string.IsNullOrWhiteSpace(otherBody))
continue;
var hull = ResolveTargetHull(projectile.TargetInstance, otherBody);
if (hull == null)
continue;
step.Hit = new AetheriaYmirProjectileHit(
hull,
new Vector3(contact.point.x, projectile.transform.position.y, contact.point.y),
new Vector3(contact.normal.x, 0, contact.normal.y).normalized);
break;
}
return true;
}
public bool TryOverlapDaemonBodies(
Vector3 center,
float radius,
out IReadOnlyList<YmirCircleOverlapHit> hits)
{
hits = Array.Empty<YmirCircleOverlapHit>();
if (!TryBuildDaemonWorld(out var world) || radius <= 0)
return false;
var request = new YmirCircleOverlapRequest
{
world = world,
center = ToVec2(center),
radius = radius
};
return TryOverlapCircle(request, out hits);
}
public bool TryCastDaemonBodies(
Vector3 origin,
Vector3 direction,
float distance,
float radius,
out IReadOnlyList<YmirCircleCastHit> hits)
{
hits = Array.Empty<YmirCircleCastHit>();
if (!TryBuildDaemonWorld(out var world) || distance <= 0 || radius < 0)
return false;
var planarDirection = new Vector3(direction.x, 0, direction.z);
if (planarDirection.sqrMagnitude <= float.Epsilon)
return false;
var request = new YmirCircleCastRequest
{
world = world,
origin = ToVec2(origin),
direction = ToVec2(planarDirection.normalized),
distance = distance,
radius = radius
};
return TryCastCircle(request, "Ymir daemon body circle cast query", out hits);
}
public bool TryOverlapTargetHulls(
EntityInstance target,
Vector3 center,
float radius,
out IReadOnlyList<AetheriaYmirOverlapHit> hits)
{
hits = Array.Empty<AetheriaYmirOverlapHit>();
if (!EnableProjectileCutover || target == null || radius <= 0)
return false;
var world = BuildTargetWorld(target);
if (world.bodies.Length == 0)
return false;
var request = new YmirCircleOverlapRequest
{
center = ToVec2(center),
radius = radius,
world = world
};
if (!TryOverlapCircle(request, out var ymirHits))
return false;
var resolved = new List<AetheriaYmirOverlapHit>();
foreach (var hit in ymirHits)
{
var hull = ResolveTargetHull(target, hit.bodyId);
if (hull == null)
continue;
resolved.Add(new AetheriaYmirOverlapHit(
hull,
new Vector3(hit.point.x, center.y, hit.point.y),
new Vector3(hit.normal.x, 0, hit.normal.y).normalized,
hit.penetration,
hit.distance));
}
hits = resolved;
return true;
}
public bool TryOverlapSphere(
IReadOnlyList<YmirSphereQueryBody> bodies,
Vector3 center,
float radius,
out IReadOnlyList<YmirSphereOverlapHit> hits)
{
hits = Array.Empty<YmirSphereOverlapHit>();
if (bodies == null || bodies.Count == 0)
return false;
var bodyArray = bodies as YmirSphereQueryBody[] ?? CopySphereQueryBodies(bodies);
return TryOverlapSphere(bodyArray, center, radius, "Ymir sphere overlap query", out hits);
}
public bool TryCastTargetHulls(
EntityInstance target,
Vector3 origin,
Vector3 direction,
float distance,
float radius,
out IReadOnlyList<AetheriaYmirCastHit> hits)
{
hits = Array.Empty<AetheriaYmirCastHit>();
if (!EnableProjectileCutover || target == null || distance <= 0 || direction.sqrMagnitude <= 0.000001f)
return false;
var world = BuildTargetWorld(target);
if (world.bodies.Length == 0)
return false;
var request = new YmirCircleCastRequest
{
origin = ToVec2(origin),
direction = ToVec2(direction.normalized),
distance = distance,
radius = Mathf.Max(0, radius),
world = world
};
if (!TryCastCircle(request, "Ymir target cast query", out var ymirHits))
return false;
var resolved = new List<AetheriaYmirCastHit>();
foreach (var hit in ymirHits)
{
var hull = ResolveTargetHull(target, hit.bodyId);
if (hull == null)
continue;
resolved.Add(new AetheriaYmirCastHit(
hull,
ResolveShield(hull),
new Vector3(hit.point.x, origin.y, hit.point.y),
new Vector3(hit.normal.x, 0, hit.normal.y).normalized,
hit.distance));
}
hits = resolved;
return true;
}
public bool TryCastClickables(
IReadOnlyList<ClickableCollider> clickables,
Ray ray,
float distance,
out AetheriaYmirClickableHit hit)
{
hit = default;
if (!EnableProjectileCutover || clickables == null || clickables.Count == 0 || distance <= 0)
return false;
_clickableBodyMap.Clear();
_clickableBodies.Clear();
for (var i = 0; i < clickables.Count; i++)
{
var clickable = clickables[i];
if (clickable == null || !clickable.isActiveAndEnabled)
continue;
var bounds = clickable.ClickBounds;
var bodyId = $"aetheria.clickable.{i}";
_clickableBodyMap[bodyId] = clickable;
_clickableBodies.Add(new YmirSphereQueryBody
{
id = bodyId,
position = ToVec3(bounds.center),
radius = Mathf.Max(0.001f, bounds.extents.magnitude)
});
}
if (_clickableBodies.Count == 0)
return false;
var bodyArray = _clickableBodies.ToArray();
var request = new YmirSphereCastRequest
{
origin = ToVec3(ray.origin),
direction = ToVec3(ray.direction.normalized),
distance = distance,
radius = 0,
bodies = bodyArray
};
if (!TryCastSphere(request, "Ymir clickable cast query", out var ymirHits))
return false;
foreach (var resultHit in ymirHits)
{
if (!_clickableBodyMap.TryGetValue(resultHit.bodyId, out var clickable) || clickable == null)
continue;
hit = new AetheriaYmirClickableHit(
clickable,
ToVector3(resultHit.point),
ToVector3(resultHit.normal).normalized,
resultHit.distance);
return true;
}
return false;
}
public bool TryOverlapZoneHulls(
ZoneRenderer zoneRenderer,
Entity sourceEntity,
Vector3 center,
float radius,
out IReadOnlyList<AetheriaYmirOverlapHit> hits)
{
hits = Array.Empty<AetheriaYmirOverlapHit>();
if (!EnableProjectileCutover || zoneRenderer == null || radius <= 0)
return false;
_hullBodyMap.Clear();
var world = BuildZoneWorld(zoneRenderer, sourceEntity, _hullBodyMap);
if (world.bodies.Length == 0)
return false;
var request = new YmirCircleOverlapRequest
{
center = ToVec2(center),
radius = radius,
world = world
};
if (!TryOverlapCircle(request, out var ymirHits))
return false;
var resolved = new List<AetheriaYmirOverlapHit>();
foreach (var hit in ymirHits)
{
if (!_hullBodyMap.TryGetValue(hit.bodyId ?? "", out var hull))
continue;
resolved.Add(new AetheriaYmirOverlapHit(
hull,
new Vector3(hit.point.x, center.y, hit.point.y),
new Vector3(hit.normal.x, 0, hit.normal.y).normalized,
hit.penetration,
hit.distance));
}
hits = resolved;
return true;
}
public bool TryCastZoneHulls(
ZoneRenderer zoneRenderer,
Entity sourceEntity,
Vector3 origin,
Vector3 direction,
float distance,
float radius,
out IReadOnlyList<AetheriaYmirCastHit> hits)
{
hits = Array.Empty<AetheriaYmirCastHit>();
if (!EnableProjectileCutover || zoneRenderer == null || distance <= 0 || direction.sqrMagnitude <= 0.000001f)
return false;
_hullBodyMap.Clear();
var world = BuildZoneWorld(zoneRenderer, sourceEntity, _hullBodyMap);
if (world.bodies.Length == 0)
return false;
var request = new YmirCircleCastRequest
{
origin = ToVec2(origin),
direction = ToVec2(direction.normalized),
distance = distance,
radius = Mathf.Max(0, radius),
world = world
};
if (!TryCastCircle(request, "Ymir zone cast query", out var ymirHits))
return false;
var resolved = new List<AetheriaYmirCastHit>();
foreach (var hit in ymirHits)
{
if (!_hullBodyMap.TryGetValue(hit.bodyId ?? "", out var hull))
continue;
resolved.Add(new AetheriaYmirCastHit(
hull,
ResolveShield(hull),
new Vector3(hit.point.x, origin.y, hit.point.y),
new Vector3(hit.normal.x, 0, hit.normal.y).normalized,
hit.distance));
}
hits = resolved;
return true;
}
private YmirStepRequest BuildProjectileRequest(Projectile projectile, float deltaTime)
{
var transform = projectile.transform;
_projectileBodies.Clear();
_projectileBodies.Add(new YmirPhysicsBody
{
id = ProjectileBodyId,
position = ToVec2(transform.position),
velocity = ToVec2(projectile.Velocity),
direction = ToVec2(transform.forward),
angularVelocity = 0,
torque = 0,
momentOfInertia = 1,
radius = Mathf.Max(ProjectileRadius, 0.001f),
mass = Mathf.Max(ProjectileMass, 0.001f),
isStatic = false,
restitution = 0
});
var target = projectile.TargetInstance;
if (target == null)
{
return new YmirStepRequest
{
deltaTime = deltaTime,
world = new YmirWorld
{
time = projectile.YmirWorldTime,
bodies = _projectileBodies.ToArray(),
fields = Array.Empty<YmirRadialField>()
}
};
}
_projectileBodies.AddRange(BuildTargetWorld(target).bodies);
return new YmirStepRequest
{
deltaTime = deltaTime,
world = new YmirWorld
{
time = projectile.YmirWorldTime,
bodies = _projectileBodies.ToArray(),
fields = Array.Empty<YmirRadialField>()
}
};
}
private YmirWorld BuildTargetWorld(EntityInstance target)
{
_targetBodies.Clear();
for (var i = 0; i < target.HullColliders.Length; i++)
{
var hull = target.HullColliders[i];
if (hull == null)
continue;
var bounds = hull.YmirBounds;
_targetBodies.Add(new YmirPhysicsBody
{
id = TargetBodyPrefix + i,
position = ToVec2(bounds.center),
velocity = default,
direction = ToVec2(hull.transform.forward),
angularVelocity = 0,
torque = 0,
momentOfInertia = 1,
radius = hull.YmirPlanarRadius + TargetRadiusPadding,
mass = 1,
isStatic = true,
restitution = 0
});
}
return new YmirWorld
{
time = 0,
bodies = _targetBodies.ToArray(),
fields = Array.Empty<YmirRadialField>()
};
}
private YmirWorld BuildZoneWorld(
ZoneRenderer zoneRenderer,
Entity sourceEntity,
Dictionary<string, HullCollider> bodyMap)
{
_zoneBodies.Clear();
var entityIndex = 0;
foreach (var entityInstance in zoneRenderer.EntityInstances.Values)
{
if (entityInstance == null || entityInstance.Entity == sourceEntity)
continue;
for (var hullIndex = 0; hullIndex < entityInstance.HullColliders.Length; hullIndex++)
{
var hull = entityInstance.HullColliders[hullIndex];
if (hull == null)
continue;
var bounds = hull.YmirBounds;
var bodyId = $"aetheria.entity.{entityIndex}.hull.{hullIndex}";
bodyMap[bodyId] = hull;
_zoneBodies.Add(new YmirPhysicsBody
{
id = bodyId,
position = ToVec2(bounds.center),
velocity = default,
direction = ToVec2(hull.transform.forward),
angularVelocity = 0,
torque = 0,
momentOfInertia = 1,
radius = hull.YmirPlanarRadius + TargetRadiusPadding,
mass = 1,
isStatic = true,
restitution = 0
});
}
entityIndex++;
}
return new YmirWorld
{
time = 0,
bodies = _zoneBodies.ToArray(),
fields = Array.Empty<YmirRadialField>()
};
}
private bool TryBuildDaemonWorld(out YmirWorld world)
{
world = null;
var observer = ResolveDaemonObserver();
if (observer == null || !observer.HasRenderNativeView)
return false;
var view = observer.LastRenderNativeView;
if (!view.IsCreated || !view.HasPhysicsRadius)
return false;
_daemonBodies.Clear();
for (var i = 0; i < view.Count; i++)
{
var radius = view.PhysicsBodyRadius[i];
if (radius <= 0)
continue;
var inverseMass = view.HasPhysicsInverseMass ? view.PhysicsBodyInverseMass[i] : 1.0f;
var mass = view.HasPhysicsMass && view.PhysicsBodyMass[i] > 0
? view.PhysicsBodyMass[i]
: inverseMass > 0
? 1.0f / inverseMass
: 1.0f;
var rotation = view.HasRotation ? view.RotationRadians[i] : 0;
_daemonBodies.Add(new YmirPhysicsBody
{
id = $"aetheria.daemon.body.{i}",
position = new YmirVec2 { x = view.PositionX[i], y = view.PositionZ[i] },
velocity = view.HasVelocity
? new YmirVec2 { x = view.VelocityX[i], y = view.VelocityZ[i] }
: default,
direction = new YmirVec2 { x = Mathf.Sin(rotation), y = Mathf.Cos(rotation) },
angularVelocity = 0,
torque = 0,
momentOfInertia = 1,
radius = radius,
mass = Mathf.Max(mass, 0.001f),
isStatic = view.HasPhysicsInverseMass && inverseMass <= 0,
restitution = 0
});
}
if (_daemonBodies.Count == 0)
return false;
world = new YmirWorld
{
time = Time.time,
bodies = _daemonBodies.ToArray(),
fields = Array.Empty<YmirRadialField>()
};
return true;
}
private AetheriaDaemonObserver ResolveDaemonObserver()
{
if (DaemonObserver != null)
return DaemonObserver;
DaemonObserver = FindAnyObjectByType<AetheriaDaemonObserver>();
return DaemonObserver;
}
private bool TryOverlapSphere(
YmirSphereQueryBody[] bodies,
Vector3 center,
float radius,
string failureContext,
out IReadOnlyList<YmirSphereOverlapHit> hits)
{
hits = Array.Empty<YmirSphereOverlapHit>();
if (!EnableProjectileCutover || bodies == null || bodies.Length == 0 || radius <= 0)
return false;
try
{
var result = PostJson<YmirSphereOverlapRequest, YmirSphereOverlapResult>(OverlapSphereUrl, new YmirSphereOverlapRequest
{
center = ToVec3(center),
radius = radius,
bodies = bodies
});
hits = result.hits ?? Array.Empty<YmirSphereOverlapHit>();
return true;
}
catch (Exception error)
{
Debug.LogWarning($"{failureContext} failed: {error.Message}");
return false;
}
}
private bool TryCastSphere(
YmirSphereCastRequest request,
string failureContext,
out IReadOnlyList<YmirSphereCastHit> hits)
{
hits = Array.Empty<YmirSphereCastHit>();
if (!EnableProjectileCutover || request == null || request.bodies == null || request.bodies.Length == 0 || request.distance <= 0 || request.radius < 0)
return false;
try
{
var result = PostJson<YmirSphereCastRequest, YmirSphereCastResult>(CastSphereUrl, request);
hits = result.hits ?? Array.Empty<YmirSphereCastHit>();
return true;
}
catch (Exception error)
{
Debug.LogWarning($"{failureContext} failed: {error.Message}");
return false;
}
}
private bool TryOverlapCircle(
YmirCircleOverlapRequest request,
out IReadOnlyList<YmirCircleOverlapHit> hits)
{
hits = Array.Empty<YmirCircleOverlapHit>();
if (!EnableProjectileCutover || request == null || request.world == null || request.world.bodies == null || request.world.bodies.Length == 0 || request.radius <= 0)
return false;
if (string.IsNullOrWhiteSpace(OverlapCircleUrl))
{
Debug.LogWarning("Ymir circle overlap query failed: daemon URL is not configured.");
return false;
}
try
{
var result = PostJson<YmirCircleOverlapRequest, YmirCircleOverlapResult>(OverlapCircleUrl, request);
hits = result.hits ?? Array.Empty<YmirCircleOverlapHit>();
return true;
}
catch (Exception error)
{
Debug.LogWarning($"Ymir circle overlap query failed: {error.Message}");
return false;
}
}
private bool TryCastCircle(
YmirCircleCastRequest request,
string failureContext,
out IReadOnlyList<YmirCircleCastHit> hits)
{
hits = Array.Empty<YmirCircleCastHit>();
if (!EnableProjectileCutover || request == null || request.world == null || request.world.bodies == null || request.world.bodies.Length == 0 || request.distance <= 0 || request.radius < 0)
return false;
try
{
var result = PostJson<YmirCircleCastRequest, YmirCircleCastResult>(CastCircleUrl, request);
hits = result.hits ?? Array.Empty<YmirCircleCastHit>();
return true;
}
catch (Exception error)
{
Debug.LogWarning($"{failureContext} failed: {error.Message}");
return false;
}
}
private static YmirSphereQueryBody[] CopySphereQueryBodies(IReadOnlyList<YmirSphereQueryBody> bodies)
{
var copy = new YmirSphereQueryBody[bodies.Count];
for (var i = 0; i < copy.Length; i++)
copy[i] = bodies[i];
return copy;
}
private TResult PostJson<TRequest, TResult>(string url, TRequest request)
where TResult : class
{
if (string.IsNullOrWhiteSpace(url))
throw new InvalidOperationException("Ymir daemon URL is not configured.");
var json = JsonUtility.ToJson(request);
var bytes = Encoding.UTF8.GetBytes(json);
var http = (HttpWebRequest)WebRequest.Create(url);
http.Method = "POST";
http.ContentType = "application/json; charset=utf-8";
http.ContentLength = bytes.Length;
http.Timeout = TimeoutMilliseconds;
http.ReadWriteTimeout = TimeoutMilliseconds;
using (var stream = http.GetRequestStream())
stream.Write(bytes, 0, bytes.Length);
using var response = (HttpWebResponse)http.GetResponse();
using var reader = new StreamReader(response.GetResponseStream());
var payload = reader.ReadToEnd();
if (string.IsNullOrWhiteSpace(payload))
throw new InvalidDataException("Ymir daemon returned an empty response.");
var result = JsonUtility.FromJson<TResult>(payload);
if (result == null)
throw new InvalidDataException("Ymir daemon returned a response that could not be parsed.");
return result;
}
private static YmirPhysicsBody FindBody(YmirWorld world, string bodyId)
{
foreach (var body in world.bodies ?? Array.Empty<YmirPhysicsBody>())
{
if (string.Equals(body.id, bodyId, StringComparison.Ordinal))
return body;
}
return null;
}
private static HullCollider ResolveTargetHull(EntityInstance target, string bodyId)
{
if (!bodyId.StartsWith(TargetBodyPrefix, StringComparison.Ordinal))
return null;
return int.TryParse(bodyId.Substring(TargetBodyPrefix.Length), out var index) &&
index >= 0 &&
index < target.HullColliders.Length
? target.HullColliders[index]
: null;
}
private static ShieldManager ResolveShield(HullCollider hull)
{
var instance = hull != null ? hull.GetComponentInParent<EntityInstance>() : null;
return instance != null ? instance.Shield : null;
}
private static YmirVec2 ToVec2(Vector3 value) => new YmirVec2 { x = value.x, y = value.z };
private static YmirVec2 ToVec2(CultMath.float2 value) => new YmirVec2 { x = value.x, y = value.y };
private static YmirVec3 ToVec3(Vector3 value) => new YmirVec3 { x = value.x, y = value.y, z = value.z };
private static Vector3 ToVector3(YmirVec3 value) => new Vector3(value.x, value.y, value.z);
private static Bounds ObjectBounds(GameObject gameObject, Vector3 fallbackCenter)
{
var renderers = gameObject.GetComponentsInChildren<Renderer>();
if (renderers.Length == 0)
return new Bounds(fallbackCenter, Vector3.one);
var bounds = renderers[0].bounds;
for (var i = 1; i < renderers.Length; i++)
bounds.Encapsulate(renderers[i].bounds);
return bounds;
}
}
public struct AetheriaYmirProjectileStep
{
public Vector3 Position;
public Vector3 Velocity;
public AetheriaYmirProjectileHit Hit;
public bool HasHit => Hit.Hull != null;
}
public readonly struct AetheriaYmirProjectileHit
{
public AetheriaYmirProjectileHit(HullCollider hull, Vector3 point, Vector3 normal)
{
Hull = hull;
Point = point;
Normal = normal;
}
public HullCollider Hull { get; }
public Vector3 Point { get; }
public Vector3 Normal { get; }
}
public readonly struct AetheriaYmirOverlapHit
{
public AetheriaYmirOverlapHit(HullCollider hull, Vector3 point, Vector3 normal, float penetration, float distance)
{
Hull = hull;
Point = point;
Normal = normal;
Penetration = penetration;
Distance = distance;
}
public HullCollider Hull { get; }
public Vector3 Point { get; }
public Vector3 Normal { get; }
public float Penetration { get; }
public float Distance { get; }
}
public readonly struct AetheriaYmirCastHit
{
public AetheriaYmirCastHit(HullCollider hull, ShieldManager shield, Vector3 point, Vector3 normal, float distance)
{
Hull = hull;
Shield = shield;
Point = point;
Normal = normal;
Distance = distance;
}
public HullCollider Hull { get; }
public ShieldManager Shield { get; }
public Vector3 Point { get; }
public Vector3 Normal { get; }
public float Distance { get; }
}
public readonly struct AetheriaYmirClickableHit
{
public AetheriaYmirClickableHit(ClickableCollider clickable, Vector3 point, Vector3 normal, float distance)
{
Clickable = clickable;
Point = point;
Normal = normal;
Distance = distance;
}
public ClickableCollider Clickable { get; }
public Vector3 Point { get; }
public Vector3 Normal { get; }
public float Distance { get; }
}