Degree Project

This is where I document my progress for my degree project at FutureGames. The goal of the project is to create a dynamic game world with procedurally generated terrain, biomes, structures, and weather. Plus some creature AI and a sound system for fading in and out different ambiance based on where you are. In total I will use 10 weeks for this project, and the engine I’m working in is Unity.

Latest version:   DOWNLOAD v0.4.0


2018-12-07

Week 8

This week I didn’t get as much time to work on this project as I would have liked. But I still got some things done.

Much faster loading
I made some tweaks and improvements to the generation of the world that makes everything load faster.

  • Made the coroutines do slightly more work each frame.
  • Waiting until the generation is done before turning on any other logic (like enemies and weather and such)
  • Removed one of the terrain copies and simplified the texture drawing somewhat.


Terrain generation modification
The terrain generation got a slight modification that makes the mountains be taller and the valleys be flatter.


Stones on sand
Added small stones on sandy ground.


Hide foliage and leaf particles
Foliage and falling leaves now gets hidden when changing to the snowy or dry versions of the island.


Rain becomes snow
In the snowy version falling rain becomes falling snow and no longer makes rain sounds.


Wind particles
To make it more apparent which way the wind blows I added some particles that fly in the direction of the wind.

 


2018-11-30

Week 7 – A week of many things

Since last time I made many small additions, some fixes and a bunch of busy-work that was really needed.
For some reason though, this week’s build has a way too long loading time. In the editor it’s also a bit longer than I would want, but the build more than triples the time. It will be a priority for next week to try and find a solution.

Small change to loading
I added a queue of IEnumerators that that automatically starts the next one in the queue when it has finished. It was just a small change that made the loading of all the systems easier to keep track of. It is also easier to add new things to load now.

Umbrella
A small cosmetic thing I added was an umbrella that can be picked up. The only thing that it does is change the sound of the rain around the player. It worked adds something to the atmosphere.

Running
After walking around the island for what feels like the 1000th time during tests I decided to add a running mode when holding shift. It will probably only be available as a debug thing. The speed increases the longer you hold shift up to a point.

if (Input.GetKeyDown(KeyCode.LeftShift))
{
    runMode = true;
    accelerationTimer = 0f;
}
else if (Input.GetKeyUp(KeyCode.LeftShift))
{
    runMode = false;
}

//----------------------------------------------

if (runMode)
{
    if (accelerationTimer < 1f)
    {
        accelerationTimer += Time.fixedDeltaTime / 4f;
    }

    moveDirection += moveDirection * accelerationTimer * 5f;
}

 

Improvement on flocking creatures
I made the flocking creatures not walk through obstacles. They now check a sphere in front of them to see if there is any colliders that of the correct layers blocking the way. If so they start turning.

if (Vector3.Distance(pos2, parentPos2) >= globalFlock.tankSize || CheckForObstacles())
{
    turning = true;
}
else
{
    turning = false;
}

// -----------------------------


private bool CheckForObstacles()
{
    return Physics.CheckSphere(transform.position + transform.forward, 0.5f, ObjectManager.Instance.obstacleMask, QueryTriggerInteraction.Ignore);
}

 

New tree-prefabs
Since I needed to remake all the tree prefabs anyway in order to be able to change the tree meshes based on which version of the island that is loaded, I decided to also use LOD-versions of the meshes to increase performance.
The new prefabs all have three versions of the same tree, one green, one dead and one with snow. All of these also have three different LOD levels

Work on the “gameplay”
This week I started on implementing the gameplay of picking up effects and transferring them to the trees.
As I mentioned last week, the idea is that the player chooses which effect they want to put on the island. And when enough trees have been given an effect, a portal will open up in the middle of the five pillars. If the player then steps through the portal they will be taken to a different version of the island.

More structures
I added two structures that can spawn in the world, a well and some stacked stones. Both with unique ambient sounds. I also made the tower and the field be spawned by the same ObjectManager as all the other structures, since they were special cases before.
Aside from that I made sure that every structure has an effect that the player can get.

More versions of the terrain
I added three more versions of the terrain, making the total five. Default, snow, muddy, dry, and dead. Each of these are copied from the default one, but has unique materials that I paint on with a coroutine. Unfortunately this is bad for the loading time. I will try to see later if I can optimize it somehow.

GameObject prefab = transform.GetChild(0).gameObject;
coldTerrain = Instantiate(prefab, transform);
coldTerrain.GetComponent<Renderer>().material = coldMat;
changeTerrain.snowTerrain = coldTerrain;
loadingQueue.Enqueue(PaintVertices(coldTerrain, paintLogic.coldRenderTexture, rockPathTexture, snowTexture, snowTexture, snowTexture, snowTexture));

dryTerrain = Instantiate(prefab, transform);
dryTerrain.GetComponent<Renderer>().material = dryMat;
changeTerrain.dryTerrain = dryTerrain;
loadingQueue.Enqueue(PaintVertices(dryTerrain, paintLogic.dryRenderTexture, sandTexture, sandTexture, sandTexture, sandTexture, sandTexture));

wetTerrain = Instantiate(prefab, transform);
wetTerrain.GetComponent<Renderer>().material = wetMat;
changeTerrain.wetTerrain = wetTerrain;
loadingQueue.Enqueue(PaintVertices(wetTerrain, paintLogic.wetRenderTexture, pathTexture, grassTexture2, pathTexture, rockTexture1, snowTexture));

deadTerrain = Instantiate(prefab, transform);
deadTerrain.GetComponent<Renderer>().material = desolationMat;
changeTerrain.deadTerrain = deadTerrain;
loadingQueue.Enqueue(PaintVertices(deadTerrain, paintLogic.desolationRenderTexture, rockTexture1, rockPathTexture, rockPathTexture, rockPathTexture, rockPathTexture));

// ------------------------------

