From 58d87c628a261751439584263c18e93a7d8d8708 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 06:08:45 +0100 Subject: [PATCH] Harden generic authoritative aim lifecycle --- .../EveUnityAimPresentationRenderer.cs | 13 ++++-- ...veUnityGameObjectPlayableWorldSceneSink.cs | 14 +++++- .../Runtime/EveUnityPlayableWorldCameraRig.cs | 28 +++++++++++- .../EveUnityPlayableWorldClientHost.cs | 19 ++++++++ .../EveUnityPlayableWorldInputDriver.cs | 43 +++++++++++++++--- .../Runtime/EveUnityPlayableWorldRuntime.cs | 4 ++ .../Runtime/EveUnitySceneSurfaceLowerer.cs | 9 +++- .../EveUnitySceneSurfaceLowererTests.cs | 45 ++++++++++++++++++- .../org.gamecult.eve.unity-scene/package.json | 2 +- 9 files changed, 160 insertions(+), 17 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs index 25b8ce8..78c7788 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs @@ -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); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs index 0035a83..84b7e8c 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs @@ -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(StringComparer.Ordinal); + _presentedByIndex = new Dictionary(); + CurrentGeneration = null; + } + private GameObject InstantiateEntity( EveUnityPlayableWorldEntity entity, EveUnityPlayableWorldAssetBinding asset, diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index 69d717d..7b1334b 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -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(); 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; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs index 56ed0ef..bdc2469 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs @@ -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? FeedbackAvailable; public event Action? 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); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs index ffd3f75..9abd10a 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs @@ -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 diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs index c81e6b6..28100fb 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs @@ -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() diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index 1b67179..2ef7006 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -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; } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 3ebb32b..60d6a5b 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -993,13 +993,28 @@ namespace GameCult.Eve.UnityScene.Tests var controlledYaw = rootObject.GetComponentsInChildren(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(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(); + 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() .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(StringComparer.Ordinal) + { + ["controlledEntityId"] = "player-vanguard", + ["convergenceTargetEntityId"] = "", + ["viewDotRole"] = "pilot.view-direction", + ["minimumConvergenceDistance"] = "50", + ["viewDotRadius"] = "0.8" + }, + Array.Empty())); + 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", diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 895bac3..609948c 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -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": {