Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
TerrainGenerator.cs 4.95 KiB
using System.Threading;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Collections.Concurrent;
using System;

[System.Serializable]
public struct Octave
{
    public float amplitude;
    public float frequency;
    public NoiseType type;
}

[System.Serializable]
public enum NoiseType
{
    MathfPerlin,
    MathematicsPerlin,
    Simplex,
    Celluar
}

public class TerrainGenerator : MonoBehaviour
{
    [Header("User settings")]
    public List<Octave> octaves;

    [Tooltip("The seed of the generation.")]
    public Vector2 crawlStartPosition;

    public bool generateNewSeed = true;

    public bool refresh;

    [Tooltip("How fine grain the generated terrain will be. " +
        "Larger values = more precise. Recommended value is 513.")]
    public int samples = 513;

    [Tooltip("How tall the terrain is. May be changed to allow for higher terrains.")]
    public float terrainHeight = 600;

    [Tooltip("What percentage of threads available should we use?")]
    public float threadsUsage = 0.7f;

    public int MaxThreads => Mathf.FloorToInt(Environment.ProcessorCount * threadsUsage);


    [Header("Autogenerated values")]
    [Tooltip("Which terrain grid are we on right now?")]
    public Vector2Int currentGridPosition;

    [Tooltip("Array of terrains to reuse.")]
    public Terrain[] terrains = new Terrain[9];

    [Tooltip("Dictionary of previously loaded terrains.")]
    public Dictionary<Vector2Int, TerrainData> previouslyLoaded = new();

    public Queue<TerrainGenThread> queuedThreads = new();

    public ConcurrentBag<TerrainGenThread> completedGenerators = new();


    [Header("Stats")]
    public int runningThreads = 0;

    /// <summary>
    /// Apply the terrain that was generated by genThread.
    /// </summary>
    /// <param name="genThread">The terrain gen thread that has finished 
    /// generating its terrain heights.</param>
    private void ApplyGeneratedTerrain(TerrainGenThread genThread)
    {
        genThread.TerrainData.SetHeights(0, 0, genThread.GetHeights());

        GameObject obj = Terrain.CreateTerrainGameObject(genThread.TerrainData);
        obj.name = genThread.InitialCrawlPosition.ToString() + "Terrain";
        obj.layer = LayerMask.NameToLayer("Terrain");
        obj.transform.SetParent(transform, false);
        Vector3 offset = new Vector3(genThread.GridPosition.x, 0, genThread.GridPosition.y) *
            genThread.TerrainData.size.x;
        obj.transform.position += offset;
    }

    private void GenerateThread(Vector2 crawlPos, Vector2Int gridPos,
         bool forceRefresh)
    {
        if (!previouslyLoaded.ContainsKey(gridPos) || forceRefresh)
        {
            TerrainData data = Instantiate(Resources.Load<TerrainData>("Template"));
            TerrainGenThread tGenThread = new(this, data, crawlPos, gridPos);
            previouslyLoaded[gridPos] = data;

            if (runningThreads >= MaxThreads)
            {
                // Too many threads.
                queuedThreads.Enqueue(tGenThread);
            }
            else
            {
                StartThread(tGenThread);
            }
        }
    }

    private void StartThread(TerrainGenThread tGenThread)
    {
        runningThreads++;
        ThreadStart starter = new(tGenThread.Generate);
        Thread thread = new(starter);
        thread.Start();
    }

    public virtual void GenerateSurronding(Vector2 initCrawlPos, bool forceRefresh = false)
    {
        int count = 0;
        for (int i = -1; i <= 1; i++)
        {
            for (int j = 1; j >= -1; j--)
            {
                GenerateThread(currentGridPosition + initCrawlPos + new Vector2(i, j),
                    currentGridPosition + new Vector2Int(i, j),
                    forceRefresh);
                count++;
            }
        }
    }

    private void Update()
    {
        // Check for refresh.
        if (refresh)
        {
            refresh = false;

            foreach (Transform child in transform)
                Destroy(child.gameObject);

            GenerateSurronding(crawlStartPosition, true);
        }

        // Check for queued threads.
        if (queuedThreads.TryDequeue(out TerrainGenThread queuedThread)
            && runningThreads < MaxThreads)
        {
            StartThread(queuedThread);
        }

        // Check for completed generators.
        if (completedGenerators.TryTake(out TerrainGenThread completedThread))
        {
            ApplyGeneratedTerrain(completedThread);
            runningThreads--;
        }
    }

    /// <summary>
    /// Gets the square distance between two points.
    /// If A is located at (0,0) and B is located at (5,-10),
    /// the distance would be 5+10=15.
    /// </summary>
    /// <param name="a">A position on the grid.</param>
    /// <param name="b">Another position.</param>
    /// <returns></returns>
    public int SquareDistance(Vector2Int a, Vector2Int b)
    {
        Vector2Int diff = b - a;

        return Mathf.Abs(diff.x) + Mathf.Abs(diff.y);
    }
}