Need suggestions Optimizing Pathfinding in unity ecs

My program struggles to meet get near the performance of this tutorial

using System;
using System.Diagnostics;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.VisualScripting;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.Experimental.GlobalIllumination;
using static PathfindingSystemChunk;

//[ExtensionOfNativeClass]
[BurstCompile]
public partial class PathfindingSystemChunk : SystemBase
{
    /// <summary>
    /// Y is actualy the Z big important
    /// and thanks code monkey :)
    /// </summary>
    //private static float lastCallTime = 0f;
    private const int moveStraightCost = 10;
    private const int moveDiagonalCost = 14;
    private const int frameBudget = 40;
    [ReadOnly] private const int size = 200;
    /*    private void Start()
        {
            //FindPath(new int2(0, 0), new int2(3, 3));
            FindPathJob findPathJob = new FindPathJob()
            {
                startPosition = new int2(0,0),
                endPosition = new int2(3,3),
            };
            findPathJob.Schedule();
        }*/
    public void OnCreate(ref SystemState state)
    {
        //LastCallTime = SystemAPI.Time.ElapsedTime;
    }
    public void OnUpdate(ref SystemState state)
    {

        /*NativeList<JobHandle> jobHandleList = new NativeList<JobHandle>(100,Allocator.Temp);
        Entities
        };
        findPathJob.ScheduleParallel();
            .ForEach((Entity entity, ref LocalTransform transform, ref Pather pather, ref DynamicBuffer<PathPosition> pathBuffer) =>
        {
            
            if (pather.needsUpdate)
            {

                //DynamicBuffer<PathPosition> pathBuffer = pather.pathBuffer;
                //pather.needsUpdate = false;
                //for (int i = 0; i < 0; i++)
                //{

                    //JobHandle jobHandle = findPathJob.ScheduleSingle(entity);
                //
                    //jobHandleList.Add(findPathJob.Schedule());
                //dependency = jobHandleList[jobHandleList.Length];
                //}

                //job.Complete();
            }
        }).Run();
        JobHandle.CompleteAll(jobHandleList.AsArray());
        //JobHandle.CompleteAll(jobHandleList.AsArray());
        jobHandleList.Dispose();*/
    }

    protected override void OnUpdate()
    {
        int budget = frameBudget;
        NativeList<JobHandle> jobList = new NativeList<JobHandle> (Allocator.Temp);
        Entities
            .ForEach((Entity entity, ref LocalTransform transform, ref Pather pather, ref DynamicBuffer<PathPosition> pathBuffer) =>
            {
                if (budget > 0)
                {
                    //UnityEngine.Debug.Log("pathing sorta run");
                    UpdatePathsSegementJob updatePathsSegementJob = new UpdatePathsSegementJob()
                    {
                        pathPositionBuffer = pathBuffer,
                        startPositionWorld = new int2((int)transform.Position.x, (int)transform.Position.z),
                        endPositionWorld = pather.destination,
                        gridSize = size,
                    };
                    
                    jobList.Add(updatePathsSegementJob.Schedule());
                    budget--;
                }
            }).Run();
        JobHandle.CombineDependencies(jobList.AsArray());
        JobHandle.CompleteAll(jobList.AsArray());
        jobList.Dispose();
    }
    
