Harden generic authoritative aim lifecycle

This commit is contained in:
Meta Vens 2026-07-15 06:08:45 +01:00
commit 58d87c628a
9 changed files with 160 additions and 17 deletions

View file

@ -52,10 +52,15 @@ namespace GameCult.Eve.UnityScene
ViewDotPosition = controlled.transform.position + forward * distance;
EnsureVisuals();
var camera = Camera.main;
var right = camera == null ? Vector3.right : camera.transform.right;
var up = camera == null ? Vector3.up : camera.transform.up;
var center = ViewDotPosition + (camera == null ? Vector3.zero : -camera.transform.forward * 0.1f);
var camera = _host?.ActiveCameraTransform;
if (camera == null)
{
SetVisible(false);
return;
}
var right = camera.right;
var up = camera.up;
var center = ViewDotPosition - camera.forward * 0.1f;
Ring(_ring!, center, right, up, Current.ViewDotRadius);
Segment(_horizontal!, center, right, Current.ViewDotRadius * 1.6f);
Segment(_vertical!, center, up, Current.ViewDotRadius * 1.6f);

View file

@ -20,7 +20,8 @@ namespace GameCult.Eve.UnityScene
public sealed class EveUnityGameObjectPlayableWorldSceneSink :
IEveUnityPlayableWorldSceneSink,
IEveUnityEntityGenerationSink,
IEveUnityPresentedEntityRegistry
IEveUnityPresentedEntityRegistry,
IDisposable
{
private readonly Transform _root;
private readonly IEveUnityGameObjectAssetProvider _assetProvider;
@ -179,6 +180,17 @@ namespace GameCult.Eve.UnityScene
DestroyInstance(instance);
}
public void Dispose()
{
foreach (var instance in _instances.Values)
if (instance != null)
DestroyInstance(instance);
_instances.Clear();
_presentedById = new Dictionary<string, EveUnityPresentedEntityHandle>(StringComparer.Ordinal);
_presentedByIndex = new Dictionary<int, EveUnityPresentedEntityHandle>();
CurrentGeneration = null;
}
private GameObject InstantiateEntity(
EveUnityPlayableWorldEntity entity,
EveUnityPlayableWorldAssetBinding asset,

View file

@ -20,6 +20,7 @@ namespace GameCult.Eve.UnityScene
[SerializeField] private float yawDegrees = 35f;
[SerializeField] private float followDamping = 12f;
private IEveUnityCameraRenderPolicySource? _renderPolicySource;
private EveUnityPlayableWorldClientHost? _cameraHost;
private bool _ownsAmbientEnvironment;
private AmbientMode _previousAmbientMode;
private Color _previousAmbientLight;
@ -27,7 +28,12 @@ namespace GameCult.Eve.UnityScene
public EveUnityPlayableWorldClientHost? Host
{
get => host;
set => host = value;
set
{
if (ReferenceEquals(host, value)) return;
ReleaseCamera();
host = value;
}
}
public Transform? CameraTransform
@ -42,7 +48,11 @@ namespace GameCult.Eve.UnityScene
set => _renderPolicySource = value;
}
public void ReleaseRig() => RestoreAmbientEnvironment();
public void ReleaseRig()
{
ReleaseCamera();
RestoreAmbientEnvironment();
}
public bool ApplyRig(float deltaTime)
{
@ -50,12 +60,14 @@ namespace GameCult.Eve.UnityScene
var activeWorld = resolvedHost?.ActiveWorld;
if (resolvedHost == null || activeWorld == null)
{
ReleaseCamera();
RestoreAmbientEnvironment();
return false;
}
if (activeWorld.CameraRig == "planar.top-down-follow.v1" && !HasValidPlanarContract(activeWorld))
{
ReleaseCamera();
RestoreAmbientEnvironment();
return false;
}
@ -69,6 +81,7 @@ namespace GameCult.Eve.UnityScene
activeWorld.CameraRig != "arpg.orbital-follow.v1" &&
activeWorld.CameraRig != "third-person-orbit")
{
ReleaseCamera();
return false;
}
@ -78,7 +91,12 @@ namespace GameCult.Eve.UnityScene
: activeWorld.CameraTargetEntityId;
var player = FindEntity(resolvedHost, targetEntityId);
if (player == null)
{
ReleaseCamera();
return false;
}
resolvedHost.ClaimWorldCamera(this, camera);
_cameraHost = resolvedHost;
var cameraComponent = camera.GetComponent<Camera>();
if (cameraComponent != null && _renderPolicySource != null)
@ -186,6 +204,12 @@ namespace GameCult.Eve.UnityScene
_ownsAmbientEnvironment = false;
}
private void ReleaseCamera()
{
_cameraHost?.ReleaseWorldCamera(this);
_cameraHost = null;
}
private static Bounds? CalculateVisualBounds(Transform player, int cullingMask)
{
Bounds? bounds = null;

View file

@ -21,6 +21,7 @@ namespace GameCult.Eve.UnityScene
[SerializeField] private bool renderShotTrajectories = true;
private float _nextRefreshAt;
private object? _cameraOwner;
public EveUnityPlayableWorldRuntime? Runtime { get; private set; }
@ -37,6 +38,8 @@ namespace GameCult.Eve.UnityScene
(providerSurfaceDocuments as IEveUnityInputCapabilitySource)?.CurrentInputCapability;
public long ActiveVersion => Runtime?.ActiveVersion ?? 0;
public int ConnectionEpoch { get; private set; }
public Transform? ActiveCameraTransform { get; private set; }
public event Action<EveUnityFeedbackEvent>? FeedbackAvailable;
public event Action<EveUnityShotReceipt>? ShotAvailable;
@ -105,10 +108,24 @@ namespace GameCult.Eve.UnityScene
thermal.Bind(this, Runtime.GameObjectAssetProvider as IEveUnityNativeAssetProvider);
var presentation = Runtime.Connect();
ConnectionEpoch++;
_nextRefreshAt = Time.unscaledTime + Math.Max(0.01f, refreshIntervalSeconds);
return presentation;
}
internal void ClaimWorldCamera(object owner, Transform camera)
{
_cameraOwner = owner ?? throw new ArgumentNullException(nameof(owner));
ActiveCameraTransform = camera != null ? camera : throw new ArgumentNullException(nameof(camera));
}
internal void ReleaseWorldCamera(object owner)
{
if (!ReferenceEquals(_cameraOwner, owner)) return;
_cameraOwner = null;
ActiveCameraTransform = null;
}
public EveUnityPlayableWorldPresentation Refresh()
{
RefreshProviderSources();
@ -180,6 +197,8 @@ namespace GameCult.Eve.UnityScene
}
Runtime?.Dispose();
Runtime = null;
_cameraOwner = null;
ActiveCameraTransform = null;
}
private void OnFeedbackAvailable(EveUnityFeedbackEvent value) => FeedbackAvailable?.Invoke(value);

