Drive Unity worlds from live entity SoA
This commit is contained in:
parent
c5b04d33c1
commit
712d91e67c
11 changed files with 184 additions and 6 deletions
|
|
@ -5,6 +5,8 @@
|
|||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Description>Portable Eve surface and provider contracts.</Description>
|
||||
<BaseOutputPath>$(MSBuildThisFileDirectory)..\..\..\.dotnet\bin\GameCult.Eve.Surface\</BaseOutputPath>
|
||||
<BaseIntermediateOutputPath>$(MSBuildThisFileDirectory)..\..\..\.dotnet\obj\GameCult.Eve.Surface\</BaseIntermediateOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<EveUnitySceneCommandReceipt>? CommandReceiptAvailable;
|
||||
|
||||
public event Action<EveEntitySoaViewDocument>? 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()
|
||||
|
|
|
|||
|
|
@ -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<EveEntitySoaViewDocument> _pendingEntityViews = new ConcurrentQueue<EveEntitySoaViewDocument>();
|
||||
|
||||
public EveEntitySoaViewDocument? CurrentEntityView { get; private set; }
|
||||
|
||||
public event Action<EveEntitySoaViewDocument>? 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<EveEntitySoaViewDocument> EntityViewAvailable;
|
||||
}
|
||||
|
||||
/// <summary>Projects portable entity SoA generations into the ordinary Unity scene sink.</summary>
|
||||
public sealed class EveUnityEntitySoaPresenter
|
||||
{
|
||||
private readonly IEveUnityPlayableWorldSceneSink _sink;
|
||||
private readonly HashSet<string> _known = new HashSet<string>(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<string>(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<string, string>()),
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 28748c85c1677c04593ae92d59399070
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Reference in a new issue