[solved] Get a variable from a job when job is completed

Hey Guys!

Simple situation: I have a bunch of entities (let’s say 2000) that are updated in position in a job (IJobChunk) moving towards a target position, works fine so far. The only thing I need to know is if at least one of these entities is farther away than x from its target position. The result should be a simple boolean true or false.

Here the (simplified) code:

NativeArray<bool> soldierfardistance=new NativeArray<bool>(1,Allocator.TempJob);
MoveSoldiersByChunk movejob = new()
 {
     _soldierfardistance = soldierfardistance
 };
 state.Dependency = movejob.ScheduleParallel(groupquery, state.Dependency);

public struct MoveSoldiersByChunk : IJobChunk {
    [NativeDisableContainerSafetyRestriction] public NativeArray<bool> _soldierfardistance;
    public void Execute(in ArchetypeChunk chunk, int entityInQueryIndex, bool useEnabledMask, in v128 chunkEnabledMask)
    {
        //...change positions, check distances
          float distancetotarget = math.distancesq(localtr.Position, soldiermovementdata.targetposition);
          if (distancetotarget > _fallingbehinddistance)
                  _soldierfardistance[0] = true;
    }
}

The problem here is that
a) if I read the results of _soldierfardistance immediately after starting the job the variable is always false (I guess that’s because the job did not even start or was not completed yet)
b) if I save the jobhandle & MoveSoldiersByChunk struct in a component and check a few frames later if the job was completed, the _soldierfardistance array is always null. And interestingly this is also the case if I change the allocator of the array from TempJob to Persistent.


if (groupmovementstats.soldierpositionsjobhandle.IsCompleted) {
    groupmovementstats.soldierpositionfaraway = groupmovementstats.soldierpositionsjobstruct._soldierfardistance[0];
    groupmovementstats.soldierpositionsjobstruct._soldierfardistance.Dispose();
}

Does anyone have a clue why the array always disappears and/or are there any other - maybe easier - solutions to that topic?

Thanks!

You can use a NativeReference<bool> to store this boolean value but you can only access this value if you complete the JobHandle or if the code that uses it is also a job that is scheduled after MoveSoldiersByChunk.

Thanks. Just tried this one and resulted in the same issue though:


The NativeReference is created as Allocater.Persistent and checked after the job was completed. The variables work within the job while writing, but it seems they are cleared immediately after the job is completed and/or cannot be accessed from outside a job anyways?

Make sure you properly store the array/reference. Null references shouldn’t happen out of the blue. If you’re unsure, show all the code related to the array/reference, like what field it’s stored in besides the copy for the job, where you assign it, and where you read it.

Also, you’ll want to call JobHandle.Complete() (even if the job handle has IsCompleted set) before accessing the array/reference to satisfy the safety system.

I might have misunderstood. I think using NativeArray was already in the right direction. The problem is just the usage after the job executes. Can you show the whole scheduling code and how you access the result? The pattern that I would use is like this:

void OnUpdate() {
    NativeArray<bool> soldierFarDistance = new(yourQuery.CalculateEntityLength(), Allocator.TempJob);

    // Schedule MoveSoldiersByChunk
    MoveSoldiersByChunk moveSoldiers = new() {
        _soldierFarDistance = soldierFarDistance,
        // Other variables that you need
    };
    this.Dependency = moveSoldiers.ScheduleParallel(yourQuery, this.Dependency);

    // To access the results of MoveSoldiersByChunk, you need to schedule another job
    // that uses the array, say an IJobFor
    AccessFarDistanceJob accessFarDistanceJob = new() {
        _soldierFarDistanceResults = soldierFarDistance,
        // Other variables that you need
    };
    this.Dependency = accessFarDistanceJob.ScheduleParallel(this.Dependency);

    // Don't forget to dispose the temporary array
    this.Dependency = soldierFarDistance.Dispose(this.Dependency);

    // Alternatively, you can Complete() then access soldierFarDistance directly, but this is not
    // advisable as this acts like a sync point
    this.Dependency.Complete();

    for(int i = 0; i < soldierFarDistance.Length; ++i) {
        // Do something with the result
    } 
} 

