Render provider feedback effects

This commit is contained in:
Meta Vens 2026-07-12 05:27:38 +01:00
commit 290b384703
5 changed files with 165 additions and 0 deletions

View file

@ -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<ActiveEffect> _active = new List<ActiveEffect>();
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; }
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f0b10c28d7386e54c8e7ffe269a9bfce

View file

@ -85,6 +85,9 @@ namespace GameCult.Eve.UnityScene
if (trajectories == null) trajectories = gameObject.AddComponent<EveUnityShotTrajectoryRenderer>();
trajectories.Bind(this, Runtime.GameObjectAssetProvider);
}
var feedbackEffects = GetComponent<EveUnityFeedbackEffectRenderer>();
if (feedbackEffects == null) feedbackEffects = gameObject.AddComponent<EveUnityFeedbackEffectRenderer>();
feedbackEffects.Bind(this, Runtime.GameObjectAssetProvider);
return Runtime.Connect();
}

View file

@ -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<EveUnityPlayableWorldClientHost>();
var renderer = root.AddComponent<EveUnityFeedbackEffectRenderer>();
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<string, string>(StringComparer.Ordinal)
{
["eventId"] = id, ["eventKind"] = kind, ["position"] = position
}, Empty(), Empty(), 0, 0, Array.Empty<EveUnitySceneEmbeddedDocumentSlot>(), null,
Array.Empty<EveUnitySceneNode>()));
private static IReadOnlyDictionary<string, string> Empty() =>
new Dictionary<string, string>(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;
}
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a5f577609431418418367103f778ed67