- Home /
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.
Have you read through the manual? It'll tell you why to use the JobComponentSystem and how to access entity data.
From what I'm understanding, JobComponentSystem is used to resolve dependency issues with different Jobs writing and using the same component types. But specifically, it looks like EntityQuery is what I want to use. Though, I'm still unsure, as it looks like it gets all components of one type, and I already have a NativeArray of the specific entities I want to use.
You still use EntityQuery in the JobComponentSystem
An example:
public class RotationSpeedSystem : JobComponentSystem
{
private EntityQuery m_Query;
protected override void OnCreate()
{
m_Query = GetEntityQuery(typeof(RotationQuaternion), ComponentType.ReadOnly<RotationSpeed>());
}
//…
}
Answer by andrew-lukasik · Nov 18, 2020 at 08:32 PM
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 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[i];
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 );
}
}
Thanks! This helped out a bit.
I'm still confused though - how do these SystemBase Jobs know which entities to operate on? Is there any way I can define that with more control, as well as when I want it to happen? For example, if I only want to begin changing velocities after a few seconds.
Additionally, is there a way to serialize the values being used for the $$anonymous$$imum velocity and random velocity, and pass them to these jobs?
Thanks again!
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.
Ah, okay. Is there a way to narrow that down? Would I have to define a custom component, add it to the entities, and use that component in that function?
For example, if I only want to begin changing velocities after a few seconds.
Yes. I present to you power of (drum roll) If
statement:
protected override void OnUpdate ()
{
if( Time.ElapsedTime>3f )
{
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();
}
else { /* no job is scheduled so nothing happens */ }
}
What @myzzie said. SystemBase creates implicit EntityQuery-ies based on what components did you use in lambda expressions (`ref` prefix means read+write access,how do these SystemBase Jobs know which entities to operate on?
in
means read only access).