The following documents are from the ECS website:
https://docs.unity3d.com/Packages/com.unity.entities@0.0/manual/ecs_entities.html
You can define IJobForEach jobs in JobComponent System to read and write component data. When this Job runs, the ECS framework finds all entities with the required components and calls Job's Execute() method for each entity. Data will be processed in the order in which it is laid out in memory, and Job will run in parallel, so IJobForEach is simple and efficient.
The following example describes a simple system using IJobForEach. Job reads the Rotation Speedcomponent data and writes it to the Rotation Quality component.
public class RotationSpeedSystem : JobComponentSystem { // Use the [BurstCompile] attribute to compile a job with Burst. [BurstCompile] struct RotationSpeedJob : IJobForEach<RotationQuaternion, RotationSpeed> { public float DeltaTime; // The [ReadOnly] attribute tells the job scheduler that this job will not write to rotSpeed public void Execute(ref RotationQuaternion rotationQuaternion, [ReadOnly] ref RotationSpeed rotSpeed) { // Rotate something about its up vector at the speed given by RotationSpeed. rotationQuaternion.Value = math.mul(math.normalize(rotationQuaternion.Value), quaternion.AxisAngle(math.up(), rotSpeed.RadiansPerSecond * DeltaTime)); } } // OnUpdate runs on the main thread. // Any previously scheduled jobs reading/writing from Rotation or writing to RotationSpeed // will automatically be included in the inputDependencies. protected override JobHandle OnUpdate(JobHandle inputDependencies) { var job = new RotationSpeedJob() { DeltaTime = Time.deltaTime }; return job.Schedule(this, inputDependencies); } }
Note: The above system is based on ECS Samples repository The example in HelloCube I Job ForEach.
Define generic parameters of IJobForEast
The generic parameters of the IJobForEast structure identify which components your system will operate on:
struct RotationSpeedJob : IJobForEach<RotationQuaternion, RotationSpeed>
By using the following attributes, you can modify this Job's filtering of entities:
- [Exclude Component (typeof (T)] - Exclude entities whose prototypes contain T-type components
- [Require Component Tag (typeof (T)] - Includes only those entities with T-type components in the prototype. You can use this property when you need to keep a component associated with an entity, but you need to avoid reading and writing it.
For example, the following Job definitions select entities that include Gravity, Rotation Quaternion, and RotationSpeed components in the prototype, but do not contain Frozen component components:
[ExcludeComponent(typeof(Frozen))] [RequireComponentTag(typeof(Gravity))] [BurstCompile] struct RotationSpeedJob : IJobForEach<RotationQuaternion, RotationSpeed>
If you need more complex requests to select the operating entity, you can use the IJobChunk job instead of the IJobForEach job.
Writing of Execute() Method
JobComponentSystem will call Execute() methods on successive entities one by one, passing in those components that you define in the generic parameters. That is, the parameters of your Execute() method should match the generic parameters defined in this Job structure.
For example, write the following Execute() method, which maintains a match with generic parameters defined by the structure, and declares parameter attributes: read the RotationSpeed component, read and write the RotationQuaternion component. (Read/write is the default, so you don't need to declare attributes)
public void Execute(ref RotationQuaternion rotationQuaternion, [ReadOnly] ref RotationSpeed rotSpeed){}
You can add the following attributes to help ECS optimize your system:
- [ReadOnly] - This component data is read-only and not written in this method.
- [WriteOnly] - This component data is written but not read in this method.
- [ChangeFilter] - This method is only executed when the component data changes (since the last system was run).
Declaring it as read-only or write-only components will make Job more efficient to be scheduled for execution. For example, when a Job is reading a component, the scheduler does not schedule another Job to write the component, but when both Jobs are only reading a component, they can execute in parallel.
Note that in order to consider performance, ChangeFilter runs only on the entire entity block (Chunk), and it does not track a single entity. If an entity block is accessed by a Job that can be written to a certain class of components, the ECS framework will assume that the whole Chunk, including all entities in the whole block, has changed. Otherwise, the ECS framework will exclude all entities in the whole block. Change} to ___________.
Use IJobForEachWithEntity
Job, which implements the IJobForEachWithEntity interface, behaves very similar to Job, which implements the IJobForEach interface. The difference is that the Execute() function parameter in IJobForEachWithEntity provides you with the entity object currently being processed, as well as the index in an expanded, parallel array of components.
Using Entity parameters
You can use this entity object to add entity operation commands to EntityCommand Buffer. For example, you can add commands to add or delete components to the entity, or even delete the entity -- all commands will not be executed immediately within the Job to avoid competitive conditions. Command caching will allow you to perform potentially expensive calculations on worker threads that queue actual insert and delete operations and then execute them on the main thread.
The following system, derived from the example HelloCube SpawnFromEntity, uses a command cache in Job to apply results to entities after calculating the location of entities:
public class SpawnerSystem : JobComponentSystem { // EndFrameBarrier provides Command Buffer EndFrameBarrier m_EndFrameBarrier; protected override void OnCreate() { // Store EndFrameBarrier references internally so that we don't need to retrieve each frame m_EndFrameBarrier = World.GetOrCreateSystem<EndFrameBarrier>(); } struct SpawnJob : IJobForEachWithEntity<Spawner, LocalToWorld> { public EntityCommandBuffer CommandBuffer; public void Execute(Entity entity, int index, [ReadOnly] ref Spawner spawner, [ReadOnly] ref LocalToWorld location) { for (int x = 0; x < spawner.CountX; x++) { for (int y = 0; y < spawner.CountY; y++) { var __instance __= CommandBuffer.Instantiate(spawner.Prefab); // Place the instantiated in a grid with some noise var position = math.transform(location.Value, new float3(x * 1.3F, noise.cnoise(new float2(x, y) * 0.21F) * 2, y * 1.3F)); CommandBuffer.SetComponent(instance, new Translation {Value = position}); } } CommandBuffer.DestroyEntity(entity); } } protected override JobHandle OnUpdate(JobHandle inputDeps) { // Assign a Job and initialize the EntityCommand Buffer inside the Job var job = new SpawnJob { CommandBuffer = m_EndFrameBarrier.CreateCommandBuffer() }.ScheduleSingle(this, inputDeps); //We need to tell other barrier systems that the Job must be completed before the command is executed. m_EndFrameBarrier.AddJobHandleForProducer(job); return job; } }
Note: This example uses IJobForEach.ScheduleSingle(), which will allow this Job to execute on a single thread. If you use Schedule() instead, the system will allocate parallel multiple Jobs to handle these entities. In the case of parallel, you need to use the concurrent form of entity command caching (EntityCommand Buffer. Concurrent).
View ECS samples repository To see the entire example code.
Using index parameters
You can use this index parameter when adding commands to the concurrent command cache. When running Job in parallel to process multiple entities, concurrent command caching can be used. In an IJobForEachWithEntity job, if you use the Schedule() method instead of the SchduleSingle () method used in the example above, Job System will process all entities in parallel. In parallel operations, concurrent command buffers should always be used to ensure thread security and deterministic execution of cached commands.
In the same system, you can also use indexes to refer to the same entity across jobs. For example, if you need to process the same set of entities in multiple processes and collect temporary data at the same time, you can insert temporary data into a Native Array using an index in a Job, and then use the index to access the data in subsequent Jobs. (Of course, you have to pass the same Native Array to two Jobs)