    [BurstCompile]
    public partial struct UpdatePathsSegementJob : IJob
    {
        [NativeDisableContainerSafetyRestriction]
        public DynamicBuffer<PathPosition> pathPositionBuffer;
        public int2 startPositionWorld;
        public int2 endPositionWorld;
        [ReadOnly]public int gridSize;
        [BurstCompile]
        public void Execute()
        {
            // throwing if no buffer is on that entity.
            //UnityEngine.Debug.Log("hi");
            int2 startPosition = ToGrid(startPositionWorld, gridSize);
            int2 endPosition = ToGrid(endPositionWorld, gridSize);
            if (!IsPositionInsideGrid(startPosition, gridSize) || !IsPositionInsideGrid(endPosition, gridSize))
            {
                return;
            }
            NativeArray<PathNode> pathNodeArray = new NativeArray<PathNode>(gridSize * gridSize, Allocator.Temp);

            for (int x = 0; x < gridSize; x++)
            {
                for (int y = 0; y < gridSize; y++)
                {
                    PathNode pathNode = new PathNode();
                    pathNode.x = x;
                    pathNode.y = y;
                    pathNode.index = CalculateIndex(x, y, gridSize);
                    pathNode.gCost = int.MaxValue;
                    pathNode.hCost = CalculateDistanceCost(new int2(x, y), endPosition);
                    pathNode.isWalkable = true;
                    pathNode.cameFromNodeIndex = -1;
                    pathNode.CalculateFCost();
                    pathNodeArray[pathNode.index] = pathNode;
                }
            }
            PathNode pathNodeNonWalkable = pathNodeArray[CalculateIndex(1, 1, gridSize)];
            pathNodeNonWalkable.SetIsWalkable(false);
            pathNodeArray[CalculateIndex(1, 1, gridSize)] = pathNodeNonWalkable;

            NativeArray<int2> neighborOffsetArray = new NativeArray<int2>(8, Allocator.Temp);
            neighborOffsetArray[0] = new int2(-1, 0);
            neighborOffsetArray[1] = new int2(0, 1);
            neighborOffsetArray[2] = new int2(1, 0);
            neighborOffsetArray[3] = new int2(0, -1);
            neighborOffsetArray[4] = new int2(-1, 1);
            neighborOffsetArray[5] = new int2(1, 1);
            neighborOffsetArray[6] = new int2(1, -1);
            neighborOffsetArray[7] = new int2(-1, -1);

            int endNodeIndex = CalculateIndex(endPosition.x, endPosition.y, gridSize);
            PathNode startNode = pathNodeArray[CalculateIndex(startPosition.x, startPosition.y, gridSize)];
            startNode.gCost = 0;
            startNode.CalculateFCost();
            pathNodeArray[startNode.index] = startNode;
            NativeList<int> openList = new NativeList<int>(Allocator.Temp);
            NativeList<int> closedList = new NativeList<int>(Allocator.Temp);

            openList.Add(startNode.index);

            while (openList.Length > 0)
            {
                int currentNodeIndex = GetLowestCostFNodeIndex(openList, pathNodeArray);
                PathNode currentNode = pathNodeArray[currentNodeIndex];
                if (currentNodeIndex == endNodeIndex)
                {
                    //we reached the destination :))))))))
                    break;
                }
                // removeing current node from open
                for (int i = 0; i < openList.Length; i++)
                {
                    if (openList[i] == currentNodeIndex)
                    {
                        openList.RemoveAtSwapBack(i);
                        break;
                    }
                }
                closedList.Add(currentNodeIndex);

                for (int i = 0; i < neighborOffsetArray.Length; i++)
                {
                    int2 neighbourOffset = neighborOffsetArray[i];
                    int2 neighbourPosition = new int2(currentNode.x + neighbourOffset.x, currentNode.y + neighbourOffset.y);
                    if (!IsPositionInsideGrid(neighbourPosition, gridSize))
                    {
                        //not in da grid
                        continue;
                    }
                    int neighbourNodeIndex = CalculateIndex(neighbourPosition.x, neighbourPosition.y, gridSize);
                    if (closedList.Contains(neighbourNodeIndex))
                    {
                        // we already seached this node
                        continue;
                    }
                    PathNode neighbourNode = pathNodeArray[neighbourNodeIndex];
                    if (!neighbourNode.isWalkable)
                    {
                        //no go is obstacle
                        continue;
                    }
                    int2 CurrentNodePosition = new int2(currentNode.x, currentNode.y);
                    int tentativeGCost = currentNode.gCost + CalculateDistanceCost(CurrentNodePosition.x, CurrentNodePosition.y);
                    if (tentativeGCost < neighbourNode.gCost)
                    {
                        neighbourNode.cameFromNodeIndex = currentNodeIndex;
                        neighbourNode.gCost = tentativeGCost;
                        neighbourNode.CalculateFCost();
                        // actualy setting the array from the "sample"
                        pathNodeArray[neighbourNodeIndex] = neighbourNode;
                        if (!openList.Contains(neighbourNode.index))
                        {
                            openList.Add(neighbourNode.index);
                        }
                    }
                }
            }
            pathPositionBuffer.Clear();
            PathNode endNode = pathNodeArray[endNodeIndex];
            if (endNode.cameFromNodeIndex == -1)
            {
                // no path :(
            }
            else
            {
                CalculatePath(pathNodeArray, endNode, pathPositionBuffer);

                //path.Dispose();
                // found one :)
            }

            pathNodeArray.Dispose();
            openList.Dispose();
            closedList.Dispose();
            neighborOffsetArray.Dispose();
        }
        [BurstCompile]
        private int2 ToGrid(int2 pos,int size)
        {
            return pos + new int2(size / 2, size / 2);
        }
        [BurstCompile]
        private int2 ToWorld(int2 pos,int size)
        {
            return pos - new int2(size / 2, size / 2);
        }
        [BurstCompile]
        private void CalculatePath(NativeArray<PathNode> pathNodeArray, PathNode endNode, DynamicBuffer<PathPosition> pathPositionBuffer)
        {
            if (endNode.cameFromNodeIndex == -1)
            {
                // no path :(

            }
            else
            {

                // found one :)
                //adding fist element of end
                pathPositionBuffer.Add(new PathPosition
                {
                    position = ToWorld(new int2(endNode.x, endNode.y), gridSize),
                });
                //path.Add(new int2(endNode.x, endNode.y));
                PathNode currentNode = endNode;
                //tracking back from the end
                while (currentNode.cameFromNodeIndex != -1)
                {

                    PathNode cameFromNode = pathNodeArray[currentNode.cameFromNodeIndex];
                    pathPositionBuffer.Add(new PathPosition
                    {
                        position = ToWorld(new int2(cameFromNode.x, cameFromNode.y), gridSize),
                    });
                    //path.Add(new int2(cameFromNode.x, cameFromNode.y));
                    currentNode = cameFromNode;
                }
            }
        }
        [BurstCompile]
        private bool IsPositionInsideGrid(int2 gridPosition, int2 gridSize)
        {
            return
                gridPosition.x >= 0 &&
                gridPosition.y >= 0 &&
                gridPosition.x < gridSize.x &&
                gridPosition.y < gridSize.y;
        }
        [BurstCompile]
        private int CalculateIndex(int x, int z, int gridWidth)
        {
            return x + z * gridWidth;
        }
        [BurstCompile]
        private int GetLowestCostFNodeIndex(NativeList<int> openList, NativeArray<PathNode> pathNodeArray)
        {
            PathNode lowestCostPathNode = pathNodeArray[openList[0]];
            for (int i = 1; i < openList.Length; i++)
            {
                PathNode testPathNode = pathNodeArray[openList[i]];
                if (testPathNode.fCost < lowestCostPathNode.fCost)
                {
                    lowestCostPathNode = testPathNode;
                }
            }
            return lowestCostPathNode.index;
        }
        [BurstCompile]
        private int CalculateDistanceCost(int2 aPosition, int2 bPosition)
        {
            int xDistance = math.abs(aPosition.x - bPosition.x);
            int yDistance = math.abs(aPosition.y - bPosition.y);
            int remaining = math.abs(xDistance - yDistance);
            return moveDiagonalCost * math.min(xDistance, yDistance) + moveStraightCost * remaining;
        }
        [BurstCompile]
        private struct PathNode
        {
            public int x;
            public int y;

