Untitled

mail@pastecode.io avatar
unknown
csharp
a year ago
51 kB
1
Indexable
Never
using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using Gameplay.Character;
using GridScene.ECS;
using GridScene.ECS.Non_Entitas;
using GridScene.ECS.Systems;
using GridScene.Events;
using Id;
using Modules.Character;
using Modules.Database;
using Modules.Event;
using Modules.Level;
using Modules.UI;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.Jobs;
using UnityEngine.PlayerLoop;
using UnityEngine.ResourceManagement.AsyncOperations;
using Random = UnityEngine.Random;

namespace GridScene
{
    public class GridManager : MonoBehaviour
    {
        #region Structs
        [BurstCompile]
        private readonly struct MoveEnemyGameObjectsJob : IJobParallelForTransform
        {
            [ReadOnly] private readonly NativeArray<float2> _positions;
            [ReadOnly] private readonly NativeArray<float2> _forwardDirections;

            public MoveEnemyGameObjectsJob
            (
                NativeArray<float2> positions, 
                NativeArray<float2> forwardDirections
            )
            {
                _positions = positions;
                _forwardDirections = forwardDirections;
            }
            
            public void Execute(int index, TransformAccess transform)
            {
                transform.position = new Vector3(_positions[index].x, transform.position.y, _positions[index].y);

                if (MoveEnemiesSystem.Magnitude(_forwardDirections[index]) > math.EPSILON)
                {
                    transform.rotation = Quaternion.LookRotation
                    (
                        math.normalize
                        (
                            new Vector3
                            (
                                _forwardDirections[index].x,
                                0.0f,
                                _forwardDirections[index].y
                            )
                        ),
                        Vector3.up
                    );
                }
            }
        }
        #endregion Structs
        
        #region DataClasses
        public class TileInfo
        {
            #region MemberVars
            private readonly int _x;
            private readonly int _y;
            private readonly bool _isObstacle;
            private readonly int _scale;
            private readonly bool _isChild;
            private readonly int2[] _children;
            
            private uint _enemyId;
            #endregion MemberVars

            #region Properties
            public int X => _x;
            public int Y => _y;
            public bool IsObstacle => _isObstacle;

            public uint EnemyId
            {
                get => _enemyId;
                set => _enemyId = value;
            }
            
            public int Scale => _scale;
            public bool IsChild => _isChild;
            public int2[] Children => _children;
            #endregion Properties
            
            #region Constructors
            public TileInfo(int x, int y, bool isObstacle = false)
            {
                _x = x;
                _y = y;
                _isObstacle = isObstacle;
                _enemyId = uint.MaxValue;
                _scale = 1;
                _isChild = false;
                _children = null;

                BaseEvent<FreeTileIncrementEvent>.Raise(new FreeTileIncrementEvent(!_isObstacle));
            }

            public TileInfo
            (
                int x, 
                int y, 
                uint enemyId, 
                int scale = 1, 
                bool isChild = false, 
                int2[] children = null
            )
            {
                _x = x;
                _y = y;
                _isObstacle = false;
                _enemyId = enemyId;
                _scale = scale;
                _isChild = isChild;
                _children = children;

                BaseEvent<FreeTileIncrementEvent>.Raise(new FreeTileIncrementEvent(false));
            }

            #endregion Constructors
        }
        #endregion DataClasses
        
        #region MemberVars
        private static GridManager _instance;

        private CharacterModel _characterModel;
        private TileInfo[,] _tileInfos;
        private Dictionary<uint, EnemyUnitId> _enemyById;
        private int _freeTileCount;
        private bool _freeTileCountChanged;
        private TileInfo[] _freeTiles;
        private Dictionary<int2, GameEntity> _enemyGridPositions;
        private Transform _playerTransform;
        private IReadOnlyDictionary<EnemyId, EnemyDefinition> _enemyDatabase;
        private Dictionary<string, AsyncOperationHandle<GameObject>> _asyncOperationHandles = 
            new Dictionary<string, AsyncOperationHandle<GameObject>>();
        private TransformAccessArray _transformAccessArray;
        private JobHandle _moveEnemyGameObjectsJobHandle;
        private IUIModule _uiModule;
        #if CHEATS_ACTIVE
        private GridCheats _cheats;
        private string[,] _chunkNames;
        #endif
        private NativeArray<Entity> _enemyEntities;
        private Dictionary<int2, int2[]> _parentToChildren;
        private Dictionary<uint, Entity> _enemyIdToEntity;
        private EntityManager _entityManager;
        private Dictionary<uint, int2> _enemyIdToGridPosition;

        [SerializeField] private CharacterId _characterId;
        [SerializeField] private CameraController _cameraController;
        [SerializeField] private EnemyController _enemyController;
        [SerializeField] private int _gridWidth = 1000;
        [SerializeField] private int _gridHeight = 1000;
        [SerializeField] private GPUIHordeManager _gpuiHordeManager = null;
        
        #region Temporary
        [SerializeField] private bool _placeEnemiesRandomly = false;
        [SerializeField] private int _numRandomEnemies = 25000;
        [SerializeField] private int _numRandomHeavyEnemies = 50;
        [SerializeField] private bool _useGPUI = false;
        #endregion Temportary
        #endregion MemberVars
        
        #region Properties
        public static GridManager Instance => _instance;
        public Transform PlayerTransform => _playerTransform;
        public Dictionary<int2, GameEntity> EnemyGridPositions => _enemyGridPositions;
        #if CHEATS_ACTIVE
        public bool IsDebugHitZoneEnabled => _cheats.IsDebugHitZoneEnabled;
        #endif
        #endregion Properties
        