IEnumerator PaintVertices(GameObject gO, RenderTexture renderTex, Texture texture1, Texture texture2, Texture texture3, Texture texture4, Texture texture5)
{
    Mesh m = gO.GetComponent<MeshFilter>().mesh;
    Vector3[] mVerts = m.vertices;
    Vector3 tmpPos = Vector3.zero;
    float progress;

    for (int i = 0; i < mVerts.Length; i++)
    {
        tmpPos = new Vector3(mVerts[i].x * terrainScale, 200f, mVerts[i].z * terrainScale);
        RaycastHit hitInfo = new RaycastHit();
        bool hit = Physics.Raycast(tmpPos, Vector3.down, out hitInfo, 300f, layerMask);

        if (hit)
        {
            if (mVerts[i].y > 30f && mVerts[i].y < sandBorder)
            {
                paintLogic.RenderToTexture(hitInfo.textureCoord * 2048f, texture1, renderTex);
            }
            else if (mVerts[i].y > sandBorder && mVerts[i].y < grassBorder)
            {
                paintLogic.RenderToTexture(hitInfo.textureCoord * 2048f, texture2, renderTex);
            }
            else if (mVerts[i].y > grassBorder && mVerts[i].y < darkGrassBorder)
            {
                paintLogic.RenderToTexture(hitInfo.textureCoord * 2048f, texture3, renderTex);
            }
            else if (mVerts[i].y > darkGrassBorder && mVerts[i].y < rockBorder)
            {
                paintLogic.RenderToTexture(hitInfo.textureCoord * 2048f, texture4, renderTex);
            }
            else if (mVerts[i].y > rockBorder)
            {
                paintLogic.RenderToTexture(hitInfo.textureCoord * 2048f, texture5, renderTex);
            }
        }

        if (i % 500 == 0)
        {
            progress = (1f * ((float)i / (float)vertices.Length)) / 10f;
            SceneLoader.Instance.SetProgress(progress);
            yield return new WaitForEndOfFrame();
        }
    }

    SceneLoader.Instance.SetProgress(0.1f);
    SceneLoader.Instance.finalizedProgress += SceneLoader.Instance.progress;
    SceneLoader.Instance.progress = 0f;
        
    gO.SetActive(false);

    if (loadingQueue.Count > 0)
    {
        StartCoroutine(loadingQueue.Dequeue());
    }
    else
    {
        GenerateObjects();
        GeneratePaths();
    }
}

 

Effect particles
I created two different particle systems for the effects. One when the player picks up an effect and another when a tree gets a new effect. Both get colored based on which effect it is.

New simple creature
To increase the number of ways the player can get effects I added a creature type that gives the player an effect when touched. They are basically colored cubes that roam the map with colors corresponding to the effect they give.

Functionality for shifting between versions of the island
For testing purposes I added an event that fires when pressing 1-5 on the keyboard. These buttons switch between the different versions of the island. All the trees and the terrain are listening to this event, and hide and show the correct meshes.
In the end this event will only fire when the player steps through the portal, but it’s a great way to test the functionality.

 


2018-11-23

Week 6

This week I made some enhancements to the code, added some small features and planned and prototyped some bigger features.

Better loading
In order for the loading bar to be accurate and take every system into consideration I first created an empty scene that starts the loading. It applies the loading progress of the main scene to the progress bar. When the main scene is loaded, the generate terrain script starts a coroutine that moves the vertices of the ground to the correct height. Every 500 vertices the coroutine waits for the next frame and updates the progress bar.
When that is finished, the next coroutine starts and instantiates a certain number of objects each frame in the same way. This goes on for each system, foliage, trees, paths etc.
This way the progress bar gets constantly updated and Unity doesn’t get bogged down trying to do too much on a single frame.

 

Different textures under trees
I added functionality to paint different textures on the ground under each type of tree to add some variation.

// "firGround" is the texture that will be drawn under the tree
SpawnTreeOnPoint(hitInfo.point, hitInfo.textureCoord, firList, firGround, 2, new Vector3(4f, 4f, 4f));


private void SpawnTreeOnPoint(Vector3 pos, Vector2 texPos, List<GameObject> prefabs, Texture tex, int num, Vector3 scale)
{
    RaycastHit hitInfo = new RaycastHit();
    GameObject prefab = prefabs[Random.Range(0, prefabs.Count)];

    for (int i = 0; i < num; i++)
    {
        if (pos.y > sandBorder && pos.y < darkGrassBorder)
        {
            Physics.Raycast(new Vector3(pos.x + Random.Range(-40f, 40f), 200f, pos.z + Random.Range(-40f, 40f)), Vector3.down, out hitInfo, 250f, layerMask);

            if (hitInfo.collider != null && hitInfo.collider.gameObject.layer == 9) // Ground layer
            {
                if (hitInfo.point.y >= CalculateHeight((pos.x / terrainScale) * -xSize, (pos.z / terrainScale) * -zSize))
                {
                    Quaternion tmpRotation = Quaternion.Euler(0f, Random.Range(0f, 360f), 0f);

                    GameObject newTree = Instantiate(prefab, hitInfo.point, tmpRotation);
                    newTree.transform.localScale = scale;

                    paintLogic.RenderToTexture(hitInfo.textureCoord * 2048f, tex, paintLogic.defaultRenderTexture);
                }
            }
        }
    }
}

 

Change ambient sound at night
When deciding which ambient sound to play I added an extra check to see if it is night or not. In the earlier version I just checked if the player is standing beneath a rain cloud.

private void CheckForRain()
{
    RaycastHit hitInfo;
    bool hit = Physics.Raycast(transform.position, Vector3.up, out hitInfo, 300f, ObjectManager.Instance.rainCloudMask);

    if (SoundManager.Instance.CheckWeatherVolumeHigherThan(0.9f))
    {
        if (hit)
        {
            // TODO: Add different sound if player is holding umbrella
            SoundManager.Instance.ChangeWeatherSound(rainClip);
        }
        else if (skyObject.transform.rotation.eulerAngles.z > 180f)
        {
            SoundManager.Instance.ChangeWeatherSound(defaultNightClip);
        }
        else
        {
            SoundManager.Instance.ChangeWeatherSound(defaultClip);
        }
    }
}

 