View file

@ -30,11 +30,18 @@ namespace GameCult.Eve.UnityScene
private bool _wasMoving;
private float _lookYawRadians;
private bool _hasLookYaw;
private float _pendingLookHorizontal;
private string _lookOwnerKey = "";
public EveUnityPlayableWorldClientHost? Host
{
get => host;
set => host = value;
set
{
if (ReferenceEquals(host, value)) return;
host = value;
ResetLookState();
}
}
public Transform? CameraTransform
@ -104,14 +111,17 @@ namespace GameCult.Eve.UnityScene
{
var resolvedHost = ResolveHost();
var world = resolvedHost?.ActiveWorld;
if (world == null || string.IsNullOrWhiteSpace(world.LookCommand) ||
if (world == null || !string.Equals(world.LookModel, "planar-yaw.v1", StringComparison.Ordinal) ||
string.IsNullOrWhiteSpace(world.LookCommand) ||
Mathf.Abs(horizontalDelta) <= 0.0001f)
return null;
if (!_hasLookYaw)
var ownerKey = $"{resolvedHost!.ConnectionEpoch}\n{world.WorldRootId}\n{world.PlayerEntityId}";
if (!_hasLookYaw || !string.Equals(_lookOwnerKey, ownerKey, StringComparison.Ordinal))
{
_lookYawRadians = ResolveControlledYawRadians(resolvedHost!, world.PlayerEntityId);
_hasLookYaw = true;
_lookOwnerKey = ownerKey;
}
var sensitivity = Mathf.Abs(world.LookSensitivityRadians) > 0.000001f
? world.LookSensitivityRadians
@ -126,6 +136,15 @@ namespace GameCult.Eve.UnityScene
issuedAt);
}
public void QueueLookInput(float horizontalDelta) => _pendingLookHorizontal += horizontalDelta;
public EveSurfaceCommandRequest? SubmitPendingLookInput(DateTimeOffset? issuedAt = null)
{
var pending = _pendingLookHorizontal;
_pendingLookHorizontal = 0f;
return SubmitLookInput(pending, issuedAt);
}
public static string ResolvePrimaryActionId(EveInputCapabilityDocument? capability)
{
if (capability == null)
@ -148,13 +167,17 @@ namespace GameCult.Eve.UnityScene
private void Update()
{
if (!driveInUpdate || Time.unscaledTime < _nextCommandAt)
if (!driveInUpdate)
return;
if (!string.IsNullOrWhiteSpace(lookHorizontalAxis))
QueueLookInput(Input.GetAxisRaw(lookHorizontalAxis));
if (Time.unscaledTime < _nextCommandAt)
return;
_nextCommandAt = Time.unscaledTime + Math.Max(0.01f, commandIntervalSeconds);
SubmitMoveVectorInput(Input.GetAxisRaw(horizontalAxis), Input.GetAxisRaw(verticalAxis));
if (!string.IsNullOrWhiteSpace(lookHorizontalAxis))
SubmitLookInput(Input.GetAxisRaw(lookHorizontalAxis));
SubmitPendingLookInput();
if (!string.IsNullOrWhiteSpace(actionButton) && Input.GetButtonDown(actionButton))
SubmitPrimaryAction();
@ -176,6 +199,14 @@ namespace GameCult.Eve.UnityScene
return marker.transform.eulerAngles.y * Mathf.Deg2Rad;
return 0f;
}
private void ResetLookState()
{
_lookYawRadians = 0f;
_hasLookYaw = false;
_pendingLookHorizontal = 0f;
_lookOwnerKey = "";
}
}
public readonly struct EveUnityPlayableWorldLookDirection