        #region MonoBehaviorCallbacks
        private void Awake()
        {
            _instance = this;
            _uiModule = GameEngine.Module<IUIModule>();
            _uiModule.ShowView(typeof(HudScreen));
            
            BaseEvent<FreeTileIncrementEvent>.Register(OnFreeTileIncrement);
            BaseEvent<MoveEnemyGridPositionEvent>.Register(OnMoveEnemyGridPosition);
            BaseEvent<EnemiesKilledEvent>.Register(OnEnemiesKilled);
            
            _freeTileCount = 0;
            _enemyById = new Dictionary<uint, EnemyUnitId>();
            _freeTileCountChanged = false;
            _enemyDatabase = GameEngine.Module<IDatabaseModule>().Database<EnemyDatabase>().Dictionary;
            _parentToChildren = new Dictionary<int2, int2[]>();
            _enemyIdToEntity = new Dictionary<uint, Entity>();
            _entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
            _enemyIdToGridPosition = new Dictionary<uint, int2>();
            _transformAccessArray = new TransformAccessArray(0);

            _enemyGridPositions = new Dictionary<int2, GameEntity>();
            for (int iY = 0; iY < _gridHeight; iY++)
            {
                for (int iX = 0; iX < _gridWidth; iX++)
                {
                    _enemyGridPositions.Add(new int2(iX, iY), null);
                }
            }
            
            #if CHEATS_ACTIVE
            _cheats = new GridCheats();
            #endif
            
            StartCoroutine(LoadAssets());
        }

#if CHEATS_ACTIVE
        private void Update()
        {
            
            DisplayPlayerPosition();
            
        }
#endif

        private void OnDestroy()
        {
            #if CHEATS_ACTIVE
            _cheats.Dispose();
            #endif
            
            if (_characterModel != null)
            {
                _characterModel.StatHandler.OnCharacterDied -= OnCharacterDied;
                _characterModel.DestroyGameplayInstance();
            }
            
            foreach (var asyncOperation in _asyncOperationHandles)
            {
                if (asyncOperation.Value.IsValid())
                {
                    Addressables.Release(asyncOperation.Value);
                }
            }
            
            _instance = null;
            _asyncOperationHandles.Clear();
            _transformAccessArray.Dispose();
            _enemyEntities.Dispose();
            
            BaseEvent<FreeTileIncrementEvent>.Unregister(OnFreeTileIncrement);
            BaseEvent<MoveEnemyGridPositionEvent>.Unregister(OnMoveEnemyGridPosition);
            BaseEvent<EnemiesKilledEvent>.Unregister(OnEnemiesKilled);
        }
        #endregion MonoBehaviorCallbacks
        
        #region OtherCallbacks
        private void OnEnemiesKilled(EnemiesKilledEvent eventData)
        {
            _moveEnemyGameObjectsJobHandle.Complete();

            NativeArray<Entity> remainingEntities =
                new NativeArray<Entity>(_enemyEntities.Length - eventData.Killed.Count, Allocator.Persistent);
            for (int iKilled = 0; iKilled < eventData.Killed.Count; iKilled++)
            {
                if (_useGPUI)
                {
                    int iRemaining = 0;
                    for (int iEntity = 0; iEntity < _enemyEntities.Length; iEntity++)
                    {
                        if (_enemyIdToEntity.ContainsKey(eventData.Killed[iKilled]))
                        {
                            _entityManager.DestroyEntity(_enemyIdToEntity[eventData.Killed[iKilled]]);
                            _enemyIdToEntity.Remove(eventData.Killed[iKilled]);
                        }
                        else
                        {
                            remainingEntities[iRemaining] = _enemyEntities[iEntity];
                            iRemaining++;
                        }
                    }
                }
                else
                {
                    List<int> toRemove = new List<int>();
                    for (int iTransform = 0; iTransform < _transformAccessArray.length; iTransform++)
                    {
                        if (_transformAccessArray[iTransform] == _enemyById[eventData.Killed[iKilled]].transform)
                        {
                            toRemove.Add(iTransform);
                        }

                        if (toRemove.Count == eventData.Killed.Count)
                        {
                            break;
                        }
                    }

                    for (int iRemove = 0; iRemove < toRemove.Count; iRemove++)
                    {
                        GameObject go = _transformAccessArray[toRemove[iRemove]].gameObject;
                        _transformAccessArray.RemoveAtSwapBack(toRemove[iRemove]);
                        Destroy(go);
                    }
                }
            }

            for (int iKilled = 0; iKilled < eventData.Killed.Count; iKilled++)
            {
                DestroyEnemyGridData(eventData.Killed[iKilled]);
            }

            if (_useGPUI)
            {
                _enemyEntities.Dispose();
                _enemyEntities = remainingEntities;
            }
            else
            {
                remainingEntities.Dispose();
            }
        }
        
        private void OnFreeTileIncrement(FreeTileIncrementEvent eventData)
        {
            _freeTileCount += eventData.Increased ? 1 : -1;
            if (_freeTileCount < 0)
            {
                _freeTileCount = 0;
            }

            if (!_freeTileCountChanged)
            {
                _freeTileCountChanged = true;
            }
        }