The code of calling the job (very complex, therefore all unneeded parts removed):

void CheckSoldierMovementsGroupwise(ref SystemState state, NativeParallelMultiHashMap <int,Entity> groupclusters, NativeParallelHashMap<Entity, BattleUnits_GroupMovementData> groupmovementdata,NativeParallelMultiHashMap<Entity,BattleUnits_GroupHistoricPositions> groupmovementdatahistory) {
    //...a lot of sorting and preparation stuff...    

    BattleUnits_GroupData groupdata = entitymanager.GetComponentData<BattleUnits_GroupData>(entity);
    EntityQuery groupquery = groupdata.soldiersformovement;
    BattleUnits_GroupMovementStats groupmovementstats = entitymanager.GetComponentData<BattleUnits_GroupMovementStats>(entity);
    bool newrun = true;
    
    //in BattleUnits_GroupMovementStats the handle of the job from prior run was saved, checking if completed, and if so -> read out the result
    if (groupmovementstats.soldierpositionsjobhandle != null) {
        if (groupmovementstats.soldierpositionsjobhandle.IsCompleted == false)
            newrun = false;
        else {
            groupmovementstats.soldierpositionsjobhandle.Complete();
            groupmovementstats.soldierpositionfaraway = groupmovementstats.soldierpositionsjobstruct._soldierfardistance.Value;
            groupmovementstats.soldierpositionsjobstruct._soldierfardistance.Dispose();
        }
    }                        
    BattleUnits_GroupMovementData groupmovementdatanew = groupmovementdata[entity]; 
    float deltatime = ((float)currenttime - groupmovementdatanew.lastsoldierupdate) * 1F * UnitController._debugunitspeedmodifier;
    
    //create the job assign all necessary variables
    MoveSoldiersByChunk movejob = new() {
        soldiermovementdatatypehandle = state.GetComponentTypeHandle<BattleUnits_SoldierMovementData>(),
        localtransformtypehandle = state.GetComponentTypeHandle<LocalTransform>(),
        _deltatime = deltatime,
        _groupmovementdata = groupmovementdata,
        _groupmovementdatahistory = groupmovementdatahistory,
        _maxhistorypoints = UnitController.historicgrouppositionsamount,
        _fallingbehinddistance = UnitController.soldierspeedmultiplierdistance,
        _fallingbehindspeedadjustment = UnitController.soldierspeedmultiplierfallenbehind,
        _movementspeedvariation = UnitController.soldiermovementspeedvariation,
        _randomnumbergenerators = randomnumbergenerators,
        _terrainheightdata = terrainheightdata,
        _soldierfardistance=new NativeReference<bool>(Allocator.Persistent),
        _soldierfardistance = soldierpositionsfarawayarray
    };
    state.Dependency = movejob.ScheduleParallel(groupquery, state.Dependency);
    
    //update time of last group movement
    groupmovementdatanew.lastsoldierupdate = (float)currenttime;
    entitymanager.SetComponentData<BattleUnits_GroupMovementData>(entity, groupmovementdatanew);
    
    //if the prior job was completed, save the variables for a new run to the BattleUnits_GroupMovementStats component
    if (newrun){
        groupmovementstats.soldierpositionsjobstruct = movejob;
        groupmovementstats.soldierpositionsjobhandle = state.Dependency;
        entitymanager.SetComponentData<BattleUnits_GroupMovementStats>(entity, groupmovementstats);
    }
}

And here the MoveSoldiersByChunk job, again unneeded stuff removed:

public struct MoveSoldiersByChunk : IJobChunk {
    public ComponentTypeHandle<BattleUnits_SoldierMovementData> soldiermovementdatatypehandle;
    public ComponentTypeHandle<LocalTransform> localtransformtypehandle;
    [ReadOnly] public float _deltatime;
    [ReadOnly] public NativeParallelHashMap<Entity, BattleUnits_GroupMovementData> _groupmovementdata;
    [ReadOnly] public NativeParallelMultiHashMap<Entity, BattleUnits_GroupHistoricPositions> _groupmovementdatahistory;
    [ReadOnly] public int _maxhistorypoints;
    [ReadOnly] public float _movementspeedvariation;
    [ReadOnly] public float _fallingbehinddistance;
    [ReadOnly] public float _fallingbehindspeedadjustment;
    [Unity.Collections.LowLevel.Unsafe.NativeDisableContainerSafetyRestriction] public NativeArray<Random> _randomnumbergenerators;
    [Unity.Collections.LowLevel.Unsafe.NativeSetThreadIndex] int threadid;
    [ReadOnly] public TerrainHeightData _terrainheightdata;
    [NativeDisableContainerSafetyRestriction] public NativeReference<bool> _soldierfardistance;

    public void Execute(in ArchetypeChunk chunk, int entityInQueryIndex, bool useEnabledMask, in v128 chunkEnabledMask)
    {
        NativeArray<BattleUnits_SoldierMovementData> soldiermovementdatas= chunk.GetNativeArray<BattleUnits_SoldierMovementData>(ref soldiermovementdatatypehandle);
        NativeArray<LocalTransform> localtransforms = chunk.GetNativeArray<LocalTransform>(ref localtransformtypehandle);

        for (int i = 0; i < soldiermovementdatas.Length; i++) {
            BattleUnits_SoldierMovementData soldiermovementdata = soldiermovementdatas[i];
            LocalTransform localtr = localtransforms[i];

           //...a lot of interpolation and calculation stuff...

            float distancetotarget = math.distancesq(localtr.Position, soldiermovementdata.targetposition);
            if (distancetotarget > _fallingbehinddistance)
            {
                speedadjustment = _fallingbehindspeedadjustment;
                _soldierfardistance.Value = true;
            }
            //...a lot of position and rotation setting...
            soldiermovementdatas[i] = soldiermovementdata;
            localtransforms[i] = localtr;
        }
        soldiermovementdatas.Dispose();
        localtransforms.Dispose();
    }
}

The idea behind it is to let the job run 1-2 frames, and check (before starting new jobs) if the old one was completed (to read out the _soldierfardistance variable), no matter if further jobs are created. I need to have the value of this variable not every frame, so jumping over 2-3 jobs is ok. Of course I would need to get rid of the persistent allocations, but this seems not to be the issue at the moment…

Line 10 in your first snippet:

    if (groupmovementstats.soldierpositionsjobhandle != null) {

Comparing to null here is wrong. It will use System.Nullable<T> (for making value type nullable, e.g. int?) and the comparison ends up being comparing a non-null value to a null value, which will always be true.
The correct (general) expression to use for the condition is !jobHandle.Equals(default).

Wow, that was it! Of course it ran into the execution by that, even if the job did not exist! Thanks for helping out!

The usage is weird to be honest. I’m not even sure that it’s a good idea to let the job run 1-2 frames. When I started using DOTS, I thought that this was a good idea but it was unwieldy and unreliable in my experience. The way I think of it now is that every scheduled job that you chain in a system’s or SystemState’s dependency are expected to be completed on the same frame. It’s either you schedule or you don’t. I avoid scheduling jobs where I expect the result in later frames. I don’t think that’s what it was made for. It also makes your code easier to reason about when you avoid thinking about it this way.

In your case, just refactor it to schedule every frame. If you did everything right and the job runs in parallel, it should be fast enough. Once you do this, you can try scheduling this job only once every 2 frames. You still process the result in the same frame but you only do it every 2 frames.

1 Like