            public int index;

            public int gCost;
            public int hCost;
            public int fCost;

            public bool isWalkable;

            public int cameFromNodeIndex;

            public void CalculateFCost()
            {
                fCost = gCost + hCost;
            }
            public void SetIsWalkable(bool value)
            {
                isWalkable = value;
            }
        }
    }
    // Start is called before the first frame update
}

Bugs

This is a serious bug in your code. If you’re not sure how come you missed it, read my old rant on Unity.Mathematics’ cursed implicit cast bonanza here.
Unintentionally miss-calculated h costs affects performance significantly (either lowering or skewing it in wrong direction), because wrong cost values result in search heuristic not working properly and searching too many grid cells (the primary performance factor in search).

Achilles heel of grid-based pathfinding

You need to know that grid-based pathfinding algorithm works fast for small grids (<100 size) and will always tank it’s performance very fast when going for >100 size grids. This is because size dictates the maximum number of cells (which is size²) that must be searched (in worst-case scenario) and it increases geometrically i.e. double the size - quadruple the time (!).

grid size 30 60 120 240
max num cells 900 3600 14400 57600

This is why spatially sparse graph-based & navmesh solutions are so advantageous and popular.

Refactor

I attempted to refactor your code a bit. Haven’t tested it nor even compiled so I broke something in there for sure. Consider it a collection of early suggestions.

Reasons for these changes I gave in this post here, while answering similar question: c# - Pathfinding with Unity Jobs and Burst is slower than without - Stack Overflow

The main point was to get rid of PathNode struct (32 bytes in this case) and replace it with dedicated arrays. Got rid of neighborOffsetArray as well (less things to read from memory in loops - the better). Optimized how arrays are initialized (i.e. separately and in faster index order).

This should make the code a little bit faster, but not that much, 1-3x times max maybe. Optimizing this code to it’s limits is a longer endeavor that requires many tweaks and meticulous attention to Profiler measurements after each one.

using Unity.Mathematics;
using Unity.Entities;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;
using Unity.Transforms;
using Unity.Profiling;

