ECS And the Job System

I’m having issues understanding how I should be using the Jobs System to interact with the ECS system.

My current goal:

  • Create 10,000 physics objects and add
    a random velocity
  • Add a random velocity if their
    velocity drops below a threshold

Creating the physics objects doesn’t have to be in a Job, since it’s a one-time action. However, iterating over 10k objects and adding a velocity when needed seems like something that could benefit from the Jobs System.

My questions:

  • What is the difference between
    creating and defining a job within a
    monobehavior, and creating a script
    that derives from JobComponentSystem?
    Which should I use for this use-case?
  • How do I access entity commands such
    as getting the components I need to
    compare against? Getting
    PhysicsVelocity from entities is
    important for this task as I’m
    comparing it to a minimum velocity
    before adding a new velocity.
    However, EntityManager doesn’t seem
    to be something I can access within a
    job.

What is the difference between creating and defining a job within a monobehavior, and creating a script that derives from JobComponentSystem? Which should I use for this use-case?

You can schedule jobs from monobehaviours successfully whenever you need that, that’s ok use case. Instantiate entities too. But processing entities from an gameObject doesn’t makes much sense.

ComponentSystem is just a kind of of back-port of an idea of (dots-) System for Monobehaviours. I think about them as invisible Monobehaviour-singletons.

SystemBase (new and universal) and JobComponentSystem (older implementation) are examples of some of the proper entity systems. Meaning: those are types where you want to define how data associated with entities is being changed (what happens to them after after creation).

Sidenote: there is always max 1 system instance of given type per World.

GameObjectConversionSystem is system type that is dedicated to what they name suggest - converting monobehaviours into entities.

How do I access entity commands such as getting the components I need to compare against? Getting PhysicsVelocity from entities is important for this task as I’m comparing it to a minimum velocity before adding a new velocity. However, EntityManager doesn’t seem to be something I can access within a job.

Answer to this greatly depends on your entities package version and system you choose to do that with. I, personally, prefer [SystemBase][1] workflow (i.e: jobs generated from lambda expressions):

public class VelocityEntitiesSpawner : UnityEngine.MonoBehaviour
{
	public int _count = 100;
	void Start ()
	{
		var world = World.DefaultGameObjectInjectionWorld;
		var EntityManager = world.EntityManager;
		var archetype = EntityManager.CreateArchetype( typeof(Velocity) , typeof(LocalToWorld) );

		float3 pos = transform.position;
		quaternion rot = transform.rotation;
		for( int i=0 ; i<_count ; i++ )
		{
			var entity = EntityManager.CreateEntity( archetype );
			EntityManager.SetComponentData( entity , new LocalToWorld{
				Value=float4x4.TRS( pos , rot , new float3{x=1,y=1,z=1} )
			} );
		}
	}
}

public struct Velocity : IComponentData
{
	public float3 Value;
}

[UpdateInGroup( typeof(InitializationSystemGroup) )]
public class MinVelocitySystem : SystemBase
{
	Unity.Mathematics.Random _random;
	protected override void OnCreate()
	{
		_random = new Random( (uint) System.DateTime.Now.Ticks.GetHashCode() );
	}
	protected override void OnUpdate ()
	{
		const float minSpeed = 100f;
		var random = _random;// alias

		Entities
			.WithName("min_velocity_job")
			.ForEach( ( ref Velocity velocity , in int entityInQueryIndex )=>
			{
				float speed = math.length( velocity.Value );
				if( speed<minSpeed )
				{
					random.state += (uint) entityInQueryIndex;
					velocity.Value += random.NextFloat3(
						min:	new float3{ x=-1 , y=-1 , z=-1 } ,
						max:	new float3{ x=1 , y=1 , z=1 }
					);
				}
			} )
			.WithBurst().ScheduleParallel();
	}
}

[UpdateInGroup( typeof(SimulationSystemGroup) )]
public class VelocitySystem : SystemBase
{
	protected override void OnUpdate ()
	{
		float dt = Time.DeltaTime;

		Entities
			.WithName("apply_velocity_job")
			.ForEach( ( ref LocalToWorld ltr , in Velocity velocity )=>
			{
				ltr.Value = math.mul(
					ltr.Value ,
					float4x4.Translate( velocity.Value * dt )
				);
			} )
			.WithBurst().ScheduleParallel();
	}
}

#if UNITY_EDITOR
[UpdateInGroup( typeof(PresentationSystemGroup) )]
public class VelocityDebugSystem : SystemBase
{
	protected override void OnUpdate ()
	{
		Entities
			.WithName("debug_velocity_job")
			.ForEach( ( in LocalToWorld ltr , in Velocity velocity )=>
			{
				UnityEngine.Debug.DrawLine( ltr.Position , ltr.Position + velocity.Value , UnityEngine.Color.yellow , 0f );
			} )
			.WithoutBurst().Run();// no burst for UnityEngine namespace
	}
}
#endif

How to compare entities against each other:

// ClosestOther.cs
using Unity.Entities;
using Unity.Mathematics;
using Unity.Collections;
using Unity.Transforms;

[GenerateAuthoringComponent]
public struct ClosestOther : IComponentData
{
	public Entity Value;
}

