DOTS NativeContainers error

Hello there! I have (another :D) a question regarding dots. I have this quite simple script where using jobs and native containers i activate and load chunks based on location, transfer them to one nativelist, then from that one into another, but when i test the .Length of the lists, the first one works fine, but when i change the coordinates at run time, instead of being the same number (adding the new chunks, removing the old) it adds the new ones to the old ones and then lower the total number by 1 every frame. Heres my entire script:


NOTE: In the code I have added if function with two different float2 positions and debug.log with the finalised nativelists for the purposes of testing/debugging. Also i will turn this monobehavior into component system in the fututre, but i am using mono for easier testing.


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Collections;
using Unity.Mathematics;
using Unity.Entities;
using Unity.Jobs;
using Unity.Burst;
using static WorldUtils;

public class WorldMaster : MonoBehaviour
{
    public float2 Position;
    public int ChunkSize = 20;
    public int LoadDistance = 40;

    NativeList<int2> ActiveChunks;
    NativeList<int2> LoadedChunks;

    private void Start()
    {
        ActiveChunks = new NativeList<int2>(0, Allocator.Persistent);
        LoadedChunks = new NativeList<int2>(0, Allocator.Persistent);
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.G))
        {
            Position = new float2(500, 500);
        }else if (Input.GetKeyDown(KeyCode.H))
        {
            Position = new float2(0, 0);
        }

        ActiveChunks.Clear();

        JobHandle activate = ActivateChunks(Position, ChunkSize, LoadDistance, ActiveChunks);
        activate.Complete();

        JobHandle load = LoadChunks(ActiveChunks, LoadedChunks);
        load.Complete();
        Debug.Log(ActiveChunks.Length + " " + LoadedChunks.Length);
    }

    public JobHandle ActivateChunks(float2 Position, int ChunkSize, int LoadDistance, NativeList<int2> ActiveChunks)
    {
        ActivateChunksJob job = new ActivateChunksJob();
        job.Position = Position;
        job.ChunkSize = ChunkSize;
        job.LoadDistance = LoadDistance;
        job.ActiveChunks = ActiveChunks;
        return job.Schedule();
    }

    public JobHandle LoadChunks(NativeList<int2> ActiveChunks, NativeList<int2> LoadedChunks)
    {
        LoadChunksJob job = new LoadChunksJob();
        job.ActiveChunks = ActiveChunks;
        job.LoadedChunks = LoadedChunks;
        return job.Schedule();
    }
}

[BurstCompile]
public struct ActivateChunksJob : IJob
{
    public float2 Position;
    public int ChunkSize;
    public int LoadDistance;

    int2 chunk;
    public NativeList<int2> ActiveChunks;

    public void Execute()
    {
        for (int x = 0; x < LoadDistance; x++)
        {
            for (int y = 0; y < LoadDistance; y++)
            {
                chunk = GetChunkFromWorld(Position, ChunkSize, new float2(0, 0)) - LoadDistance / 2 + new int2(x, y);
                if (!ActiveChunks.Contains(chunk))
                {
                    ActiveChunks.Add(chunk);
                }
            }
        }
    }
}

[BurstCompile]
public struct LoadChunksJob : IJob
{
    public NativeList<int2> ActiveChunks;
    public NativeList<int2> LoadedChunks;

    public void Execute()
    {
        foreach (int2 chunk in ActiveChunks)
        {
            if (!LoadedChunks.Contains(chunk))
            {
                LoadedChunks.Add(chunk);
            }
        }
        foreach (int2 chunk in LoadedChunks)
        {
            if (!ActiveChunks.Contains(chunk))
            {
                LoadedChunks.RemoveAt(LoadedChunks.IndexOf(chunk));
            }
        }
    }
}

I am using my own tiny library of code (thats that using static WorldUtils), from which i am using only one function in this code: GetChunkFromWorld. I am putting the function here as well:


public static int2 GetChunkFromWorld(float2 WorldPosition, int ChunkSize, float2 OriginPosition)
    {
        int2 ChunkPosition;
        ChunkPosition.x = (int)(math.floor((WorldPosition.x - OriginPosition.x) / ChunkSize));
        ChunkPosition.y = (int)(math.floor((WorldPosition.y - OriginPosition.y) / ChunkSize));
        return ChunkPosition;
    }

So, if you would help me debug this code, i would be very happy. Also sorry for spamming the forum. Many thanks :slight_smile:

Hey, nice job! While I fail to reproduce issue you described I noticed something different:

foreach( int2 chunk in LoadedChunks )
    if( !ActiveChunks.Contains(chunk) )
        LoadedChunks.RemoveAt( LoadedChunks.IndexOf(chunk) );

This code gives me compiler warning ( entities 0.16.0-preview.21 here). And I suspect, on your machine, this code is not throwing anything (or maybe is? not sure) because it’s being auto-magically replaced with standard for loop syntax:

for( int i=0 ; i<LoadedChunks.Length ; i++ )
{
    int2 chunk = LoadedChunks*;
    if( !ActiveChunks.Contains(chunk) )
        LoadedChunks.RemoveAt( LoadedChunks.IndexOf(chunk) );
}

Which is bad because it obfuscate what’s happening here really: some indices are being skipped in the process. Visually producing this pattern:
some cells not unloading immediately
To fix this replace it with for loop that counts indices from last to first:

for( int i=LoadedChunks.Length-1 ; i!=-1 ; i-- )
{
    int2 chunk = LoadedChunks*;
    if( !ActiveChunks.Contains(chunk) )
        LoadedChunks.RemoveAt( LoadedChunks.IndexOf(chunk) );
}

This is the proper way of removing items from a list while in a loop.

includes code for this mouse-operated debug gizmo:

 using UnityEngine;
 using Unity.Collections;
 using Unity.Mathematics;
 using Unity.Entities;
 using Unity.Jobs;
 
 public class WorldMaster : MonoBehaviour
 {
	[SerializeField] float2 _position;
	[SerializeField] float _chunkSize = 20;
	[SerializeField] int _loadDistance = 40;
	NativeList<int2> _activeChunks;
	NativeList<int2> _loadedChunks;
	JobHandle _dependency;
 	void Start ()
 	{
		_activeChunks = new NativeList<int2>(0, Allocator.Persistent);
		_loadedChunks = new NativeList<int2>(0, Allocator.Persistent);
 	}
 	void Update ()
 	{
		_dependency.Complete();
		// Debug.Log($"num active:{_activeChunks.Length}, num loaded:{_loadedChunks.Length}");
 
		_dependency = ActivateChunks( _position , _chunkSize , _loadDistance , _activeChunks );
		_dependency = LoadChunks( _activeChunks , _loadedChunks , _dependency );
 	}
 	void OnDestroy ()
 	{
		_activeChunks.Dispose();
		_loadedChunks.Dispose();
 	}
	#if UNITY_EDITOR
 	void OnDrawGizmos ()
 	{
 		if( !Application.isPlaying ) return;
 
		_dependency.Complete();
 
 		Gizmos.color = new Color{ r=0 , g=1 , b=1 , a=0.3f };
		foreach( int2 i2 in _loadedChunks )
 			Gizmos.DrawCube( new Vector3{ x=i2.xchunkSize + _chunkSize*0.5f , z=i2.ychunkSize + _chunkSize*0.5f } , new Vector3{ x=_chunkSize , z=_chunkSize } );
 
 		Gizmos.color = new Color{ r=1 , g=0.92f , b=0.016f , a=0.3f };
		foreach( int2 i2 in _activeChunks )
 			Gizmos.DrawWireCube( new Vector3{ x=i2.xchunkSize + _chunkSize*0.5f , z=i2.ychunkSize + _chunkSize*0.5f } , new Vector3{ x=_chunkSize , z=_chunkSize } );
 
 		Gizmos.color = Color.red;
 		Gizmos.DrawWireSphere( new Vector3{ x=_position.x , z=_position.y }  , _chunkSize*0.1f );
 
 		{
 			Ray ray = UnityEditor.HandleUtility.GUIPointToWorldRay( Event.current.mousePosition );
 			new Plane( inNormal:Vector3.up , inPoint:Vector3.zero ).Raycast( ray , out float dist );
 			float3 hit = ray.origin + ray.direction*dist;
			_position = new float2{ x=hit.x , y=hit.z };
 		}
 	}
 	#endif
 	public JobHandle ActivateChunks ( float2 position , float chunkSize , int loadDistance , NativeList<int2> activeChunks , JobHandle dependency = default(JobHandle) )
 	{
 		ActivateChunksJob job = new ActivateChunksJob();
 		job.position = position;
 		job.chunkSize = chunkSize;
 		job.loadDistance = loadDistance;
 		job.activeChunks = activeChunks;
 		return job.Schedule( dependency );
 	}
 	public JobHandle LoadChunks ( NativeList<int2> activeChunks , NativeList<int2> loadedChunks , JobHandle dependency = default(JobHandle) )
 	{
 		LoadChunksJob job = new LoadChunksJob();
 		job.activeChunks = activeChunks;
 		job.loadedChunks = loadedChunks;
 		return job.Schedule( dependency );
 	}
 }
 public struct ActivateChunksJob : IJob
 {
 	public float2 position;
 	public float chunkSize;
 	public int loadDistance;
 	public NativeList<int2> activeChunks;
 	void IJob.Execute ()
 	{
 		activeChunks.Clear();
 		for( int x=0 ; x<loadDistance ; x++ )
 		for( int y=0 ; y<loadDistance ; y++ )
 		{
 			int2 chunk = GetChunkFromWorld( position , chunkSize ) + new int2{ x = x - loadDistance/2 , y = y - loadDistance/2 };
 			if( !activeChunks.Contains(chunk) )
 				activeChunks.Add( chunk );
 		}
 	}
 	int2 GetChunkFromWorld ( float2 worldPosition , float chunkSize )
 		=> (int2) math.floor( worldPosition/chunkSize );
 }
 public struct LoadChunksJob : IJob
 {
 	public NativeList<int2> activeChunks;
 	public NativeList<int2> loadedChunks;
 	void IJob.Execute ()
 	{
 		for( int i=0 ; i<activeChunks.Length ; i++ )
 		{
 			int2 chunk = activeChunks*;
 			if( !loadedChunks.Contains(chunk) )
 				loadedChunks.Add(chunk);
 		}
 		for( int i=loadedChunks.Length-1 ; i!=-1 ; i-- )
 		{
 			int2 chunk = loadedChunks*;
 			if( !activeChunks.Contains(chunk) )
 				loadedChunks.RemoveAt( loadedChunks.IndexOf(chunk) );
 		}
 	}
 }