using System; using System.Collections.Generic; using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.Tilemaps; namespace InfiniteWorld { public class InfiniteWorldGenerator : MonoBehaviour { [Header("References")] [SerializeField] private Transform player; [SerializeField] private WorldAutotileProfile profile; [Header("Chunk Settings")] [SerializeField] private int chunkSize = 16; [SerializeField] private int generationRadius = 2; [SerializeField] private int seed = 12345; [Header("Rock Noise")] [SerializeField] private float macroNoiseScale = 0.05f; [SerializeField] private float detailNoiseScale = 0.12f; [SerializeField] private float ridgeNoiseScale = 0.18f; [SerializeField] private float wallThreshold = 0.6f; [SerializeField] private float rockBias = 0.04f; [SerializeField] private int smoothingPasses = 2; [Header("Global Passes")] [SerializeField] private float passNoiseScale = 0.018f; [SerializeField] private float passDetailScale = 0.041f; [SerializeField] private float passThreshold = 0.22f; [SerializeField] private float passFeather = 0.12f; [Header("Environment")] [SerializeField] private float environmentNoiseScale = 0.19f; [SerializeField] private float environmentThreshold = 0.7f; [Header("Random Objects")] [SerializeField, Range(0f, 1f)] private float randomPrefabChance = 0.06f; [SerializeField] private float randomPrefabZOffset = -0.1f; [Header("Streaming")] [SerializeField, Min(1)] private int maxAsyncChunkJobs = 2; [SerializeField, Min(1)] private int maxChunkRendersPerFrame = 1; [SerializeField, Min(0)] private int blockingGenerationRadius = 0; private readonly Dictionary chunks = new Dictionary(); private readonly Queue completedBuilds = new Queue(); private readonly object generationLock = new object(); private static readonly Vector2Int[] NeighborOffsets = { Vector2Int.up, Vector2Int.right, Vector2Int.down, Vector2Int.left }; private Grid grid; private Transform chunkRoot; private Vector2Int lastGeneratedCenter = new Vector2Int(int.MinValue, int.MinValue); private WorldAutotileProfile runtimeFallbackProfile; private int activeGenerationJobs; private bool isLoadingPaused; private float pausedTimeScale = 1f; private TileBase cachedGroundTile; private TileBase[] cachedGroundTiles; private void Awake() { EnsureSceneInfrastructure(); EnsureRuntimeData(); TryFindPlayer(); } private void Update() { if (player == null && !TryFindPlayer()) { return; } Vector2Int playerChunk = WorldToChunk(player.position); if (playerChunk != lastGeneratedCenter) { lastGeneratedCenter = playerChunk; UnloadDistantChunks(playerChunk); } DrainCompletedBuilds(isLoadingPaused ? int.MaxValue : maxChunkRendersPerFrame); ScheduleChunkGeneration(playerChunk); EnsureBlockingChunksLoaded(playerChunk); } private void OnDisable() { SetLoadingPaused(false); } public bool UsesProfile(WorldAutotileProfile candidateProfile) { return profile == candidateProfile; } public void EditorRefreshFromProfile() { EnsureSceneInfrastructure(); EnsureRuntimeData(); cachedGroundTile = null; cachedGroundTiles = null; List coords = new List(chunks.Keys); for (int i = 0; i < coords.Count; i++) { if (!chunks.TryGetValue(coords[i], out ChunkRuntime runtime) || !runtime.HasData) { continue; } runtime.State = ChunkState.ReadyToRender; RenderChunk(coords[i]); } for (int i = 0; i < coords.Count; i++) { RefreshNeighborBorders(coords[i]); } } private void EnsureSceneInfrastructure() { grid = GetComponentInChildren(); if (grid == null) { GameObject gridObject = new GameObject("Grid", typeof(Grid)); gridObject.transform.SetParent(transform, false); grid = gridObject.GetComponent(); } Transform existingChunkRoot = grid.transform.Find("Chunks"); if (existingChunkRoot == null) { GameObject root = new GameObject("Chunks"); root.transform.SetParent(grid.transform, false); chunkRoot = root.transform; } else { chunkRoot = existingChunkRoot; } } private void EnsureRuntimeData() { if (profile == null || !profile.HasAnyAssignedTiles()) { runtimeFallbackProfile = RuntimeWorldProfileFactory.CreateFallbackProfile(); return; } runtimeFallbackProfile = null; } private bool TryFindPlayer() { if (player != null) { return true; } SimplePlayerInputMover mover = FindFirstObjectByType(); if (mover == null) { return false; } player = mover.transform; return true; } private void ScheduleChunkGeneration(Vector2Int centerChunk) { List coords = GetCoordsByPriority(centerChunk, generationRadius); for (int i = 0; i < coords.Count; i++) { Vector2Int coord = coords[i]; ChunkRuntime runtime = GetOrCreateChunkRuntime(coord); if (runtime.IsRendered || runtime.HasData || runtime.State == ChunkState.Generating) { continue; } if (IsWithinRadius(coord, centerChunk, blockingGenerationRadius)) { continue; } if (!TryReserveGenerationSlot()) { break; } runtime.State = ChunkState.Generating; runtime.Version++; GenerateChunkDataAsync(coord, runtime.Version).Forget(); } } private void UnloadDistantChunks(Vector2Int centerChunk) { List coordsToRemove = new List(); foreach (KeyValuePair pair in chunks) { if (IsWithinActiveRadius(pair.Key, centerChunk)) { continue; } coordsToRemove.Add(pair.Key); } for (int i = 0; i < coordsToRemove.Count; i++) { Vector2Int coord = coordsToRemove[i]; if (!chunks.TryGetValue(coord, out ChunkRuntime chunk)) { continue; } chunks.Remove(coord); if (chunk.Chunk.Root != null) { Destroy(chunk.Chunk.Root.gameObject); } RefreshNeighborBorders(coord); } } private bool IsWithinActiveRadius(Vector2Int coord, Vector2Int centerChunk) { int dx = Mathf.Abs(coord.x - centerChunk.x); int dy = Mathf.Abs(coord.y - centerChunk.y); return dx <= generationRadius && dy <= generationRadius; } private GeneratedChunk CreateChunk(Vector2Int coord) { GameObject chunkObject = new GameObject($"Chunk_{coord.x}_{coord.y}"); chunkObject.transform.SetParent(chunkRoot, false); chunkObject.transform.localPosition = new Vector3(coord.x * chunkSize, coord.y * chunkSize, 0f); Tilemap ground = CreateTilemap("Ground", chunkObject.transform, 0, false); Tilemap walls = CreateTilemap("Walls", chunkObject.transform, 1, true); Tilemap environment = CreateTilemap("Environment", chunkObject.transform, 2, false); Transform objectsRoot = new GameObject("Objects").transform; objectsRoot.SetParent(chunkObject.transform, false); return new GeneratedChunk(chunkObject.transform, ground, walls, environment, objectsRoot); } private Tilemap CreateTilemap(string name, Transform parent, int sortingOrder, bool addCollision) { GameObject tilemapObject = new GameObject(name, typeof(Tilemap), typeof(TilemapRenderer)); tilemapObject.transform.SetParent(parent, false); TilemapRenderer renderer = tilemapObject.GetComponent(); renderer.sortingOrder = sortingOrder; if (addCollision) { Rigidbody2D rb = tilemapObject.GetComponent(); if (rb == null) { rb = tilemapObject.AddComponent(); } rb.bodyType = RigidbodyType2D.Static; CompositeCollider2D composite = tilemapObject.GetComponent(); if (composite == null) { composite = tilemapObject.AddComponent(); } composite.geometryType = CompositeCollider2D.GeometryType.Polygons; TilemapCollider2D collider = tilemapObject.GetComponent(); if (collider == null) { collider = tilemapObject.AddComponent(); } collider.compositeOperation = Collider2D.CompositeOperation.Merge; } return tilemapObject.GetComponent(); } private ChunkBuildResult GenerateChunkData(Vector2Int coord, int version) { int margin = Mathf.Max(2, smoothingPasses + 1); int sampleSize = chunkSize + margin * 2; bool[,] sampled = new bool[sampleSize, sampleSize]; for (int y = 0; y < sampleSize; y++) { for (int x = 0; x < sampleSize; x++) { int localX = x - margin; int localY = y - margin; Vector2Int worldCell = ChunkToWorldCell(coord, localX, localY); sampled[x, y] = SampleRock(worldCell); } } for (int pass = 0; pass < smoothingPasses; pass++) { sampled = SmoothSampledMask(sampled); } bool[,] wallMask = new bool[chunkSize, chunkSize]; for (int y = 0; y < chunkSize; y++) { for (int x = 0; x < chunkSize; x++) { wallMask[x, y] = sampled[x + margin, y + margin]; } } bool[,] environmentMask = BuildEnvironment(coord, wallMask); return new ChunkBuildResult(coord, wallMask, environmentMask, version); } private bool[,] SmoothSampledMask(bool[,] source) { int width = source.GetLength(0); int height = source.GetLength(1); bool[,] result = new bool[width, height]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int solidNeighbors = CountSampledWallNeighbors(source, x, y); if (solidNeighbors >= 5) { result[x, y] = true; } else if (solidNeighbors <= 2) { result[x, y] = false; } else { result[x, y] = source[x, y]; } } } return result; } private int CountSampledWallNeighbors(bool[,] sampled, int x, int y) { int width = sampled.GetLength(0); int height = sampled.GetLength(1); int count = 0; for (int oy = -1; oy <= 1; oy++) { for (int ox = -1; ox <= 1; ox++) { if (ox == 0 && oy == 0) { continue; } int nx = x + ox; int ny = y + oy; if (nx < 0 || ny < 0 || nx >= width || ny >= height) { count++; continue; } if (sampled[nx, ny]) { count++; } } } return count; } private bool SampleRock(Vector2Int worldCell) { float macro = Mathf.PerlinNoise((worldCell.x + seed * 0.13f) * macroNoiseScale, (worldCell.y - seed * 0.17f) * macroNoiseScale); float detail = Mathf.PerlinNoise((worldCell.x - seed * 0.23f) * detailNoiseScale, (worldCell.y + seed * 0.19f) * detailNoiseScale); float ridge = 1f - Mathf.Abs(Mathf.PerlinNoise((worldCell.x + seed * 0.31f) * ridgeNoiseScale, (worldCell.y + seed * 0.29f) * ridgeNoiseScale) * 2f - 1f); float rockValue = macro * 0.62f + detail * 0.18f + ridge * 0.20f + rockBias; if (IsInsideGlobalPass(worldCell)) { rockValue -= 0.45f; } return rockValue >= wallThreshold; } private bool IsInsideGlobalPass(Vector2Int worldCell) { float primary = Mathf.PerlinNoise((worldCell.x + seed * 0.41f) * passNoiseScale, (worldCell.y - seed * 0.43f) * passNoiseScale); float detail = Mathf.PerlinNoise((worldCell.x - seed * 0.17f) * passDetailScale, (worldCell.y + seed * 0.23f) * passDetailScale); float ridged = Mathf.Abs(primary * 2f - 1f); float warped = Mathf.Lerp(ridged, Mathf.Abs(detail * 2f - 1f), 0.35f); return warped <= passThreshold + passFeather * detail; } private bool[,] BuildEnvironment(Vector2Int coord, bool[,] wallMask) { bool[,] environmentMask = new bool[chunkSize, chunkSize]; for (int y = 0; y < chunkSize; y++) { for (int x = 0; x < chunkSize; x++) { if (wallMask[x, y]) { continue; } if (HasAdjacentOpenTiles(wallMask, x, y, 1)) { continue; } Vector2Int worldCell = ChunkToWorldCell(coord, x, y); float noise = Mathf.PerlinNoise((worldCell.x + seed * 0.53f) * environmentNoiseScale, (worldCell.y - seed * 0.61f) * environmentNoiseScale); environmentMask[x, y] = noise >= environmentThreshold; } } return environmentMask; } private bool HasAdjacentOpenTiles(bool[,] wallMask, int x, int y, int radius) { for (int oy = -radius; oy <= radius; oy++) { for (int ox = -radius; ox <= radius; ox++) { int nx = x + ox; int ny = y + oy; if (nx < 0 || ny < 0 || nx >= chunkSize || ny >= chunkSize) { continue; } if (!wallMask[nx, ny]) { return true; } } } return false; } private void RenderChunk(Vector2Int coord) { if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !runtime.HasData) { return; } WorldAutotileProfile activeProfile = profile != null && profile.HasAnyAssignedTiles() ? profile : runtimeFallbackProfile; if (activeProfile == null) { return; } if (runtime.Chunk.Root == null) { runtime.Chunk = CreateChunk(coord); } GeneratedChunk chunk = runtime.Chunk; int tileCount = chunkSize * chunkSize; TileBase[] groundTiles = GetGroundTiles(activeProfile.baseGroundTile, tileCount); TileBase[] wallTiles = new TileBase[tileCount]; TileBase[] environmentTiles = new TileBase[tileCount]; int index = 0; for (int y = 0; y < chunkSize; y++) { for (int x = 0; x < chunkSize; x++) { if (runtime.WallMask[x, y]) { Vector2Int worldCell = ChunkToWorldCell(coord, x, y); AutoTileShape shape = ResolveWallShape(worldCell); wallTiles[index] = activeProfile.GetWallTile(shape); } else if (runtime.EnvironmentMask[x, y]) { TileBase tile = PickEnvironmentTile(ChunkToWorldCell(coord, x, y), activeProfile); environmentTiles[index] = tile; } index++; } } BoundsInt bounds = new BoundsInt(0, 0, 0, chunkSize, chunkSize, 1); chunk.Ground.SetTilesBlock(bounds, groundTiles); chunk.Walls.SetTilesBlock(bounds, wallTiles); chunk.Environment.SetTilesBlock(bounds, environmentTiles); RenderRandomPrefabs(coord, runtime, activeProfile); runtime.State = ChunkState.Rendered; } private void RefreshNeighborBorders(Vector2Int coord) { for (int i = 0; i < NeighborOffsets.Length; i++) { RenderChunk(coord + NeighborOffsets[i]); } } private AutoTileShape ResolveWallShape(Vector2Int worldCell) { bool top = HasWallAt(worldCell + Vector2Int.up); bool right = HasWallAt(worldCell + Vector2Int.right); bool bottom = HasWallAt(worldCell + Vector2Int.down); bool left = HasWallAt(worldCell + Vector2Int.left); bool topLeft = HasWallAt(worldCell + new Vector2Int(-1, 1)); bool topRight = HasWallAt(worldCell + new Vector2Int(1, 1)); bool bottomRight = HasWallAt(worldCell + new Vector2Int(1, -1)); bool bottomLeft = HasWallAt(worldCell + new Vector2Int(-1, -1)); if (!top && !left) { return AutoTileShape.OuterTopLeft; } if (!top && !right) { return AutoTileShape.OuterTopRight; } if (!bottom && !right) { return AutoTileShape.OuterBottomRight; } if (!bottom && !left) { return AutoTileShape.OuterBottomLeft; } if (top && left && !topLeft) { return AutoTileShape.InnerTopLeft; } if (top && right && !topRight) { return AutoTileShape.InnerTopRight; } if (bottom && right && !bottomRight) { return AutoTileShape.InnerBottomRight; } if (bottom && left && !bottomLeft) { return AutoTileShape.InnerBottomLeft; } if (!top) { return AutoTileShape.Top; } if (!right) { return AutoTileShape.Right; } if (!bottom) { return AutoTileShape.Bottom; } if (!left) { return AutoTileShape.Left; } return AutoTileShape.Center; } private bool HasWallAt(Vector2Int worldCell) { Vector2Int coord = new Vector2Int(Mathf.FloorToInt(worldCell.x / (float)chunkSize), Mathf.FloorToInt(worldCell.y / (float)chunkSize)); if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !runtime.HasData) { return SampleRock(worldCell); } int localX = worldCell.x - coord.x * chunkSize; int localY = worldCell.y - coord.y * chunkSize; if (localX < 0 || localY < 0 || localX >= chunkSize || localY >= chunkSize) { return SampleRock(worldCell); } return runtime.WallMask[localX, localY]; } private TileBase PickEnvironmentTile(Vector2Int worldCell, WorldAutotileProfile activeProfile) { if (activeProfile.environmentTiles == null || activeProfile.environmentTiles.Count == 0) { return null; } float total = 0f; for (int i = 0; i < activeProfile.environmentTiles.Count; i++) { EnvironmentTileEntry entry = activeProfile.environmentTiles[i]; if (entry != null && entry.tile != null) { total += Mathf.Max(0.01f, entry.weight); } } if (total <= 0f) { return null; } float selector = Hash01(worldCell.x, worldCell.y, seed + 701); float threshold = selector * total; float cumulative = 0f; for (int i = 0; i < activeProfile.environmentTiles.Count; i++) { EnvironmentTileEntry entry = activeProfile.environmentTiles[i]; if (entry == null || entry.tile == null) { continue; } cumulative += Mathf.Max(0.01f, entry.weight); if (threshold <= cumulative) { return entry.tile; } } return activeProfile.environmentTiles[0].tile; } private void RenderRandomPrefabs(Vector2Int coord, ChunkRuntime runtime, WorldAutotileProfile activeProfile) { if (runtime.Chunk.ObjectsRoot == null) { return; } ClearSpawnedObjects(runtime.Chunk.ObjectsRoot); if (randomPrefabChance <= 0f || activeProfile.randomPrefabs == null || activeProfile.randomPrefabs.Count == 0) { return; } for (int y = 0; y < chunkSize; y++) { for (int x = 0; x < chunkSize; x++) { if (runtime.WallMask[x, y] || runtime.EnvironmentMask[x, y]) { continue; } Vector2Int worldCell = ChunkToWorldCell(coord, x, y); if (Hash01(worldCell.x, worldCell.y, seed + 1103) > randomPrefabChance) { continue; } GameObject prefab = PickRandomPrefab(worldCell, activeProfile); if (prefab == null) { continue; } GameObject instance = Instantiate(prefab, runtime.Chunk.ObjectsRoot); instance.transform.localPosition = new Vector3(x + 0.5f, y + 0.5f, randomPrefabZOffset); instance.transform.localRotation = Quaternion.identity; instance.name = prefab.name; } } } private GameObject PickRandomPrefab(Vector2Int worldCell, WorldAutotileProfile activeProfile) { float total = 0f; for (int i = 0; i < activeProfile.randomPrefabs.Count; i++) { RandomPrefabEntry entry = activeProfile.randomPrefabs[i]; if (entry != null && entry.prefab != null) { total += Mathf.Max(0.01f, entry.weight); } } if (total <= 0f) { return null; } float selector = Hash01(worldCell.x, worldCell.y, seed + 1409) * total; float cumulative = 0f; for (int i = 0; i < activeProfile.randomPrefabs.Count; i++) { RandomPrefabEntry entry = activeProfile.randomPrefabs[i]; if (entry == null || entry.prefab == null) { continue; } cumulative += Mathf.Max(0.01f, entry.weight); if (selector <= cumulative) { return entry.prefab; } } for (int i = 0; i < activeProfile.randomPrefabs.Count; i++) { RandomPrefabEntry entry = activeProfile.randomPrefabs[i]; if (entry != null && entry.prefab != null) { return entry.prefab; } } return null; } private static void ClearSpawnedObjects(Transform root) { for (int i = root.childCount - 1; i >= 0; i--) { UnityEngine.Object.Destroy(root.GetChild(i).gameObject); } } private Vector2Int WorldToChunk(Vector3 position) { return new Vector2Int( Mathf.FloorToInt(position.x / chunkSize), Mathf.FloorToInt(position.y / chunkSize)); } private Vector2Int ChunkToWorldCell(Vector2Int coord, int localX, int localY) { return new Vector2Int(coord.x * chunkSize + localX, coord.y * chunkSize + localY); } private static int Hash(int x, int y, int seed) { int hash = x; hash = hash * 397 ^ y; hash = hash * 397 ^ seed; hash = (hash << 13) ^ hash; return hash * (hash * hash * 15731 + 789221) + 1376312589; } private static float Hash01(int x, int y, int seed) { return (Hash(x, y, seed) & int.MaxValue) / (float)int.MaxValue; } private async UniTaskVoid GenerateChunkDataAsync(Vector2Int coord, int version) { try { ChunkBuildResult result = await UniTask.RunOnThreadPool(() => GenerateChunkData(coord, version)); lock (generationLock) { completedBuilds.Enqueue(result); activeGenerationJobs = Mathf.Max(0, activeGenerationJobs - 1); } } catch (Exception exception) { lock (generationLock) { activeGenerationJobs = Mathf.Max(0, activeGenerationJobs - 1); } Debug.LogException(exception, this); } } private void DrainCompletedBuilds(int maxRenders) { int renders = 0; while (renders < maxRenders) { ChunkBuildResult result; lock (generationLock) { if (completedBuilds.Count == 0) { break; } result = completedBuilds.Dequeue(); } if (!ApplyBuildResult(result)) { continue; } RenderChunk(result.Coord); RefreshNeighborBorders(result.Coord); renders++; } } private bool ApplyBuildResult(ChunkBuildResult result) { if (!chunks.TryGetValue(result.Coord, out ChunkRuntime runtime)) { return false; } if (result.Version != runtime.Version) { return false; } runtime.WallMask = result.WallMask; runtime.EnvironmentMask = result.EnvironmentMask; if (!runtime.IsRendered) { runtime.State = ChunkState.ReadyToRender; } return !runtime.IsRendered; } private void EnsureBlockingChunksLoaded(Vector2Int centerChunk) { List requiredCoords = GetCoordsByPriority(centerChunk, blockingGenerationRadius); bool isMissingRequiredChunk = false; for (int i = 0; i < requiredCoords.Count; i++) { Vector2Int coord = requiredCoords[i]; ChunkRuntime runtime = GetOrCreateChunkRuntime(coord); if (runtime.IsRendered) { continue; } isMissingRequiredChunk = true; if (runtime.State == ChunkState.Generating) { continue; } runtime.Version++; runtime.State = ChunkState.SyncBuilding; ApplyBuildResult(GenerateChunkData(coord, runtime.Version)); RenderChunk(coord); RefreshNeighborBorders(coord); } DrainCompletedBuilds(isMissingRequiredChunk ? int.MaxValue : maxChunkRendersPerFrame); SetLoadingPaused(HasMissingRequiredChunks(requiredCoords)); } private bool HasMissingRequiredChunks(List requiredCoords) { for (int i = 0; i < requiredCoords.Count; i++) { if (!chunks.TryGetValue(requiredCoords[i], out ChunkRuntime runtime) || !runtime.IsRendered) { return true; } } return false; } private bool TryReserveGenerationSlot() { lock (generationLock) { if (activeGenerationJobs >= maxAsyncChunkJobs) { return false; } activeGenerationJobs++; return true; } } private List GetCoordsByPriority(Vector2Int centerChunk, int radius) { List coords = new List((radius * 2 + 1) * (radius * 2 + 1)); for (int y = -radius; y <= radius; y++) { for (int x = -radius; x <= radius; x++) { coords.Add(new Vector2Int(centerChunk.x + x, centerChunk.y + y)); } } coords.Sort((left, right) => CompareChunkPriority(centerChunk, left, right)); return coords; } private static int CompareChunkPriority(Vector2Int centerChunk, Vector2Int left, Vector2Int right) { int leftDx = Mathf.Abs(left.x - centerChunk.x); int leftDy = Mathf.Abs(left.y - centerChunk.y); int rightDx = Mathf.Abs(right.x - centerChunk.x); int rightDy = Mathf.Abs(right.y - centerChunk.y); int leftChebyshev = Mathf.Max(leftDx, leftDy); int rightChebyshev = Mathf.Max(rightDx, rightDy); int chebyshevCompare = leftChebyshev.CompareTo(rightChebyshev); if (chebyshevCompare != 0) { return chebyshevCompare; } int leftDistance = leftDx * leftDx + leftDy * leftDy; int rightDistance = rightDx * rightDx + rightDy * rightDy; int distanceCompare = leftDistance.CompareTo(rightDistance); if (distanceCompare != 0) { return distanceCompare; } int yCompare = left.y.CompareTo(right.y); return yCompare != 0 ? yCompare : left.x.CompareTo(right.x); } private ChunkRuntime GetOrCreateChunkRuntime(Vector2Int coord) { if (!chunks.TryGetValue(coord, out ChunkRuntime runtime)) { runtime = new ChunkRuntime(); chunks.Add(coord, runtime); } return runtime; } private bool IsWithinRadius(Vector2Int coord, Vector2Int centerChunk, int radius) { int dx = Mathf.Abs(coord.x - centerChunk.x); int dy = Mathf.Abs(coord.y - centerChunk.y); return dx <= radius && dy <= radius; } private TileBase[] GetGroundTiles(TileBase groundTile, int tileCount) { if (cachedGroundTiles == null || cachedGroundTiles.Length != tileCount || cachedGroundTile != groundTile) { cachedGroundTiles = new TileBase[tileCount]; cachedGroundTile = groundTile; if (groundTile != null) { Array.Fill(cachedGroundTiles, groundTile); } } return cachedGroundTiles; } private void SetLoadingPaused(bool pause) { if (pause == isLoadingPaused) { return; } if (pause) { pausedTimeScale = Time.timeScale > 0f ? Time.timeScale : 1f; Time.timeScale = 0f; } else { Time.timeScale = pausedTimeScale; } isLoadingPaused = pause; } private readonly struct GeneratedChunk { public GeneratedChunk(Transform root, Tilemap ground, Tilemap walls, Tilemap environment, Transform objectsRoot) { Root = root; Ground = ground; Walls = walls; Environment = environment; ObjectsRoot = objectsRoot; } public Transform Root { get; } public Tilemap Ground { get; } public Tilemap Walls { get; } public Tilemap Environment { get; } public Transform ObjectsRoot { get; } } private sealed class ChunkRuntime { public GeneratedChunk Chunk; public bool[,] WallMask; public bool[,] EnvironmentMask; public ChunkState State; public int Version; public bool HasData => WallMask != null && EnvironmentMask != null; public bool IsRendered => State == ChunkState.Rendered && Chunk.Root != null; } private readonly struct ChunkBuildResult { public ChunkBuildResult(Vector2Int coord, bool[,] wallMask, bool[,] environmentMask, int version) { Coord = coord; WallMask = wallMask; EnvironmentMask = environmentMask; Version = version; } public Vector2Int Coord { get; } public bool[,] WallMask { get; } public bool[,] EnvironmentMask { get; } public int Version { get; } } private enum ChunkState { None, Generating, SyncBuilding, ReadyToRender, Rendered } } }