diff --git a/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj b/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj
index 16839dd..57ef76e 100644
--- a/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj
+++ b/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj
@@ -5,6 +5,8 @@
latest
enable
Portable Eve surface and provider contracts.
+ $(MSBuildThisFileDirectory)..\..\..\.dotnet\bin\GameCult.Eve.Surface\
+ $(MSBuildThisFileDirectory)..\..\..\.dotnet\obj\GameCult.Eve.Surface\
diff --git a/packages/org.gamecult.eve.surface/package.json b/packages/org.gamecult.eve.surface/package.json
index 8d57c63..bc5c6d0 100644
--- a/packages/org.gamecult.eve.surface/package.json
+++ b/packages/org.gamecult.eve.surface/package.json
@@ -5,7 +5,7 @@
"unity": "2021.3",
"description": "Unity-compatible DTOs for gamecult.eve.surface.v1 documents.",
"dependencies": {
- "org.gamecult.cultlib": "1.0.3"
+ "org.gamecult.cultlib": "1.0.4"
},
"keywords": [
"Eve",
diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs
index 24c8a70..e9bfde7 100644
--- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs
+++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs
@@ -28,6 +28,7 @@ namespace GameCult.Eve.UnityScene
typeof(EveSurfaceCommandRequest),
typeof(EveCommandReceiptDocument),
typeof(EveAssetCatalogDocument),
+ typeof(EveEntitySoaViewDocument),
typeof(CultMeshCdnArtifactManifest),
typeof(CultMeshCdnArtifactChunk)
};
@@ -47,6 +48,7 @@ namespace GameCult.Eve.UnityScene
private CultMeshSnapshotSession? _snapshot;
private CultNetDocumentRegistry? _networkRegistry;
private CultNetShardDescriptor? _replicaShard;
+ private CultNetDatabaseSubscriptionClient? _subscriptions;
private EveProviderAdvertisementDocument? _advertisement;
private EveAdvertisedSurface? _advertisedSurface;
@@ -90,6 +92,8 @@ namespace GameCult.Eve.UnityScene
public event Action? CommandReceiptAvailable;
+ public event Action? EntityViewAvailable;
+
public void Connect()
{
EnsureOpen();
@@ -152,8 +156,10 @@ namespace GameCult.Eve.UnityScene
_nativeAssets.Clear();
_node?.Dispose();
_snapshot?.Dispose();
+ _subscriptions?.Dispose();
_node = null;
_snapshot = null;
+ _subscriptions = null;
_networkRegistry = null;
_replicaShard = null;
_pendingCommandIds.Clear();
@@ -273,6 +279,47 @@ namespace GameCult.Eve.UnityScene
surface.Version);
SurfaceDocumentAvailable?.Invoke(CurrentSurfaceDocument);
AssetManifestDocumentAvailable?.Invoke(CurrentAssetManifestDocument);
+ EnsureLiveSubscriptions();
+ }
+
+ private void EnsureLiveSubscriptions()
+ {
+ if (_subscriptions != null) return;
+ var endpoint = new Uri(_endpoint);
+ var client = CultNetSchemaClients.CreateForEndpoint(_endpoint);
+ client.Connect(endpoint.Host, endpoint.Port);
+ var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5);
+ while (!client.Connected && DateTime.UtcNow < deadline)
+ System.Threading.Thread.Sleep(1);
+ if (!client.Connected)
+ {
+ client.Dispose();
+ throw new TimeoutException($"Timed out connecting live Eve state subscription to '{_endpoint}'.");
+ }
+
+ _subscriptions = new CultNetDatabaseSubscriptionClient(client, _node!.Database.Cache, _networkRegistry!);
+ _subscriptions.Changed += OnReplicatedDocumentChanged;
+ var lowered = new EveUnitySceneSurfaceLowerer()
+ .Lower(CurrentSurfaceDocument.SurfaceDocument, CurrentSurfaceDocument.AdvertisedSurface);
+ string entityViewPointer = lowered.PlayableWorld == null
+ ? ""
+ : lowered.PlayableWorld.EntityViewPointerId;
+ if (!string.IsNullOrWhiteSpace(entityViewPointer))
+ {
+ _subscriptions.SubscribeAsync(
+ "eve-unity-entity-view",
+ recordKeys: new[] { entityViewPointer },
+ schemaIds: new[] { EveEntitySoaViewDocument.SchemaId })
+ .GetAwaiter().GetResult();
+ if (_node.Database.Cache.Get(new CultRecordKey(entityViewPointer)) is EveEntitySoaViewDocument current)
+ EntityViewAvailable?.Invoke(current);
+ }
+ }
+
+ private void OnReplicatedDocumentChanged(CultNetReplicatedDocumentChange change)
+ {
+ if (change.Document is EveEntitySoaViewDocument entityView)
+ EntityViewAvailable?.Invoke(entityView);
}
private void RefreshAssetCatalog()
diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs
index bbc6b35..94fbbee 100644
--- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs
+++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs
@@ -1,5 +1,6 @@
using System;
using System.IO;
+using System.Collections.Concurrent;
using GameCult.Eve.Surface;
using UnityEngine;
@@ -14,6 +15,7 @@ namespace GameCult.Eve.UnityScene
IEveUnitySceneCommandSink,
IEveUnitySceneCommandReceiptSource,
IEveUnityProviderRefreshSource,
+ IEveUnityEntitySoaViewDocumentSource,
IEveUnityCameraRenderPolicySource,
IEveUnityNativeAssetProvider
{
@@ -27,6 +29,11 @@ namespace GameCult.Eve.UnityScene
private EveUnityCultMeshLiveProviderTransport? _transport;
private EveUnitySceneLiveProviderBridge? _bridge;
+ private readonly ConcurrentQueue _pendingEntityViews = new ConcurrentQueue();
+
+ public EveEntitySoaViewDocument? CurrentEntityView { get; private set; }
+
+ public event Action? EntityViewAvailable;
public EveUnityCultMeshProviderSelection? Selection { get; private set; }
@@ -117,6 +124,15 @@ namespace GameCult.Eve.UnityScene
Selection = null;
}
+ private void Update()
+ {
+ EveEntitySoaViewDocument? latest = null;
+ while (_pendingEntityViews.TryDequeue(out var next)) latest = next;
+ if (latest == null) return;
+ CurrentEntityView = latest;
+ EntityViewAvailable?.Invoke(latest);
+ }
+
private EveUnitySceneLiveProviderBridge Bridge
{
get
@@ -148,6 +164,7 @@ namespace GameCult.Eve.UnityScene
Selection.ProviderId,
Selection.SurfaceId,
runtimeId);
+ _transport.EntityViewAvailable += view => _pendingEntityViews.Enqueue(view);
_bridge = new EveUnitySceneLiveProviderBridge(_transport);
}
}
diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs
new file mode 100644
index 0000000..de83f46
--- /dev/null
+++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using GameCult.Eve.Surface;
+
+#nullable enable
+
+namespace GameCult.Eve.UnityScene
+{
+ public interface IEveUnityEntitySoaViewDocumentSource
+ {
+ EveEntitySoaViewDocument? CurrentEntityView { get; }
+ event Action EntityViewAvailable;
+ }
+
+ /// Projects portable entity SoA generations into the ordinary Unity scene sink.
+ public sealed class EveUnityEntitySoaPresenter
+ {
+ private readonly IEveUnityPlayableWorldSceneSink _sink;
+ private readonly HashSet _known = new HashSet(StringComparer.Ordinal);
+
+ public EveUnityEntitySoaPresenter(IEveUnityPlayableWorldSceneSink sink)
+ {
+ _sink = sink ?? throw new ArgumentNullException(nameof(sink));
+ }
+
+ public void Apply(EveEntitySoaViewDocument document)
+ {
+ using var view = EveUnityEntitySoaView.Open(document);
+ var next = new HashSet(StringComparer.Ordinal);
+ for (var row = 0; row < view.EntityCount; row++)
+ {
+ if (!view.TryReadInt32("entity.index", row, out var entityIndex)) continue;
+ if (view.TryReadByte("render.visibility", row, out var visible) && visible == 0) continue;
+ if (!view.TryReadVector3("transform.position", row, out var position)) continue;
+ view.TryReadFloat("transform.rotation.radians", row, out var rotationRadians);
+ view.TryReadFloat("render.scale", row, out var scale);
+ view.TryReadUInt32("render.group.id", row, out var groupId);
+ var renderGroup = view.RenderGroups.FirstOrDefault(group => group.GroupId == groupId);
+ var entityId = $"entity:{entityIndex}";
+ next.Add(entityId);
+ var assetRef = renderGroup?.MeshAssetRef ?? "";
+ _sink.UpsertEntity(
+ new EveUnityPlayableWorldEntity(
+ entityId,
+ entityId,
+ "entity",
+ entityId,
+ "",
+ assetRef,
+ position.x,
+ position.y,
+ position.z,
+ rotationRadians * 57.2957795f,
+ scale > 0f ? scale : renderGroup?.DefaultScale ?? 1f,
+ selectable: true,
+ controllable: false,
+ focusCommand: "",
+ moveCommand: "",
+ targetCommand: "",
+ actionCommand: "",
+ props: new Dictionary()),
+ new EveUnityPlayableWorldAssetBinding(
+ assetRef,
+ "entity",
+ string.IsNullOrWhiteSpace(assetRef) ? "unity-generated-placeholder" : "provider-asset-ref"));
+ }
+
+ foreach (var entityId in _known.Where(entityId => !next.Contains(entityId)).ToArray())
+ _sink.RemoveEntity(entityId);
+ _known.Clear();
+ foreach (var entityId in next) _known.Add(entityId);
+ }
+ }
+}
diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs.meta
new file mode 100644
index 0000000..281db18
--- /dev/null
+++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 28748c85c1677c04593ae92d59399070
\ No newline at end of file
diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs
index 2c2253e..f1498ef 100644
--- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs
+++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs
@@ -47,7 +47,7 @@ namespace GameCult.Eve.UnityScene
public bool TryReadVector3(string semantic, int index, out Vector3 value)
{
value = default;
- if (!TryColumn(semantic, index, "float32", 12, out var column, out var accessor)) return false;
+ if (!TryColumn(semantic, index, "float3", 12, out var column, out var accessor)) return false;
var offset = column.ByteOffset + (long)index * column.ElementStride;
value = new Vector3(accessor.ReadSingle(offset), accessor.ReadSingle(offset + 4), accessor.ReadSingle(offset + 8));
return true;
@@ -69,6 +69,18 @@ namespace GameCult.Eve.UnityScene
return true;
}
+ public bool TryReadUInt32(string semantic, int index, out uint value)
+ {
+ value = default;
+ if (!TryColumn(semantic, index, "uint32", 4, out var column, out var accessor)) return false;
+ value = accessor.ReadUInt32(column.ByteOffset + (long)index * column.ElementStride);
+ return true;
+ }
+
+ public int EntityCount => Document.Columns.Length == 0
+ ? 0
+ : Document.Columns.Min(column => column.ElementCount);
+
public bool TryReadByte(string semantic, int index, out byte value)
{
value = default;
diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs
index c16284f..2dfe724 100644
--- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs
+++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs
@@ -16,7 +16,7 @@ namespace GameCult.Eve.UnityScene
[SerializeField] private MonoBehaviour? receiptSource;
[SerializeField] private MonoBehaviour? fallbackAssetProvider;
[SerializeField] private bool connectOnEnable;
- [SerializeField] private bool refreshInUpdate = true;
+ [SerializeField] private bool refreshInUpdate;
[SerializeField] private float refreshIntervalSeconds = 1f;
[SerializeField] private bool renderShotTrajectories = true;
diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs
index 995eb49..0fc0ad1 100644
--- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs
+++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs
@@ -12,6 +12,9 @@ namespace GameCult.Eve.UnityScene
private readonly EveUnitySceneProviderConnection _connection;
private readonly EveUnityPlayableWorldLiveClient _client;
private readonly EveUnityPlayableWorldAssetManifestDocumentSource? _assetManifestSource;
+ private readonly IEveUnityEntitySoaViewDocumentSource? _entityViews;
+ private readonly EveUnityEntitySoaPresenter _entityPresenter;
+ private bool _entityViewsConnected;
private bool _assetManifestConnected;
public EveUnityPlayableWorldRuntime(
@@ -29,6 +32,8 @@ namespace GameCult.Eve.UnityScene
AssetManifests = assetManifests ?? new EveUnityPlayableWorldAssetManifestCache();
_surfaceSource = new EveUnitySceneProviderSurfaceDocumentSource(surfaceDocuments);
+ _entityViews = surfaceDocuments as IEveUnityEntitySoaViewDocumentSource;
+ _entityPresenter = new EveUnityEntitySoaPresenter(sceneSink);
_connection = new EveUnitySceneProviderConnection(_surfaceSource, commandSink);
_client = new EveUnityPlayableWorldLiveClient(
_connection,
@@ -104,7 +109,9 @@ namespace GameCult.Eve.UnityScene
public EveUnityPlayableWorldPresentation Connect()
{
EnsureAssetManifestConnected();
- return _client.Connect();
+ var presentation = _client.Connect();
+ EnsureEntityViewsConnected();
+ return presentation;
}
public EveUnityPlayableWorldPresentation Refresh()
@@ -158,6 +165,11 @@ namespace GameCult.Eve.UnityScene
public void Disconnect()
{
+ if (_entityViewsConnected && _entityViews != null)
+ {
+ _entityViews.EntityViewAvailable -= OnEntityViewAvailable;
+ _entityViewsConnected = false;
+ }
_client.Disconnect();
if (_assetManifestConnected && _assetManifestSource != null)
@@ -184,6 +196,17 @@ namespace GameCult.Eve.UnityScene
AssetManifests.Connect(_assetManifestSource);
_assetManifestConnected = true;
}
+
+ private void EnsureEntityViewsConnected()
+ {
+ if (_entityViewsConnected || _entityViews == null) return;
+ _entityViews.EntityViewAvailable += OnEntityViewAvailable;
+ _entityViewsConnected = true;
+ if (_entityViews.CurrentEntityView != null)
+ _entityPresenter.Apply(_entityViews.CurrentEntityView);
+ }
+
+ private void OnEntityViewAvailable(EveEntitySoaViewDocument document) => _entityPresenter.Apply(document);
}
public sealed class EveUnityLivePlayableWorldAssetProvider : IEveUnityNativeAssetProvider
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 9e133dd..131d941 100644
--- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs
+++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs
@@ -37,7 +37,7 @@ namespace GameCult.Eve.UnityScene.Tests
Buffers = new[] { new EveEntitySoaBuffer { BufferId = "hot", Backend = "memory_mapped_file", Location = location, ByteLength = 32, Generation = 7 } },
Columns = new[]
{
- new EveEntitySoaColumn { ColumnId = "position", Semantic = "transform.position", BufferId = "hot", ScalarType = "float32", ElementStride = 12, ElementCount = 2 },
+ new EveEntitySoaColumn { ColumnId = "position", Semantic = "transform.position", BufferId = "hot", ScalarType = "float3", ElementStride = 12, ElementCount = 2 },
new EveEntitySoaColumn { ColumnId = "entity-index", Semantic = "entity.index", BufferId = "hot", ScalarType = "int32", ByteOffset = 24, ElementStride = 4, ElementCount = 2 }
}
};
diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json
index fd38100..6c7f9e5 100644
--- a/packages/org.gamecult.eve.unity-scene/package.json
+++ b/packages/org.gamecult.eve.unity-scene/package.json
@@ -5,7 +5,7 @@
"unity": "2021.3",
"description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.",
"dependencies": {
- "org.gamecult.cultlib": "1.0.3",
+ "org.gamecult.cultlib": "1.0.4",
"org.gamecult.eve.plugin-fields": "0.1.1",
"org.gamecult.eve.surface": "0.1.0",
"com.unity.mathematics": "1.3.2"