diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityFeedbackEffectRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityFeedbackEffectRenderer.cs new file mode 100644 index 0000000..e5e6d11 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityFeedbackEffectRenderer.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using UnityEngine; + +#nullable enable + +namespace GameCult.Eve.UnityScene +{ + public sealed class EveUnityFeedbackEffectRenderer : MonoBehaviour + { + [SerializeField] private float fallbackLifetimeSeconds = 5f; + + private readonly List _active = new List(); + private EveUnityPlayableWorldClientHost? _host; + private IEveUnityGameObjectAssetProvider? _assetProvider; + + public int ActiveEffectCount => _active.Count; + + public void Bind(EveUnityPlayableWorldClientHost host, IEveUnityGameObjectAssetProvider? assetProvider = null) + { + if (host == null) throw new ArgumentNullException(nameof(host)); + if (ReferenceEquals(_host, host) && ReferenceEquals(_assetProvider, assetProvider)) return; + Unbind(); + _host = host; + _assetProvider = assetProvider; + _host.FeedbackAvailable += Present; + } + + public void Unbind() + { + if (_host != null) _host.FeedbackAvailable -= Present; + _host = null; + _assetProvider = null; + } + + public void Present(EveUnityFeedbackEvent feedback) + { + if (feedback == null) throw new ArgumentNullException(nameof(feedback)); + if (string.IsNullOrWhiteSpace(feedback.Kind)) return; + var prefab = ResolveRolePrefab("effect.feedback." + feedback.Kind); + if (prefab == null) return; + var effect = Instantiate(prefab, transform); + effect.name = "Eve feedback " + feedback.Kind + " " + feedback.EventId; + if (TryVector(feedback.Position, out var position)) effect.transform.position = position; + _active.Add(new ActiveEffect(effect, Time.unscaledTime + Math.Max(0.01f, fallbackLifetimeSeconds))); + } + + private void Update() + { + var now = Time.unscaledTime; + for (var index = _active.Count - 1; index >= 0; index--) + { + var effect = _active[index]; + if (effect.Root != null && now < effect.DestroyAt) continue; + if (effect.Root != null) Destroy(effect.Root); + _active.RemoveAt(index); + } + } + + private GameObject? ResolveRolePrefab(string role) => _assetProvider?.ResolvePrefab( + new EveUnityPlayableWorldAssetBinding("", role, "provider-asset-ref")); + + private static bool TryVector(string value, out Vector3 vector) + { + vector = Vector3.zero; + var parts = (value ?? "").Split(','); + if (parts.Length < 2 || + !float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var x) || + !float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var z)) + return false; + var y = 0f; + if (parts.Length >= 3) + { + if (!float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out y) || + !float.TryParse(parts[2], NumberStyles.Float, CultureInfo.InvariantCulture, out z)) + return false; + } + vector = new Vector3(x, y, z); + return true; + } + + private void OnDestroy() + { + Unbind(); + foreach (var effect in _active) + if (effect.Root != null) Destroy(effect.Root); + _active.Clear(); + } + + private readonly struct ActiveEffect + { + public ActiveEffect(GameObject root, float destroyAt) { Root = root; DestroyAt = destroyAt; } + public GameObject Root { get; } + public float DestroyAt { get; } + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityFeedbackEffectRenderer.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityFeedbackEffectRenderer.cs.meta new file mode 100644 index 0000000..0b82704 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityFeedbackEffectRenderer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f0b10c28d7386e54c8e7ffe269a9bfce \ No newline at end of file diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs index a8787dc..c6592b6 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs @@ -85,6 +85,9 @@ namespace GameCult.Eve.UnityScene if (trajectories == null) trajectories = gameObject.AddComponent(); trajectories.Bind(this, Runtime.GameObjectAssetProvider); } + var feedbackEffects = GetComponent(); + if (feedbackEffects == null) feedbackEffects = gameObject.AddComponent(); + feedbackEffects.Bind(this, Runtime.GameObjectAssetProvider); return Runtime.Connect(); } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityFeedbackEffectRendererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityFeedbackEffectRendererTests.cs new file mode 100644 index 0000000..39d3730 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityFeedbackEffectRendererTests.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using UnityEngine; + +#nullable enable + +namespace GameCult.Eve.UnityScene.Tests +{ + public sealed class EveUnityFeedbackEffectRendererTests + { + [Test] + public void RendererResolvesSemanticFeedbackRoleAtProviderPosition() + { + var root = new GameObject("feedback-renderer-test"); + var prefab = new GameObject("destruction-prefab"); + try + { + var host = root.AddComponent(); + var renderer = root.AddComponent(); + var provider = new CapturingProvider(prefab); + renderer.Bind(host, provider); + renderer.Present(Event("destroyed-1", "entity.destroyed", "4,2,7")); + + Assert.That(provider.LastRole, Is.EqualTo("effect.feedback.entity.destroyed")); + Assert.That(renderer.ActiveEffectCount, Is.EqualTo(1)); + Assert.That(root.transform.childCount, Is.EqualTo(1)); + Assert.That(root.transform.GetChild(0).position, Is.EqualTo(new Vector3(4, 2, 7))); + } + finally + { + UnityEngine.Object.DestroyImmediate(root); + UnityEngine.Object.DestroyImmediate(prefab); + } + } + + private static EveUnityFeedbackEvent Event(string id, string kind, string position) => + new EveUnityFeedbackEvent(new EveUnitySceneNode(id, "feedback.event", "scene-node", + new Dictionary(StringComparer.Ordinal) + { + ["eventId"] = id, ["eventKind"] = kind, ["position"] = position + }, Empty(), Empty(), 0, 0, Array.Empty(), null, + Array.Empty())); + + private static IReadOnlyDictionary Empty() => + new Dictionary(StringComparer.Ordinal); + + private sealed class CapturingProvider : IEveUnityGameObjectAssetProvider + { + private readonly GameObject _prefab; + public CapturingProvider(GameObject prefab) { _prefab = prefab; } + public string LastRole { get; private set; } = ""; + public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) + { + LastRole = asset.EntityKind; + return _prefab; + } + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityFeedbackEffectRendererTests.cs.meta b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityFeedbackEffectRendererTests.cs.meta new file mode 100644 index 0000000..203d010 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityFeedbackEffectRendererTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a5f577609431418418367103f778ed67 \ No newline at end of file