[RequireMatchingQueriesForUpdate]
[BurstCompile]
[UpdateInGroup( typeof(FixedStepSimulationSystemGroup) )]
public partial struct PathfindingSystem : ISystem
{
	[BurstCompile]
	public void OnUpdate ( ref SystemState state )
	{
        var pathfindingJob = new UpdatePathsSegmentJob{};
        state.Dependency = pathfindingJob.Schedule( state.Dependency );
    }
    
    [WithChangeFilter( typeof(Pather) )]// job runs only when Pather component was accessed in RW/ref mode (potentially modified) somewhere previous frame
    [BurstCompile]
    public partial struct UpdatePathsSegmentJob : IJobEntity
    {
        const int k_move_straight_cost = 10;
        const int k_move_diagonal_cost = 14;
        const int k_grid_size = 200;
        public void Execute ( ref DynamicBuffer<PathPosition> pathPositionBuffer , in LocalTransform transform , in Pather pather )
        {
            var ___initialization = new ProfilerMarker("initialization");
            ___initialization.Begin();

            int2 startPositionWorld  = new int2( (int)transform.Position.x , (int)transform.Position.z );
            int2 endPositionWorld    = pather.destination;
            int2 startPosition = ToGrid( startPositionWorld , k_grid_size );
            int2 endPosition = ToGrid( endPositionWorld , k_grid_size );
            if( !IsPositionInsideGrid(startPosition,k_grid_size) || !IsPositionInsideGrid(endPosition,k_grid_size) )
            {
                return;
            }
            int numGridNodes = k_grid_size * k_grid_size;
            NativeBitArray isWalkable = new ( numGridNodes , Allocator.Temp );
            {
                for( int i=0 ; i<numGridNodes ; i++ ) isWalkable.Set( i , true );
            }
            NativeArray<int> G = new ( numGridNodes , Allocator.Temp );
            {
                for( int i=0 ; i<numGridNodes ; i++ ) G[i] = int.MaxValue;
            }
            NativeArray<int> H = new ( numGridNodes , Allocator.Temp );
            {
                for( int i=0 ; i<numGridNodes ; i++ ) H[i] = int.MaxValue;
            }
            NativeArray<int> F = new ( numGridNodes , Allocator.Temp );
            {
                for( int i=0 ; i<numGridNodes ; i++ ) F[i] = int.MaxValue;
            }
            NativeArray<int> cameFromNodeIndex = new ( numGridNodes , Allocator.Temp );
            {
                for( int i=0 ; i<numGridNodes ; i++ ) cameFromNodeIndex[i] = -1;
            }
            
            NativeArray<int2> pathNodeArray = new ( numGridNodes , Allocator.Temp );
            for( int y=0 ; y<k_grid_size ; y++ )
            for( int x=0 ; x<k_grid_size ; x++ )
            {
                int index = CalculateIndex(x,y,k_grid_size);
                pathNodeArray[index] = new int2{
                    x = x ,
                    y = y ,
                };
            }
            NativeList<int> openList = new ( Allocator.Temp );
            NativeList<int> closedList = new ( Allocator.Temp );
            int endNodeIndex = CalculateIndex( endPosition.x , endPosition.y , k_grid_size );
            int startNodeIndex = CalculateIndex( startPosition.x , startPosition.y , k_grid_size );
            G[startNodeIndex] = 0;
            openList.Add( startNodeIndex );
            isWalkable.Set( CalculateIndex(1,1,k_grid_size) , false );

            ___initialization.End();
            var ___search = new ProfilerMarker("search");
            ___search.Begin();

            while( openList.Length>0 )
            {
                int currentNodeIndex = GetLowestCostFNodeIndex( openList:openList , F:F , pathNodeArray:pathNodeArray );
                int2 currentNode = pathNodeArray[currentNodeIndex];
                if (currentNodeIndex == endNodeIndex)
                {
                    //we reached the destination :))))))))
                    break;
                }
                // removeing current node from open
                for (int i = 0; i < openList.Length; i++)
                {
                    if (openList[i] == currentNodeIndex)
                    {
                        openList.RemoveAtSwapBack(i);
                        break;
                    }
                }
                closedList.Add(currentNodeIndex);

                for( int i=0 ; i<8 ; i++ )
                {
                    int2 neighbourOffset;
                    switch( i )
                    {
                        case 0: neighbourOffset = new int2(-1,-1);	break;// y-1
					    case 1:	neighbourOffset = new int2(0,-1);	break;
					    case 2:	neighbourOffset = new int2(+1,-1);	break;
					    case 3:	neighbourOffset = new int2(-1,0);	break;// y
					    case 4:	neighbourOffset = new int2(+1,0);	break;
					    case 5:	neighbourOffset = new int2(-1,+1);	break;// y+1
					    case 6:	neighbourOffset = new int2(0,+1);	break;
					    case 7:	neighbourOffset = new int2(+1,+1);	break;
                        default:    throw new System.ArgumentOutOfRangeException("valid range is 0-7");
                    }

                    int2 neighbourPosition = new int2( currentNode.x + neighbourOffset.x, currentNode.y + neighbourOffset.y);
                    if( !IsPositionInsideGrid(neighbourPosition,k_grid_size) )
                    {
                        //not in da grid
                        continue;
                    }
                    int neighbourNodeIndex = CalculateIndex(neighbourPosition.x, neighbourPosition.y, k_grid_size);
                    if( closedList.Contains(neighbourNodeIndex) )
                    {
                        // we already seached this node
                        continue;
                    }
                    int2 neighbourNode = pathNodeArray[neighbourNodeIndex];
                    if( !isWalkable.IsSet(neighbourNodeIndex) )
                    {
                        //no go is obstacle
                        continue;
                    }
                    int2 currentNodePosition = new int2( currentNode.x, currentNode.y);
                    int tentativeGCost = G[currentNodeIndex] + CalculateDistanceCost(currentNodePosition,neighbourPosition);
                    if( tentativeGCost<G[neighbourNodeIndex] )
                    {
                        cameFromNodeIndex[neighbourNodeIndex] = currentNodeIndex;
                        int g = tentativeGCost;
                        int h = CalculateDistanceCost( neighbourPosition , endPosition );
                        int f = g + h;
                        G[neighbourNodeIndex] = g;
                        H[neighbourNodeIndex] = h;
                        F[neighbourNodeIndex] = f;

                        // actualy setting the array from the "sample"
                        pathNodeArray[neighbourNodeIndex] = neighbourNode;
                        if( !openList.Contains(neighbourNodeIndex) )
                        {
                            openList.Add( neighbourNodeIndex );
                        }
                    }
                }
            }

            ___search.End();
            var ___finalization = new ProfilerMarker("finalization");
            ___finalization.Begin();

            pathPositionBuffer.Clear();
            int2 endNode = pathNodeArray[endNodeIndex];
            if (cameFromNodeIndex[endNodeIndex] == -1)
            {
                // no path :(
            }
            else
            {
                CalculatePath(
                    pathNodeArray:      pathNodeArray ,
                    endNode:            endNode ,
                    cameFromNodeIndex:  cameFromNodeIndex ,
                    pathPositionBuffer: pathPositionBuffer
                );

                // found one :)
            }

            ___finalization.End();
        }
        private int2 ToGrid( int2 pos,int size) => pos + new int2( size / 2, size / 2);
        private int2 ToWorld( int2 pos,int size) => pos - new int2( size / 2, size / 2);
        private void CalculatePath( NativeArray<int2> pathNodeArray , int2 endNode , NativeArray<int> cameFromNodeIndex , DynamicBuffer<PathPosition> pathPositionBuffer )
        {
            int endNodeIndex = CalculateIndex( endNode.x , endNode.y , k_grid_size );
            if( cameFromNodeIndex[endNodeIndex]==-1 )
            {
                // no path :(
            }
            else
            {
                // found one :)
                //adding fist element of end
                pathPositionBuffer.Add(new PathPosition
                {
                    position = ToWorld( new int2( endNode.x, endNode.y), k_grid_size ) ,
                });
                //path.Add(new int2(endNode.x, endNode.y));
                int2 currentNode = endNode;
                //tracking back from the end
                
                while( cameFromNodeIndex[CalculateIndex(currentNode.x,currentNode.y,k_grid_size)]!=-1 )
                {
                    int2 cameFromNode = pathNodeArray[cameFromNodeIndex[CalculateIndex(currentNode.x,currentNode.y,k_grid_size)]];
                    pathPositionBuffer.Add(new PathPosition
                    {
                        position = ToWorld( new int2( cameFromNode.x, cameFromNode.y), k_grid_size ) ,
                    });
                    //path.Add(new int2(cameFromNode.x, cameFromNode.y));
                    currentNode = cameFromNode;
                }
            }
        }
        bool IsPositionInsideGrid( int2 gridPosition, int2 gridSize) => gridPosition.x >= 0 && gridPosition.y >= 0 && gridPosition.x < gridSize.x && gridPosition.y < gridSize.y;
        int CalculateIndex (int x, int z, int gridWidth) => x + z * gridWidth;
        int GetLowestCostFNodeIndex( NativeList<int> openList , NativeArray<int> F , NativeArray<int2> pathNodeArray )
        {
            int2 lowestCostPathNode = pathNodeArray[openList[0]];
            for (int i = 1; i < openList.Length; i++)
            {
                int testPathNodeIndex = openList[i];
                int2 testPathNode = pathNodeArray[testPathNodeIndex];
                if( F[testPathNodeIndex]<F[CalculateIndex(lowestCostPathNode.x,lowestCostPathNode.y,k_grid_size)] )
                {
                    lowestCostPathNode = testPathNode;
                }
            }
            return CalculateIndex(lowestCostPathNode.x,lowestCostPathNode.y,k_grid_size);
        }
        int CalculateDistanceCost( int2 aPosition, int2 bPosition)
        {
            int xDistance = math.abs(aPosition.x - bPosition.x);
            int yDistance = math.abs(aPosition.y - bPosition.y);
            int remaining = math.abs(xDistance - yDistance);
            return k_move_diagonal_cost * math.min(xDistance, yDistance) + k_move_straight_cost * remaining;
        }
    }
}

