From 456fe76e8689842d0457d9e622ed2d9b8f225bc6 Mon Sep 17 00:00:00 2001 From: Konstantin Dyachenko Date: Sun, 29 Mar 2026 03:31:05 +0700 Subject: [PATCH] [Add] UniTask --- .../WorldGen/InfiniteWorldGenerator.cs | 429 +++++++++++++++--- 1 file changed, 363 insertions(+), 66 deletions(-) diff --git a/Assets/Scripts/WorldGen/InfiniteWorldGenerator.cs b/Assets/Scripts/WorldGen/InfiniteWorldGenerator.cs index d927c7ac..aaa5c948 100644 --- a/Assets/Scripts/WorldGen/InfiniteWorldGenerator.cs +++ b/Assets/Scripts/WorldGen/InfiniteWorldGenerator.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.Tilemaps; @@ -33,11 +35,31 @@ namespace InfiniteWorld [SerializeField] private float environmentNoiseScale = 0.19f; [SerializeField] private float environmentThreshold = 0.7f; - private readonly Dictionary chunks = new Dictionary(); + [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() { @@ -54,13 +76,20 @@ namespace InfiniteWorld } Vector2Int playerChunk = WorldToChunk(player.position); - if (playerChunk == lastGeneratedCenter) + if (playerChunk != lastGeneratedCenter) { - return; + lastGeneratedCenter = playerChunk; + UnloadDistantChunks(playerChunk); } - lastGeneratedCenter = playerChunk; - UpdateActiveChunks(playerChunk); + DrainCompletedBuilds(isLoadingPaused ? int.MaxValue : maxChunkRendersPerFrame); + ScheduleChunkGeneration(playerChunk); + EnsureBlockingChunksLoaded(playerChunk); + } + + private void OnDisable() + { + SetLoadingPaused(false); } private void EnsureSceneInfrastructure() @@ -111,37 +140,38 @@ namespace InfiniteWorld return true; } - private void UpdateActiveChunks(Vector2Int centerChunk) + private void ScheduleChunkGeneration(Vector2Int centerChunk) { - GenerateAround(centerChunk); - UnloadDistantChunks(centerChunk); - } - - private void GenerateAround(Vector2Int centerChunk) - { - for (int y = -generationRadius; y <= generationRadius; y++) + List coords = GetCoordsByPriority(centerChunk, generationRadius); + for (int i = 0; i < coords.Count; i++) { - for (int x = -generationRadius; x <= generationRadius; x++) + Vector2Int coord = coords[i]; + ChunkRuntime runtime = GetOrCreateChunkRuntime(coord); + if (runtime.IsRendered || runtime.HasData || runtime.State == ChunkState.Generating) { - Vector2Int coord = new Vector2Int(centerChunk.x + x, centerChunk.y + y); - if (chunks.ContainsKey(coord)) - { - continue; - } - - GeneratedChunk chunk = CreateChunk(coord); - chunks.Add(coord, chunk); - BuildChunkData(coord, chunk); - RenderChunk(coord); - RefreshNeighborBorders(coord); + 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) + foreach (KeyValuePair pair in chunks) { if (IsWithinActiveRadius(pair.Key, centerChunk)) { @@ -154,21 +184,18 @@ namespace InfiniteWorld for (int i = 0; i < coordsToRemove.Count; i++) { Vector2Int coord = coordsToRemove[i]; - if (!chunks.TryGetValue(coord, out GeneratedChunk chunk)) + if (!chunks.TryGetValue(coord, out ChunkRuntime chunk)) { continue; } chunks.Remove(coord); - if (chunk.Root != null) + if (chunk.Chunk.Root != null) { - Destroy(chunk.Root.gameObject); + Destroy(chunk.Chunk.Root.gameObject); } - RefreshNeighborBorders(coord + Vector2Int.up); - RefreshNeighborBorders(coord + Vector2Int.right); - RefreshNeighborBorders(coord + Vector2Int.down); - RefreshNeighborBorders(coord + Vector2Int.left); + RefreshNeighborBorders(coord); } } @@ -189,7 +216,7 @@ namespace InfiniteWorld Tilemap walls = CreateTilemap("Walls", chunkObject.transform, 1, true); Tilemap environment = CreateTilemap("Environment", chunkObject.transform, 2, false); - return new GeneratedChunk(chunkObject.transform, ground, walls, environment, new bool[chunkSize, chunkSize], new bool[chunkSize, chunkSize]); + return new GeneratedChunk(chunkObject.transform, ground, walls, environment); } private Tilemap CreateTilemap(string name, Transform parent, int sortingOrder, bool addCollision) @@ -227,7 +254,7 @@ namespace InfiniteWorld return tilemapObject.GetComponent(); } - private void BuildChunkData(Vector2Int coord, GeneratedChunk chunk) + private ChunkBuildResult GenerateChunkData(Vector2Int coord, int version) { int margin = Mathf.Max(2, smoothingPasses + 1); int sampleSize = chunkSize + margin * 2; @@ -249,15 +276,17 @@ namespace InfiniteWorld sampled = SmoothSampledMask(sampled); } + bool[,] wallMask = new bool[chunkSize, chunkSize]; for (int y = 0; y < chunkSize; y++) { for (int x = 0; x < chunkSize; x++) { - chunk.WallMask[x, y] = sampled[x + margin, y + margin]; + wallMask[x, y] = sampled[x + margin, y + margin]; } } - BuildEnvironment(coord, chunk); + bool[,] environmentMask = BuildEnvironment(coord, wallMask); + return new ChunkBuildResult(coord, wallMask, environmentMask, version); } private bool[,] SmoothSampledMask(bool[,] source) @@ -346,29 +375,30 @@ namespace InfiniteWorld return warped <= passThreshold + passFeather * detail; } - private void BuildEnvironment(Vector2Int coord, GeneratedChunk chunk) + 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 (chunk.WallMask[x, y]) + if (wallMask[x, y]) { - chunk.EnvironmentMask[x, y] = false; continue; } - if (HasAdjacentOpenTiles(chunk.WallMask, x, y, 1)) + if (HasAdjacentOpenTiles(wallMask, x, y, 1)) { - chunk.EnvironmentMask[x, y] = false; continue; } Vector2Int worldCell = ChunkToWorldCell(coord, x, y); float noise = Mathf.PerlinNoise((worldCell.x + seed * 0.53f) * environmentNoiseScale, (worldCell.y - seed * 0.61f) * environmentNoiseScale); - chunk.EnvironmentMask[x, y] = noise >= environmentThreshold; + environmentMask[x, y] = noise >= environmentThreshold; } } + + return environmentMask; } private bool HasAdjacentOpenTiles(bool[,] wallMask, int x, int y, int radius) @@ -396,7 +426,7 @@ namespace InfiniteWorld private void RenderChunk(Vector2Int coord) { - if (!chunks.TryGetValue(coord, out GeneratedChunk chunk)) + if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !runtime.HasData) { return; } @@ -407,41 +437,51 @@ namespace InfiniteWorld return; } - chunk.Ground.ClearAllTiles(); - chunk.Walls.ClearAllTiles(); - chunk.Environment.ClearAllTiles(); + 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++) { - Vector3Int localCell = new Vector3Int(x, y, 0); - chunk.Ground.SetTile(localCell, activeProfile.baseGroundTile); - - if (chunk.WallMask[x, y]) + if (runtime.WallMask[x, y]) { Vector2Int worldCell = ChunkToWorldCell(coord, x, y); AutoTileShape shape = ResolveWallShape(worldCell); - chunk.Walls.SetTile(localCell, activeProfile.GetWallTile(shape)); + wallTiles[index] = activeProfile.GetWallTile(shape); } - else if (chunk.EnvironmentMask[x, y]) + else if (runtime.EnvironmentMask[x, y]) { TileBase tile = PickEnvironmentTile(ChunkToWorldCell(coord, x, y), activeProfile); - if (tile != null) - { - chunk.Environment.SetTile(localCell, tile); - } + 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); + runtime.State = ChunkState.Rendered; } private void RefreshNeighborBorders(Vector2Int coord) { - RenderChunk(coord + Vector2Int.up); - RenderChunk(coord + Vector2Int.right); - RenderChunk(coord + Vector2Int.down); - RenderChunk(coord + Vector2Int.left); + for (int i = 0; i < NeighborOffsets.Length; i++) + { + RenderChunk(coord + NeighborOffsets[i]); + } } private AutoTileShape ResolveWallShape(Vector2Int worldCell) @@ -521,7 +561,7 @@ namespace InfiniteWorld 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 GeneratedChunk chunk)) + if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !runtime.HasData) { return SampleRock(worldCell); } @@ -533,7 +573,7 @@ namespace InfiniteWorld return SampleRock(worldCell); } - return chunk.WallMask[localX, localY]; + return runtime.WallMask[localX, localY]; } private TileBase PickEnvironmentTile(Vector2Int worldCell, WorldAutotileProfile activeProfile) @@ -606,24 +646,281 @@ namespace InfiniteWorld 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, bool[,] wallMask, bool[,] environmentMask) + public GeneratedChunk(Transform root, Tilemap ground, Tilemap walls, Tilemap environment) { Root = root; Ground = ground; Walls = walls; Environment = environment; - WallMask = wallMask; - EnvironmentMask = environmentMask; } public Transform Root { get; } public Tilemap Ground { get; } public Tilemap Walls { get; } public Tilemap Environment { 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 } } }