JobComponentSystem lambda functions
JobComponentSystem lambda functions provide a concise way to define and execute your algorithms over entities, their components, or over native containers.
The JobComponentSystem supports two types of lambda functions:
JobComponentSystem.Entities.ForEach(lambda)
executes the lambda function over all the entities selected by an entity query (which is defined by the Entities.ForEach options and lambda parameters).JobComponentSystem.Job.WithCode(lambda)
executes the lambda function once as an IJob.
To execute a job lambda function, you define the lambda using either ForEach()
or WithCode()
, and then you either schedule the job using Schedule()
or execute it immediately (on the main thread) with Run()
. Whether you use Entities.ForEach or Job.WthCode, you can use additional methods defined on these objects to set various job options and parameters.
Entities.ForEach example:
The following example illustrates a simple JobComponentSystem that uses Entities.ForEach to read one component (Velocity in this case) and write to another (Translation):
Note the use of the keywords ref
and in
on the parameters of the ForEach lambda function. Use ref
for components that you write to, and in
for components that you only read. Marking components as read-only helps the job scheduler execute your jobs more efficiently.
Job.WithCode example
The following example illustrates a simple example that uses one Job.WithCode() lambda function to fill a native array with random numbers and another to add those numbers together:
In a real application, the first job might calculate intermediate results from a set of entity components in a parallel job, while the second job combines those results to calculate the solution.
Entities.ForEach entity query
The entities and chunks processed by an Entities.ForEach lambda are selected by an entity query, which is implicitly created when the JobComponentSystem is created. (Use WithStoreEntityQueryInField(ref EntityQuery)
to get access to this implicit EntityQuery object.)
The query is constructed by combining the parameters you declare for the lambda function with the component types you add to the query explicitly using WithAll<T>, WithAny<T>, and WithNone<T> functions. You can also set specific query options using additional Entities functions. The query-related Entities functions include:
WithAll<T>
— An entity must have all of these component types (in addition to having all the component types found in the lambda parameter list)WithAny<T,U>
— An entity must have one or more of these component types. Note that specifying a single component type using WithAny is allowed; however, because an entity must have one or more of these “optional” component types to be selected by the query, using WithAny with a single type is equivalent to putting that type in the WithAll statement.WithNone<T>
— An entity must not have any of these component types.WithChangeFilter<T>
— Only selects entities in chunks in which the specified component might have changed since the last time this JobComponentSystem updated.WithSharedComponentFilter
— Only select chunks that have a specified value for a shared component.WithStoreEntityQueryInField
— stores the EntityQuery object generated by the Entities.ForEach in an EntityQuery field on your JobComponentSystem. You can use this EntityQuery object for such purposes as getting the number of entities that will be selected by the query. Note that this function assigns the EntityQuery instance to your field when the JobComponentSystem is created. This means that you can use the query before the first execution of the lambda function.
Important: Do not add components in the parameter list to the query using WithAny<T,U> or WithNone<T>. All of the components you add to the lambda function parameter list are added to the WithAll list of the entity query automatically; adding a component to both the WithAll list and the WithAny or WithNone lists creates an illogical query.
Entity query example:
The following example selects entities that have the components, Destination, Source, and LocalToWorld; and have at least one of the components, Rotation, Translation, or Scale; but which do not have a LocalToParent component.
In this example, only the Destination and Source components can be accessed inside the lambda function since they are the only components in the parameter list.
Access to EntityQuery object example:
The following example illustrates how to access the EntityQuery object implicitly created for an Entities.ForEach construction. In this case, the example uses the EntityQuery object to invoke the CalculateEntityCount() method. The example use this count to create a native array with enough space to store one value per entity selected by the query:
Optional components
You cannot create a query specifying optional components (using WithAny<T,U>) and also access those components in the lambda function. If you need to read or write to a component that is optional, you can split the Entities.ForEach construction into multiple jobs, one for each combination of the optional components. For example, if you had two optional components, you would need three ForEach constructions: one including the first optional component, one including the second, and one including both components. Another alternative is to iterate by chunk using IJobChunk.
Change filtering
In cases where you only want to process an entity component when another entity of that component has changed since the last time the current JobComponentSystem has run, you can enable change filtering using WithChangeFilter<T>. The component type used in the change filter must either be in the lambda function parameter list or part of a WithAll<T> statement.
An entity query supports change filtering on up to two component types.
Note that change filtering is applied at the chunk level. If any code accesses a component in a chunk with write access, then that component type in that chunk is marked as changed -- even if the code didn’t actually change any data.
Shared component filtering
Entities with shared components are grouped into chunks with other entities having the same value for their shared components. You can select groups of entities that have specific shared component values using the WithSharedComponentFilter() function.
The following example selects entities grouped by a Cohort ISharedComponentData. The lambda function in this example sets a DisplayColor IComponentData component based on the entity’s cohort:
The example uses the EntityManager to get all the unique cohort values. It then schedules a lambda job for each cohort, passing the new color to the lambda function as a captured variable.
Lambda parameters
When you define the lambda function to use with Entities.ForEach, you can declare parameters that the JobComponentSystem uses to pass information about the current entity (or chunk) when it executes the function. (A Job.WithCode lambda function does not take any parameters.)
You can pass up to eight parameters to an Entities.ForEach lambda function. The parameters must be grouped in the following order:
1. Parameters passed-by-value first (no parameter modifiers)
2. Writable parameters second (`ref` parameter modifier)
3. Read-only parameters last (`in` parameter modifier)
All components should use either the ref
or the in
parameter modifier keywords.
If your function does not obey these rules, the compiler provides an error similar to:
error CS1593: Delegate 'Invalid_ForEach_Signature_See_ForEach_Documentation_For_Rules_And_Restrictions' does not take N arguments
(Note that the error message cites the number of arguments as the issue even when the problem is the parameter order.)
Component parameters
To access a component associated with an entity, you must pass a parameter of that component type to the Entities.ForEach lambda function (unless you are iterating over chunks instead of entities). The compiler automatically adds all components passed to the function to the entity query as required components.
To update a component value, you must pass it to the lambda function by reference using the ref
keyword in the parameter list. (Without the ref
keyword, any modifications would be made to a temporary copy of the component since it would be passed by value.) Note that using ref
means that the components in the current chunk are marked as changed, even if the lambda function does not actually modify them. For efficiency, always designate components that your lambda function does not modify as read only.
To designate a component passed to the lambda function as read-only, use the in
keyword in the parameter list.
The following example passes a Source component parameter to the job as read-only, and a Destination component parameter as writable:
Note: Currently, you cannot pass chunk components to the Entities.ForEach lambda function.
For dynamic buffers, use DynamicBuffer<T> rather than the Component type stored in the buffer:
Special, named parameters
In addition to components, you can pass the following special, named parameters to the Entities.ForEach lambda function, which are assigned values based on the entity the job is currently processing:
Entity entity
— the Entity instance of the current entity. (The parameter can be named anything as long as the type is Entity.)int entityInQueryIndex
— the index of the entity in the list of all entities selected by the query. Use the entity index value when you have a native array that you need to fill with a unique value for each entity. You can use the entityInQueryIndex as the index in that array. The entityInQueryIndex should also be used as the jobIndex for adding commands to a concurrent EntityCommandBuffer.int nativeThreadIndex
— a unique index for the thread executing the current iteration of the lambda function. When you execute the lambda function using Run(), nativeThreadIndex is always zero.
Capturing variables
You can capture local variables for Entities.ForEach and Job.WithCode lambda functions. When you execute the function using a job (by calling Schedule() instead of Run()) there are some restrictions on the captured variables and how you use them:
Only native containers and blittable types can be captured. A job can only write to captured variables that are native containers. (To “return” a single value, create a native array with one element.)
You can use the following functions to apply modifiers and attributes to the captured variables:
WithReadOnly(myvar)
— restricts access to the variable as read-only.WithDeallocateOnJobCompletion(myvar)
— deallocates the native container after the job is complete. See DeallocateOnJobCompletionAttribute.WithNativeDisableParallelForRestriction(myvar)
— permits multiple threads to access the same writable native container. Parallel access is only safe when each thread only accesses its own, unique range of elements in the container. If more than one thread accesses the same element a race condition is created in which the timing of the access changes the result. See NativeDisableParallelForRestriction.WithNativeDisableContainerSafetyRestriction(myvar)
— disables normal safety restrictions that prevent dangerous access to the native container. Disabling safety restrictions unwisely can lead to race conditions, subtle bugs, and crashes in your application. See NativeDisableContainerSafetyRestrictionAttribute.WithNativeDisableUnsafePtrRestrictionAttribute(myvar)
— Allows you to use unsafe pointers provided by the native container. Incorrect pointer use can lead to subtle bugs, instability, and crashes in your application. See NativeDisableUnsafePtrRestrictionAttribute.
Job options
You can use the following methods with both Entities.ForEach and Job.WithCode lambda functions:
JobHandle Schedule(JobHandle)
— schedules the lambda function to execute as a job:- Entities.ForEach — the job executes the lambda function on parallel background, job threads. Each job iterates over the entities in the chunks selected by the ForEach query. (A job instance processes the entities in a single chunk at minimum.)
- Job.WithCode — the job executes a single instance of the lambda function on a background, job thread.
void Run()
— executes the lambda function synchronously on the main thread:- Entities.ForEach — the lambda function is executed once for each entity in the chunks selected by the ForEach query. Note that
Run()
does not take a JobHandle parameter and does not return a JobHandle since your lambda function does not run as a job. - Job.WithCode — the lambda function is executed once.
- Entities.ForEach — the lambda function is executed once for each entity in the chunks selected by the ForEach query. Note that
WithBurst(FloatMode, FloatPrecision, bool)
— sets options for the Burst compiler:- floatMode — sets the floating point math optimization mode. Fast mode executes faster, but produces larger floating point error than Strict mode. Defaults to Strict. See Burst FloatMode.
- floatPrecision — sets the floating point math precision. See Burst FloatPrecision.
- synchronousCompilation — compiles the function immediately instead of scheduling the function for compilation later.
WithoutBurst()
— disables Burst compilation. Use this function when your lambda function contains code not supported by Burst.WithStructuralChanges()
— executes the lambda function on the main thread and disables Burst so that you can make structural changes to your entity data within the function. For better performance, use a concurrent EntityCommandBuffer instead.WithName(string)
— assigns the specified string as the name of the generated job class. Assigning a name is optional, but can help identify the function when debugging and profiling.
Job dependencies
The JobHandle object passed to the JobComponentSystem.OnUpdate method encapsulates all the component-related, read-write job dependencies declared by the JobComponentSystem instances that have already updated so far in the frame. When you pass the input dependencies from previous systems to your Schedule method, ECS ensures the prior completion of any jobs writing to the same component data accessed by the current lambda function. When you call Run(), the lambda function executes on the main thread, so any jobs scheduled by earlier systems are completed immediately.
Likewise, your OnUpdate() function must pass its dependencies to subsequent systems by returning a JobHandle. If your update function constructs a single job, you can return the JobHandle provided by Schedule(). If your update function constructs multiple jobs, you can either chain the individual dependencies by passing the JobHandle returned by one to the Schedule() method of the next, or, if the jobs do not depend on each other, you can combine their dependencies using JobHandle.CombineDependencies().
Note, the JobHandle only includes dependences for component data, not native containers. If you have a system or job that reads data in a native container populated by another system or job, you must manage the dependency manually. One way to do this is to provide a method or property that allows the producing system to add a JobHandle as a dependency for the consuming system. (See the AddProducerForJob() method of the EntityCommandBufferSystem for an example of this technique.)
Using Entities.ForEach with an EntityCommandBuffer
You cannot perform structural changes to entities in a job, including creating entities, adding or removing components, or destroying entities. Instead, you must defer structural changes to a later point in the frame using an entity command buffer. The default ECS system group setup provides entity command buffer systems at the beginning and end of the standard system groups. In general, you should pick the last entity command buffer system that runs before any of your other systems that rely on your structural changes. For example, if you create entities in the simulation system group and want to render those entities in the same frame, you could use entity command buffers created by the EndSimulationEntityCommandBufferSystem when creating the entities.
To create entity command buffers, you store a reference to the entity command buffer system you want to use. Them in your OnUpdate function, you use that reference to create an EntityCommandBuffer instance to use for the current frame. (You must create a new entity command buffer for every update.)
The following example illustrates how to create an entity command buffer, in this case, obtaining it from the EndSimulationEntityCommandBufferSystem:
Since Entities.ForEach.Schedule()
creates a parallel job, you must use the concurrent interface of the entity command buffer.
Entites.ForEach lambda with entity command buffer example
The following example illustrates using entity command buffers in a JobComponentSystem that implements a simple particle system:
Implementation notes
Entities.ForEach and Job.WithCode use a compiler extension to convert the code that you write into efficient, job-based C# code. Essentially, when writing an Entities.ForEach or Job.WithCode construction, you are describing what you want the job to do and the compiler extension generates the required code to make that happen. In general, this conversion should be transparent to you; however, take note of the following:
- Performance drawbacks of lambda functions such as extra managed memory allocations when capturing variables do not apply.
- Code completion in an IDE may not list the correct parameters for the Entities and Job object methods.
- You may see the generated class names in places like warnings, error messages, and IL code disassembly.
- When you use
WithStoreEntityQueryInField(ref query)
, the compiler extension assigns a value to the query field before the system'sOnCreate()
method. This means that you can access the EntityQuery object referenced by the field before the the Entities.ForEach lambda function runs for the first time.