Adventures in intentional H score modifications:

If you multiply h with a value >1 (around 1.5f is usually fine) it will make your heuristic search pattern more aggressive and, depending on input isWalkable data, will make this search job significantly faster (or potentially slower, in case of mazes and maze-like interior rooms).

int h = (int)( CalculateDistanceCost( neighbourPosition , endPosition ) * 1.5f );

Custom debugging tools

I strongly suggest you invest in creating a dedicated debugging tool. Without it you are walking in the dark here, having no idea what happens inside. VS debugging is ok-ish but code stepping line by line just won’t give you this level of immediate understanding.

68747470733a2f2f692e696d6775722e636f6d2f487346584147492e676966

68747470733a2f2f692e696d6775722e636f6d2f656e4b36554f732e676966

Thank you this solved my issue. This is an incredibly well formatted response. And only took 5 minutes to fix. On my mid range laptop I can run <6,000 paths per second at about 65fps.

Don’t run pathing continuously this is only for testing purposes.

using System;
using System.Diagnostics;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.VisualScripting;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
using UnityEngine.Experimental.GlobalIllumination;
using static PathfindingSystemChunk;
//[ExtensionOfNativeClass]
[BurstCompile]
public partial class PathfindingSystem : SystemBase
{
    /// <summary>
    /// Y is actualy the Z big important
    /// and thanks code monkey :)
    /// </summary>
    //private static float lastCallTime = 0f;
    private const int moveStraightCost = 10;
    private const int moveDiagonalCost = 14;
    private const int frameBudget = 50;
    [ReadOnly] private const int size = 200;
    /*    private void Start()
        {
            //FindPath(new int2(0, 0), new int2(3, 3));
            FindPathJob findPathJob = new FindPathJob()
            {
                startPosition = new int2(0,0),
                endPosition = new int2(3,3),
            };
            findPathJob.Schedule();
        }*/
    public void OnCreate(ref SystemState state)
    {
        //LastCallTime = SystemAPI.Time.ElapsedTime;
    }
    public void OnUpdate(ref SystemState state)
    {

        /*NativeList<JobHandle> jobHandleList = new NativeList<JobHandle>(100,Allocator.Temp);
        Entities
        };
        findPathJob.ScheduleParallel();
            .ForEach((Entity entity, ref LocalTransform transform, ref Pather pather, ref DynamicBuffer<PathPosition> pathBuffer) =>
        {
            
            if (pather.needsUpdate)
            {

                //DynamicBuffer<PathPosition> pathBuffer = pather.pathBuffer;
                //pather.needsUpdate = false;
                //for (int i = 0; i < 0; i++)
                //{

                    //JobHandle jobHandle = findPathJob.ScheduleSingle(entity);
                //
                    //jobHandleList.Add(findPathJob.Schedule());
                //dependency = jobHandleList[jobHandleList.Length];
                //}

                //job.Complete();
            }
        }).Run();
        JobHandle.CompleteAll(jobHandleList.AsArray());
        //JobHandle.CompleteAll(jobHandleList.AsArray());
        jobHandleList.Dispose();*/
    }