Better spawning of clouds
My earlier system for spawning clouds just instantiated all the clouds on the same frame. Now I made it so that when it’s time to spawn a new batch of clouds they spawn one each frame.
I also removed the variable windDirection and instead use the forward vector of the cloudManager object itself  in order to make it easier to change the cloud direction.

private void Update()
{
    if (windChangeTimer > 0f)
    {
        windChangeTimer -= Time.deltaTime;

        if (windChangeTimer <= 0f)
        {
            transform.Rotate(new Vector3(0f, Random.Range(-5f, 5f), 0f));
            windChangeTimer = Random.Range(30f, 40f);
        }
    }

    if (cloudSpawnTimer > 0f)
    {
        cloudSpawnTimer -= Time.deltaTime;

        if (cloudSpawnTimer <= 0f)
        {
            SpawnCloud();
        }
    }


    if (isSpawningClouds)
    {
        if (lineIndex < numLines)
        {
            if (cloudCount == 0)
            {
                numClouds = Random.Range(3, 10);
            }

            if (cloudCount < numClouds)
            {
                Vector3 spawnPos = tmpPos + transform.right * -100f * (numClouds / 2f) + transform.right * cloudCount * 100f;
                spawnPos += Vector3.up * Random.Range(-40f, 40f);
                spawnPos += transform.forward * -100f * lineIndex;
                Instantiate(activeCloud, spawnPos, Quaternion.identity);

                cloudCount++;

                if (cloudCount == numClouds)
                {
                    cloudCount = 0;
                    lineIndex++;
                }
            }
        }
        else
        {
            isSpawningClouds = false;
        }
    }
}

private void SpawnCloud()
{
    if (Random.Range(0, 2) == 0)
    {
        activeCloud = cloudPrefab;
    }
    else
    {
        activeCloud = rainCloudPrefab;
    }

    tmpPos = transform.position + (transform.forward * -1200f) + Vector3.up * Random.Range(-20f, 20f);

    lineIndex = 0;
    isSpawningClouds = true;

    cloudSpawnTimer = Random.Range(240f, 260f);
}

 

Prototype of ”gameplay”
I also thought a lot about the gameplay of this little game. It’s basically just walking around on an island at the moment.
To change that I added a sort of platform with five pillars that spawns somewhere on the island. These pillars are indicators of how much the player has changed the game world. The idea is that when the player stands close to different structures and objects they are are imbued with different effects. If they touch a tree while having an active effect the tree changes hue slightly and the corresponding pillar lights up more. After enough trees have been changed a portal will open up on the platform, and based on which pillar has the most trees the portal leads to a different version of the island.
The planned versions are: cold, lush, dry, wet and dead.
Each of these will keep the terrain shape, the positions of trees and paths and structures, but will need different textures, tree meshes and creatures.

Multiple copies of terrain mesh
In order to save time when generating different versions of the terrain I had to make some changes to the generation.
The PathPainter class now draws on a separate RenderTexture so that I can reuse the same paths on multiple terrains without having to generate them again. And I have a unique material for each version of the terrain that has their own RenderTexture for the ground color, but shares the path texture.

public void MakeCopiesOfTerrain()
{
    GameObject prefab = transform.GetChild(0).gameObject;

    GameObject copy = Instantiate(prefab, transform);
    copy.GetComponent<Renderer>().material = coldMat;
    PaintVertices(copy, paintLogic.coldRenderTexture, rockPathTexture, snowTexture, snowTexture, snowTexture, snowTexture);
}

private void PaintVertices(GameObject gO, RenderTexture renderTex, Texture texture1, Texture texture2, Texture texture3, Texture texture4, Texture texture5)
{
    Mesh m = gO.GetComponent<MeshFilter>().mesh;
    Vector3[] mVerts = m.vertices;
    Vector3 tmpPos = Vector3.zero;

    for (int i = 0; i < mVerts.Length; i++)
    {
        tmpPos = new Vector3(mVerts[i].x * terrainScale, 200f, mVerts[i].z * terrainScale);
        RaycastHit hitInfo = new RaycastHit();
        bool hit = Physics.Raycast(tmpPos, Vector3.down, out hitInfo, 300f, layerMask);

        if (hit)
        {
            if (mVerts[i].y > 30f && mVerts[i].y < sandBorder)
            {
                paintLogic.RenderToTexture(hitInfo.textureCoord * 2048f, texture1, renderTex);
            }
            else if (mVerts[i].y > sandBorder && mVerts[i].y < grassBorder)
            {
                paintLogic.RenderToTexture(hitInfo.textureCoord * 2048f, texture2, renderTex);
            }
            else if (mVerts[i].y > grassBorder && mVerts[i].y < darkGrassBorder)
            {
                paintLogic.RenderToTexture(hitInfo.textureCoord * 2048f, texture3, renderTex);
            }
            else if (mVerts[i].y > darkGrassBorder && mVerts[i].y < rockBorder)
            {
                paintLogic.RenderToTexture(hitInfo.textureCoord * 2048f, texture4, renderTex);
            }
            else if (mVerts[i].y > rockBorder)
            {
                paintLogic.RenderToTexture(hitInfo.textureCoord * 2048f, texture5, renderTex);
            }
        }
    }
}

 

 


2018-11-16

The five-week mark

As of right now I have used up five of the ten weeks for this project (minus the time spent looking for internship, making work tests and having interviews). And I think it is time to describe the different systems I have made.

Terrain generation
I based the terrain generation on the old island prototype I made for a Unity course. I loop through  every vertex on a plane and use Perlin noise to move them up or down to create hills and valleys. The further from the center a vertex is, the more it is pushed down in order to always make islands and hide the edges of the plane under water. On each startup I randomize a new seed.