        private void OnMoveEnemyGridPosition(MoveEnemyGridPositionEvent eventData)
        {
            TileInfo enemy = _tileInfos[eventData.PreviousPosition.x, eventData.PreviousPosition.y];
            int scale = enemy.Scale;
            bool hasChildren = enemy.Children != null;
            
            if (hasChildren)
            {
                for (int iChild = 0; iChild < enemy.Children.Length; iChild++)
                {
                    _tileInfos[enemy.Children[iChild].x, enemy.Children[iChild].y] = new TileInfo
                    (
                        enemy.Children[iChild].x,
                        enemy.Children[iChild].y
                    );
                }
            }
            _tileInfos[enemy.X, enemy.Y] =
                new TileInfo(eventData.PreviousPosition.x, eventData.PreviousPosition.y);


            int2[] newChildren = hasChildren ? new int2[scale * scale - 1] : null;
            _tileInfos[eventData.NewPosition.x, eventData.NewPosition.y] = new TileInfo
            (
                eventData.NewPosition.x, 
                eventData.NewPosition.y, 
                eventData.EnemyId,
                scale,
                false,
                newChildren
            );
            if (!_enemyIdToGridPosition.ContainsKey(eventData.EnemyId))
            {
                _enemyIdToGridPosition.Add(eventData.EnemyId, eventData.NewPosition);
            }
            else
            {
                _enemyIdToGridPosition[eventData.EnemyId] = eventData.NewPosition;
            }

            if (!hasChildren)
            {
                return;
            }
            
            int iChildren = 0;
            for (int iChildY = eventData.NewPosition.y; iChildY < eventData.NewPosition.y + scale; iChildY++)
            {
                for (int iChildX = eventData.NewPosition.x; iChildX < eventData.NewPosition.x + scale; iChildX++)
                {
                    if (iChildX == eventData.NewPosition.x && iChildY == eventData.NewPosition.y)
                    {
                        continue;
                    }

                    _tileInfos[iChildX, iChildY] = new TileInfo
                    (
                        iChildX,
                        iChildY,
                        eventData.EnemyId,
                        scale,
                        true
                    );
                    newChildren[iChildren] = new int2(iChildX, iChildY);
                    iChildren++;
                }
            }

            if (_parentToChildren.ContainsKey(eventData.PreviousPosition))
            {
                _parentToChildren.Remove(eventData.PreviousPosition);
            }

            if (!_parentToChildren.ContainsKey(eventData.NewPosition))
            {
                _parentToChildren.Add(eventData.NewPosition, newChildren);
            }
            else
            {
                _parentToChildren[eventData.NewPosition] = newChildren;
            }
        }
        #endregion OtherCallbacks
        
        #region Methods
        private void DestroyEnemyGridData(uint id)
        {
            int2 gridPos = _enemyIdToGridPosition[id];
            TileInfo tileInfo = _tileInfos[gridPos.x, gridPos.y];
            if (tileInfo.Children != null)
            {
                for (int iChild = 0; iChild < tileInfo.Children.Length; iChild++)
                {
                    _tileInfos[tileInfo.Children[iChild].x, tileInfo.Children[iChild].y] =
                        new TileInfo(tileInfo.Children[iChild].x, tileInfo.Children[iChild].y);
                }
            }
            _tileInfos[gridPos.x, gridPos.y] = new TileInfo(gridPos.x, gridPos.y);
            
            if (_parentToChildren.ContainsKey(_enemyIdToGridPosition[id]))
            {
                _parentToChildren.Remove(_enemyIdToGridPosition[id]);
            }
            _enemyIdToGridPosition.Remove(id);
            _enemyById.Remove(id);

            for (int iY = 0; iY < _gridHeight; iY++)
            {
                for (int iX = 0; iX < _gridWidth; iX++)
                {
                    if (_tileInfos[iX, iY] == null)
                    {
                        continue;
                    }

                    if (_tileInfos[iX, iY].EnemyId != id)
                    {
                        continue;
                    }
                    
                    _tileInfos[iX, iY] = new TileInfo(iX, iY);
                }
            }
        }
        
        private IEnumerator LoadAssets()
        {
            yield return CreateGrid();
            InitTiles();
            yield return LoadCharacterVisual();
            _enemyController.Init();
        }
        
        private IEnumerator CreateGrid()
        {
            _tileInfos = new TileInfo[_gridWidth, _gridHeight];
            int widthGenerated = 0;
            int heightGenerated = 0;
            BiomeDefinition biomeDefinition = GameEngine.Module<ILevelModule>().GetCurrentBiome();
            List<ChunkGenerationData> chunkGenerationDatas = new List<ChunkGenerationData>();
            foreach (var biomeDefinitionChunk in biomeDefinition.Chunks)
            {
                AsyncOperationHandle<GameObject> asyncOperation = Addressables.LoadAssetAsync<GameObject>(biomeDefinitionChunk);
                yield return asyncOperation;
                ChunkData chunkData = asyncOperation.Result.GetComponent<ChunkData>();
                chunkGenerationDatas.Add(new ChunkGenerationData(asyncOperation.Result.name, chunkData.StartingWeightSelection, chunkData.SelectionWeightLoss, chunkData.ChunkPosition));
                _asyncOperationHandles.Add(asyncOperation.Result.name, asyncOperation);
            }

            List<List<ChunkGenerationData>> chunksByPosition = new List<List<ChunkGenerationData>>();
            int chunkPositionCount = Enum.GetValues(typeof(EChunkPosition)).Length;
            for (int i = 0; i < chunkPositionCount; i++)
            {
                chunksByPosition.Add(chunkGenerationDatas.Where(x => (int)x.ChunkPosition == i).ToList());
            }
            #if CHEATS_ACTIVE
            int verticalChunkCount = _gridHeight / GridConfiguration.ChunkHeight;
            int horizontalChunkCount = _gridWidth / GridConfiguration.ChunkWidth;
             _chunkNames = new string[horizontalChunkCount,verticalChunkCount];                   
            #endif
            while (heightGenerated < _gridHeight)
            {
                while (widthGenerated < _gridWidth)
                {
                    EChunkPosition chunkPosition = GetCurrentChunkPosition(heightGenerated, widthGenerated);
                    string randomChunk = GridConfiguration.SelectNextChunk(chunksByPosition[(int)chunkPosition]);
                    
                    #if CHEATS_ACTIVE
                    _chunkNames[widthGenerated / GridConfiguration.ChunkWidth, heightGenerated / GridConfiguration.ChunkHeight] = randomChunk;
                    #endif
                    
                    GameObject instantiatedChunk = Instantiate(_asyncOperationHandles[randomChunk].Result, new Vector3(widthGenerated, 0f, heightGenerated), Quaternion.identity, transform);
                    instantiatedChunk.transform.rotation = quaternion.identity;
                    ChunkData chunkData = instantiatedChunk.GetComponent<ChunkData>();
                    foreach (var chunkDataObstaclePosition in chunkData.ObstaclePositions)
                    {
                        _tileInfos[widthGenerated + chunkDataObstaclePosition.x, heightGenerated + chunkDataObstaclePosition.y] = 
                            new TileInfo(widthGenerated + chunkDataObstaclePosition.x, heightGenerated + chunkDataObstaclePosition.y, true);
                    }

                    widthGenerated += GridConfiguration.ChunkWidth;
                }

                heightGenerated += GridConfiguration.ChunkHeight;
                widthGenerated = 0;
            }
        }

