#if GRAPH_DESIGNER /// --------------------------------------------- /// Graph Designer /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.GraphDesigner.Runtime.Variables.ECS { using System; using System.Collections.Generic; using Unity.Collections.LowLevel.Unsafe; using Unity.Entities; using Unity.Mathematics; using UnityEngine; /// /// Managed registry created once per entity during graph initialization. Collects SharedVariable registrations from all ECS tasks, deduplicates by name+scope, /// bakes initial values into DynamicBuffer, and supports syncing runtime values between the ECS buffer and managed SharedVariables. /// public class ECSVariableRegistry { private readonly Dictionary<(string, SharedVariable.SharingScope), int> m_IndexMap = new(); private readonly List m_InitialValues = new(); private readonly List m_LastSyncedValues = new(); private readonly List>> m_SyncToManagedActions = new(); private readonly List>> m_SyncToECSActions = new(); private readonly List m_DirtyManagedValues = new(); private readonly List m_TrackedSharedVariables = new(); private readonly List m_ManagedChangeTrackingActions = new(); private bool m_SuppressManagedValueTracking; /// /// Gets the number of variables registered so far. /// public int Count => m_InitialValues.Count; /// /// Registers a SharedVariable with the registry and returns its buffer index. /// /// The SharedVariable that should be registered. /// The buffer index of the registered SharedVariable, or -1 if the SharedVariable is null. public unsafe int Register(SharedVariable sharedVariable) where T : unmanaged { if (sharedVariable == null) { return -1; } var size = UnsafeUtility.SizeOf(); Debug.Assert(size <= 16, $"SharedVariable<{typeof(T).Name}> value size ({size} bytes) exceeds the 16-byte SharedVariableElement limit. Use a smaller unmanaged type."); // Deduplicate named shared variables by name + scope. var name = sharedVariable.Name ?? string.Empty; var key = (name, sharedVariable.Scope); if (!string.IsNullOrEmpty(name) && m_IndexMap.TryGetValue(key, out var existingIndex)) { return existingIndex; } var index = m_InitialValues.Count; if (!string.IsNullOrEmpty(name)) { m_IndexMap[key] = index; } // Encode the initial value into a float4 via raw memory copy. T value = sharedVariable.Value; float4 f4 = default; UnsafeUtility.MemCpy(&f4, &value, size); m_InitialValues.Add(f4); m_LastSyncedValues.Add(f4); m_DirtyManagedValues.Add(false); // Capture sync delegates so managed and ECS tasks stay in sync while the tree is running. var capturedVar = sharedVariable; var capturedIndex = index; Action trackingAction = () => { if (!m_SuppressManagedValueTracking) { m_DirtyManagedValues[capturedIndex] = true; } }; sharedVariable.OnValueChange += trackingAction; m_TrackedSharedVariables.Add(sharedVariable); m_ManagedChangeTrackingActions.Add(trackingAction); m_SyncToManagedActions.Add((buffer) => { capturedVar.Value = buffer.Get(capturedIndex); }); m_SyncToECSActions.Add((buffer) => { buffer.Set(capturedIndex, capturedVar.Value); }); return index; } /// /// Creates the DynamicBuffer on the entity and writes all registered initial values. Call once after all tasks have registered their variables. /// /// The world that owns the entity. /// The entity that should receive the shared variable buffer. public void Bake(World world, Entity entity) { if (m_InitialValues.Count == 0) { return; } DynamicBuffer buffer; if (world.EntityManager.HasBuffer(entity)) { buffer = world.EntityManager.GetBuffer(entity); buffer.Clear(); } else { buffer = world.EntityManager.AddBuffer(entity); } for (int i = 0; i < m_InitialValues.Count; ++i) { buffer.Add(new SharedVariableElement { Value = m_InitialValues[i] }); } } /// /// Writes the current managed SharedVariable values into the ECS buffer. /// /// The world that owns the entity. /// The entity whose shared variable buffer should be synced. public void SyncToECS(World world, Entity entity) { if (m_SyncToECSActions.Count == 0 || world == null || entity == Entity.Null) { return; } if (!world.EntityManager.HasBuffer(entity)) { return; } var buffer = world.EntityManager.GetBuffer(entity); for (int i = 0; i < m_SyncToECSActions.Count; ++i) { if (!m_DirtyManagedValues[i]) { continue; } m_SyncToECSActions[i](buffer); m_LastSyncedValues[i] = buffer[i].Value; m_DirtyManagedValues[i] = false; } } /// /// Reads current ECS buffer values back into the managed SharedVariable instances. /// /// The world that owns the entity. /// The entity whose shared variable buffer should be synced. public void SyncToManaged(World world, Entity entity) { if (m_SyncToManagedActions.Count == 0 || world == null || entity == Entity.Null) { return; } if (!world.EntityManager.HasBuffer(entity)) { return; } var buffer = world.EntityManager.GetBuffer(entity); m_SuppressManagedValueTracking = true; try { for (int i = 0; i < m_SyncToManagedActions.Count; ++i) { var bufferValue = buffer[i].Value; if (SharedVariableBufferValueEquals(m_LastSyncedValues[i], bufferValue)) { continue; } m_SyncToManagedActions[i](buffer); m_LastSyncedValues[i] = bufferValue; m_DirtyManagedValues[i] = false; } } finally { m_SuppressManagedValueTracking = false; } } /// /// Returns true if the two raw shared variable buffer values are byte-for-byte identical. /// /// The first variable buffer. /// The second variable buffer. /// True if the variable values are equal. private static unsafe bool SharedVariableBufferValueEquals(float4 lhs, float4 rhs) { return UnsafeUtility.MemCmp(&lhs, &rhs, UnsafeUtility.SizeOf()) == 0; } /// /// Removes any managed SharedVariable change listeners registered by the registry. /// public void Dispose() { for (int i = 0; i < m_TrackedSharedVariables.Count; ++i) { if (m_TrackedSharedVariables[i] == null) { continue; } m_TrackedSharedVariables[i].OnValueChange -= m_ManagedChangeTrackingActions[i]; } m_TrackedSharedVariables.Clear(); m_ManagedChangeTrackingActions.Clear(); } } } #endif