private void Generate()
{
  mesh = GetComponent<MeshFilter>().mesh;
  meshCollider = GetComponent<MeshCollider>();
  vertices = mesh.vertices;

  float tmpHeight;
  List<int> tmpTriangleList = new List<int>();
  Vector3 tmpPos = Vector3.zero;

  for (int i = 0; i < vertices.Length; i++)
  {
    tmpHeight = CalculateHeight(vertices[i].x * -xSize, vertices[i].z * -zSize);
    vertices[i].y = tmpHeight;

    /*
     * Logic for generating terrain
     */
  }

  mesh.vertices = vertices;
  mesh.RecalculateNormals();
}

public float CalculateHeight(float x, float z)
{
  float xCoord = x / xSize * frequency;
  float zCoord = z / zSize * frequency;
  float elevation = 0.0f;
  float inside = 1;
  float outside = 1;
  Vector2 tempPos = new Vector2(xCoord, zCoord);

  // Calculate noise height for number of octaves
  for (int i = 0; i < octaves; i++, outside /= 2, inside *= 2)
  {
    elevation += outside * Mathf.PerlinNoise(inside * (xCoord + seed), inside * (zCoord + seed)) * amplitude;
  }

  // Vertices closer to the edges get pushed down more
  elevation = elevation + upConstant - downConstant * Mathf.Pow(tempPos.magnitude, dropOffSpeed);

  if (elevation > darkGrassBorder)
  {
    elevation += elevation / 30f;

    if (elevation > darkGrassBorder + 10f)
    {
      elevation += elevation / 40f;

      if (elevation > darkGrassBorder + 20f)
      {
        elevation += elevation / 22f;
      }
    }
  }

  return elevation;
}

 

Draw on terrain
I use a RenderTexture in order to be able to draw with different textures on the terrain, which I do based on the height of the vertices.

private void Generate()
{
    /*
     * Logic for generating terrain
     */
    for (int i = 0; i < vertices.Length; i++)
    {
        tmpHeight = CalculateHeight(vertices[i].x * -xSize, vertices[i].z * -zSize);

        if (tmpHeight > 30f && i % 4 == 0)
        {
            // Raycast and fetch the actual textureCoord
            tmpPos = new Vector3(vertices[i].x * 400f, 150f, vertices[i].z * 400f);
            RaycastHit hitInfo = new RaycastHit();
            bool hit = Physics.Raycast(tmpPos, Vector3.down, out hitInfo, 200f, layerMask);

            if (tmpHeight < sandBorder)
            {
                paintLogic.RenderToTexture(hitInfo.textureCoord * 2048f, sandTexture);
            }
            else if (tmpHeight > sandBorder && tmpHeight < grassBorder)
            {
                paintLogic.RenderToTexture(hitInfo.textureCoord * 2048f, grassTexture1);
            }
            else if (tmpHeight > grassBorder && tmpHeight < darkGrassBorder)
            {
                paintLogic.RenderToTexture(hitInfo.textureCoord * 2048f, grassTexture2);
            }
            else if (tmpHeight > darkGrassBorder && tmpHeight < rockBorder)
            {
                if (Random.Range(0, 2) == 0)
                {
                    paintLogic.RenderToTexture(hitInfo.textureCoord * 2048f, rockTexture1);
                }
                else
                {
                    paintLogic.RenderToTexture(hitInfo.textureCoord * 2048f, rockTexture2);
                }
            }
            else if (tmpHeight > rockBorder)
            {
                paintLogic.RenderToTexture(hitInfo.textureCoord * 2048f, snowTexture);
            }
        }
    }
    /*
     * Logic for generating terrain
     */
}



public class PaintLogic : MonoBehaviour
{
    public LayerMask layerMask;
    public RenderTexture renderTexture;
    public Texture clearTexture;

    private void Awake()
    {
        renderTexture.Release();
    }

    public void RenderToTexture(Vector2 position, Texture texture)
    {
        ReadyRenderTexture();

        //Draw texture on renderTexture at position
        Graphics.DrawTexture(
            new Rect(
                position.x - texture.width / 2,
                (renderTexture.height - position.y) - texture.height / 2,
                texture.width,
                texture.height),
            texture);

        DeactivateRenderTexture();
    }

    private void ReadyRenderTexture()
    {
        Graphics.SetRenderTarget(renderTexture);

        //Set RenderTexture active so DrawTexture will draw to it.
        RenderTexture.active = renderTexture;
        //Saves both projection and modelview matrices to the matrix stack.
        GL.PushMatrix();
        //Setup a matrix for pixel-correct rendering.
        GL.LoadPixelMatrix(0, 2048, 2048, 0);
    }

    private void DeactivateRenderTexture()
    {
        //Restores both projection and modelview matrices off the top of the matrix stack.
        GL.PopMatrix();
        RenderTexture.active = null;
    }
}

Trees and biomes
Besides making the terrain itself I also spawn in different types of trees based on different “biomes”. These biomes are, like the terrain, generated from Perlin noise but with a much lower frequency.