View file

@ -15,6 +15,7 @@ namespace GameCult.Eve.UnityScene
private readonly IEveUnityEntitySoaViewDocumentSource? _entityViews;
private readonly EveUnityEntitySoaPresenter _entityPresenter;
private readonly IEveUnityPresentedEntityRegistry? _presentedEntities;
private IDisposable? _ownedSceneSink;
private bool _entityViewsConnected;
private bool _assetManifestConnected;
@ -106,6 +107,7 @@ namespace GameCult.Eve.UnityScene
new EveUnityAssetRefResolver(),
assetManifests);
runtime.GameObjectAssetProvider = liveAssetProvider;
runtime._ownedSceneSink = sceneSink;
return runtime;
}
@ -198,6 +200,8 @@ namespace GameCult.Eve.UnityScene
Disconnect();
_client.Dispose();
_assetManifestSource?.Dispose();
_ownedSceneSink?.Dispose();
_ownedSceneSink = null;
}
private void EnsureAssetManifestConnected()

View file

@ -138,7 +138,8 @@ namespace GameCult.Eve.UnityScene
ParseFloat(worldRoot.GetProp("cameraNearClipPlane"), 0f),
ParseFloat(worldRoot.GetProp("cameraFarClipPlane"), 0f),
worldRoot.GetProp("lookCommand"),
ParseFloat(worldRoot.GetProp("lookSensitivityRadians"), 0f));
ParseFloat(worldRoot.GetProp("lookSensitivityRadians"), 0f),
worldRoot.GetProp("lookModel"));
}
private static EveUnityPlayableWorldEntity BuildPlayableEntity(EveSurfaceComponent component)
@ -361,7 +362,8 @@ namespace GameCult.Eve.UnityScene
float cameraNearClipPlane = 0f,
float cameraFarClipPlane = 0f,
string lookCommand = "",
float lookSensitivityRadians = 0f)
float lookSensitivityRadians = 0f,
string lookModel = "")
{
WorldRootId = worldRootId ?? "";
StatePointerId = statePointerId ?? "";
@ -394,6 +396,7 @@ namespace GameCult.Eve.UnityScene
CameraFarClipPlane = cameraFarClipPlane;
LookCommand = lookCommand ?? "";
LookSensitivityRadians = lookSensitivityRadians;
LookModel = lookModel ?? "";
}
public string WorldRootId { get; }
@ -448,6 +451,8 @@ namespace GameCult.Eve.UnityScene
public float LookSensitivityRadians { get; }
public string LookModel { get; }
public string FocusCommand { get; }
public string TargetCommand { get; }

View file

