Aetheria/Assets/Scripts/Gameplay/Physics/AetheriaYmirPhysicsBridge.cs
2026-07-04 22:46:04 +01:00

789 lines
26 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine;
public sealed class AetheriaYmirPhysicsBridge : MonoBehaviour
{
private const string ProjectileBodyId = "aetheria.projectile";
private const string DaemonEntityBodyPrefix = "aetheria.daemon.entity.";
private static AetheriaYmirPhysicsBridge _instance;
public bool EnableProjectileCutover = true;
public float ProjectileRadius = 0.1f;
public float ProjectileMass = 1.0f;
public AetheriaDaemonObserver DaemonObserver;
private readonly List<YmirPhysicsBody> _projectileBodies = 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 = YmirPhysicsQueries.Step(request);
}
catch (Exception error)
{
Debug.LogWarning($"Ymir projectile typed 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;
if (!TryResolveTargetDaemonHull(projectile.TargetInstance, otherBody, out var hull))
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(null, null, 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(null, null, 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;
if (!TryBuildDaemonWorld(null, TargetDaemonEntityIndex(target), out var world))
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 (!TryResolveTargetDaemonHull(target, 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 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;
if (!TryBuildDaemonWorld(null, TargetDaemonEntityIndex(target), out var world))
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 daemon target cast query", out var ymirHits))
return false;
var resolved = new List<AetheriaYmirCastHit>();
foreach (var hit in ymirHits)
{
if (!TryResolveTargetDaemonHull(target, 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;
}
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;
if (!TryBuildDaemonWorld(sourceEntity, null, out var world))
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 (!TryResolveDaemonEntityHull(zoneRenderer, 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;
if (!TryBuildDaemonWorld(sourceEntity, null, out var world))
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 daemon zone cast query", out var ymirHits))
return false;
var resolved = new List<AetheriaYmirCastHit>();
foreach (var hit in ymirHits)
{
if (!TryResolveDaemonEntityHull(zoneRenderer, 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 targetDaemonEntityIndex = TargetDaemonEntityIndex(projectile.TargetInstance);
if (targetDaemonEntityIndex < 0 ||
!TryBuildDaemonWorld(projectile.SourceEntity, targetDaemonEntityIndex, out var targetWorld))
{
return new YmirStepRequest
{
deltaTime = deltaTime,
world = new YmirWorld
{
time = projectile.YmirWorldTime,
bodies = _projectileBodies.ToArray(),
fields = Array.Empty<YmirRadialField>()
}
};
}
_projectileBodies.AddRange(targetWorld.bodies);
return new YmirStepRequest
{
deltaTime = deltaTime,
world = new YmirWorld
{
time = projectile.YmirWorldTime,
bodies = _projectileBodies.ToArray(),
fields = Array.Empty<YmirRadialField>()
}
};
}
private bool TryBuildDaemonWorld(Entity sourceEntity, int? onlyDaemonEntityIndex, out YmirWorld world)
{
world = null;
var observer = ResolveDaemonObserver();
if (observer == null || !observer.HasRenderNativeView)
return false;
var view = observer.LastRenderNativeView;
if (!view.IsCreated || !view.HasEntityIndex || !view.HasPhysicsRadius)
return false;
_daemonBodies.Clear();
for (var i = 0; i < view.Count; i++)
{
var radius = view.PhysicsBodyRadius[i];
if (radius <= 0)
continue;
var daemonEntityIndex = view.EntityIndex[i];
if (sourceEntity != null && sourceEntity.DaemonEntityIndex == daemonEntityIndex)
continue;
if (onlyDaemonEntityIndex.HasValue && onlyDaemonEntityIndex.Value != daemonEntityIndex)
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;
var position = view.Position[i];
var velocity = view.HasVelocity ? view.Velocity[i] : default;
_daemonBodies.Add(new YmirPhysicsBody
{
id = DaemonEntityBodyPrefix + daemonEntityIndex,
position = new YmirVec2 { x = position.x, y = position.z },
velocity = view.HasVelocity
? new YmirVec2 { x = velocity.x, y = velocity.z }
: 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 static int TargetDaemonEntityIndex(EntityInstance target)
{
return target != null && target.Entity != null ? target.Entity.DaemonEntityIndex : -1;
}
private static bool TryResolveTargetDaemonHull(
EntityInstance target,
string bodyId,
out HullCollider hull)
{
hull = null;
if (target == null ||
target.HullColliders == null ||
!TryParseDaemonEntityBodyId(bodyId, out var daemonEntityIndex) ||
TargetDaemonEntityIndex(target) != daemonEntityIndex)
{
return false;
}
for (var i = 0; i < target.HullColliders.Length; i++)
{
hull = target.HullColliders[i];
if (hull != null)
return true;
}
hull = null;
return false;
}
private static bool TryResolveDaemonEntityHull(
ZoneRenderer zoneRenderer,
string bodyId,
out HullCollider hull)
{
hull = null;
if (zoneRenderer == null || !TryParseDaemonEntityBodyId(bodyId, out var daemonEntityIndex))
return false;
if (!zoneRenderer.TryGetEntityInstance(daemonEntityIndex, out var instance) ||
instance == null ||
instance.HullColliders == null)
{
return false;
}
for (var i = 0; i < instance.HullColliders.Length; i++)
{
hull = instance.HullColliders[i];
if (hull != null)
return true;
}
hull = null;
return false;
}
private static bool TryParseDaemonEntityBodyId(string bodyId, out int daemonEntityIndex)
{
daemonEntityIndex = -1;
return !string.IsNullOrWhiteSpace(bodyId) &&
bodyId.StartsWith(DaemonEntityBodyPrefix, StringComparison.Ordinal) &&
int.TryParse(
bodyId.Substring(DaemonEntityBodyPrefix.Length),
out daemonEntityIndex);
}
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 = YmirPhysicsQueries.OverlapSphere(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 = YmirPhysicsQueries.CastSphere(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;
try
{
var result = YmirPhysicsQueries.OverlapCircle(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 = YmirPhysicsQueries.CastCircle(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 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 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; }
}