public void GenerateTrees()
{
    meshCollider.sharedMesh = mesh;

    // Loop through points on map

    for (int x = -1000; x < 1000; x += 20)
    {
        for (int z = -1000; z < 1000; z += 20)
        {
            RaycastHit hitInfo;

            if (Physics.Raycast(new Vector3(x + Random.Range(-20f, 20f), 150f, z + Random.Range(-20f, 20f)), Vector3.down, out hitInfo, 200f, layerMask))
            {
                if (hitInfo.collider.gameObject.layer == 9) // Ground layer
                {
                    ZoneEnum zone = CalculateZone(x, z);

                    switch (zone)
                    {
                        case ZoneEnum.Clear:
                            break;
                        case ZoneEnum.Fir:
                            SpawnTreeOnPoint(hitInfo.point, hitInfo.textureCoord, firList, 2, new Vector3(4f, 4f, 4f));
                            break;
                        case ZoneEnum.Oak:
                            SpawnTreeOnPoint(hitInfo.point, hitInfo.textureCoord, oakList, 2, new Vector3(4f, 4f, 4f));
                            break;
                        case ZoneEnum.Birch:
                            SpawnTreeOnPoint(hitInfo.point, hitInfo.textureCoord, birchList, 3, new Vector3(3f, 3f, 3f));
                            break;
                        case ZoneEnum.Apple:
                            SpawnTreeOnPoint(hitInfo.point, hitInfo.textureCoord, appleList, 2, new Vector3(4f, 4f, 4f));
                            break;
                        case ZoneEnum.Thuja:
                            SpawnTreeOnPoint(hitInfo.point, hitInfo.textureCoord, thujaList, 3, new Vector3(5f, 5f, 5f));
                            break;
                        case ZoneEnum.Pine:
                            SpawnTreeOnPoint(hitInfo.point, hitInfo.textureCoord, pineList, 2, new Vector3(3f, 3f, 3f));
                            break;
                        case ZoneEnum.Dead:
                            SpawnTreeOnPoint(hitInfo.point, hitInfo.textureCoord, deadList, 1, new Vector3(4f, 4f, 4f));
                            break;
                    }
                }
            }
        }
    }
}


public ZoneEnum CalculateZone(float x, float z)
{
    float xCoord = x / xSize * 0.5f;
    float zCoord = z / zSize * 0.5f;
    float zoneValue = 0.0f;

    zoneValue += Mathf.PerlinNoise(xCoord + zoneSeed, zCoord + zoneSeed);

    ZoneEnum tmpZone = ZoneEnum.Clear;

    if (zoneValue > 0f && zoneValue < 0.1f)
    {
        tmpZone = ZoneEnum.Fir;
    }
    else if (zoneValue > 0.1f && zoneValue < 0.2f)
    {
        tmpZone = ZoneEnum.Oak;
    }
    else if (zoneValue > 0.2f && zoneValue < 0.3f)
    {
        tmpZone = ZoneEnum.Birch;
    }
    else if (zoneValue > 0.3f && zoneValue < 0.4f)
    {
        tmpZone = ZoneEnum.Apple;
    }
    else if (zoneValue > 0.4f && zoneValue < 0.5f)
    {
        tmpZone = ZoneEnum.Thuja;
    }
    else if (zoneValue > 0.5f && zoneValue < 0.6f)
    {
        tmpZone = ZoneEnum.Pine;
    }
    else if (zoneValue > 0.7f && zoneValue < 0.8f)
    {
        tmpZone = ZoneEnum.Dead;
    }

    return tmpZone;
}

 

Foliage
I made some grass and flowers from simple planes with alpha channels. These then gets spawned randomly across the terrain. In order to reduce performance cost I cull the foliage layer 150 units in front of the player. I might have to tweak this value since there is some visible pop-in.

Dynamic weather
In order to make the world seem more alive I’m working on a system to spawn clouds and rain clouds and make them glide across the sky. I spawn in prefabs with multiple cloud meshes in a random pattern on one side of the island and let them move according to a global wind direction vector. From time to time the wind direction changes slightly, and all clouds are affected.
The rain itself is made from multiple particle systems.

One for sheets of rain that uses a texture, one for single rain streaks, another for small particles that creates square splashes on the ground on impact.

using UnityEngine;

public class CloudManager : MonoBehaviour {
    [SerializeField] GameObject cloudPrefab;
    [SerializeField] GameObject rainCloudPrefab;
    private GameObject activeCloud;
    public static Vector3 windDirection;
    public static float windSpeed = 10f;
    private float cloudSpawnTimer;
    private float windChangeTimer;

    private void Start ()
    {
        cloudSpawnTimer = Random.Range(1f, 2f);
        windChangeTimer = Random.Range(30f, 40f);
        windDirection = new Vector3(Random.Range(-1f, 1f), 0f, Random.Range(-1f, 1f)).normalized;
    }
  
    private void Update ()
    {
        if (windChangeTimer > 0f)
        {
            windChangeTimer -= Time.deltaTime;

            if (windChangeTimer <= 0f)
            {
                windDirection = Quaternion.AngleAxis(Random.Range(-5f, 5f), Vector3.up) * windDirection;
                windChangeTimer = Random.Range(30f, 40f);
            }
        }

        if (cloudSpawnTimer > 0f)
        {
            cloudSpawnTimer -= Time.deltaTime;

            if (cloudSpawnTimer <= 0f)
            {
                SpawnCloud();
            }
        }
    }

    private void SpawnCloud()
    {
        if (Random.Range(0, 2) == 0)
        {
            activeCloud = cloudPrefab;
        }
        else
        {
            activeCloud = rainCloudPrefab;
        }

        Vector3 tmpPos = windDirection * -1200f + Vector3.up * Random.Range(150f, 180f);

        // Spawn randomized lines of clouds
        for (int i = 0; i < 8; i++)
        {
            int numClouds = Random.Range(0, 6);

            for (int j = 0; j < numClouds; j++)
            {
                Instantiate(
                    activeCloud,
                    tmpPos + new Vector3(100f * j + Random.Range(-40f, 40f), -50 + Random.Range(-20f, 20f), 100 * i + Random.Range(-40f, 40f)),
                    Quaternion.identity);
            }
        }

        cloudSpawnTimer = Random.Range(80f, 90f);
    }
}

 

Dynamic sound
Every biome has an ambient sound effect associated with it that fades in when the player gets close to a tree of that biome. In my SoundManager script I use one Audio source for each biome sound and fade the volume of these based on which biomes are around the player.

using UnityEngine;
using System.Collections.Generic;
using System.Collections;

public class SoundManager : MonoBehaviour
{   
    [SerializeField] private AudioSource weatherSource;
    [SerializeField] private AudioSource weatherOverlapSource;
    [SerializeField] private AudioSource singleSoundSource;

