This commit is contained in:
2026-03-29 02:26:31 +07:00
parent 1e458a4f09
commit 99c70886a5
22 changed files with 2430 additions and 44 deletions
@@ -0,0 +1,582 @@
using System.Collections.Generic;
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;
private readonly Dictionary<Vector2Int, GeneratedChunk> chunks = new Dictionary<Vector2Int, GeneratedChunk>();
private Grid grid;
private Transform chunkRoot;
private Vector2Int lastGeneratedCenter = new Vector2Int(int.MinValue, int.MinValue);
private WorldAutotileProfile runtimeFallbackProfile;
private void Awake()
{
EnsureSceneInfrastructure();
EnsureRuntimeData();
TryFindPlayer();
}
private void Update()
{
if (player == null && !TryFindPlayer())
{
return;
}
Vector2Int playerChunk = WorldToChunk(player.position);
if (playerChunk == lastGeneratedCenter)
{
return;
}
lastGeneratedCenter = playerChunk;
GenerateAround(playerChunk);
}
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();
}
}
private bool TryFindPlayer()
{
if (player != null)
{
return true;
}
SimplePlayerInputMover mover = FindFirstObjectByType<SimplePlayerInputMover>();
if (mover == null)
{
return false;
}
player = mover.transform;
return true;
}
private void GenerateAround(Vector2Int centerChunk)
{
for (int y = -generationRadius; y <= generationRadius; y++)
{
for (int x = -generationRadius; x <= generationRadius; x++)
{
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);
}
}
}
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);
return new GeneratedChunk(chunkObject.transform, ground, walls, environment, new bool[chunkSize, chunkSize], new bool[chunkSize, chunkSize]);
}
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 void BuildChunkData(Vector2Int coord, GeneratedChunk chunk)
{
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);
}
for (int y = 0; y < chunkSize; y++)
{
for (int x = 0; x < chunkSize; x++)
{
chunk.WallMask[x, y] = sampled[x + margin, y + margin];
}
}
BuildEnvironment(coord, chunk);
}
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 void BuildEnvironment(Vector2Int coord, GeneratedChunk chunk)
{
for (int y = 0; y < chunkSize; y++)
{
for (int x = 0; x < chunkSize; x++)
{
if (chunk.WallMask[x, y])
{
chunk.EnvironmentMask[x, y] = false;
continue;
}
if (HasAdjacentOpenTiles(chunk.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;
}
}
}
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 GeneratedChunk chunk))
{
return;
}
WorldAutotileProfile activeProfile = profile != null && profile.HasAnyAssignedTiles() ? profile : runtimeFallbackProfile;
if (activeProfile == null)
{
return;
}
chunk.Ground.ClearAllTiles();
chunk.Walls.ClearAllTiles();
chunk.Environment.ClearAllTiles();
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])
{
Vector2Int worldCell = ChunkToWorldCell(coord, x, y);
AutoTileShape shape = ResolveWallShape(worldCell);
chunk.Walls.SetTile(localCell, activeProfile.GetWallTile(shape));
}
else if (chunk.EnvironmentMask[x, y])
{
TileBase tile = PickEnvironmentTile(ChunkToWorldCell(coord, x, y), activeProfile);
if (tile != null)
{
chunk.Environment.SetTile(localCell, tile);
}
}
}
}
}
private void RefreshNeighborBorders(Vector2Int coord)
{
RenderChunk(coord + Vector2Int.up);
RenderChunk(coord + Vector2Int.right);
RenderChunk(coord + Vector2Int.down);
RenderChunk(coord + Vector2Int.left);
}
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 GeneratedChunk chunk))
{
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 chunk.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 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 readonly struct GeneratedChunk
{
public GeneratedChunk(Transform root, Tilemap ground, Tilemap walls, Tilemap environment, bool[,] wallMask, bool[,] environmentMask)
{
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; }
public bool[,] WallMask { get; }
public bool[,] EnvironmentMask { get; }
}
}
}