Created
April 29, 2020 22:48
-
-
Save joelpryde/2134fb8a897c353e54f9b8239f2fbf1e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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