    [SerializeField] private AudioSource sourceFir, sourceOak, sourceBirch, sourceApple, sourceThuja, sourcePine, sourceDead;
    private bool firActivated, oakActivated, birchActivated, appleActivated, thujaActivated, pineActivated, deadActivated;
    private float weatherFadeTimer;

    public static SoundManager Instance { get; set; }

    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
        }
        else
        {
            print("Warning: multiple " + this + " in scene!");
        }

        DontDestroyOnLoad(Instance);
    }

    private void Start()
    {
        weatherOverlapSource.time = weatherOverlapSource.clip.length / 2f;
    }

    private void Update()
    {
        HandleWeatherVolume();
        HandleZoneVolumes();
    }

    private void HandleWeatherVolume()
    {
        if (weatherFadeTimer <= 0f && weatherSource.time > weatherSource.clip.length - 2f)
        {
            weatherFadeTimer = 4f;
        }
    }

    public void ChangeWeatherSound(AudioClip clip)
    {
        if (weatherOverlapSource.clip != clip)
        {
            weatherOverlapSource.clip = clip;
            weatherOverlapSource.time = weatherOverlapSource.clip.length / 2f;
            weatherOverlapSource.Play();

            weatherFadeTimer = 4f;
        }
    }

    private void HandleZoneVolumes()
    {

        // TODO : Move this to own function
        if (weatherFadeTimer > 0f)
        {
            weatherFadeTimer -= Time.deltaTime;

            if (weatherFadeTimer > 2f)
            {
                weatherSource.volume -= Time.deltaTime / 2f;
                weatherOverlapSource.volume += Time.deltaTime;
            }
            else if (weatherFadeTimer <= 2f)
            {
                if (weatherSource.clip != weatherOverlapSource.clip)
                {
                    weatherSource.clip = weatherOverlapSource.clip;
                    weatherSource.Play();
                }
                weatherSource.volume += Time.deltaTime;
                weatherOverlapSource.volume -= Time.deltaTime / 2f;
            }
        }

        if (firActivated)
        {
            if (sourceFir.volume < 0.7f)
            {
                sourceFir.volume += Time.deltaTime / 2f;
            }
        }
        else if (!firActivated)
        {
            if (sourceFir.volume > 0f)
            {
                sourceFir.volume -= Time.deltaTime / 2f;
            }
        }

        if (oakActivated)
        {
            if (sourceOak.volume < 0.7f)
            {
                sourceOak.volume += Time.deltaTime / 2f;
            }
        }
        else if (!oakActivated)
        {
            if (sourceOak.volume > 0f)
            {
                sourceOak.volume -= Time.deltaTime / 2f;
            }
        }

        if (birchActivated)
        {
            if (sourceBirch.volume < 0.7f)
            {
                sourceBirch.volume += Time.deltaTime / 2f;
            }
        }
        else if (!birchActivated)
        {
            if (sourceBirch.volume > 0f)
            {
                sourceBirch.volume -= Time.deltaTime / 2f;
            }
        }

        if (appleActivated)
        {
            if (sourceApple.volume < 0.7f)
            {
                sourceApple.volume += Time.deltaTime / 2f;
            }
        }
        else if (!appleActivated)
        {
            if (sourceApple.volume > 0f)
            {
                sourceApple.volume -= Time.deltaTime / 2f;
            }
        }

        if (thujaActivated)
        {
            if (sourceThuja.volume < 0.7f)
            {
                sourceThuja.volume += Time.deltaTime / 2f;
            }
        }
        else if (!thujaActivated)
        {
            if (sourceThuja.volume > 0f)
            {
                sourceThuja.volume -= Time.deltaTime / 2f;
            }
        }

        if (pineActivated)
        {
            if (sourcePine.volume < 0.7f)
            {
                sourcePine.volume += Time.deltaTime / 2f;
            }
        }
        else if (!pineActivated)
        {
            if (sourcePine.volume > 0f)
            {
                sourcePine.volume -= Time.deltaTime / 2f;
            }
        }

        if (deadActivated)
        {
            if (sourceDead.volume < 0.7f)
            {
                sourceDead.volume += Time.deltaTime / 2f;
            }
        }
        else if (!deadActivated)
        {
            if (sourceDead.volume > 0f)
            {
                sourceDead.volume -= Time.deltaTime / 2f;
            }
        }
    }


    public void ActivateZoneSounds(List<ZoneEnum> zones)
    {
        firActivated = false;
        oakActivated = false;
        birchActivated = false;
        appleActivated = false;
        thujaActivated = false;
        pineActivated = false;
        deadActivated = false;

        foreach (ZoneEnum zone in zones)
        {
            switch (zone)
            {
                case ZoneEnum.Clear:
                    break;
                case ZoneEnum.Fir:
                    firActivated = true;
                    break;
                case ZoneEnum.Oak:
                    oakActivated = true;
                    break;
                case ZoneEnum.Birch:
                    birchActivated = true;
                    break;
                case ZoneEnum.Apple:
                    appleActivated = true;
                    break;
                case ZoneEnum.Thuja:
                    thujaActivated = true;
                    break;
                case ZoneEnum.Pine:
                    pineActivated = true;
                    break;
                case ZoneEnum.Dead:
                    deadActivated = true;
                    break;
                default:
                    break;
            }
        }
    }
}

Day-night cycle and post processing
The sky above the island is a prefab that has three parts: The sun, the blue sky and the starry sky. The whole prefab rotates slowly around one axis.

When the sun gets close to setting, the fog starts changing color from blue to orange to black at the same time as the blue sky starts to fade out and reveal the starry sky behind. The process is reversed at sunrise.

using UnityEngine;
using UnityEngine.Rendering.PostProcessing;

public class SkyLogic : MonoBehaviour {
    private float rotationSpeed = 0.2f;
    private Material blueSkyMat;
    private Color skyColor;
    private Color defaultColor;
    private Color fromColor;
    private Color toColor;
    private bool fadeInStarted;
    private bool fadeOutStarted;
    private float alpha = 0f;
    private float fadeTimer = 0f;
    private Color defaultFogColor;
    public Color dawnDuskColor;
    private Color changingFogColor;
    private Color nightColor;
    [SerializeField] private AnimationCurve dawnDuskCurve;
    private Gradient dawnGradient;
    private Gradient duskGradient;
    private GradientColorKey[] dawnColorKey;
    private GradientAlphaKey[] dawnAlphaKey;
    private GradientColorKey[] duskColorKey;
    private GradientAlphaKey[] duskAlphaKey;
    private Light sunLight;
    [SerializeField] private GameObject nightVolume;
    private PostProcessVolume postProcessVolume;

    private void Start()
    {
        sunLight = GetComponentInChildren<Light>();

        blueSkyMat = GetComponentInChildren<Renderer>().material;
        defaultColor = blueSkyMat.color;
        fromColor = defaultColor;
        defaultFogColor = RenderSettings.fogColor;
        nightColor = new Color(3f, 7f, 27f);

        blueSkyMat.color = new Color(defaultColor.r, defaultColor.g, defaultColor.b, 1f);
        fadeInStarted = true;
        alpha = 1f;

        SetGradients();
        postProcessVolume = nightVolume.GetComponent<PostProcessVolume>();
    }

    private void Update ()
    {
        transform.Rotate(new Vector3(0f, 0f, rotationSpeed * Time.deltaTime));
        HandleColors();
    }

    private void HandleColors()
    {
        if (transform.rotation.eulerAngles.z > 350f && !fadeInStarted)
        {
            fadeInStarted = true;
            fadeOutStarted = false;
            fadeTimer = 0f;
            alpha = 0f;
        }
        else if (transform.rotation.eulerAngles.z > 170f && transform.rotation.eulerAngles.z < 350f && !fadeOutStarted)
        {
            fadeOutStarted = true;
            fadeInStarted = false;
            fadeTimer = 0f;
        }

        if (fadeInStarted && alpha < 1f)
        {
            alpha += rotationSpeed * Time.deltaTime / 20f;
            fadeTimer += rotationSpeed * Time.deltaTime / 20f;
            RenderSettings.fogColor = dawnGradient.Evaluate(fadeTimer);

            blueSkyMat.color = new Color(dawnGradient.Evaluate(fadeTimer).r, dawnGradient.Evaluate(fadeTimer).g, dawnGradient.Evaluate(fadeTimer).b, alpha);
            sunLight.intensity = alpha / 2f;

            postProcessVolume.weight = 1f - alpha;
        }

        if (fadeOutStarted && alpha > 0f)
        {
            alpha -= rotationSpeed * Time.deltaTime / 20f;
            fadeTimer += rotationSpeed * Time.deltaTime / 20f;

            RenderSettings.fogColor = duskGradient.Evaluate(fadeTimer);

            blueSkyMat.color = new Color(duskGradient.Evaluate(fadeTimer).r, duskGradient.Evaluate(fadeTimer).g, duskGradient.Evaluate(fadeTimer).b, alpha);
            sunLight.intensity = alpha / 2f;

            postProcessVolume.weight = 1f - alpha;
        }
    }


    private void SetGradients()
    {
        dawnGradient = new Gradient();

        // Populate the color keys at the relative time 0 and 1 (0 and 100%)
        dawnColorKey = new GradientColorKey[3];
        dawnColorKey[0].color = Color.black;
        dawnColorKey[0].time = 0.0f;
        dawnColorKey[1].color = dawnDuskColor;
        dawnColorKey[1].time = 0.5f;
        dawnColorKey[2].color = defaultFogColor;
        dawnColorKey[2].time = 1.0f;

        // Populate the alpha  keys at relative time 0 and 1  (0 and 100%)
        dawnAlphaKey = new GradientAlphaKey[3];
        dawnAlphaKey[0].alpha = 1.0f;
        dawnAlphaKey[0].time = 0.0f;
        dawnAlphaKey[1].alpha = 1.0f;
        dawnAlphaKey[1].time = 0.5f;
        dawnAlphaKey[2].alpha = 1.0f;
        dawnAlphaKey[2].time = 1.0f;

        dawnGradient.SetKeys(dawnColorKey, dawnAlphaKey);


        duskGradient = new Gradient();
        // Populate the color keys at the relative time 0 and 1 (0 and 100%)
        duskColorKey = new GradientColorKey[3];
        duskColorKey[0].color = defaultFogColor;
        duskColorKey[0].time = 0.0f;
        duskColorKey[1].color = dawnDuskColor;
        duskColorKey[1].time = 0.5f;
        duskColorKey[2].color = Color.black;
        duskColorKey[2].time = 1.0f;

        // Populate the alpha  keys at relative time 0 and 1  (0 and 100%)
        duskAlphaKey = new GradientAlphaKey[3];
        duskAlphaKey[0].alpha = 1.0f;
        duskAlphaKey[0].time = 0.0f;
        duskAlphaKey[1].alpha = 1.0f;
        duskAlphaKey[1].time = 0.5f;
        duskAlphaKey[2].alpha = 1.0f;
        duskAlphaKey[2].time = 1.0f;

        duskGradient.SetKeys(duskColorKey, duskAlphaKey);
    }
}

 

Placement of paths
The paths are created by a script called PathPainter that is attached to a game object in the world. The script has as a goal to create a set number of path pieces on the ground while still adhering to certain rules. It takes a random spot on the map and paints a path texture on it and pushes the vertices down slightly. From there it rotates a small random amount, moves forward and repeats the process of painting a path piece. If the height of the terrain is too high or too low, the PathPainter aborts the current path and tries again somewhere else.
I also instantiate random rocks on the side of paths as decoration.