        private EChunkPosition GetCurrentChunkPosition(int heightGenerated, int widthGenerated)
        {
            bool isBottom = heightGenerated == 0;
            bool isTop = (heightGenerated + GridConfiguration.ChunkHeight) == _gridHeight;
            bool isLeft = widthGenerated == 0;
            bool isRight = (widthGenerated + GridConfiguration.ChunkWidth) == _gridWidth;

            if (isBottom)
            {
                if (isLeft)
                {
                    return EChunkPosition.BottomLeftCorner;    
                }

                return isRight ? EChunkPosition.BottomRightCorner : EChunkPosition.Bottom;
            }
            
            if (isTop)
            {
                if (isLeft)
                {
                    return EChunkPosition.TopLeftCorner;    
                }

                return isRight ? EChunkPosition.TopRightCorner : EChunkPosition.Top;
            }

            if (isLeft)
            {
                return EChunkPosition.Left;
            }

            return isRight ? EChunkPosition.Right : EChunkPosition.Center;
        }
        
        private IEnumerator LoadCharacterVisual()
        {
            ICharacterModule characterModule = GameEngine.Module<ICharacterModule>();
            CharacterInstanceId characterInstanceId = characterModule.CreateCharacter(_characterId);
            _characterModel = characterModule.GetCharacterModel(characterInstanceId);
            yield return _characterModel.CreateGameplayInstance();
            _playerTransform = _characterModel.VisualHandler.GetInstance().transform;
            _characterModel.VisualHandler.GetController().Init(_cameraController.gameObject);
            _cameraController.Initialize(_playerTransform);
            _characterModel.StatHandler.OnCharacterDied += OnCharacterDied;
        }
        
        private void InitTiles()
        {
            for (int yTile = 0; yTile < _gridHeight; yTile++)
            {
                for (int xTile = 0; xTile < _gridWidth; xTile++)
                {
                    if (_tileInfos[xTile, yTile] != null)
                    {
                        continue;
                    }

                    _tileInfos[xTile, yTile] = new TileInfo(xTile, yTile);
                }
            }
        }

        private bool IsFreeTile(int x, int y)
        {
            Vector3 playerPos = _playerTransform.position;
            return !_tileInfos[x, y].IsObstacle &&
                   ((int)playerPos.x != x || (int)playerPos.z != y) &&
                   _tileInfos[x, y].EnemyId == uint.MaxValue &&
                   !_tileInfos[x, y].IsChild;
        }
        
        private void GetAllFreeTiles(bool randomized = false, int scale = 1)
        {
            if (_freeTileCountChanged)
            {
                _freeTileCountChanged = false;
            }
            else
            {
                return;
            }
            
            List<TileInfo> freeTiles = new List<TileInfo>();
            int freeTile = 0;
            for (int iY = 0; iY < _gridHeight; iY++)
            {
                for (int iX = 0; iX < _gridWidth; iX++)
                {
                    if (!IsFreeTile(iX, iY))
                    {
                        continue;
                    }

                    if (scale == 1)
                    {
                        freeTiles.Add(_tileInfos[iX, iY]);
                        freeTile++;
                    }
                    else
                    {
                        if (iX + scale - 1 > _gridWidth - 1)
                        {
                            continue;
                        }

                        if (iY + scale - 1 > _gridHeight - 1)
                        {
                            continue;
                        }

                        bool allTilesAvailable = true;
                        int2[] children = new int2[scale * scale - 1];
                        int iChild = 0;
                        int2 parent = new int2(iX, iY);
                        for (int iChildY = iY; iChildY < iY + scale; iChildY++)
                        {
                            for (int iChildX = iX; iChildX < iX + scale; iChildX++)
                            {
                                if (iChildX == iX && iChildY == iY)
                                {
                                    continue;
                                }

                                if (!IsFreeTile(iChildX, iChildY))
                                {
                                    iChildY = iY + scale;
                                    allTilesAvailable = false;
                                    break;
                                }

                                children[iChild] = new int2(iChildX, iChildY);
                                _tileInfos[iChildX, iChildY] =
                                    new TileInfo(iChildX, iChildY, uint.MaxValue, scale, true);
                                iChild++;
                            }
                        }

                        if (!allTilesAvailable)
                        {
                            continue;
                        }
                        
                        if (!_parentToChildren.ContainsKey(parent))
                        {
                            _parentToChildren.Add(parent, children);
                        }
                        else
                        {
                            _parentToChildren[parent] = children;
                        }
                        
                        freeTiles.Add(_tileInfos[iX, iY]);
                        freeTile++;
                    }
                }
            }

            _freeTiles = freeTiles.ToArray();
            
            if (!randomized)
            {
                return;
            }
            
            TileInfo temp = null;
            int index = 0;
            for (int iTile = 0; iTile < _freeTiles.Length; iTile++)
            {
                index = Random.Range(0, _freeTiles.Length);
                temp = _freeTiles[index];
                _freeTiles[index] = _freeTiles[iTile];
                _freeTiles[iTile] = temp;
            }
        }

