Lower provider-authored continuous beams
This commit is contained in:
parent
44b441a2ff
commit
e4d83f342f
11 changed files with 381 additions and 6 deletions
|
|
@ -48,10 +48,11 @@
|
|||
"depth": 0,
|
||||
"source": "local",
|
||||
"dependencies": {
|
||||
"org.gamecult.cultlib": "1.0.9",
|
||||
"org.gamecult.cultlib": "1.0.13",
|
||||
"org.gamecult.eve.plugin-fields": "0.1.1",
|
||||
"org.gamecult.eve.surface": "0.2.0",
|
||||
"com.unity.mathematics": "1.3.2"
|
||||
"org.gamecult.eve.surface": "0.2.1",
|
||||
"com.unity.mathematics": "1.3.2",
|
||||
"com.unity.modules.particlesystem": "1.0.0"
|
||||
}
|
||||
},
|
||||
"org.gamecult.eve.unity-uitoolkit": {
|
||||
|
|
@ -92,6 +93,12 @@
|
|||
"source": "builtin",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.modules.particlesystem": {
|
||||
"version": "1.0.0",
|
||||
"depth": 1,
|
||||
"source": "builtin",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.modules.physics": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,11 @@ new deduplicated shot receipt from the advertised controlled source to the
|
|||
advertised selection with authoritative applied or shield-absorbed damage.
|
||||
SoA remains the only transform authority; the presentation component never
|
||||
duplicates bodies or computes combat outcomes.
|
||||
Continuous `beam.presentation` nodes bind provider-authored effect assets to
|
||||
the current presented-entity generation. The generic beam renderer follows the
|
||||
advertised source transform, applies provider-owned power to particle emission,
|
||||
and reconciles asset lifecycles by semantic role. It never raycasts, applies
|
||||
force, infers contact, collects cargo, or emits gameplay receipts.
|
||||
The reusable `LightningCompute` renderer and its shader/material bundle live in
|
||||
this runtime package with their original Unity GUIDs. Provider asset bundles may
|
||||
therefore reference the effect without importing Aetheria gameplay code.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public sealed class EveUnityBeamPresentation
|
||||
{
|
||||
public const string SourceForwardDirection = "source-forward.v1";
|
||||
|
||||
public EveUnityBeamPresentation(EveUnitySceneNode node)
|
||||
{
|
||||
if (node == null) throw new ArgumentNullException(nameof(node));
|
||||
Id = node.Id ?? "";
|
||||
SourceEntityId = Get(node, "sourceEntityId");
|
||||
AssetRole = Get(node, "assetRole");
|
||||
DirectionMode = Get(node, "directionMode");
|
||||
if (string.IsNullOrWhiteSpace(DirectionMode)) DirectionMode = SourceForwardDirection;
|
||||
RenderChannel = Get(node, "renderChannel");
|
||||
ActivationActionId = Get(node, "activationActionId");
|
||||
Power = NonNegative(node, "power", 0f);
|
||||
ActivationThreshold = NonNegative(node, "activationThreshold", 0.01f);
|
||||
Radius = NonNegative(node, "radius", 0f);
|
||||
MaximumDistance = NonNegative(node, "maximumDistance", 0f);
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
public string SourceEntityId { get; }
|
||||
public string AssetRole { get; }
|
||||
public string DirectionMode { get; }
|
||||
public string RenderChannel { get; }
|
||||
public string ActivationActionId { get; }
|
||||
public float Power { get; }
|
||||
public float ActivationThreshold { get; }
|
||||
public float Radius { get; }
|
||||
public float MaximumDistance { get; }
|
||||
public bool UsesSourceForward =>
|
||||
string.Equals(DirectionMode, SourceForwardDirection, StringComparison.Ordinal);
|
||||
|
||||
public static IReadOnlyList<EveUnityBeamPresentation> FindAll(EveUnitySceneProjection? projection)
|
||||
{
|
||||
var found = new List<EveUnityBeamPresentation>();
|
||||
if (projection != null) FindAll(projection.Root, found);
|
||||
return found;
|
||||
}
|
||||
|
||||
private static void FindAll(EveUnitySceneNode node, ICollection<EveUnityBeamPresentation> found)
|
||||
{
|
||||
if (string.Equals(node.ComponentKind, "beam.presentation", StringComparison.Ordinal))
|
||||
found.Add(new EveUnityBeamPresentation(node));
|
||||
foreach (var child in node.Children) FindAll(child, found);
|
||||
}
|
||||
|
||||
private static string Get(EveUnitySceneNode node, string key) =>
|
||||
node.Props.TryGetValue(key, out var value) ? value ?? "" : "";
|
||||
|
||||
private static float NonNegative(EveUnitySceneNode node, string key, float fallback)
|
||||
{
|
||||
if (!float.TryParse(Get(node, key), NumberStyles.Float, CultureInfo.InvariantCulture, out var value) ||
|
||||
float.IsNaN(value) || float.IsInfinity(value))
|
||||
return fallback;
|
||||
return Math.Max(0f, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3086cd42c94f2624197d19704909f3f5
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public sealed class EveUnityBeamPresentationRenderer : MonoBehaviour
|
||||
{
|
||||
private readonly Dictionary<string, ActiveBeam> _active = new Dictionary<string, ActiveBeam>(StringComparer.Ordinal);
|
||||
private EveUnityPlayableWorldClientHost? _host;
|
||||
private IEveUnityGameObjectAssetProvider? _assetProvider;
|
||||
|
||||
public int ActiveBeamCount => _active.Count;
|
||||
public int ActiveParticleSystemCount
|
||||
{
|
||||
get
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var beam in _active.Values)
|
||||
if (beam.Root != null)
|
||||
count += beam.Root.GetComponentsInChildren<ParticleSystem>(true).Length;
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
public void Bind(
|
||||
EveUnityPlayableWorldClientHost host,
|
||||
IEveUnityGameObjectAssetProvider? assetProvider = null)
|
||||
{
|
||||
_host = host != null ? host : throw new ArgumentNullException(nameof(host));
|
||||
_assetProvider = assetProvider;
|
||||
}
|
||||
|
||||
public void RefreshNow()
|
||||
{
|
||||
Apply(
|
||||
EveUnityBeamPresentation.FindAll(_host?.ActiveProjection),
|
||||
_host?.PresentedEntities,
|
||||
_assetProvider);
|
||||
}
|
||||
|
||||
public void Apply(
|
||||
IReadOnlyList<EveUnityBeamPresentation> presentations,
|
||||
IEveUnityPresentedEntityRegistry? entities,
|
||||
IEveUnityGameObjectAssetProvider? assetProvider)
|
||||
{
|
||||
var retained = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var presentation in presentations ?? Array.Empty<EveUnityBeamPresentation>())
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(presentation.Id) ||
|
||||
string.IsNullOrWhiteSpace(presentation.SourceEntityId) ||
|
||||
string.IsNullOrWhiteSpace(presentation.AssetRole) ||
|
||||
!presentation.UsesSourceForward || entities == null ||
|
||||
!entities.TryGetByEntityId(presentation.SourceEntityId, out var source))
|
||||
continue;
|
||||
|
||||
retained.Add(presentation.Id);
|
||||
if (!_active.TryGetValue(presentation.Id, out var beam) ||
|
||||
!string.Equals(beam.SourceEntityId, presentation.SourceEntityId, StringComparison.Ordinal) ||
|
||||
!string.Equals(beam.AssetRole, presentation.AssetRole, StringComparison.Ordinal) ||
|
||||
beam.Root == null)
|
||||
{
|
||||
Remove(presentation.Id);
|
||||
var prefab = assetProvider?.ResolvePrefab(new EveUnityPlayableWorldAssetBinding(
|
||||
"", presentation.AssetRole, "provider-asset-ref"));
|
||||
if (prefab == null) continue;
|
||||
var root = Instantiate(prefab, source.Transform, false);
|
||||
root.name = "Eve beam " + presentation.Id;
|
||||
beam = new ActiveBeam(root, presentation.SourceEntityId, presentation.AssetRole);
|
||||
_active[presentation.Id] = beam;
|
||||
}
|
||||
else if (beam.Root.transform.parent != source.Transform)
|
||||
{
|
||||
beam.Root.transform.SetParent(source.Transform, false);
|
||||
}
|
||||
|
||||
var visiblePower = presentation.Power > presentation.ActivationThreshold
|
||||
? presentation.Power
|
||||
: 0f;
|
||||
foreach (var particles in beam.Root.GetComponentsInChildren<ParticleSystem>(true))
|
||||
{
|
||||
var emission = particles.emission;
|
||||
emission.rateOverTimeMultiplier = visiblePower;
|
||||
}
|
||||
beam.Power = visiblePower;
|
||||
}
|
||||
|
||||
var stale = new List<string>();
|
||||
foreach (var id in _active.Keys)
|
||||
if (!retained.Contains(id)) stale.Add(id);
|
||||
foreach (var id in stale) Remove(id);
|
||||
}
|
||||
|
||||
public bool TryGetPower(string presentationId, out float power)
|
||||
{
|
||||
if (_active.TryGetValue(presentationId ?? "", out var beam) && beam.Root != null)
|
||||
{
|
||||
power = beam.Power;
|
||||
return true;
|
||||
}
|
||||
power = 0f;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void LateUpdate() => RefreshNow();
|
||||
|
||||
private void Remove(string id)
|
||||
{
|
||||
if (!_active.TryGetValue(id, out var beam)) return;
|
||||
_active.Remove(id);
|
||||
if (beam.Root != null)
|
||||
{
|
||||
if (Application.isPlaying) Destroy(beam.Root);
|
||||
else DestroyImmediate(beam.Root);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
foreach (var beam in _active.Values)
|
||||
if (beam.Root != null)
|
||||
{
|
||||
if (Application.isPlaying) Destroy(beam.Root);
|
||||
else DestroyImmediate(beam.Root);
|
||||
}
|
||||
_active.Clear();
|
||||
_host = null;
|
||||
_assetProvider = null;
|
||||
}
|
||||
|
||||
private sealed class ActiveBeam
|
||||
{
|
||||
public ActiveBeam(GameObject root, string sourceEntityId, string assetRole)
|
||||
{
|
||||
Root = root;
|
||||
SourceEntityId = sourceEntityId;
|
||||
AssetRole = assetRole;
|
||||
}
|
||||
|
||||
public GameObject Root { get; }
|
||||
public string SourceEntityId { get; }
|
||||
public string AssetRole { get; }
|
||||
public float Power { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f91c99c24bc0e20488fa03daef2eec06
|
||||
|
|
@ -103,6 +103,9 @@ namespace GameCult.Eve.UnityScene
|
|||
var aimPresentation = GetComponent<EveUnityAimPresentationRenderer>();
|
||||
if (aimPresentation == null) aimPresentation = gameObject.AddComponent<EveUnityAimPresentationRenderer>();
|
||||
aimPresentation.Bind(this);
|
||||
var beamPresentation = GetComponent<EveUnityBeamPresentationRenderer>();
|
||||
if (beamPresentation == null) beamPresentation = gameObject.AddComponent<EveUnityBeamPresentationRenderer>();
|
||||
beamPresentation.Bind(this, Runtime.GameObjectAssetProvider);
|
||||
var thermal = GetComponent<EveUnityThermalPresenter>();
|
||||
var thermalHud = GetComponent<EveUnityThermalHudSink>();
|
||||
if (thermalHud == null) thermalHud = gameObject.AddComponent<EveUnityThermalHudSink>();
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
|
||||
var previous = RenderTexture.active;
|
||||
RenderTexture.active = output;
|
||||
var readback = new Texture2D(64, 64, TextureFormat.RGBAFloat, false, true);
|
||||
var readback = new Texture2D(64, 64, TextureFormat.RGBA32, false, true);
|
||||
readback.ReadPixels(new Rect(0, 0, 64, 64), 0, 0);
|
||||
readback.Apply();
|
||||
RenderTexture.active = previous;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene.Tests
|
||||
{
|
||||
public sealed class EveUnityBeamPresentationTests
|
||||
{
|
||||
[Test]
|
||||
public void ProjectionPreservesProviderBeamSourceAssetAndFossilShape()
|
||||
{
|
||||
var beam = EveUnityBeamPresentation.FindAll(Projection(Node(
|
||||
"tractor", "beam.presentation", new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["sourceEntityId"] = "run.zone.0.entity.4",
|
||||
["assetRole"] = "effect.beam.tractor",
|
||||
["directionMode"] = "source-forward.v1",
|
||||
["renderChannel"] = "world.effects",
|
||||
["activationActionId"] = "pilot.scoop",
|
||||
["power"] = "0.625",
|
||||
["activationThreshold"] = "0.01",
|
||||
["radius"] = "25",
|
||||
["maximumDistance"] = "75"
|
||||
}))).Single();
|
||||
|
||||
Assert.That(beam.SourceEntityId, Is.EqualTo("run.zone.0.entity.4"));
|
||||
Assert.That(beam.AssetRole, Is.EqualTo("effect.beam.tractor"));
|
||||
Assert.That(beam.ActivationActionId, Is.EqualTo("pilot.scoop"));
|
||||
Assert.That(beam.UsesSourceForward, Is.True);
|
||||
Assert.That(beam.Power, Is.EqualTo(0.625f).Within(0.0001f));
|
||||
Assert.That(beam.Radius, Is.EqualTo(25f));
|
||||
Assert.That(beam.MaximumDistance, Is.EqualTo(75f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RendererReconcilesProviderPrefabAgainstPresentedSourceWithoutPhysics()
|
||||
{
|
||||
var host = new GameObject("beam-test-host");
|
||||
var source = new GameObject("beam-test-source");
|
||||
var prefab = new GameObject("beam-test-prefab");
|
||||
prefab.AddComponent<ParticleSystem>();
|
||||
var renderer = host.AddComponent<EveUnityBeamPresentationRenderer>();
|
||||
var registry = new Registry("source", source.transform);
|
||||
var provider = new Provider(prefab);
|
||||
try
|
||||
{
|
||||
renderer.Apply(new[] { Presentation(0.5f) }, registry, provider);
|
||||
|
||||
Assert.That(provider.LastRole, Is.EqualTo("effect.beam.tractor"));
|
||||
Assert.That(renderer.ActiveBeamCount, Is.EqualTo(1));
|
||||
Assert.That(renderer.TryGetPower("tractor", out var power), Is.True);
|
||||
Assert.That(power, Is.EqualTo(0.5f).Within(0.0001f));
|
||||
var instance = source.transform.GetChild(0).gameObject;
|
||||
Assert.That(instance.GetComponent<ParticleSystem>().emission.rateOverTimeMultiplier,
|
||||
Is.EqualTo(0.5f).Within(0.0001f));
|
||||
Assert.That(instance.GetComponentsInChildren<Collider>(true), Is.Empty);
|
||||
Assert.That(instance.GetComponentsInChildren<Rigidbody>(true), Is.Empty);
|
||||
|
||||
renderer.Apply(new[] { Presentation(0.005f) }, registry, provider);
|
||||
Assert.That(renderer.TryGetPower("tractor", out power), Is.True);
|
||||
Assert.That(power, Is.Zero);
|
||||
|
||||
renderer.Apply(Array.Empty<EveUnityBeamPresentation>(), registry, provider);
|
||||
Assert.That(renderer.ActiveBeamCount, Is.Zero);
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(host);
|
||||
UnityEngine.Object.DestroyImmediate(source);
|
||||
UnityEngine.Object.DestroyImmediate(prefab);
|
||||
}
|
||||
}
|
||||
|
||||
private static EveUnityBeamPresentation Presentation(float power) => new EveUnityBeamPresentation(Node(
|
||||
"tractor", "beam.presentation", new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["sourceEntityId"] = "source",
|
||||
["assetRole"] = "effect.beam.tractor",
|
||||
["directionMode"] = "source-forward.v1",
|
||||
["power"] = power.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||
["activationThreshold"] = "0.01"
|
||||
}));
|
||||
|
||||
private static EveUnitySceneProjection Projection(params EveUnitySceneNode[] children) =>
|
||||
new EveUnitySceneProjection("provider", "surface", "world", "commands", "receipt", "provider", null,
|
||||
Node("root", "surface", Empty(), children));
|
||||
|
||||
private static EveUnitySceneNode Node(
|
||||
string id,
|
||||
string kind,
|
||||
IReadOnlyDictionary<string, string> props,
|
||||
params EveUnitySceneNode[] children) =>
|
||||
new EveUnitySceneNode(id, kind, "scene-node", props, Empty(), Empty(), 0, 0,
|
||||
Array.Empty<EveUnitySceneEmbeddedDocumentSlot>(), null, children);
|
||||
|
||||
private static IReadOnlyDictionary<string, string> Empty() =>
|
||||
new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
|
||||
private sealed class Registry : IEveUnityPresentedEntityRegistry
|
||||
{
|
||||
private readonly EveUnityPresentedEntityHandle _handle;
|
||||
public Registry(string entityId, Transform transform)
|
||||
{
|
||||
_handle = new EveUnityPresentedEntityHandle(
|
||||
new EveUnityPresentedEntity(0, entityId, "ship", "", "", Vector3.zero, 0, Vector3.zero,
|
||||
1, 1, 0, 0, true, true, ""),
|
||||
transform);
|
||||
}
|
||||
public EveUnityPresentedEntityGeneration? CurrentGeneration => null;
|
||||
public bool TryGetByEntityId(string entityId, out EveUnityPresentedEntityHandle handle)
|
||||
{
|
||||
handle = _handle;
|
||||
return string.Equals(entityId, _handle.Entity.EntityId, StringComparison.Ordinal);
|
||||
}
|
||||
public bool TryGetBySourceIndex(int sourceIndex, out EveUnityPresentedEntityHandle handle)
|
||||
{
|
||||
handle = _handle;
|
||||
return sourceIndex == _handle.Entity.SourceIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class Provider : IEveUnityGameObjectAssetProvider
|
||||
{
|
||||
private readonly GameObject _prefab;
|
||||
public Provider(GameObject prefab) { _prefab = prefab; }
|
||||
public string LastRole { get; private set; } = "";
|
||||
public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset)
|
||||
{
|
||||
LastRole = asset.EntityKind;
|
||||
return _prefab;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7cd6b9cdacbecf14593d6ca2e0044849
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
{
|
||||
"name": "org.gamecult.eve.unity-scene",
|
||||
"displayName": "Eve Unity Scene Lowering",
|
||||
"version": "0.3.27",
|
||||
"version": "0.3.28",
|
||||
"unity": "2021.3",
|
||||
"description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.",
|
||||
"dependencies": {
|
||||
"org.gamecult.cultlib": "1.0.13",
|
||||
"org.gamecult.eve.plugin-fields": "0.1.1",
|
||||
"org.gamecult.eve.surface": "0.2.1",
|
||||
"com.unity.mathematics": "1.3.2"
|
||||
"com.unity.mathematics": "1.3.2",
|
||||
"com.unity.modules.particlesystem": "1.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"Eve",
|
||||
|
|
|
|||
Reference in a new issue