Skip to content

Instantly share code, notes, and snippets.

@joelpryde
Created April 29, 2020 22:48
Show Gist options
  • Save joelpryde/2134fb8a897c353e54f9b8239f2fbf1e to your computer and use it in GitHub Desktop.
Save joelpryde/2134fb8a897c353e54f9b8239f2fbf1e to your computer and use it in GitHub Desktop.
// IJobEntity also allows users to define a reactive job that will handle much of the management of system state for them.
// The interface has no abstract (or other) methods but it is assumed that users will define a set of common methods
// that will handle changes to state when they are detected (the methods will only get called when precise changes occurr).
//
// The methods are as follows:
// - OnAdd(out TState stateType, in TInputType inputType1, ...) - Called when all inputTypes are present, adds stateType
// before method is called from job.
// - OnChange(ref TState stateType, in TInputType inputType1, ...) - Called when any inputType has changed and stateType is
// present, allows user to update stateType based on inputs.
// - (Optional) OnRemove(ref TState stateType) - Called when only stateType exists but the inputTypes no longer do. This will
// get called with the stateType component before being removed.
//
// InputTypes have a state tracking component (With unique type index) will be added that we can memcmp against. This
// is used to ensure that OnChange only gets called for actual component changes (automatic prefiltering happens).
//
// ---------- EXAMPLE #1 - Compute an output if the input changes, with precise change tracking
struct Input : IComponentData { public float Size; }
struct Output : IComponentData { public float Value; }
// Use attributes to define optional components needed for queries
struct SimpleReactiveJob : IJobEntity
{
// "On" Methods indicate reaction to changes must have same signature except for OnRemove (only state components)
//
// `out` parameters indicate state components that are added
// `ref` parameters are state parameters and must come first
// `in` parameters are reactive parameters and come after
// Called when an Input component is added:
// 1. Creates Output component and adds it then updates it value by calling through OnChange
public void OnAdd(out Output output, in Input input)
{
output = new Output();
OnChange(input);
}
// Called when an Input component is changed (updates Output in response)
public void OnChange(ref Output output, in Input input)
{
output.Value = SomeComplexTransformation(input.Value);
}
// No OnRemove, so removal of Output component happens automatically in OnDestroyForCompiler method of scheduling system
}
class MySystem : SystemBase
{
override void OnUpdate()
{
// Create SimpleReactiveJob and schedule both OnAdd and OnChange
// These methods must match OnAdd/OnChange methods defined in the structs
var simpleReactiveJob = new SimpleReactiveJob();
Entities.OnAdd(simpleReactiveJob).Schedule();
Entities.OnChange(simpleReactiveJob).Schedule();
}
}
// ---------- EXAMPLE #2 - Create / Destroy a resource & keep it up to date based on input settings
[AllInQuery(typeof(SomeComponent))]
struct MeshLifetimeJob : IJobEntity
also {
// Data captured from the system can be stored here
public float SystemScale;
// Called when Translation AND MeshGenerationInput have been added to entity
// GeneratedMesh will be automatically added to the component when it matches the reactive query
public void OnAdd(out GeneratedMesh mesh, in Translation translation, in MeshGenerationInput input)
{
// Create Mesh resource
mesh.Mesh = new Mesh("Blah");
// Do update of Mesh through OnChange method
OnChange(mesh, translation, input);
}
// Called when Translation OR MeshGenerationInput changes
public void OnChange(ref GeneratedMesh mesh, in Translation translation, in MeshGenerationInput input)
{
// Update Mesh resource - possibly complex operation
GenerateMeshWithSize(mesh.Mesh, translation, input.Size, SystemScale);
}
// Defined and called explicitly from System because we need to do custom cleanup
// Automatically removes GeneratedMesh component after destroying Mesh resource
public void OnRemove(ref GeneratedMesh mesh)
{
// Destroy Mesh resource
DestroyImmediate(mesh.Mesh);
}
}
class MySystem : SystemBase
{
// Some scale specific to this system
float m_Scale;
override void OnDestroy()
{
// Generate a run-time error if users do not call OnRemove explicitly for cleanup
var meshLifetime = new MeshLifetimeJob() { SystemScale = m_Scale };
Entities.OnRemove(meshLifetime).Run();
}
override void OnUpdate()
{
// NOTE: It is possible to have OnAdd be main thread only, while OnChange is scheduled.
// Eg. some resource creation in Unity can only be done on the main thread.
var meshLifetimeJob = new MeshLifetimeJob() { SystemScale = m_Scale };
Entities.OnAdd(meshLifetimeJob).Run();
Entities.OnChange(meshLifetimeJob).Schedule();
Entities.OnRemove(meshLifetimeJob).Run();
}
}
// ---------- EXAMPLE #3 Scatter prefab instances
struct PreviousInstances : IBufferElementData
{
public Entity Entity;
}
struct ScatterPrefab : IBufferElementData
{
public Entity Prefab;
}
struct ScatterData : IComponentData
{
public float Radius;
public int Count;
}
[AllInQuery(typeof(SomeComponent))]
struct ScatterPrefabJob : IJobEntity
also {
// Helper method, called from multiple reactions
void ClearPrefabs()
{
foreach(var instance in previousInstances)
EntityManager.DestroyEntity(instance.Entity);
previousInstances.Clear();
}
// Helper method, called from multiple reactions
void UpdatePrefabs(ref DynamicBuffer<PreviousInstances> previousInstances,
in ScatterData scatterData, in DynamicBuffer<ScatterPrefab> prefabs, in Translation translation)
{
for (int i = 0; i != scatterData.Count;i++)
{
var prefab = prefabs[Random.Range(0, prefabs.Length)];
var position = translation.Value + Random.insideSphere * scatterData.Radius;
var instance = EntityManager.Instantiate(prefab, position);
previousInstances.Add(instance);
}
}
// Called when ScatterData, DynamicBuffer<ScatterPrefab> and Translation have been added to an entity
public void OnAdd(out DynamicBuffer<PreviousInstances> previousInstances,
in ScatterData scatterData, in DynamicBuffer<ScatterPrefab> prefabs, in Translation translation)
{
previousInstances = new DynamicBuffer<PreviousInstances>();
UpdatePrefabs(ref previousInstances, in scatterData, in prefabs, in translation);
}
// Called when Translation OR MeshGenerationInput changes
public void OnChange(ref DynamicBuffer<PreviousInstances> previousInstances,
in ScatterData scatterData, in DynamicBuffer<ScatterPrefab> prefabs, in Translation translation)
{
ClearPrefabs(ref previousInstances);
UpdatePrefabs(ref previousInstances, in scatterData, in prefabs, in translation);
}
// Defined and called explicitly from System because we need to do custom cleanup
public void OnRemove(ref DynamicBuffer<PreviousInstances> previousInstances)
{
ClearPrefabs(ref previousInstances);
}
}
class MySystem : SystemBase
{
void OnDestroy()
{
var scatterPrefabJob = new ScatterPrefabJob();
Entities.OnRemove(scatterPrefabJob).Run();
}
void OnUpdate()
{
// Scatter prefab instances whenever ScatterData, ScatterPrefab or Translation changes
var scatterPrefabJob = new ScatterPrefabJob();
Entities.OnAdd(scatterPrefabJob).Run();
Entities.OnChange(scatterPrefabJob).Schedule();
}
}
// ---------- NEW Unity.Entities builtin API / Utility methods
// * Generates a list of indices based on what input / compare values are different
// * Updates compare based on input
// * returns the number of changes
// * @TODO: this design doesn't support multiple components to react to.
// We need to generate an index list / bitmask / ranges based on ORing all the changed component types
static int ChunkUtility.DetectChangedAndUpdateProduce(void* input, void* compare, int sizeOf, int* indices, int count);
// Creates & destroys a unique type index from another type index.
// The type index is unique (GetComponentData<> will only find it with that specific typeIndex explicitly provided)
// and it has the exact same type info as the provided type index.
// Essentially this is a way of putting the same actual type info on an entity, without it conflicting with the original type.
// The typeinfo is also turned into system state
// NOTE: An alternative to this API would be to code-gen the system state, but I would guess that this is simpler to implement overall
// and also simplifies manually written code.
int TypeManager.AllocateUniqueTypeIndexAsSystemState(int typeIndex);
void TypeManager.DeallocateUniqueTypeIndex(int typeIndex);
// Simplify query creation by supporting a ReactiveRemove flag.
// You can give it the same query that you use for adding the state.
// But internally it will do the reverse check.
// Most importantly this automatically handles an entity becoming disabled, after it already had the system state added.
// Almost all of the reactive code i have seen, forgets about this case.
// So lets make it simple and have users stop shooting themselves in the foot even for manually written code
EntityQueryDescriptionFlags.ReactiveRemove
// ---------- EXAMPLE OF GENERATED CODE for #1
// --- USER-WRITTEN Reactive IJobEntity ---
struct SimpleReactiveJob : IJobEntity
{
public void OnAdd(out Output output, in Input input)
{
output = new Output();
OnChange(input);
}
public void OnChange(ref Output output, in Input input)
{
output.Value = SomeComplexTransformation(input.Value);
}
}
// --- USER-WRITTEN OnUpdate Method ---
class MySystem : SystemBase
{
override void OnUpdate()
{
var simpleReactiveJob = new SimpleReactiveJob();
Entities.OnAdd(simpleReactiveJob).Schedule();
Entiteis.OnChange(simpleReactiveJob).Schedule();
}
}
// --- System with code-generated additions ---
// Can be decompiled and hoisted out intact with Rider's ability to "View Generated Type"
class MySystem : SystemBase
{
// CODEGEN - Type for doing MemCmp to detect precise change
int _InputCompareType;
// CODEGEN - Queries for detecting additions and changes
EntityQuery _OnAddQuery;
EntityQuery _OnChangeQuery;
// CODEGEN - Generated job to add Output when Input is added (and Output does not exist)
struct _OnAddJob : IJobChunk
{
public SimpleReactiveJob _JobData;
public int _InputCompareType;
void Execute(ArchetypeChunk chunk)
{
var outputs = (Output*)chunk.GetNativeArray<Output>().GetUnsafePtr();
var inputs = (Input*)chunk.GetNativeArray<Input>().GetUnsafePtr();
for (int i = 0; i != chunk.Count; i++)
jobData.OnAdd(out output[index], out inputs[index]);
}
}
// CODEGEN - Generated job to update Output when Input is changed
struct _OnChangeJob : IJobChunk
{
public SimpleReactiveJob _JobData;
public int _InputCompareType;
void Execute(ArchetypeChunk chunk)
{
var outputs = (Output*)chunk.GetNativeArray<Output>().GetUnsafePtr();
var inputs = (Input*)chunk.GetNativeArray<Input>().GetUnsafePtr();
var inputsCompare = (Input*)chunk.GetNativeArray<Input>(_InputCompareType).GetUnsafePtr();
int* filtered = stackalloc int[chunk.Count];
int filteredCount = DetectChangedAndUpdate<Input>(inputs, inputsCompare, sizof(input), indices, chunk.Count);
for (int i = 0; i != filteredCount; i++)
{
int index = filtered[i];
jobData.OnChange(out output[index], out inputs[index]);
}
}
}
override void OnUpdate()
{
var simpleReactiveJob = new SimpleReactiveJob();
// CODEGEN - This section replaces the Entities.OnAdd invocation
EntityManager.AddComponent(_OnAddQuery, new ComponentType(_InputCompareType));
Dependency = new _OnAddJob { _JobData = simpleReactiveJob, InputCompareType = _InputCompareType }
.ScheduleParallel(_OnAddQuery, Dependency);
// CODEGEN - This section replaces the Entities.OnChange invocation
Dependency = new _OnChangeJob { _JobData = simpleReactiveJob, InputCompareType = _InputCompareType }
.ScheduleParallel(_OnChangeQuery, Dependency);
}
// CODEGEN - Automatically created to destroy all of the InputCompare components when this system is destroyed
override void OnDestroyForCompiler()
{
EntityManager.RemoveComponent(_OnRemove, new ComponentType(_InputCompareType));
TypeManager.DeallocateUniqueTypeIndex(_InputCompareType);
}
// CODEGEN - Automatically created to created needed queries and setup compare backing type
override void OnCreateForCompiler()
{
// Clone the Input type so that we can have a unique type for this system
_InputCompareType = TypeManager.AllocateUniqueTypeIndexAsSystemState(TypeManager.GetType<Input>);
_OnAddQuery = GetEntityQuery(new EntityQueryDesc()
{
All = { typeof(Input) },
None = { _InputCompareType }
});
_OnRemove = GetEntityQuery(new EntityQueryDesc
{
All = { typeof(Input) },
None = { _InputCompareType },
// See above. This is a new simpler way of correctly defining reactive remove. User specifies the added requirements.
// ReactiveRemove reverses them & handles disabled components correctly
Flags = ReactiveRemove
});
// Early out if we know nothing in the chunk has changed
_OnChangeQuery = GetEntityQuery(ComponentType.Create<Input>());
_OnChangeQuery.SetChangeFilter<Input>();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment