using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Jobs;
using UnityEngine.Profiling;
namespace Pathfinding.Util {
/// Helper for batching updates to many objects efficiently
[HelpURL("https://arongranberg.com/astar/documentation/stable/batchedevents.html")]
public class BatchedEvents : VersionedMonoBehaviour {
const int ArchetypeOffset = 22;
const int ArchetypeMask = 0xFF << ArchetypeOffset;
static Archetype[] data = new Archetype[0];
static BatchedEvents instance;
static int isIteratingOverTypeIndex = -1;
static bool isIterating = false;
[System.Flags]
public enum Event {
Update = 1 << 0,
LateUpdate = 1 << 1,
FixedUpdate = 1 << 2,
Custom = 1 << 3,
None = 0,
};
struct Archetype {
public object[] objects;
public int objectCount;
public System.Type type;
public TransformAccessArray transforms;
public int variant;
public int archetypeIndex;
public Event events;
public System.Action action;
public CustomSampler sampler;
public void Add (Component obj) {
objectCount++;
UnityEngine.Assertions.Assert.IsTrue(objectCount < (1 << ArchetypeOffset));
if (objects == null) objects = (object[])System.Array.CreateInstance(type, math.ceilpow2(objectCount));
if (objectCount > objects.Length) {
var newObjects = System.Array.CreateInstance(type, math.ceilpow2(objectCount));
objects.CopyTo(newObjects, 0);
objects = (object[])newObjects;
}
objects[objectCount-1] = obj;
if (!transforms.isCreated) transforms = new TransformAccessArray(16, -1);
transforms.Add(obj.transform);
((IEntityIndex)obj).EntityIndex = (archetypeIndex << ArchetypeOffset) | (objectCount-1);
}
public void Remove (int index) {
objectCount--;
((IEntityIndex)objects[objectCount]).EntityIndex = (archetypeIndex << ArchetypeOffset) | index;
((IEntityIndex)objects[index]).EntityIndex = 0;
objects[index] = objects[objectCount];
objects[objectCount] = null;
transforms.RemoveAtSwapBack(index);
if (objectCount == 0) transforms.Dispose();
}
}
#if UNITY_EDITOR
void DelayedDestroy () {
UnityEditor.EditorApplication.update -= DelayedDestroy;
GameObject.DestroyImmediate(gameObject);
}
#endif
void OnEnable () {
if (instance == null) instance = this;
if (instance != this) {
// We cannot destroy the object while it is being enabled, so we need to delay it a bit
#if UNITY_EDITOR
// This is only important in the editor to avoid a build-up of old managers.
// In an actual game at most 1 (though in practice zero) old managers will be laying around.
// It would be nice to use a coroutine for this instead, but unfortunately they do not work for objects marked with HideAndDontSave.
UnityEditor.EditorApplication.update += DelayedDestroy;
#endif
}
}
static void CreateInstance () {
// If scripts are recompiled the the static variable will be lost.
// Some users recompile scripts in play mode and then reload the scene (https://forum.arongranberg.com/t/rts-game-pathfinding/6623/48?u=aron_granberg)
// which makes handling this a requirement.
// Here one might try to look for existing instances of the class that haven't yet been enabled.
// However, this turns out to be tricky.
// Resources.FindObjectsOfTypeAll() is the only call that includes HideInInspector GameObjects.
// But it is hard to distinguish between objects that are internal ones which will never be enabled and objects that will be enabled.
// Checking .gameObject.scene.isLoaded doesn't work reliably (object may be enabled and working even if isLoaded is false)
// Checking .gameObject.scene.isValid doesn't work reliably (object may be enabled and working even if isValid is false)
// So instead we just always create a new instance. This is not a particularly heavy operation and it only happens once per game, so why not.
// The OnEnable call will clean up duplicate managers if there are any.
var go = new GameObject("Batch Helper") {
hideFlags = HideFlags.DontSave | HideFlags.NotEditable | HideFlags.HideInInspector | HideFlags.HideInHierarchy
};
instance = go.AddComponent();
DontDestroyOnLoad(go);
}
public static T Find(K key, System.Func predicate) where T : class, IEntityIndex {
var t = typeof(T);
for (int i = 0; i < data.Length; i++) {
if (data[i].type == t) {
var objs = data[i].objects as T[];
for (int j = 0; j < data[i].objectCount; j++) {
if (predicate(objs[j], key)) return objs[j];
}
}
}
return null;
}
public static void Remove(T obj) where T : IEntityIndex {
int index = obj.EntityIndex;
if (index == 0) return;
var archetypeIndex = ((index & ArchetypeMask) >> ArchetypeOffset) - 1;
index &= ~ArchetypeMask;
UnityEngine.Assertions.Assert.IsTrue(data[archetypeIndex].type == obj.GetType());
if (isIterating && isIteratingOverTypeIndex == archetypeIndex) throw new System.Exception("Cannot add or remove entities during an event (Update/LateUpdate/...) that this helper initiated");
data[archetypeIndex].Remove(index);
}
public static int GetComponents(Event eventTypes, out TransformAccessArray transforms, out T[] components) where T : Component, IEntityIndex {
if (instance == null) CreateInstance();
// Add in a hash of the event types
var archetypeVariant = (int)eventTypes * 12582917;
if (isIterating && isIteratingOverTypeIndex == archetypeVariant) throw new System.Exception("Cannot add or remove entities during an event (Update/LateUpdate/...) that this helper initiated");
var type = typeof(T);
for (int i = 0; i < data.Length; i++) {
if (data[i].type == type && data[i].variant == archetypeVariant) {
transforms = data[i].transforms;
components = data[i].objects as T[];
return data[i].objectCount;
}
}
transforms = default;
components = null;
return 0;
}
public static bool Has(T obj) where T : IEntityIndex => obj.EntityIndex != 0;
public static void Add(T obj, Event eventTypes, System.Action action, int archetypeVariant = 0) where T : Component, IEntityIndex {
Add(obj, eventTypes, null, action, archetypeVariant);
}
public static void Add(T obj, Event eventTypes, System.Action action, int archetypeVariant = 0) where T : Component, IEntityIndex {
Add(obj, eventTypes, action, null, archetypeVariant);
}
static void Add(T obj, Event eventTypes, System.Action action1, System.Action action2, int archetypeVariant = 0) where T : Component, IEntityIndex {
if (obj.EntityIndex != 0) {
throw new System.ArgumentException("This object is already registered. Call Remove before adding the object again.");
}
if (instance == null) CreateInstance();
// Add in a hash of the event types
archetypeVariant = (int)eventTypes * 12582917;
if (isIterating && isIteratingOverTypeIndex == archetypeVariant) throw new System.Exception("Cannot add or remove entities during an event (Update/LateUpdate/...) that this helper initiated");
var type = obj.GetType();
for (int i = 0; i < data.Length; i++) {
if (data[i].type == type && data[i].variant == archetypeVariant) {
data[i].Add(obj);
return;
}
}
{
Memory.Realloc(ref data, data.Length + 1);
// A copy is made here so that these variables are captured by the lambdas below instead of the original action1/action2 parameters.
// If this is not done then the C# JIT will allocate a lambda capture object every time this function is executed
// instead of only when we need to create a new archetype. Doing that would create a lot more unnecessary garbage.
var ac1 = action1;
var ac2 = action2;
System.Action a1 = (objs, count, tr, ev) => ac1((T[])objs, count, tr, ev);
System.Action a2 = (objs, count, tr, ev) => ac2((T[])objs, count);
data[data.Length - 1] = new Archetype {
type = type,
events = eventTypes,
variant = archetypeVariant,
archetypeIndex = (data.Length - 1) + 1, // Note: offset by +1 to ensure that entity index = 0 is an invalid index
action = ac1 != null ? a1 : a2,
sampler = CustomSampler.Create(type.Name),
};
data[data.Length - 1].Add(obj);
}
}
void Process (Event eventType, System.Type typeFilter) {
try {
isIterating = true;
for (int i = 0; i < data.Length; i++) {
ref var archetype = ref data[i];
if (archetype.objectCount > 0 && (archetype.events & eventType) != 0 && (typeFilter == null || typeFilter == archetype.type)) {
isIteratingOverTypeIndex = archetype.variant;
try {
archetype.sampler.Begin();
archetype.action(archetype.objects, archetype.objectCount, archetype.transforms, eventType);
} finally {
archetype.sampler.End();
}
}
}
} finally {
isIterating = false;
}
}
public static void ProcessEvent(Event eventType) {
instance?.Process(eventType, typeof(T));
}
void Update () {
Process(Event.Update, null);
}
void LateUpdate () {
Process(Event.LateUpdate, null);
}
void FixedUpdate () {
Process(Event.FixedUpdate, null);
}
}
}