        private void ShuffleArray<T>(T[] array)
        {
            System.Random rng = new System.Random();
            int length = array.Length;
            int rnd = 0;
            T value;
            while (length > 1)
            {
                length--;
                rnd = rng.Next(length + 1);
                value = array[rnd];
                array[rnd] = array[length];
                array[length] = value;
            }
        }

        private void CollectUsableTiles(int distance, List<int2> usable, int count)
        {
            float3 playerPos3d = _playerTransform.position; 
            int2 playerPos = new int2((int)playerPos3d.x, (int)playerPos3d.z);
            TileInfo[] tileInfos = GetTilesOnRadius(playerPos.x, playerPos.y, distance);
            ShuffleArray(tileInfos);
            for (int iTile = 0; iTile < tileInfos.Length; iTile++)
            {
                if (tileInfos[iTile] == null)
                {
                    continue;
                }

                if (Mathf.Approximately(1, tileInfos[iTile].Scale))
                {
                    if (!IsFreeTile(tileInfos[iTile].X, tileInfos[iTile].Y))
                    {
                        continue;
                    }
                    usable.Add(new int2(tileInfos[iTile].X, tileInfos[iTile].Y));
                    if (usable.Count == count)
                    {
                        return;
                    }
                }
                else
                {
                    bool canUse = true;
                    for (int iScaleY = 0; iScaleY < tileInfos[iTile].Scale; iScaleY++)
                    {
                        for (int iScaleX = 0; iScaleX < tileInfos[iTile].Scale; iScaleX++)
                        {
                            if (IsFreeTile(tileInfos[iTile].X + iScaleX, tileInfos[iTile].Y + iScaleY))
                            {
                                continue;
                            }
                            
                            canUse = false;
                            iScaleY = tileInfos[iTile].Scale;
                            break;
                        }
                    }

                    if (!canUse)
                    {
                        continue;
                    }

                    int2[] children = new int2[tileInfos[iTile].Scale * tileInfos[iTile].Scale - 1];
                    int iChild = 0;
                    for (int iY = 0; iY < tileInfos[iTile].Scale; iY++)
                    {
                        for (int iX = 0; iX < tileInfos[iTile].Scale; iX++)
                        {
                            if (iX == tileInfos[iTile].X && iY == tileInfos[iTile].Y)
                            {
                                continue;
                            }

                            int2 pos = new int2(tileInfos[iTile].X + iX, tileInfos[iTile].Y + iY);
                            _tileInfos[pos.x, pos.y] = new TileInfo(pos.x, pos.y, uint.MaxValue, 1, true);
                            children[iChild] = pos;
                            iChild++;
                        }
                    }

                    int2 parent = new int2(tileInfos[iTile].X, tileInfos[iTile].Y);
                    if (!_parentToChildren.ContainsKey(parent))
                    {
                        _parentToChildren.Add(parent, children);
                    }
                    else
                    {
                        _parentToChildren[parent] = children;
                    }
                    
                    usable.Add(new int2(tileInfos[iTile].X, tileInfos[iTile].Y));
                    if (usable.Count == count)
                    {
                        return;
                    }
                }
            }
        }

        public float2 CapPosition(float2 position)
        {
            if (position.x < 0.0f)
            {
                position.x = 0.0f;
            }
            else if (position.x > _gridWidth - 1)
            {
                position.x = (_gridWidth - 1) + (position.x - (float)Math.Floor(position.x));
            }

            if (position.y < 0.0f)
            {
                position.y = 0.0f;
            }
            else if (position.y > _gridHeight - 1)
            {
                position.y = (_gridHeight - 1) + (position.y - (float)Math.Floor(position.y));
            }

            return position;
        }

