using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Unity.Collections; using Unity.Jobs; using Unity.VisualScripting; using UnityEngine; using UnityEngine.Tilemaps; public class GenerateTileMap : MonoBehaviour { public int? seed; public static int maxWidth = 255; public static int maxDepth = 384; public static int groundDepth = 255; private float scale; private float offsetX; private float offsetY; private NativeArray perlinNoiseCache; Tilemap tilemap; public CustomRuleTile forestRuleTile; public TileBase borderTile; private List generateables; public const int CHUNK_SIZE = 16; // Size of each chunk public int LOAD_DISTANCE = 2; // Number of chunks to load around player private const int CACHE_CLEAR_DISTANCE = 8; // Distance in chunks before clearing cache (should be > LOAD_DISTANCE) private Vector2Int lastCacheClearPosition; // Track position where cache was last cleared [NonSerialized] public List destroyedTiles = new List(); private Dictionary loadedChunks = new Dictionary(); private Dictionary chunkCache = new Dictionary(); private Transform playerTransform; // Reference to player/camera private Vector2Int lastLoadedChunk; private GameManager gameManager; private void Awake() { gameManager = FindObjectOfType(); tilemap = GetComponent(); playerTransform = GameObject.FindGameObjectWithTag("Player").transform; // Make sure your player has the "Player" tag #if DEBUG seed = 0123456789; #endif if (seed == null) { seed = GenerateSeed(9); } SetSettingsFromSeed(seed.Value); // Position adjusted to center horizontally, but align top at y=0 transform.position = new Vector3((maxWidth / 2) * -1, -1, transform.position.z); LoadGenerateablesFromResources(); } private void Update() { if (playerTransform == null) return; //Debug.Log($"Player Position: {playerTransform.position}"); Vector2Int currentChunk = GetChunkPosition(playerTransform.position); //Debug.Log($"Current Chunk: {currentChunk}"); if (currentChunk != lastLoadedChunk) { StartCoroutine(UpdateLoadedChunks(currentChunk, destroyedTiles)); lastLoadedChunk = currentChunk; // Check if we need to clear the cache ClearDistantChunks(currentChunk); } } public (int seed, Dictionary chunkCache, List destroyedTiles) GetSaveValues() { return (seed.Value, chunkCache, destroyedTiles); } public void LoadChunkDataFromSave(List serializedChunks) { chunkCache.Clear(); foreach (var chunk in serializedChunks) { TileBase[] tiles = chunk.tileNames.Select(name => name == "null" ? null : name == forestRuleTile.name ? forestRuleTile : forestRuleTile.siblings.FirstOrDefault(s => s.name == name) ).ToArray(); chunkCache[chunk.position] = tiles; } } private IEnumerator UpdateLoadedChunks(Vector2Int currentChunk, List destroyedTiles) { // Unload distant chunks List chunksToUnload = new List(); foreach (var chunk in loadedChunks.Keys) { if (Mathf.Abs(chunk.x - currentChunk.x) > LOAD_DISTANCE || Mathf.Abs(chunk.y - currentChunk.y) > LOAD_DISTANCE) { chunksToUnload.Add(chunk); UnloadChunk(chunk); } } foreach (var chunk in chunksToUnload) { loadedChunks.Remove(chunk); } // Load new chunks yield return LoadChunksAroundPosition(currentChunk, destroyedTiles); } private void UnloadChunk(Vector2Int chunk) { var emptyTiles = new TileBase[CHUNK_SIZE * CHUNK_SIZE]; BatchSetTiles(chunk, emptyTiles); } private void LoadGenerateablesFromResources() { // Clear existing siblings forestRuleTile.siblings.Clear(); // Load all Item prefabs from the "Resources/Items" folder GameObject[] generateablePrefabs = Resources.LoadAll("Generateable"); generateables = new List(); foreach (GameObject prefab in generateablePrefabs) { Generateable generateable = prefab.GetComponent(); if (generateable != null) { generateable.tile = ScriptableObject.CreateInstance(); generateable.tile.m_DefaultGameObject = prefab; generateable.tile.m_DefaultSprite = generateable.sprite; generateables.Add(generateable); forestRuleTile.siblings.Add(generateable.tile); } else { Debug.LogWarning($"Prefab {prefab.name} does not have an Item component"); } } GenerateableDatabase.Instance.InitializeFromGenerateables(generateables); if (generateables.Count == 0) { Debug.LogWarning("No items found in Resources/Items folder"); } } public void SetSettingsFromSeed(int seed) { UnityEngine.Random.State randomState = UnityEngine.Random.state; UnityEngine.Random.InitState(seed); scale = UnityEngine.Random.Range(17f, 23f); offsetX = UnityEngine.Random.Range(-10000f, 10000f); offsetY = UnityEngine.Random.Range(-10000f, 10000f); UnityEngine.Random.state = randomState; CachePerlinNoise(); } private int GenerateSeed(int size) { System.Random rand = new System.Random(); string seedNumbers = "0123456789"; char[] chars = new char[size]; for (int i = 0; i < size; i++) { chars[i] = seedNumbers[rand.Next(seedNumbers.Length)]; } return int.Parse(new string(chars)); } private void CachePerlinNoise() { perlinNoiseCache = new NativeArray(maxWidth * maxDepth, Allocator.Persistent); for (int x = 0; x < maxWidth; x++) { for (int y = 0; y < maxDepth; y++) { float xPerlin = ((float)x / maxWidth) * scale + offsetX; float yPerlin = ((float)Math.Abs(y) / groundDepth) * scale + offsetY; perlinNoiseCache[x * groundDepth + y] = Mathf.PerlinNoise(xPerlin, yPerlin); } } } public IEnumerator GenerateTiles(Action finishedCallback, List destroyedTiles) { // Get initial player chunk position Vector2Int currentChunk = GetChunkPosition(playerTransform.position); lastLoadedChunk = currentChunk; // Generate initial chunks around player yield return LoadChunksAroundPosition(currentChunk, destroyedTiles); finishedCallback(); } public Vector2Int GetChunkPosition(Vector3 worldPosition) { // Adjust for tilemap offset float adjustedX = worldPosition.x - transform.position.x; float adjustedY = (worldPosition.y - transform.position.y) + CHUNK_SIZE; return new Vector2Int( Mathf.FloorToInt(adjustedX / CHUNK_SIZE), Mathf.FloorToInt(adjustedY / CHUNK_SIZE) ); } public void UpdateChunkCache(Vector3Int cellCoord, Vector3 cellWorldPosition) { // Update chunk cache Vector2Int chunkPos = GetChunkPosition(cellWorldPosition); if (chunkCache.TryGetValue(chunkPos, out TileBase[] cachedTiles)) { int localX = cellCoord.x - (chunkPos.x * GenerateTileMap.CHUNK_SIZE); int localY = (chunkPos.y * GenerateTileMap.CHUNK_SIZE) - cellCoord.y - 1; int index = localY * GenerateTileMap.CHUNK_SIZE + localX; if (index >= 0 && index < cachedTiles.Length) { cachedTiles[index] = null; chunkCache[chunkPos] = cachedTiles; } } } private IEnumerator LoadChunksAroundPosition(Vector2Int centerChunk, List destroyedTiles) { for (int x = -LOAD_DISTANCE; x <= LOAD_DISTANCE; x++) { for (int y = -LOAD_DISTANCE; y <= LOAD_DISTANCE; y++) { Vector2Int chunkPos = new Vector2Int(centerChunk.x + x, centerChunk.y + y); if (!loadedChunks.ContainsKey(chunkPos)) { if (chunkCache.ContainsKey(chunkPos)) { // Load the chunk from the cache yield return LoadChunkFromCache(chunkPos); } else { // Generate the chunk yield return GenerateChunk(chunkPos, destroyedTiles); } loadedChunks[chunkPos] = true; } } } } private IEnumerator LoadChunkFromCache(Vector2Int chunk) { int startX = chunk.x * CHUNK_SIZE; int startY = (chunk.y * CHUNK_SIZE) - 1; if (chunkCache.TryGetValue(chunk, out TileBase[] cachedTiles)) { //for (int i = 0; i < cachedTiles.Length; i++) //{ // int x = startX + (i % CHUNK_SIZE); // int y = startY - (i / CHUNK_SIZE); // Vector3Int tilePos = new Vector3Int(x, y); // if (!IsTileDestroyed(tilePos)) // { // tilemap.SetTile(tilePos, cachedTiles[i]); // } //} BatchSetTiles(chunk, cachedTiles, destroyedTiles); } yield return null; } private void BatchSetTiles(Vector2Int chunk, TileBase[] tiles, List destroyedTiles = null) { int startX = chunk.x * CHUNK_SIZE; int startY = (chunk.y * CHUNK_SIZE) - 1; var positions = new Vector3Int[CHUNK_SIZE * CHUNK_SIZE]; var tileArray = new TileBase[CHUNK_SIZE * CHUNK_SIZE]; for (int i = 0; i < tiles.Length; i++) { int x = startX + (i % CHUNK_SIZE); int y = startY - (i / CHUNK_SIZE); Vector3Int pos = new Vector3Int(x, y); positions[i] = pos; tileArray[i] = destroyedTiles != null && destroyedTiles.Contains(pos) ? null : tiles[i]; } tilemap.SetTiles(positions, tileArray); } private void ClearDistantChunks(Vector2Int currentChunk) { // If this is the first time, initialize the last clear position if (lastCacheClearPosition == default) { lastCacheClearPosition = currentChunk; return; } // Calculate distance moved since last cache clear int distanceMoved = Mathf.Max( Mathf.Abs(currentChunk.x - lastCacheClearPosition.x), Mathf.Abs(currentChunk.y - lastCacheClearPosition.y) ); // If we've moved far enough, clear distant chunks from cache if (distanceMoved >= CACHE_CLEAR_DISTANCE) { List chunksToRemove = new List(); foreach (var chunk in chunkCache.Keys) { int distanceToPlayer = Mathf.Max( Mathf.Abs(chunk.x - currentChunk.x), Mathf.Abs(chunk.y - currentChunk.y) ); // Remove chunks that are far from current position if (distanceToPlayer > LOAD_DISTANCE * 2) { chunksToRemove.Add(chunk); } } // Remove the distant chunks from cache foreach (var chunk in chunksToRemove) { chunkCache.Remove(chunk); } lastCacheClearPosition = currentChunk; Debug.Log($"Cleared {chunksToRemove.Count} chunks from cache. Current cache size: {chunkCache.Count}"); } } private IEnumerator GenerateChunk(Vector2Int chunk, List destroyedTiles) { int startX = chunk.x * CHUNK_SIZE; int startY = (chunk.y * CHUNK_SIZE) - 1; // Check if the chunk is already in the cache LoadChunkFromCache(chunk); // Create job data var groundJob = new GenerateGroundJob { chunkStartX = startX, chunkStartY = startY, chunkSize = CHUNK_SIZE, maxWidth = maxWidth, groundDepth = groundDepth, scale = scale, offsetX = offsetX, offsetY = offsetY, groundTiles = new NativeArray(CHUNK_SIZE * CHUNK_SIZE, Allocator.TempJob), perlinNoiseCache = perlinNoiseCache }; // Schedule the job JobHandle groundJobHandle = groundJob.Schedule(CHUNK_SIZE * CHUNK_SIZE, 64); // Wait for the job to complete yield return new WaitUntil(() => groundJobHandle.IsCompleted); // Apply the results groundJobHandle.Complete(); TileBase[] newChunkTiles = new TileBase[CHUNK_SIZE * CHUNK_SIZE]; //for (int i = 0; i < CHUNK_SIZE * CHUNK_SIZE; i++) //{ // if (groundJob.groundTiles[i]) // { // int x = startX + (i % CHUNK_SIZE); // int y = startY - (i / CHUNK_SIZE); // Vector3Int tilePos = new Vector3Int(x, y); // if (!IsTileDestroyed(tilePos)) // { // tilemap.SetTile(tilePos, forestRuleTile); // newChunkTiles[i] = forestRuleTile; // } // else // { // newChunkTiles[i] = null; // } // } // else // { // newChunkTiles[i] = null; // } //} for (int i = 0; i < CHUNK_SIZE * CHUNK_SIZE; i++) { newChunkTiles[i] = groundJob.groundTiles[i] ? forestRuleTile : null; } // Batch update tiles BatchSetTiles(chunk, newChunkTiles, destroyedTiles); // Save the generated chunk to the cache chunkCache[chunk] = newChunkTiles; // Clean up native array groundJob.groundTiles.Dispose(); // Generate ores if (generateables != null) { yield return GenerateOresInChunk(chunk, destroyedTiles); } // Generate borders when needed if (startX == 0 || startX + CHUNK_SIZE >= maxWidth || startY == 0 || startY - CHUNK_SIZE <= -groundDepth) { yield return GenerateBorders(); } } private IEnumerator GenerateOresInChunk(Vector2Int chunk, List destroyedTiles) { int startX = chunk.x * CHUNK_SIZE; int startY = chunk.y * CHUNK_SIZE; // Get the cached tiles for this chunk TileBase[] cachedTiles = chunkCache[chunk]; foreach (Generateable generateable in generateables) { // Convert spawn heights to negative values if they aren't already int maxY = -Mathf.Abs(generateable.maxSpawnHeight); int minY = -Mathf.Abs(generateable.minSpawnHeight); // Changed condition: Check if the chunk's Y range overlaps with ore spawn range int chunkMaxY = startY; int chunkMinY = startY - CHUNK_SIZE; // Only process this chunk if it's within the ore's spawn height range if (chunkMinY <= maxY && chunkMaxY >= minY) { for (int x = startX; x < startX + CHUNK_SIZE; x++) { if (x >= maxWidth) continue; // Adjusted Y range calculation int rangeStart = Mathf.Max(chunkMinY, minY); int rangeEnd = Mathf.Min(chunkMaxY, maxY); for (int y = rangeStart; y <= rangeEnd; y++) { float xPerlin = ((float)x / maxWidth) * (float)generateable.clusterWeight + offsetX; float yPerlin = ((float)Mathf.Abs(y) / maxDepth) * (float)generateable.clusterWeight + offsetY; float perlinNoise = Mathf.PerlinNoise(xPerlin, yPerlin); if (perlinNoise <= (1f / (float)generateable.weight)) { Vector3Int tileSpawnCoord = new Vector3Int(x, y); if (!destroyedTiles.Contains(tileSpawnCoord) && tilemap.HasTile(tileSpawnCoord)) { int clusterSize = CountPotentialClusterSize(x, y, generateable.weight, generateable.clusterWeight); if (clusterSize >= generateable.minClusterSize) { tilemap.SetTile(tileSpawnCoord, generateable.tile); // Update cache int localX = x - startX; int localY = startY - y - 1; int index = localY * CHUNK_SIZE + localX; if (index >= 0 && index < cachedTiles.Length) { cachedTiles[index] = generateable.tile; } } } } } } // Update every few rows to maintain performance if (generateables.Count > 3) { yield return null; } } } // Update the cache with the modified tiles chunkCache[chunk] = cachedTiles; } private IEnumerator GenerateBorders() { // Vertical borders for (int x = 0; x <= maxWidth; x += maxWidth) { for (int y = -groundDepth; y <= maxDepth - groundDepth; y++) { Vector3Int borderPos = new Vector3Int(x, y); tilemap.SetTile(borderPos, borderTile); // Update cache for affected chunk Vector2Int chunkPos = GetChunkPosition(new Vector3(x + transform.position.x, y + transform.position.y)); if (chunkCache.TryGetValue(chunkPos, out TileBase[] cachedTiles)) { int localX = x - (chunkPos.x * CHUNK_SIZE); int localY = chunkPos.y * CHUNK_SIZE - y - 1; int index = localY * CHUNK_SIZE + localX; if (index >= 0 && index < cachedTiles.Length) { cachedTiles[index] = borderTile; chunkCache[chunkPos] = cachedTiles; } } } } yield return null; // Horizontal borders for (int y = -groundDepth; y <= maxDepth - groundDepth; y += maxDepth) { for (int x = 1; x < maxWidth; x++) { Vector3Int borderPos = new Vector3Int(x, y); tilemap.SetTile(borderPos, borderTile); // Update cache for affected chunk Vector2Int chunkPos = GetChunkPosition(new Vector3(x + transform.position.x, y + transform.position.y)); if (chunkCache.TryGetValue(chunkPos, out TileBase[] cachedTiles)) { int localX = x - (chunkPos.x * CHUNK_SIZE); int localY = chunkPos.y * CHUNK_SIZE - y - 1; int index = localY * CHUNK_SIZE + localX; if (index >= 0 && index < cachedTiles.Length) { cachedTiles[index] = borderTile; chunkCache[chunkPos] = cachedTiles; } } } } yield return null; } private int CountPotentialClusterSize(int startX, int startY, int weight, int clusterWeight) { int size = 0; Queue toCheck = new Queue(); HashSet checked_positions = new HashSet(); toCheck.Enqueue(new Vector2Int(startX, startY)); checked_positions.Add(new Vector2Int(startX, startY)); while (toCheck.Count > 0) { Vector2Int current = toCheck.Dequeue(); size++; // Check all 8 neighboring tiles for (int dx = -1; dx <= 1; dx++) { for (int dy = -1; dy <= 1; dy++) { if (dx == 0 && dy == 0) continue; Vector2Int neighbor = new Vector2Int(current.x + dx, current.y + dy); if (checked_positions.Contains(neighbor)) continue; float xPerlin = ((float)neighbor.x / maxWidth) * clusterWeight + offsetX; float yPerlin = ((float)Mathf.Abs(neighbor.y) / maxDepth) * clusterWeight + offsetY; float perlinNoise = Mathf.PerlinNoise(xPerlin, yPerlin); if (perlinNoise <= (1f / (float)weight)) { toCheck.Enqueue(neighbor); checked_positions.Add(neighbor); } } } } return size; } private void OnDestroy() { if (perlinNoiseCache.IsCreated) { perlinNoiseCache.Dispose(); } chunkCache.Clear(); } #if UNITY_EDITOR private void OnDrawGizmos() { if (!Application.isPlaying) return; // Draw current chunk boundaries if (playerTransform != null) { Vector2Int currentChunk = GetChunkPosition(playerTransform.position); Gizmos.color = Color.yellow; for (int x = -LOAD_DISTANCE; x <= LOAD_DISTANCE; x++) { for (int y = -LOAD_DISTANCE; y <= LOAD_DISTANCE; y++) { Vector2Int chunk = new Vector2Int(currentChunk.x + x, currentChunk.y + y); Vector3 worldPos = new Vector3( chunk.x * CHUNK_SIZE + transform.position.x, chunk.y * CHUNK_SIZE + transform.position.y, 0 ); // Draw chunk boundary Gizmos.DrawWireCube( worldPos + new Vector3(CHUNK_SIZE / 2f, -CHUNK_SIZE / 2f, 0), new Vector3(CHUNK_SIZE, CHUNK_SIZE, 0) ); // Draw chunk coordinates UnityEditor.Handles.Label( worldPos + new Vector3(CHUNK_SIZE / 2f, -CHUNK_SIZE / 2f, 0), $"({chunk.x}, {chunk.y})" ); } } } } #endif }