@ -993,13 +993,28 @@ namespace GameCult.Eve.UnityScene.Tests
var controlledYaw = rootObject.GetComponentsInChildren<EveUnityPlayableWorldEntityMarker>(true)
.Single(marker => marker.EntityId == "player-vanguard")
.transform.eulerAngles.y * Mathf.Deg2Rad;
var look = driver.SubmitLookInput(1000f);
driver.QueueLookInput(250f);
driver.QueueLookInput(250f);
driver.QueueLookInput(500f);
var look = driver.SubmitPendingLookInput();
Assert.That(look, Is.Not.Null);
Assert.That(provider.Submitted.Count, Is.EqualTo(2));
Assert.That(look!.Payload.GetString("commandId"), Is.EqualTo("aetheria.daemon.look_intent"));
Assert.That(look.Payload.GetDouble("directionX", 0), Is.EqualTo(Mathf.Sin(controlledYaw - 1f)).Within(0.0001f));
Assert.That(look.Payload.GetDouble("directionZ", 0), Is.EqualTo(Mathf.Cos(controlledYaw - 1f)).Within(0.0001f));
host.Connect();
var rebasedYaw = rootObject.GetComponentsInChildren<EveUnityPlayableWorldEntityMarker>(true)
.Single(marker => marker.EntityId == "player-vanguard")
.transform.eulerAngles.y * Mathf.Deg2Rad;
var rebasedLook = driver.SubmitLookInput(1000f);
Assert.That(rebasedLook, Is.Not.Null);
Assert.That(provider.Submitted.Count, Is.EqualTo(3));
Assert.That(rebasedLook!.Payload.GetDouble("directionX", 0),
Is.EqualTo(Mathf.Sin(rebasedYaw - 1f)).Within(0.0001f));
Assert.That(rebasedLook.Payload.GetDouble("directionZ", 0),
Is.EqualTo(Mathf.Cos(rebasedYaw - 1f)).Within(0.0001f));
var movement = EveUnityPlayableWorldMoveVector.FromCameraRelativeInput(1f, 1f, null);
Assert.That(movement.HasInput, Is.True);
Assert.That(movement.ScalarValue, Is.EqualTo(1f).Within(0.0001f));
@ -1100,6 +1115,7 @@ namespace GameCult.Eve.UnityScene.Tests
largeVisual.layer = 14;
Assert.That(rig.ApplyRig(0f), Is.True);
Assert.That(host.ActiveCameraTransform, Is.SameAs(cameraObject.transform));
Assert.That(camera.cullingMask & (1 << 14), Is.Zero);
Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f));
Assert.That(camera.orthographic, Is.False);
@ -1113,6 +1129,16 @@ namespace GameCult.Eve.UnityScene.Tests
Assert.That(RenderSettings.ambientMode, Is.EqualTo(UnityEngine.Rendering.AmbientMode.Flat));
Assert.That(RenderSettings.ambientLight.r, Is.EqualTo(0.2f).Within(0.001f));
var aim = hostObject.GetComponent<EveUnityAimPresentationRenderer>();
Assert.That(aim, Is.Not.Null);
var playerPosition = player.transform.position;
var playerRotation = player.transform.rotation;
aim.RefreshNow();
Assert.That(aim.ViewDotVisible, Is.True);
Assert.That(Vector3.Distance(playerPosition, aim.ViewDotPosition), Is.EqualTo(50f).Within(0.001f));
Assert.That(player.transform.position, Is.EqualTo(playerPosition));
Assert.That(player.transform.rotation, Is.EqualTo(playerRotation));
var selectedTarget = rootObject.GetComponentsInChildren<EveUnityPlayableWorldEntityMarker>()
.Single(marker => marker.EntityId == "raider-scout");
Assert.That(EveUnityCombatPresentation.Find(host.ActiveProjection)?.SelectedTargetEntityId,
@ -1132,6 +1158,9 @@ namespace GameCult.Eve.UnityScene.Tests
cameraObject.transform.position.x - beforeFollow.x,
Is.EqualTo(10f * (1f - Mathf.Exp(-0.5f))).Within(0.001f));
rig.ReleaseRig();
Assert.That(host.ActiveCameraTransform, Is.Null);
aim.RefreshNow();
Assert.That(aim.ViewDotVisible, Is.False);
Assert.That(RenderSettings.ambientMode, Is.EqualTo(ambientMode));
Assert.That(RenderSettings.ambientLight, Is.EqualTo(ambientLight));
}
@ -1710,6 +1739,19 @@ namespace GameCult.Eve.UnityScene.Tests
"",
""));
playableChildren.Add(new EveSurfaceComponent(
"aetheria.daemon.game.aim",
"aim.presentation",
new Dictionary<string, string>(StringComparer.Ordinal)
{
["controlledEntityId"] = "player-vanguard",
["convergenceTargetEntityId"] = "",
["viewDotRole"] = "pilot.view-direction",
["minimumConvergenceDistance"] = "50",
["viewDotRadius"] = "0.8"
},
Array.Empty<EveSurfaceComponent>()));
playableChildren.Add(new EveSurfaceComponent(
"aetheria.daemon.game.combat",
"combat.presentation",
@ -1775,6 +1817,7 @@ namespace GameCult.Eve.UnityScene.Tests
["playerEntityId"] = "player-vanguard",
["movementCommand"] = "aetheria.daemon.move_intent",
["lookCommand"] = "aetheria.daemon.look_intent",
["lookModel"] = "planar-yaw.v1",
["lookSensitivityRadians"] = "-0.001",
["focusCommand"] = "aetheria.daemon.focus",
["targetCommand"] = "aetheria.daemon.target",

View file

@ -1,7 +1,7 @@
{
"name": "org.gamecult.eve.unity-scene",
"displayName": "Eve Unity Scene Lowering",
"version": "0.3.24",
"version": "0.3.25",
"unity": "2021.3",
"description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.",
"dependencies": {