public void DrawPath()
{
    renderTexture = terrain.GetComponent<PaintLogic>().renderTexture;
    RaycastHit hitInfo;

    do
    {
        transform.position = new Vector3(Random.Range(-800f, 800f), 150f, Random.Range(-800f, 800f));

        hitInfo = new RaycastHit();
        bool hit = Physics.Raycast(transform.position, Vector3.down, out hitInfo, 300f, layerMask);
        nextPos = hitInfo.point;
    } while (nextPos.y < 50f || nextPos.y > GenerateTerrain.rockBorder);
        

    // Paint first path piece
    if (nextPos.y > 50f && nextPos.y < GenerateTerrain.darkGrassBorder)
    {
        paintLogic.RenderToTexture(hitInfo.textureCoord * 2048f, generateTerrain.pathTexture);
        FlattenVertices(hitInfo.point);
    }

    StartCoroutine(LoopPathPieces());
}


IEnumerator LoopPathPieces ()
{
    for (int i = 0; i < 100; i++) // How many pieces of path
    {
        bool foundValidPos = false;


        transform.position = new Vector3(nextPos.x, 150f, nextPos.z);

        for (int j = 0; j < 10; j++) // How many tries finding a valid next position
        {
            transform.Rotate(new Vector3(0f, Random.Range(-30f, 30f), 0f));

            RaycastHit hitInfo = new RaycastHit();
            bool hit = Physics.Raycast(transform.position + transform.forward * 10f, Vector3.down, out hitInfo, 200f, layerMask);
            Vector3 tmpPos = hitInfo.point;

            if (hit && hitInfo.point.y > 54f && hitInfo.point.y < GenerateTerrain.rockBorder)
            {
                nextPos = transform.position + transform.forward * 10f;

                if (hitInfo.point.y > GenerateTerrain.darkGrassBorder)
                {
                    paintLogic.RenderToTexture(hitInfo.textureCoord * 2048f, generateTerrain.rockPathTexture);
                }
                else
                {
                    paintLogic.RenderToTexture(hitInfo.textureCoord * 2048f, generateTerrain.pathTexture);
                }
                    
                foundValidPos = true;
                numPaintedPieces++;

                // Flatten terrain under path
                FlattenVertices(hitInfo.point);


                // For every other path piece add a side stone
                if (i % 2f == 0)
                {
                    Vector3 wallPos = hitInfo.point + transform.right * 12f;
                    Physics.Raycast(wallPos + Vector3.up * 50f, Vector3.down, out hitInfo, 200f, layerMask);
                    wallPos.y = hitInfo.point.y;

                    if (wallPos.y >= generateTerrain.CalculateHeight((wallPos.x / 400f) * -generateTerrain.xSize, (wallPos.z / 400f) * -generateTerrain.zSize))
                    {
                        Instantiate(rocks[Random.Range(0, rocks.Count)], wallPos, transform.rotation);
                    }
                }

                // Remove wallpieces, foliage and trees that collide with new path
                Collider[] cols = Physics.OverlapSphere(tmpPos, 10f, ObjectManager.Instance.otherThanFieldMask, QueryTriggerInteraction.Collide);
                foreach (Collider col in cols)
                {
                    Destroy(col.gameObject);
                }

                HandleProgressBar();

                yield return new WaitForSeconds(0.005f);
                break;
            }
        }

        if (!foundValidPos)
        {
            break;
        }
    }

        

    if (numPaintedPieces < minPaintedPieces)
    {
        DrawPath();
    }
    else if (!finishedPaths)
    {
        finishedPaths = true;
        generateTerrain.canvas.SetActive(false);
        generateTerrain.UpdateTerrainCollider();
    }

}

 

Foam particles and falling leaves
Some of the trees has leaves falling from the branches. These are simple particle systems placed in the tree prefabs.
To create a simple effect for foam on the water I use a particle system that follows the player and creates particles at sea level.

Creatures with flock behaviour
In order to populate the world with more than plant life I created some creatures with basic AI.
The first of these was a small flocking creature that hops around in groups and gets scared if the player gets too close. For these I used a simple algorithm for Boids made for schools of fish. I modified it to work in only two dimensions and based the y-values on the terrain height below the creatures.

// Small portion of the AI script that controls the rotation and speed of individual creatures

private void ApplyRules()
{
    GameObject[] gos;
    gos = globalFlock.creatureArray;

    Vector3 vCentre = Vector3.zero;
    Vector3 vAvoid = Vector3.zero;
    float gSpeed = 0.1f;

    float dist;

    int groupSize = 0;

    foreach (GameObject go in gos)
    {
        if (go != this.gameObject)
        {
            dist = Vector3.Distance(go.transform.position, this.transform.position);
            if (dist <= neighbourDistance)
            {
                Vector3 gPos = go.transform.position;
                gPos.y = 0f;
                vCentre += gPos;
                groupSize++;

                if (dist < 1f)
                {
                    vAvoid = vAvoid + (pos2 - gPos);
                }

                Flock anotherFlock = go.GetComponent<Flock>();
                if (anotherFlock != null)
                {
                    gSpeed = gSpeed + anotherFlock.speed;
                }
                else
                {
                    gSpeed = gSpeed + 5f;
                }
            }
        }
    }

    if (groupSize > 0)
    {
        vCentre = (vCentre / groupSize) + (goalPos - pos2);
        speed = gSpeed / groupSize;

        direction = (vCentre + vAvoid) - pos2;

        if (direction != Vector3.zero)
        {
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), rotationSpeed * Time.deltaTime);
        }
    }
}

 

Randomized placement of structures and objects
Another thing that makes the world a bit more interesting is the spawning of randomly picked objects and structures at different places. These all have their own rules for where they can be placed, and the world generation script just calls a function in the object to get a valid position to place it.
As of now the objects that generates are: a windmill, a tower, a stone circle, a field of wheat and some ruins.