Lower semantic entity presentation state

This commit is contained in:
Meta Vens 2026-07-12 02:21:59 +01:00
commit 0d97dcf56c
3 changed files with 112 additions and 2 deletions

View file

@ -117,6 +117,15 @@ namespace GameCult.Eve.UnityScene
marker = instance.AddComponent<EveUnityPlayableWorldEntityMarker>();
marker.Apply(entity, asset);
if (entity.Props.TryGetValue("presentationState", out var presentationState) &&
!string.IsNullOrWhiteSpace(presentationState))
{
var semanticPresentation = instance.GetComponent<EveUnitySemanticEntityPresentation>();
if (semanticPresentation == null)
semanticPresentation = instance.AddComponent<EveUnitySemanticEntityPresentation>();
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<Renderer>();
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<string, string> props)
{
props ??= new Dictionary<string, string>(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<Renderer>(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<string, string> props, string key) =>
props.TryGetValue(key, out var value) ? value ?? "" : "";
private static float ReadFloat(IReadOnlyDictionary<string, string> 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)

View file

@ -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<string, string>? props = null)
{
NodeId = nodeId ?? "";
EntityId = entityId ?? "";
@ -391,6 +398,7 @@ namespace GameCult.Eve.UnityScene
MoveCommand = moveCommand ?? "";
TargetCommand = targetCommand ?? "";
ActionCommand = actionCommand ?? "";
Props = props ?? new Dictionary<string, string>(StringComparer.Ordinal);
}
public string NodeId { get; }
@ -426,6 +434,8 @@ namespace GameCult.Eve.UnityScene
public string TargetCommand { get; }
public string ActionCommand { get; }
public IReadOnlyDictionary<string, string> Props { get; }
}
public sealed class EveUnitySceneNode

View file

@ -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<EveUnitySemanticEntityPresentation>();
presentation.Apply(new Dictionary<string, string>(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<Renderer>().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,