        public EnemyUnitId[] SpawnNextBatchOfEnemies(int distance, EnemyId enemyId, int count)
        {
            List<int2> usable = new List<int2>();
            do
            {
                CollectUsableTiles(distance, usable, count);
                distance++; //if we have to loop again it means the previous radius didn't have enough free spots
            } while (usable.Count < count);

            TileInfo tileInfo = null;
            EnemyUnitId[] ids = new EnemyUnitId[count];
            Transform enemy = null;
            Vector3 enemyScale = Vector3.zero;
            NativeArray<Entity> newEntities = new NativeArray<Entity>(count, Allocator.Persistent);
            for (int iEnemy = 0; iEnemy < count; iEnemy++)
            {
                tileInfo = _tileInfos[usable[iEnemy].x, usable[iEnemy].y];
                if (!_useGPUI)
                {
                    enemy = Instantiate(_enemyDatabase[enemyId].VisualPrefab).transform;
                    enemyScale = enemy.localScale;
                    enemy.position = 
                        new Vector3(tileInfo.X + enemyScale.x * 0.5f, 0.0f, tileInfo.Y + enemyScale.z * 0.5f);
                    _transformAccessArray.Add(enemy);
                    ids[iEnemy] = enemy.gameObject.AddComponent<EnemyUnitId>();
                }
                else
                {
                    enemyScale = _enemyDatabase[enemyId].VisualPrefab.transform.localScale;
                    newEntities[iEnemy] = _gpuiHordeManager.SpawnEnemy
                    (
                        _enemyDatabase[enemyId].GPUIType,
                        new Vector3(tileInfo.X + enemyScale.x * 0.5f, 0.0f, tileInfo.Y + enemyScale.z * 0.5f),
                        Quaternion.identity
                    );
                    GameObject temp = new GameObject("temp");
                    ids[iEnemy] = temp.AddComponent<EnemyUnitId>();
                    temp.SetActive(false);
                    temp.transform.position =
                        new Vector3(tileInfo.X + enemyScale.x * 0.5f, 0.0f, tileInfo.Y + enemyScale.z * 0.5f);
                }
                
                ids[iEnemy].Id = EnemyController.NextEnemyId;
                if (_useGPUI)
                {
                    _enemyIdToEntity.Add(ids[iEnemy].Id, newEntities[iEnemy]);
                }
                ids[iEnemy].EnemyId = enemyId;
                ids[iEnemy].RadiusSize = _enemyDatabase[enemyId].VisualScale * 0.5f;
                _enemyById.Add(ids[iEnemy].Id, ids[iEnemy]);
                _tileInfos[tileInfo.X, tileInfo.Y] = new TileInfo
                (
                    tileInfo.X,
                    tileInfo.Y,
                    ids[iEnemy].Id,
                    (int)_enemyDatabase[enemyId].VisualScale,
                    false,
                    _enemyDatabase[enemyId].VisualScale > 1 ? _parentToChildren[usable[iEnemy]] : null
                );
                _enemyIdToGridPosition.Add(ids[iEnemy].Id, new int2(tileInfo.X, tileInfo.Y));

                if (_tileInfos[tileInfo.X, tileInfo.Y].Children == null)
                {
                    continue;
                }
                
                for (int iChild = 0; iChild < _tileInfos[tileInfo.X, tileInfo.Y].Children.Length; iChild++)
                {
                    int2 pos = _tileInfos[tileInfo.X, tileInfo.Y].Children[iChild];
                    _tileInfos[pos.x, pos.y].EnemyId = _tileInfos[tileInfo.X, tileInfo.Y].EnemyId;
                }
            }

            NativeArray<Entity> combined =
                new NativeArray<Entity>(newEntities.Length + _enemyEntities.Length, Allocator.Persistent);
            NativeArray<Entity> sub1 = combined.GetSubArray(0, _enemyEntities.Length);
            int iEntity = 0;
            for (iEntity = 0; iEntity < _enemyEntities.Length; iEntity++)
            {
                sub1[iEntity] = _enemyEntities[iEntity];
            }
            NativeArray<Entity> sub2 = combined.GetSubArray(_enemyEntities.Length, newEntities.Length);
            for (iEntity = 0; iEntity < newEntities.Length; iEntity++)
            {
                sub2[iEntity] = newEntities[iEntity];
            }

            _enemyEntities = combined;

            return ids;
        }
        
