using System; using System.Collections.Generic; using System.Threading; using UnityEngine; using Random = UnityEngine.Random; namespace Terrain { public class World : MonoBehaviour { public static Settings Settings; [Header("World Generation Values")] public BiomeAttributes[] biomes; [Range(0.0f, 1.0f)] public float globalLightLevel; public Color day; public Color night; public Transform player; public Vector3 spawn; public Clouds clouds; public Material material; public Material transparent; public BlockType[] blockTypes; [Header("UI")] public GameObject creativeInventory; public GameObject cursorSlot; private readonly Chunk[,] _chunks = new Chunk[WorldData.WorldSizeInChunks, WorldData.WorldSizeInChunks]; private readonly List _activeChunks = new List(); private readonly List _chunksToCreate = new List(); public readonly List ChunksToUpdate = new List(); private readonly Queue> _modificationQueues = new Queue>(); private bool _applyingModifications; private bool _menuOpen; public readonly Queue ChunksToDraw = new Queue(); public Vector2Int lastPlayerChunk; public readonly int[,] BlockBiomes = new int[WorldData.WorldSizeInBlocks, WorldData.WorldSizeInBlocks]; private Thread _chunkUpdateThread; public readonly object ChunkUpdateThreadLock = new object(); private void Awake() { Settings ??= Settings.Load("settings.json"); } private void Start() { Random.InitState(WorldData.Seed); Shader.SetGlobalFloat("minGlobalLightLevel", WorldData.MinLightLevel); Shader.SetGlobalFloat("maxGlobalLightLevel", WorldData.MaxLightLevel); spawn = new Vector3(WorldData.WorldCenter, WorldData.ChunkSize.y - 50, WorldData.WorldCenter); lastPlayerChunk = WorldData.GetChunkPositionFromWorldPosition(spawn); if (Settings.enableThreading) { _chunkUpdateThread = new Thread(ThreadedUpdate); _chunkUpdateThread.Start(); } GenerateWorld(); CheckViewDistance(true); SetGlobalLightValue(); } private void Update() { CheckViewDistance(); if (_chunksToCreate.Count > 0) CreateChunk(); if (ChunksToDraw.Count > 0) { if (ChunksToDraw.Peek().Editable) ChunksToDraw.Dequeue().CreateMesh(); } if (!Settings.enableThreading) { if (!_applyingModifications) ApplyModifications(); if (ChunksToUpdate.Count > 0) UpdateChunks(); } } public void SetGlobalLightValue() { Shader.SetGlobalFloat("GlobalLightLevel", globalLightLevel); Camera.main.backgroundColor = Color.Lerp(night, day, globalLightLevel); } public bool MenuOpen { get => _menuOpen; set { _menuOpen = value; if (_menuOpen) { Cursor.lockState = CursorLockMode.None; Cursor.visible = true; creativeInventory.SetActive(true); cursorSlot.SetActive(true); } else { Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; creativeInventory.SetActive(false); cursorSlot.SetActive(false); } } } public bool CheckForBlock(in Vector3 block) { Vector2Int chunkPos = WorldData.GetChunkPositionFromWorldPosition(block); Chunk chunk = _chunks[chunkPos.x, chunkPos.y]; if (!IsBlockInWorld(block) || block.y < 0 || block.y > WorldData.ChunkSize.y) return false; if (chunk != null && chunk.Editable) return blockTypes[chunk.GetBlockFromWorldPosition(block).Block].solid; return blockTypes[GenerateBlock(block)].solid; } public BlockState GetBlock(in Vector3 block) { Vector2Int chunkPos = WorldData.GetChunkPositionFromWorldPosition(block); Chunk chunk = _chunks[chunkPos.x, chunkPos.y]; if (!IsBlockInWorld(block) || block.y < 0 || block.y > WorldData.ChunkSize.y) return new BlockState(BlockType.Air); if (chunk != null && chunk.Editable) return chunk.GetBlockFromWorldPosition(block); return new BlockState(GenerateBlock(block)); } public Chunk GetChunkFromWorldPosition(in Vector3 pos) { Vector2Int chunkPos = WorldData.GetChunkPositionFromWorldPosition(pos); return _chunks[chunkPos.x, chunkPos.y]; } private void GenerateWorld() { for (int x = (WorldData.WorldSizeInChunks / 2) - Settings.viewDistance; x < (WorldData.WorldSizeInChunks / 2) + Settings.viewDistance; x++) { for (int z = (WorldData.WorldSizeInChunks / 2) - Settings.viewDistance; z < (WorldData.WorldSizeInChunks / 2) + Settings.viewDistance; z++) { Vector2Int currentChunk = new Vector2Int(x, z); _chunks[x, z] = new Chunk(this, currentChunk); _chunksToCreate.Add(currentChunk); } } for (var i = 0; i < ChunksToUpdate.Count; i++) { ChunksToUpdate[0].Update(); ChunksToUpdate.RemoveAt(0); } player.position = spawn; } public byte GenerateBlock(in Vector3 block) { int yPos = Mathf.FloorToInt(block.y); Vector2 noisePosition = new Vector2(block.x, block.z); if (!IsBlockInWorld(block)) return BlockType.Air; if (yPos == 0) return BlockType.Bedrock; #region BiomePass int solidGroundHeight = 42; float sumOfHeights = 0; int count = 0; float strongestWeight = 0; int strongestBiomeIndex = 0; for (int i = 0; i < biomes.Length; i++) { float weight = Noise.Get2dPerlin(noisePosition, biomes[i].offset, biomes[i].scale); if (weight > strongestWeight) { strongestWeight = weight; strongestBiomeIndex = i; } float height = biomes[i].terrainHeight * Noise.Get2dPerlin(noisePosition, 0, biomes[i].terrainScale) * weight; if (height > 0) { sumOfHeights += height; count++; } } BiomeAttributes biome = biomes[strongestBiomeIndex]; BlockBiomes[(int)block.x, (int)block.z] = strongestBiomeIndex; sumOfHeights /= count; int terrainHeight = Mathf.FloorToInt(sumOfHeights + solidGroundHeight); #endregion #region BasicTerrainPass byte blockValue = 0; if (yPos > terrainHeight) return BlockType.Air; if (yPos == terrainHeight) blockValue = biome.surfaceBlock; if (yPos < terrainHeight && yPos > terrainHeight - 4) blockValue = biome.subSurfaceBlock; if (yPos < terrainHeight) blockValue = BlockType.Stone; #endregion #region SecondTerrainPass if (blockValue == BlockType.Stone) { foreach (var lode in biome.lodes) { if (yPos < lode.minHeight || yPos > lode.maxHeight) continue; if (Noise.Get3dPerlin(block, lode.noiseOffset, lode.scale, lode.threshold)) blockValue = lode.blockId; } } #endregion #region FloraPass if (yPos == terrainHeight && biome.placeMajorFlora) { if (Noise.Get2dPerlin(noisePosition, 0, biome.majorFloraZoneScale) > biome.majorFloraZoneThreshold) { if (Noise.Get2dPerlin(noisePosition, 0, biome.majorFloraPlacementScale) > biome.majorFloraPlacementThreshold) { _modificationQueues.Enqueue(Structure.GenerateMajorFlora(biome.majorFloraIndex, block, biome.minHeight, biome.maxHeight)); } } } #endregion return blockValue; } private void CreateChunk() { Vector2Int chunk = _chunksToCreate[0]; _chunksToCreate.RemoveAt(0); _chunks[chunk.x, chunk.y].Generate(); } private void UpdateChunks() { bool updated = false; int index = 0; lock (ChunkUpdateThreadLock) { while (!updated && index < ChunksToUpdate.Count) { if (ChunksToUpdate[index].Editable) { ChunksToUpdate[index].Update(); if (!_activeChunks.Contains(ChunksToUpdate[index].Position)) _activeChunks.Add(ChunksToUpdate[index].Position); ChunksToUpdate.RemoveAt(index); updated = true; } else index++; } } } private void ThreadedUpdate() { while (true) { if (!_applyingModifications) ApplyModifications(); if (ChunksToUpdate.Count > 0) UpdateChunks(); } } private void OnDisable() { if (Settings.enableThreading) { _chunkUpdateThread.Abort(); } } private void ApplyModifications() { _applyingModifications = true; while (_modificationQueues.Count > 0) { Queue modifications = _modificationQueues.Dequeue(); while (modifications.Count > 0) { BlockMod mod = modifications.Dequeue(); Vector2Int pos = WorldData.GetChunkPositionFromWorldPosition(mod.Position); Chunk chunk = _chunks[pos.x, pos.y]; if (chunk == null) { chunk = new Chunk(this, pos); _chunks[pos.x, pos.y] = chunk; _chunksToCreate.Add(pos); continue; } chunk.Modifications.Enqueue(mod); } } _applyingModifications = false; } private void CheckViewDistance(in bool forceUpdate = false) { Vector2Int playerChunk = WorldData.GetChunkPositionFromWorldPosition(player.position); if (playerChunk == lastPlayerChunk && !forceUpdate) return; clouds.UpdateClouds(); lastPlayerChunk = playerChunk; List prevActiveCunks = new List(_activeChunks); _activeChunks.Clear(); for (int x = playerChunk.x - Settings.viewDistance; x < playerChunk.x + Settings.viewDistance; x++) { for (int z = playerChunk.y - Settings.viewDistance; z < playerChunk.y + Settings.viewDistance; z++) { Vector2Int currentPos = new Vector2Int(x, z); prevActiveCunks.Remove(currentPos); if (!IsChunkInWorld(currentPos)) continue; Chunk currentChunk = _chunks[x, z]; if (currentChunk == null) { _chunks[x, z] = new Chunk(this, currentPos); _chunksToCreate.Add(currentPos); } else if (!currentChunk.Active) { currentChunk.Active = true; } _activeChunks.Add(currentPos); } } prevActiveCunks.ForEach(chunk => { _chunks[chunk.x, chunk.y].Active = false; }); } private bool IsChunkInWorld(in Vector2Int chunk) { return (chunk.x > 0 && chunk.x < WorldData.WorldSizeInChunks - 1 && chunk.y > 0 && chunk.y < WorldData.WorldSizeInChunks - 1); } private bool IsBlockInWorld(in Vector3 block) { return (block.x > 0 && block.x <= WorldData.WorldSizeInBlocks && block.y > 0 && block.y < WorldData.ChunkSize.y && block.z > 0 && block.z <= WorldData.WorldSizeInBlocks); } } }