1065 lines
36 KiB
C#
1065 lines
36 KiB
C#
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<Vector2Int, ChunkRuntime> chunks = new Dictionary<Vector2Int, ChunkRuntime>();
|
|
private readonly Queue<ChunkBuildResult> completedBuilds = new Queue<ChunkBuildResult>();
|
|
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<Vector2Int> coords = new List<Vector2Int>(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<Grid>();
|
|
if (grid == null)
|
|
{
|
|
GameObject gridObject = new GameObject("Grid", typeof(Grid));
|
|
gridObject.transform.SetParent(transform, false);
|
|
grid = gridObject.GetComponent<Grid>();
|
|
}
|
|
|
|
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<SimplePlayerInputMover>();
|
|
if (mover == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
player = mover.transform;
|
|
return true;
|
|
}
|
|
|
|
private void ScheduleChunkGeneration(Vector2Int centerChunk)
|
|
{
|
|
List<Vector2Int> 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<Vector2Int> coordsToRemove = new List<Vector2Int>();
|
|
foreach (KeyValuePair<Vector2Int, ChunkRuntime> 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<TilemapRenderer>();
|
|
renderer.sortingOrder = sortingOrder;
|
|
|
|
if (addCollision)
|
|
{
|
|
Rigidbody2D rb = tilemapObject.GetComponent<Rigidbody2D>();
|
|
if (rb == null)
|
|
{
|
|
rb = tilemapObject.AddComponent<Rigidbody2D>();
|
|
}
|
|
rb.bodyType = RigidbodyType2D.Static;
|
|
|
|
CompositeCollider2D composite = tilemapObject.GetComponent<CompositeCollider2D>();
|
|
if (composite == null)
|
|
{
|
|
composite = tilemapObject.AddComponent<CompositeCollider2D>();
|
|
}
|
|
composite.geometryType = CompositeCollider2D.GeometryType.Polygons;
|
|
|
|
TilemapCollider2D collider = tilemapObject.GetComponent<TilemapCollider2D>();
|
|
if (collider == null)
|
|
{
|
|
collider = tilemapObject.AddComponent<TilemapCollider2D>();
|
|
}
|
|
collider.compositeOperation = Collider2D.CompositeOperation.Merge;
|
|
}
|
|
|
|
return tilemapObject.GetComponent<Tilemap>();
|
|
}
|
|
|
|
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<Vector2Int> 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<Vector2Int> 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<Vector2Int> GetCoordsByPriority(Vector2Int centerChunk, int radius)
|
|
{
|
|
List<Vector2Int> coords = new List<Vector2Int>((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
|
|
}
|
|
}
|
|
}
|