[UpdateInGroup( typeof(SimulationSystemGroup) )]
public class FindClosestOtherSystem : SystemBase
{
	EntityQuery _query;
	protected override void OnCreate ()
	{
		_query = EntityManager.CreateEntityQuery(
				ComponentType.ReadWrite<ClosestOther>()
			,	ComponentType.ReadOnly<LocalToWorld>()
		);
	}
	protected override void OnUpdate ()
	{
		var ltwData = GetComponentDataFromEntity<LocalToWorld>( isReadOnly:true );
		NativeArray<Entity> entities = _query.ToEntityArray( Allocator.TempJob );
		
		Entities
			.WithName("closest_other_job")
			//.WithChangeFilter<LocalToWorld>()// so it wont schedule repeatedly for static objects
			.WithReadOnly(ltwData)
			.WithReadOnly(entities).WithDisposeOnCompletion(entities)
			.ForEach( ( ref ClosestOther findClosestOther , in LocalToWorld ltw , in Entity entity )=>
			{
				float3 myPos = ltw.Position;
				float closestDistSq = float.MaxValue;
				findClosestOther.Value = Entity.Null;// value when no other candidate found
				for( int i=0 ; i<entities.Length ; i++ )
				{
					Entity otherEntity = entities*;*
  •  			if( otherEntity!=entity )// not me*
    
  •  			{*
    
  •  				float3 otherPos = ltwData[otherEntity].Position;*
    
  •  				float distSq = math.lengthsq( otherPos - myPos );*
    
  •  				if( distSq<closestDistSq )*
    
  •  				{*
    
  •  					findClosestOther.Value = otherEntity;*
    
  •  					closestDistSq = distSq;*
    
  •  				}*
    
  •  			}*
    
  •  		}*
    
  •  	} )*
    
  •  	.WithBurst().ScheduleParallel();*
    
  • }*
    }

#if UNITY_EDITOR
[UpdateInGroup( typeof(PresentationSystemGroup) )]
public class ClosestOtherDebugSystem : SystemBase
{

  • protected override void OnUpdate ()*

  • {*

  •  var ltwData = GetComponentDataFromEntity<LocalToWorld>( isReadOnly:true );*
    
  •  Entities*
    
  •  	.WithName("debug_closestOther_job")*
    
  •  	.ForEach( ( in LocalToWorld ltw , in ClosestOther closestOther )=>*
    
  •  	{*
    
  •  		float3 myPos = ltw.Position;*
    
  •  		UnityEngine.Debug.DrawLine( new float3{ x=myPos.x , z=myPos.z } , myPos , UnityEngine.Color.white );*
    
  •  		if( ltwData.HasComponent(closestOther.Value) )*
    
  •  		{*
    
  •  			var otherLtw = ltwData[closestOther.Value];*
    
  •  			UnityEngine.Debug.DrawLine( myPos , otherLtw.Position , UnityEngine.Color.yellow );*
    
  •  		}*
    
  •  	} )*
    
  •  	.WithoutBurst().Run();// no burst for UnityEngine namespace*
    
  • }*
    }
    #endif
    -
    How to Add and Append to DynamicBuffer in parallel jobs:
    -
    // IWantDynamicBufferExample.cs
    using Unity.Entities;
    using Unity.Collections;

[GenerateAuthoringComponent] public struct IWantDynamicBufferExample : IComponentData { /* tag only */ }
[InternalBufferCapacity(8)] public struct DynamicBufferExample : IBufferElementData { public FixedString32 Value; }
public class DynamicBufferParallelWriteExampleSystem : SystemBase
{

  • EndSimulationEntityCommandBufferSystem _endSimulationEcbSystem;*

  • protected override void OnCreate ()*

  • {*

  •  _endSimulationEcbSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();*
    
  • }*

  • protected override void OnUpdate ()*

  • {*

  •  if( Time.ElapsedTime>0 ) this.Enabled = false;// run once only*
    
  •  var ecb = _endSimulationEcbSystem.CreateCommandBuffer();*
    
  •  var ecb_pw = ecb.AsParallelWriter();*
    
  •  Entities*
    
  •  	.WithName("add_buffers_job")*
    
  •  	.WithAll<IWantDynamicBufferExample>()*
    
  •  	.ForEach( ( in int entityInQueryIndex , in Entity entity )=>*
    
  •  	{*
    
  •  		var buffer = ecb_pw.AddBuffer<DynamicBufferExample>( entityInQueryIndex , entity );*
    
  •  		buffer.Add( new DynamicBufferExample{ Value = "initial value example" } );*
    
  •  		ecb_pw.RemoveComponent<IWantDynamicBufferExample>( entityInQueryIndex , entity );// remove tag*
    
  •  	} )*
    
  •  	.WithBurst().ScheduleParallel();*
    
  •  Entities*
    
  •  	.WithName("append_buffer_example_1_job")*
    
  •  	.WithAll<DynamicBufferExample>()*
    
  •  	.ForEach( ( in int entityInQueryIndex , in Entity entity )=>*
    
  •  	{*
    
  •  		if( entity.Index%2==0 )*
    
  •  		ecb_pw.AppendToBuffer<DynamicBufferExample>( entityInQueryIndex , entity ,*
    
  •  			new DynamicBufferExample{ Value = "entity index in even" }*
    
  •  		);*
    
  •  	} )*
    
  •  	.WithBurst().ScheduleParallel();*
    
  •  Entities*
    
  •  	.WithName("append_buffer_example_2_job")*
    
  •  	.ForEach( ( ref DynamicBuffer<DynamicBufferExample> buffer , in Entity entity )=>*
    
  •  	{*
    
  •  		if( entity.Index%2!=0 )*
    
  •  			buffer.Add( new DynamicBufferExample{ Value = "entity index in uneven" } );*
    
  •  	} )*
    
  •  	.WithBurst().ScheduleParallel();*
    
  •  _endSimulationEcbSystem.AddJobHandleForProducer( Dependency );*
    
  • }*
    }
    [1]: Class SystemBase | Entities | 0.16.0-preview.21

how do these SystemBase Jobs know which entities to operate on?

Entities.WithName("debug_velocity_job").ForEach( ( in LocalToWorld ltr , in Velocity velocity )

LocalToWorld and Velocity are components. It only iterates entities these components.