        public EnemyUnitId[] GetNextBatchOfEnemies()
        {
            if (!_placeEnemiesRandomly) //temporary
            {
                return null;
            }
            
            GetAllFreeTiles(true);
            TileInfo tileInfo = null;
            int length = _freeTiles.Length >= _numRandomEnemies ? _numRandomEnemies : _freeTiles.Length;
            EnemyUnitId[] ids = new EnemyUnitId[length];
            Transform enemy = null;
            Vector3 enemyScale = Vector3.zero;
            int iEnemy = 0;
            _transformAccessArray = new TransformAccessArray(_numRandomEnemies + _numRandomHeavyEnemies);
            _enemyEntities = new NativeArray<Entity>(_numRandomEnemies + _numRandomHeavyEnemies, Allocator.Persistent);
            int iEntity = 0;
            for (iEnemy = 0; iEnemy < length; iEnemy++)
            {
                tileInfo = _freeTiles[iEnemy];
                EnemyId morlock = new EnemyId("Morloch"); //temporary
                if (!_useGPUI) //temporary
                {
                    enemy = Instantiate(_enemyDatabase[morlock].VisualPrefab).transform;
                    enemyScale = enemy.localScale;
                    enemy.position = 
                        new Vector3(tileInfo.X + enemyScale.x * 0.5f, 0.0f, tileInfo.Y + enemyScale.z * 0.5f);
                    _transformAccessArray.Add(enemy);
                    ids[iEnemy] = enemy.gameObject.AddComponent<EnemyUnitId>();
                }
                else
                {
                    enemyScale = _enemyDatabase[morlock].VisualPrefab.transform.localScale;
                    _enemyEntities[iEntity] = _gpuiHordeManager.SpawnEnemy
                    (
                        _enemyDatabase[morlock].GPUIType,
                        new Vector3(tileInfo.X + enemyScale.x * 0.5f, 0.0f, tileInfo.Y + enemyScale.z * 0.5f),
                        Quaternion.identity
                    );
                    iEntity++;
                    GameObject temp = new GameObject("temp");
                    ids[iEnemy] = temp.AddComponent<EnemyUnitId>();
                    temp.SetActive(false);
                    temp.transform.position =
                        new Vector3(tileInfo.X + enemyScale.x * 0.5f, 0.0f, tileInfo.Y + enemyScale.z * 0.5f);
                }
                
                ids[iEnemy].Id = EnemyController.NextEnemyId;
                if (_useGPUI)
                {
                    _enemyIdToEntity.Add(ids[iEnemy].Id, _enemyEntities[iEnemy]);
                }
                ids[iEnemy].EnemyId = morlock;
                ids[iEnemy].RadiusSize = _enemyDatabase[morlock].VisualScale * 0.5f;
                _enemyById.Add(ids[iEnemy].Id, ids[iEnemy]);
                _tileInfos[tileInfo.X, tileInfo.Y] = new TileInfo(tileInfo.X, tileInfo.Y, ids[iEnemy].Id);
                _enemyIdToGridPosition.Add(ids[iEnemy].Id, new int2(tileInfo.X, tileInfo.Y));
            }

            EnemyId heavyMorlock = new EnemyId("HeavyMorloch"); //temporary
            GetAllFreeTiles(true, (int)_enemyDatabase[heavyMorlock].VisualScale);
            length = (_freeTiles.Length >= _numRandomHeavyEnemies ? _numRandomHeavyEnemies : _freeTiles.Length);
            EnemyUnitId[] idsHeavy = new EnemyUnitId[length];
            for (iEnemy = 0; iEnemy < length; iEnemy++)
            {
                tileInfo = _freeTiles[iEnemy];

                if (!_useGPUI)//temporary
                {
                    enemy = Instantiate(_enemyDatabase[heavyMorlock].VisualPrefab).transform;
                    enemyScale = enemy.localScale;
                    enemy.position = new Vector3(tileInfo.X + enemyScale.x * 0.5f, 0.0f,
                        tileInfo.Y + enemyScale.z * 0.5f);
                    _transformAccessArray.Add(enemy);
                    idsHeavy[iEnemy] = enemy.gameObject.AddComponent<EnemyUnitId>();
                }
                else
                {
                    enemyScale = _enemyDatabase[heavyMorlock].VisualPrefab.transform.localScale;
                    _enemyEntities[iEntity] = _gpuiHordeManager.SpawnEnemy
                    (
                        _enemyDatabase[heavyMorlock].GPUIType,
                        new Vector3(tileInfo.X + enemyScale.x * 0.5f, 0.0f, tileInfo.Y + enemyScale.z * 0.5f),
                        Quaternion.identity
                    );
                    iEntity++;
                    GameObject temp = new GameObject("temp");
                    idsHeavy[iEnemy] = temp.AddComponent<EnemyUnitId>();
                    temp.SetActive(false);
                    temp.transform.position =
                        new Vector3(tileInfo.X + enemyScale.x * 0.5f, 0.0f, tileInfo.Y + enemyScale.z * 0.5f);
                }

                idsHeavy[iEnemy].Id = EnemyController.NextEnemyId;
                if (_useGPUI)
                {
                    _enemyIdToEntity.Add(idsHeavy[iEnemy].Id, _enemyEntities[iEnemy]);
                }
                idsHeavy[iEnemy].EnemyId = heavyMorlock;
                idsHeavy[iEnemy].RadiusSize = _enemyDatabase[heavyMorlock].VisualScale * 0.5f;
                _enemyById.Add(idsHeavy[iEnemy].Id, idsHeavy[iEnemy]);
                int2[] children = _parentToChildren[new int2(tileInfo.X, tileInfo.Y)];
                tileInfo = new TileInfo
                (
                    tileInfo.X, 
                    tileInfo.Y, 
                    idsHeavy[iEnemy].Id,
                    (int)_enemyDatabase[heavyMorlock].VisualPrefab.transform.localScale.x,
                    false,
                    children
                );
                _tileInfos[tileInfo.X, tileInfo.Y] = tileInfo;
                _enemyIdToGridPosition.Add(idsHeavy[iEnemy].Id, new int2(tileInfo.X, tileInfo.Y));
                for (int iChild = 0; iChild < children.Length; iChild++)
                {
                    _tileInfos[children[iChild].x, children[iChild].y].EnemyId = tileInfo.EnemyId;
                }
            }

            EnemyUnitId[] combined = new EnemyUnitId[length + ids.Length];
            ids.CopyTo(combined, 0);
            idsHeavy.CopyTo(combined, ids.Length);
            
            return combined;
        }

        public EnemyUnitId GetEnemyUnitId(uint enemyId)
        {
            return _enemyById[enemyId];
        }
        
        public void MoveEnemies(NativeArray<float2> moved, NativeArray<float2> forwardDirections)
        {
            if (_useGPUI || moved.Length < 1)
            {
                moved.Dispose();
                forwardDirections.Dispose();
                return;
            }

            _moveEnemyGameObjectsJobHandle.Complete();

            MoveEnemyGameObjectsJob job = new MoveEnemyGameObjectsJob(moved, forwardDirections);
            _moveEnemyGameObjectsJobHandle = job.Schedule(_transformAccessArray);
            _moveEnemyGameObjectsJobHandle.Complete();
            moved.Dispose();
            forwardDirections.Dispose();
        }

        public TileInfo GetTileAt(int x, int y)
        {
            return x > -1 && y > -1 && x < _gridWidth && y < _gridHeight ? _tileInfos[x, y] : null;
        }

        public TileInfo GetTileAt(int2 position)
        {
            return GetTileAt(position.x, position.y);
        }
        
