From 0d97dcf56c8b813de50d319ebce10a93595ad5c4 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 12 Jul 2026 02:21:59 +0100 Subject: [PATCH] Lower semantic entity presentation state --- ...veUnityGameObjectPlayableWorldSceneSink.cs | 72 +++++++++++++++++++ .../Runtime/EveUnitySceneSurfaceLowerer.cs | 14 +++- .../EveUnitySceneSurfaceLowererTests.cs | 28 ++++++++ 3 files changed, 112 insertions(+), 2 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs index d01c30a..9238e73 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs @@ -117,6 +117,15 @@ namespace GameCult.Eve.UnityScene marker = instance.AddComponent(); marker.Apply(entity, asset); + + if (entity.Props.TryGetValue("presentationState", out var presentationState) && + !string.IsNullOrWhiteSpace(presentationState)) + { + var semanticPresentation = instance.GetComponent(); + if (semanticPresentation == null) + semanticPresentation = instance.AddComponent(); + semanticPresentation.Apply(entity.Props); + } } private static void DestroyInstance(GameObject instance) @@ -128,6 +137,69 @@ namespace GameCult.Eve.UnityScene } } + public sealed class EveUnitySemanticEntityPresentation : MonoBehaviour + { + private static readonly int Emission = Shader.PropertyToID("_Emission"); + private static readonly int EmissionColor = Shader.PropertyToID("_EmissionColor"); + private MaterialPropertyBlock? _properties; + private Renderer[] _renderers = Array.Empty(); + private string _state = ""; + private float _activePulseSeconds = 1f; + private float _triggeredPulseSeconds = 0.25f; + private float _activeEmission = 1f; + private float _triggeredEmission = 4f; + + public string PresentationState => _state; + + public void Apply(IReadOnlyDictionary props) + { + props ??= new Dictionary(StringComparer.Ordinal); + _state = Read(props, "presentationState"); + _activePulseSeconds = Positive(ReadFloat(props, "activePulseSeconds", _activePulseSeconds), 1f); + _triggeredPulseSeconds = Positive(ReadFloat(props, "triggeredPulseSeconds", _triggeredPulseSeconds), 0.25f); + _activeEmission = Math.Max(0f, ReadFloat(props, "activeEmission", _activeEmission)); + _triggeredEmission = Math.Max(0f, ReadFloat(props, "triggeredEmission", _triggeredEmission)); + _renderers = GetComponentsInChildren(true); + ApplyAt(Time.time); + } + + public void ApplyAt(float timeSeconds) + { + var triggered = string.Equals(_state, "triggered", StringComparison.Ordinal); + var active = triggered || string.Equals(_state, "active", StringComparison.Ordinal); + var period = triggered ? _triggeredPulseSeconds : _activePulseSeconds; + var peak = triggered ? _triggeredEmission : _activeEmission; + var envelope = active + ? Mathf.Lerp(0.1f, 1f, Mathf.Abs(Mathf.Cos(Mathf.PI * timeSeconds / period))) + : 0f; + var value = peak * envelope; + _properties ??= new MaterialPropertyBlock(); + foreach (var renderer in _renderers) + { + if (renderer == null) continue; + renderer.GetPropertyBlock(_properties); + _properties.SetFloat(Emission, value); + _properties.SetColor(EmissionColor, Color.white * value); + renderer.SetPropertyBlock(_properties); + } + } + + private void Update() => ApplyAt(Time.time); + + private static string Read(IReadOnlyDictionary props, string key) => + props.TryGetValue(key, out var value) ? value ?? "" : ""; + + private static float ReadFloat(IReadOnlyDictionary props, string key, float fallback) => + props.TryGetValue(key, out var value) && float.TryParse(value, + System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, + out var parsed) + ? parsed + : fallback; + + private static float Positive(float value, float fallback) => value > 0f ? value : fallback; + } + public sealed class EveUnityResourcesAssetProvider : IEveUnityGameObjectAssetProvider { public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index 055e852..7af12b0 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -114,6 +114,7 @@ namespace GameCult.Eve.UnityScene worldRoot.GetProp("assetManifest", worldRoot.GetProp("assetManifestUri")), worldRoot.GetProp("inputProfile"), worldRoot.GetProp("cameraRig"), + worldRoot.GetProp("viewId"), worldRoot.GetProp("playerEntityId"), worldRoot.GetProp("movementCommand"), worldRoot.GetProp("focusCommand"), @@ -142,7 +143,8 @@ namespace GameCult.Eve.UnityScene component.GetProp("focusCommand"), component.GetProp("moveCommand"), component.GetProp("targetCommand"), - component.GetProp("actionCommand")); + component.GetProp("actionCommand"), + component.Props); } private static EveSurfaceComponent? FindFirst( @@ -308,6 +310,7 @@ namespace GameCult.Eve.UnityScene string assetManifest, string inputProfile, string cameraRig, + string viewId, string playerEntityId, string movementCommand, string focusCommand, @@ -320,6 +323,7 @@ namespace GameCult.Eve.UnityScene AssetManifest = assetManifest ?? ""; InputProfile = inputProfile ?? ""; CameraRig = cameraRig ?? ""; + ViewId = viewId ?? ""; PlayerEntityId = playerEntityId ?? ""; MovementCommand = movementCommand ?? ""; FocusCommand = focusCommand ?? ""; @@ -338,6 +342,8 @@ namespace GameCult.Eve.UnityScene public string CameraRig { get; } + public string ViewId { get; } + public string PlayerEntityId { get; } public string MovementCommand { get; } @@ -372,7 +378,8 @@ namespace GameCult.Eve.UnityScene string focusCommand, string moveCommand, string targetCommand, - string actionCommand) + string actionCommand, + IReadOnlyDictionary? props = null) { NodeId = nodeId ?? ""; EntityId = entityId ?? ""; @@ -391,6 +398,7 @@ namespace GameCult.Eve.UnityScene MoveCommand = moveCommand ?? ""; TargetCommand = targetCommand ?? ""; ActionCommand = actionCommand ?? ""; + Props = props ?? new Dictionary(StringComparer.Ordinal); } public string NodeId { get; } @@ -426,6 +434,8 @@ namespace GameCult.Eve.UnityScene public string TargetCommand { get; } public string ActionCommand { get; } + + public IReadOnlyDictionary Props { get; } } public sealed class EveUnitySceneNode 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 ecd1983..0f6dae1 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -87,6 +87,7 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(player.PositionZ, Is.EqualTo(0f)); Assert.That(player.Controllable, Is.True); Assert.That(player.MoveCommand, Is.EqualTo("aetheria.daemon.move_intent")); + Assert.That(player.Props["presentationState"], Is.EqualTo("active")); var raider = FindEntity(projection.PlayableWorld, "raider-scout"); Assert.That(raider.EntityKind, Is.EqualTo("enemy")); @@ -95,6 +96,32 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(raider.TargetCommand, Is.EqualTo("aetheria.daemon.target")); } + [Test] + public void SemanticEntityPresentationConsumesProviderPulseState() + { + var instance = GameObject.CreatePrimitive(PrimitiveType.Cube); + try + { + var presentation = instance.AddComponent(); + presentation.Apply(new Dictionary(StringComparer.Ordinal) + { + ["presentationState"] = "triggered", + ["activePulseSeconds"] = "1", + ["triggeredPulseSeconds"] = "0.25", + ["activeEmission"] = "100", + ["triggeredEmission"] = "1000" + }); + presentation.ApplyAt(0.125f); + + Assert.That(presentation.PresentationState, Is.EqualTo("triggered")); + Assert.That(instance.GetComponent().HasPropertyBlock(), Is.True); + } + finally + { + UnityEngine.Object.DestroyImmediate(instance); + } + } + [Test] public void GenericClientSessionConsumesAetheriaPlayableWorldSnapshotWithoutAetheriaTypes() { @@ -1324,6 +1351,7 @@ namespace GameCult.Eve.UnityScene.Tests ["position"] = position, ["rotationY"] = rotationY, ["radius"] = radius, + ["presentationState"] = "active", ["selectable"] = selectable ? "true" : "false", ["controllable"] = controllable ? "true" : "false", ["focusCommand"] = focusCommand,