    [BurstCompile]
    protected override void OnUpdate()
    {
        int budget = frameBudget;
        NativeList<JobHandle> jobList = new NativeList<JobHandle>(frameBudget, Allocator.Temp);
        Entities
            .ForEach((Entity entity, ref LocalTransform transform, ref Pather pather, ref DynamicBuffer<PathPosition> pathBuffer) =>
            {
                if (budget > 0)
                {
                    //UnityEngine.Debug.Log("pathing sorta run");
                    UpdatePathsSegementJob updatePathsSegementJob = new UpdatePathsSegementJob()
                    {
                        pathPositionBuffer = pathBuffer,
                        startPositionWorld = new int2((int)transform.Position.x, (int)transform.Position.z),
                        endPositionWorld = pather.destination,
                        gridSize = size,
                    };
                    jobList.Add(updatePathsSegementJob.Schedule());
                    budget--;
                }
            }).Run();
        //JobHandle.CombineDependencies(jobList.AsArray());
        //JobHandle.CompleteAll(jobList.AsArray());
        jobList.Dispose();
    }

    [BurstCompile]
    public partial struct UpdatePathsSegementJob : IJob
    {
        [NativeDisableContainerSafetyRestriction]
        public DynamicBuffer<PathPosition> pathPositionBuffer;
        public int2 startPositionWorld;
        public int2 endPositionWorld;
        [ReadOnly] public int gridSize;
        const int k_move_straight_cost = 10;
        const int k_move_diagonal_cost = 14;
        const int k_grid_size = 256;
        public void Execute()
        {
            // throwing if no buffer is on that entity.
            //UnityEngine.Debug.Log("hi");
            int2 startPosition = ToGrid(startPositionWorld, gridSize);
            int2 endPosition = ToGrid(endPositionWorld, gridSize);
            NativeArray<PathNode> pathNodeArray = new NativeArray<PathNode>(gridSize * gridSize, Allocator.Temp);

            for (int x = 0; x < gridSize; x++)
            {
                for (int y = 0; y < gridSize; y++)
                {
                    PathNode pathNode = new PathNode();
                    pathNode.x = x;
                    pathNode.y = y;
                    pathNode.index = CalculateIndex(x, y, gridSize);
                    pathNode.gCost = int.MaxValue;
                    pathNode.hCost = CalculateDistanceCost(new int2(x, y), endPosition);
                    pathNode.isWalkable = true;
                    pathNode.cameFromNodeIndex = -1;
                    pathNode.CalculateFCost();
                    pathNodeArray[pathNode.index] = pathNode;
                }
            }
            NativeArray<int2> neighborOffsetArray = new NativeArray<int2>(8, Allocator.Temp);
            neighborOffsetArray[0] = new int2(-1, 0);
            neighborOffsetArray[1] = new int2(0, 1);
            neighborOffsetArray[2] = new int2(1, 0);
            neighborOffsetArray[3] = new int2(0, -1);
            neighborOffsetArray[4] = new int2(-1, 1);
            neighborOffsetArray[5] = new int2(1, 1);
            neighborOffsetArray[6] = new int2(1, -1);
            neighborOffsetArray[7] = new int2(-1, -1);

            int endNodeIndex = CalculateIndex(endPosition.x, endPosition.y, gridSize);
            PathNode startNode = pathNodeArray[CalculateIndex(startPosition.x, startPosition.y, gridSize)];
            startNode.gCost = 0;
            startNode.CalculateFCost();
            pathNodeArray[startNode.index] = startNode;
            NativeList<int> openList = new NativeList<int>(Allocator.Temp);
            NativeList<int> closedList = new NativeList<int>(Allocator.Temp);

            openList.Add(startNode.index);

            while (openList.Length > 0)
            {
                int currentNodeIndex = GetLowestCostFNodeIndex(openList, pathNodeArray);
                PathNode currentNode = pathNodeArray[currentNodeIndex];
                if (currentNodeIndex == endNodeIndex)
                {
                    //we reached the destination :))))))))
                    break;
                }
                // removeing current node from open
                for (int i = 0; i < openList.Length; i++)
                {
                    if (openList[i] == currentNodeIndex)
                    {
                        openList.RemoveAtSwapBack(i);
                        break;
                    }
                }
                closedList.Add(currentNodeIndex);

                for (int i = 0; i < neighborOffsetArray.Length; i++)
                {
                    int2 neighbourOffset = neighborOffsetArray[i];
                    int2 neighbourPosition = new int2(currentNode.x + neighbourOffset.x, currentNode.y + neighbourOffset.y);
                    if (!IsPositionInsideGrid(neighbourPosition, gridSize))
                    {
                        //not in da grid
                        continue;
                    }
                    int neighbourNodeIndex = CalculateIndex(neighbourPosition.x, neighbourPosition.y, gridSize);
                    if (closedList.Contains(neighbourNodeIndex))
                    {
                        // we already seached this node
                        continue;
                    }
                    PathNode neighbourNode = pathNodeArray[neighbourNodeIndex];
                    if (!neighbourNode.isWalkable)
                    {
                        //no go is obstacle
                        continue;
                    }
                    int2 CurrentNodePosition = new int2(currentNode.x, currentNode.y);
                    int tentativeGCost = currentNode.gCost + CalculateDistanceCost(CurrentNodePosition.x, CurrentNodePosition.y);
                    if (tentativeGCost < neighbourNode.gCost)
                    {
                        neighbourNode.cameFromNodeIndex = currentNodeIndex;
                        neighbourNode.gCost = tentativeGCost;
                        neighbourNode.CalculateFCost();
                        // actualy setting the array from the "sample"
                        pathNodeArray[neighbourNodeIndex] = neighbourNode;
                        if (!openList.Contains(neighbourNode.index))
                        {
                            openList.Add(neighbourNode.index);
                        }
                    }
                }
            }
            pathPositionBuffer.Clear();
            PathNode endNode = pathNodeArray[endNodeIndex];
            if (endNode.cameFromNodeIndex == -1)
            {
                // no path :(
            }
            else
            {
                CalculatePath(pathNodeArray, endNode, ref pathPositionBuffer, gridSize);

                //path.Dispose();
                // found one :)
            }

            pathNodeArray.Dispose();
            openList.Dispose();
            closedList.Dispose();
            neighborOffsetArray.Dispose();
        }
        private int2 ToGrid(int2 pos, int size)
        {
            return pos + new int2(size / 2, size / 2);
        }
        private int2 ToWorld(int2 pos, int size)
        {
            return pos - new int2(size / 2, size / 2);
        }
        private void CalculatePath(NativeArray<PathNode> pathNodeArray, PathNode endNode, ref DynamicBuffer<PathPosition> pathPositionBuffer, int size)
        {
            if (endNode.cameFromNodeIndex == -1)
            {
                // no path :(

            }
            else
            {
                //UnityEngine.Debug.Log("finders");
                // found one :)
                //adding fist element of end
                pathPositionBuffer.Add(new PathPosition
                {
                    position = ToWorld(new int2(endNode.x, endNode.y), gridSize),
                });
                //path.Add(new int2(endNode.x, endNode.y));
                PathNode currentNode = endNode;
                //tracking back from the end
                while (currentNode.cameFromNodeIndex != -1)
                {

                    PathNode cameFromNode = pathNodeArray[currentNode.cameFromNodeIndex];
                    pathPositionBuffer.Add(new PathPosition
                    {
                        position = ToWorld(new int2(cameFromNode.x, cameFromNode.y), size)
                    });
                    //path.Add(new int2(cameFromNode.x, cameFromNode.y));
                    currentNode = cameFromNode;
                }
            }
        }
        private bool IsPositionInsideGrid(int2 gridPosition, int2 gridSize)
        {
            return
                gridPosition.x >= 0 &&
                gridPosition.y >= 0 &&
                gridPosition.x < gridSize.x &&
                gridPosition.y < gridSize.y;
        }
        private int CalculateIndex(int x, int z, int gridWidth)
        {
            return x + z * gridWidth;
        }
        private int GetLowestCostFNodeIndex(NativeList<int> openList, NativeArray<PathNode> pathNodeArray)
        {
            PathNode lowestCostPathNode = pathNodeArray[openList[0]];
            for (int i = 1; i < openList.Length; i++)
            {
                PathNode testPathNode = pathNodeArray[openList[i]];
                if (testPathNode.fCost < lowestCostPathNode.fCost)
                {
                    lowestCostPathNode = testPathNode;
                }
            }
            return lowestCostPathNode.index;
        }
        private int CalculateDistanceCost(int2 aPosition, int2 bPosition)
        {
            int xDistance = math.abs(aPosition.x - bPosition.x);
            int yDistance = math.abs(aPosition.y - bPosition.y);
            int remaining = math.abs(xDistance - yDistance);
            return moveDiagonalCost * math.min(xDistance, yDistance) + moveStraightCost * remaining;
        }
        private struct PathNode
        {
            public int x;
            public int y;

            public int index;

            public int gCost;
            public int hCost;
            public int fCost;

            public bool isWalkable;

            public int cameFromNodeIndex;

            public void CalculateFCost()
            {
                fCost = gCost + hCost;
            }
            public void SetIsWalkable(bool value)
            {
                isWalkable = value;
            }
        }
    }
    // Start is called before the first frame update
}