        public TileInfo[] GetTilesFromRadius(int x, int y, int offset, int minOffset = 0)
        {
            List<TileInfo> tileInfos = new List<TileInfo>();
            int startingX = x - offset;
            int endingX = x + offset;
            int startingY = y - offset;
            int endingY = y + offset;
            TileInfo currentTile;
            for (int posX = startingX; posX <= endingX; posX++)
            {
                for (int posY = startingY; posY <= endingY; posY++)
                {
                    if (posY > y - minOffset && posY < y + minOffset && posX > x - minOffset && posX < x + minOffset)
                    {
                        continue;
                    }
                    
                    currentTile = GetTileAt(posX, posY);
                    if (currentTile != null)
                    {
                        tileInfos.Add(currentTile);
                    }
                }
            }

            return tileInfos.ToArray();
        }
        
        public TileInfo[] GetTilesOnRadius(int x, int y, int offset)
        {
            List<TileInfo> tileInfos = new List<TileInfo>();
            int startingX = x - offset;
            int endingX = x + offset;
            int startingY = y - offset;
            int endingY = y + offset;
            TileInfo currentTile;
            float distance = 0.0f;
            for (int iX = startingX; iX <= endingX; iX++)
            {
                for (int iY = startingY; iY <= endingY; iY++)
                {
                    distance = (float)Math.Sqrt((iX - x) * (iX - x) + (iY - y) * (iY - y));
                    if (distance != offset)
                    {
                        continue;
                    }
                    
                    currentTile = GetTileAt(iX, iY);
                    if (currentTile != null)
                    {
                        tileInfos.Add(currentTile);
                    }
                }
            }

            return tileInfos.ToArray();
        }

        
        public TileInfo[] GetTilesFromTriangle(float2 origin, float2 forwardVector, float length, int offset)
        {
            List<TileInfo> tileInfos = new List<TileInfo>();
            float2 perpendicularVector = new float2(-forwardVector.y, forwardVector.x) * 0.5f;
            TileInfo currentTile;
            for (int currentLength = -offset; currentLength <= length + offset; currentLength++)
            {
                float2 currentCenterPosition = origin + forwardVector * currentLength;
                for (int currentWidth = 0; currentWidth <= currentLength + offset; currentWidth++)
                {
                    float2 currentOffsetPosition = currentCenterPosition + perpendicularVector * currentWidth;
                    currentTile = GetTileAt((int)currentOffsetPosition.x, (int)currentOffsetPosition.y);
                    if (currentTile != null && !tileInfos.Contains(currentTile))
                    {
                        tileInfos.Add(currentTile);
                    }
                    currentOffsetPosition = currentCenterPosition - perpendicularVector * currentWidth;
                    currentTile = GetTileAt((int)currentOffsetPosition.x, (int)currentOffsetPosition.y);
                    if (currentTile != null && !tileInfos.Contains(currentTile))
                    {
                        tileInfos.Add(currentTile);
                    }
                }
            }

            return tileInfos.ToArray();
        }

        public float ClampRayMaxRange(float2 origin, float2 forwardVector, float length)
        {
            for (int currentLength = 0; currentLength <= length; currentLength++)
            {
                float2 currentCenterPosition = origin + forwardVector * currentLength;
                if (GetTileAt((int)currentCenterPosition.x, (int)currentCenterPosition.y).IsObstacle)
                {
                    return currentLength;
                }
            }

            return length;
        }
        
        public TileInfo[] GetTilesFromRay(float2 origin, float2 forwardVector, float length, float width, int offset, bool ignoreObstacle = true)
        {
            List<TileInfo> tileInfos = new List<TileInfo>();
            float2 perpendicularVector = new float2(-forwardVector.y, forwardVector.x) * 0.5f;
            TileInfo currentTile;
            for (int currentLength = -offset; currentLength <= length + offset; currentLength++)
            {
                float2 currentCenterPosition = origin + forwardVector * currentLength;
                if (!ignoreObstacle && GetTileAt((int)currentCenterPosition.x, (int)currentCenterPosition.y).IsObstacle && currentLength > 0)
                {
                    return tileInfos.ToArray();
                }
                
                for (int currentWidth = 0; currentWidth <= width + offset; currentWidth++)
                {
                    float2 currentOffsetPosition = currentCenterPosition + perpendicularVector * currentWidth;
                    currentTile = GetTileAt((int)currentOffsetPosition.x, (int)currentOffsetPosition.y);
                    if (currentTile != null && !tileInfos.Contains(currentTile))
                    {
                        tileInfos.Add(currentTile);
                    }
                    currentOffsetPosition = currentCenterPosition - perpendicularVector * currentWidth;
                    currentTile = GetTileAt((int)currentOffsetPosition.x, (int)currentOffsetPosition.y);
                    if (currentTile != null && !tileInfos.Contains(currentTile))
                    {
                        tileInfos.Add(currentTile);
                    }
                }
            }

            return tileInfos.ToArray();
        }
        
        private void OnCharacterDied()
        {
            _uiModule.ShowView(typeof(GameOverPrompt), EViewLayer.Prompt);
            BaseEvent<GamePausedEvent>.Raise(new GamePausedEvent(true));
        }
        
        #if CHEATS_ACTIVE
        private void DisplayPlayerPosition()
        {
            if(!_cheats.IsDebugChunkInfoEnabled) return;
            
            Vector3 playerPosition = _playerTransform.position;
            string chunkName = _chunkNames[(int)playerPosition.x / GridConfiguration.ChunkWidth, (int)playerPosition.z / GridConfiguration.ChunkHeight];
            string debugInfo =
                $"{chunkName} Position {(int)playerPosition.x % GridConfiguration.ChunkWidth} - {(int)playerPosition.z % GridConfiguration.ChunkHeight}";
            _uiModule.GetView<HudScreen>().UpdateDebugInfo(debugInfo);
        }
        #endif
        #endregion Methods
    }
}