Unity Island Prototype

Summary
This is a project I did in the process of learning Unity. Everything except the water effect is made by me.

The objective of the game is to in a limited amount of time try to make a dead island grow again. For the player that means to zoom around the map touching everything as fast as possible. The ground gets covered in grass when you glide across it. Stumps becomes trees and bushes and increases your score multiplier, which drops down again if you take too much time between stumps.
Keep an eye on the flying bird-like enemies who drops bombs that removes your grass.


Camera movement
The camera moves in the player’s direction with a speed based on the distance, which makes it lag slightly behind creating the effect I was after.

using UnityEngine;

public class FollowPlayer : MonoBehaviour {
    public GameObject followObject;
    public float speedModifier = 4;
  
    private Vector3 moveDirection;
    private float distance;
    private Vector3 position;
    private Vector3 followPosition;

    private void LateUpdate()
    {
        if (followObject != null)
        {
            position = transform.position;
            followPosition = followObject.transform.position;
            moveDirection = followPosition - position;
            moveDirection.Normalize();

            distance = Vector3.Distance(position, followObject.transform.position);
        }

        transform.localPosition += moveDirection * Time.deltaTime * distance * speedModifier;
    }
}

Player movement
I wanted the movement to feel smooth and flowing as you almost fly around the island, therefore I based the movement on adding force to a rigid body.

using UnityEngine;

public class PlayerMovement : MonoBehaviour {
    public float acceleration = 10f;
    public float camRotSpeed = 80f;
    public float jumpStrength = 40f;
    public Camera cam;
    public GameObject camFocus;
    public LayerMask layerMask;
    public GameObject ground;
    public GameObject playerDummy;

    private bool isGrounded;
    private Rigidbody rBody;
    private Vector3 moveDirection;
    private Vector3 camDirection;
    private Vector3 originPoint;
    private Vector3 position;
    private Vector3 groundUVPosition;
    private float rotationYAxis;
    private float rotationXAxis;
    private PaintLogic pl;
    private float maxVelocity = 100f;
    private float jumpTimer = 0f;

    private void Start()
    {
        rBody = GetComponent<Rigidbody>();
        pl = GetComponent<PaintLogic>();
        Physics.gravity = new Vector3(0, -15.0f, 0);
    }

    void Update () {
        position = transform.position;

        CheckIsGrounded();
        HandleMovement();
        HandleJumping();
        HandleGroundPaint();
        FallingCalculations();
        HandleCamera();
    }

    private void FixedUpdate()
    {
        HandleVelocity();
    }

    private void LateUpdate()
    {
        RaycastHit hitInfo;

        Physics.Raycast(
            rBody.position + new Vector3(0f, 100f, 0f),
            Vector3.down,
            out hitInfo,
            200f,
            layerMask
            );
        float groundY = hitInfo.point.y;

        if (transform.position.y < groundY)
        {
            transform.position = new Vector3(transform.position.x, groundY + 0.25f, transform.position.z);
        }
    }

    private void FallingCalculations()
    {
        if (rBody.velocity.y < 0f)
        {
            Physics.gravity = new Vector3(0, -40.0f, 0);
        } else
        {
            Physics.gravity = new Vector3(0, -15.0f, 0);
        }
    }

    private void HandleGroundPaint()
    {
        if (isGrounded)
        {
            pl.RenderToTexture(groundUVPosition);
        }
    }

    private void HandleVelocity()
    {
        moveDirection *= acceleration * Time.fixedDeltaTime;

        RaycastHit hitInfo;

        bool foundGround = Physics.Raycast(
            rBody.position,
            rBody.velocity * Time.fixedDeltaTime,
            out hitInfo,
            rBody.velocity.magnitude * Time.fixedDeltaTime,
            layerMask
            );
        Debug.DrawRay(rBody.position, rBody.velocity * Time.fixedDeltaTime, Color.red, 1f);

        if (foundGround)
        {
            rBody.velocity = GetForwardTangent(rBody.velocity, hitInfo.normal);
        }
        else
        {
            rBody.AddForce(moveDirection, ForceMode.Impulse);
        }


        if (rBody.velocity.magnitude > maxVelocity)
        {
            rBody.velocity = rBody.velocity.normalized * maxVelocity;
        }
    }


    private Vector3 GetForwardTangent(Vector3 mDirection, Vector3 up)
    {
        Vector3 right = Vector3.Cross(up, mDirection);
        Vector3 forward = Vector3.Cross(right, up);
        Debug.DrawRay(rBody.position, forward, Color.blue, 1f);
        return forward;
    }

    private void CheckIsGrounded()
    {
        isGrounded = Physics.Raycast(
            position + Vector3.up,
            Vector3.down,
            2.5f,
            layerMask
            );
    }

    private void HandleMovement()
    {
        moveDirection = new Vector3(
            Input.GetAxis("Horizontal"),
            0,
            Input.GetAxis("Vertical")
            );

        moveDirection = cam.transform.TransformDirection(moveDirection);

        RaycastHit hitInfo;

        Physics.Raycast(
            position + Vector3.up * 200f,
            Vector3.down * 3f,
            out hitInfo,
            1000f,
            layerMask
            );

        groundUVPosition = new Vector3(hitInfo.textureCoord.x * 2048f, hitInfo.textureCoord.y * 2048f);
    }

    private void HandleJumping()
    {
        // Check input from player
        if (Input.GetKeyDown(KeyCode.Joystick1Button0) && isGrounded && jumpTimer <= 0f)
        {
            jumpTimer = 0.08f;
        }

        if (Input.GetKey(KeyCode.Joystick1Button0))
        {
             if (jumpTimer > 0f)
            {
                rBody.AddForce(Vector3.up * jumpStrength * Time.deltaTime, ForceMode.Impulse);
                jumpTimer -= Time.deltaTime;
            }
        }
        else
        {
            jumpTimer = 0f;
        }
    }

    private void HandleCamera()
    {
        float xVelocity = Input.GetAxis("RightJoystickHorizontal") * camRotSpeed * Time.deltaTime;
        float yVelocity = Input.GetAxis("RightJoystickVertical") * camRotSpeed * Time.deltaTime;

        float xOffset = Input.GetAxis("Horizontal") * (camRotSpeed / 2) * Time.deltaTime;

        rotationYAxis += xVelocity + xOffset;
        rotationXAxis -= yVelocity;
        rotationXAxis = ClampAngle(rotationXAxis);

        Quaternion rotation = Quaternion.Euler(rotationXAxis, rotationYAxis, 0f);

        camFocus.transform.localRotation = Quaternion.identity * rotation;
        

        RaycastHit hitInfo;
        Physics.Raycast(
            cam.transform.position + Vector3.up * 200f,
            Vector3.down,
            out hitInfo,
            1000f,
            layerMask
            );

        if (cam.transform.position.y < hitInfo.point.y)
        {
            cam.transform.localPosition += new Vector3(0f, 0.5f, 0f);
        } else
        {
            Vector3 backupPos = cam.transform.localPosition;
            cam.transform.localPosition = new Vector3(-0.1f, 2.25f, -11.5f);
            if (cam.transform.position.y < hitInfo.point.y)
            {
                cam.transform.localPosition = backupPos;
            }
        }
    }

    public static float ClampAngle(float angle)
    {
        if (angle < -360F)
        {
            angle += 360F;
        }
        if (angle > 360F)
        {
            angle -= 360F;
        }
        return angle;
    }
}


Procedural island generation

The island is generated in the code and every time it is different. I used Perlin Noise to randomize the terrain, and then add stumps in lines and clumps afterwards.

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

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class GeneratePlane : MonoBehaviour {
    public int xSize = 256;
    public int zSize = 256;
    public float frequency = 20f;
    public float amplitude = 20f;
    public int octaves = 3;
    public float yOffset = 0f;
    public GameObject[] objectArr;
    public LayerMask layerMask;

    private float seed;
    private float upConstant = 0.05f;
    private float downConstant = 2.0f;
    private float dropOffSpeed = 4.5f;
    private int spawnedTrees = 0;
    private GameObject tree;

    [SerializeField]
    private int treeLines = 40;
    [SerializeField]
    private int treeClusters = 40;


    private Vector3[] vertices;
    private Mesh mesh;
    private MeshCollider meshCollider;

    private ArrayList dummyTrees = new ArrayList();
    

    private void Start()
    {
        seed = Random.Range(-100, 100);

        Generate();
        transform.Translate(new Vector3(0f, yOffset, 0f));
        mesh.bounds = new Bounds(new Vector3(0f, 0f, 0f), new Vector3(300f, 1f, 300f));

        meshCollider.sharedMesh = mesh;

        GenerateTrees();

        foreach (GameObject obj in dummyTrees)
        {
            Destroy(obj);
        }
    }

    private void GenerateTrees()
    {
        // Lines of trees
        for (int x = 0; x < treeLines; x++)
        {
            Vector3 pos = new Vector3(Random.Range(-512f, 512f), 200f, Random.Range(-512f, 512f));

            GameObject lastTree = AddTree(pos, false);

            float turn = 0f;
            int rand = Random.Range(0, 2);
            if (rand == 0)
            {
                turn = -8f;
            } else
            {
                turn = 8f;
            }
            Vector3 rotPerStep = new Vector3(0f, turn, 0f);
            Vector3 tmpRot = rotPerStep;

            lastTree = AddTree(pos += lastTree.transform.forward * 16f, false);
            lastTree.transform.Rotate(tmpRot);

            for (int i = 0; i < 16; i++)
            {
                lastTree = AddTree(pos += lastTree.transform.forward * 16f, false);
                lastTree.transform.Rotate(tmpRot += rotPerStep);
            }
        }

        // Clusters of trees
        for (int x = 0; x < treeClusters; x++)
        {
            Vector3 pos = new Vector3(Random.Range(-800f, 800f), 200f, Random.Range(-800f, 800f));

            AddTree(pos, false);

            for (int i = 0; i < 8; i++)
            {
                pos = new Vector3(pos.x + Random.Range(-16f, 16f), pos.y, pos.z + Random.Range(-16f, 16f));
                AddTree(pos, false);
            }
        }

        // Trees from vertices
        for (int i = 0; i < vertices.Length; i += 40)
        {
            AddTree(vertices[i], true);
        }
    }

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

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

        mesh.vertices = vertices;

        mesh.RecalculateNormals();

    }

    private 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;
        }

        // Factor in dropoff in elevation close to the edges
        elevation = elevation + upConstant - downConstant * Mathf.Pow(tempPos.magnitude, dropOffSpeed);

        return elevation;

    }

    private GameObject AddTree(Vector3 pos, bool vertexBased)
    {
        if (pos.y > 60f)
        {
            if (vertexBased)
            {
                pos.x *= transform.localScale.x;
                pos.z *= transform.localScale.z;
                pos.x += Random.Range(-50, 50);
                pos.z += Random.Range(-50, 50);
            }
            
            pos.y = FindHeightOnTerrain(pos);
            
            if (pos.y < 100f)
            {
                GameObject dummy = new GameObject();
                dummy.transform.position = pos;
                dummyTrees.Add(dummy);
                return dummy;
            }

            GameObject addedTree = Instantiate(objectArr[Random.Range(0, 4)], pos, Quaternion.identity);
            
            spawnedTrees++;
            float randomScale = Random.Range(1f, 2f);

            addedTree.transform.localScale = addedTree.transform.localScale * randomScale;
            return addedTree;
        }
        return null;
    }

    private float FindHeightOnTerrain(Vector3 pos)
    {
        RaycastHit hitInfo;

        Physics.Raycast(
            pos + Vector3.up * 300f,
            Vector3.down,
            out hitInfo,
            1000f,
            layerMask
            );

        return hitInfo.point.y;
    }
}

Enemies & bombs

Enemies will spawn in intervals dropping bombs on the ground, removing any grass in an area. Touching an enemy makes it stop dropping bombs and gives the player some points.

using UnityEngine;

public class FlyingMovement : MonoBehaviour {
    private Vector3 direction;
    private Vector3 newDirection;
    private float speed;
    private Vector3 position;
    private float turnTimer;
    private float startTurnTime;
    private float bombTimer;
    private bool isEnemy = true;
    [SerializeField]
    private Material convertedMat;
    [SerializeField]
    private LayerMask layerMask;

    private void Start ()
    {
        direction = new Vector3(0f, Random.Range(-180f, 180f), 0f);
        speed = Random.Range(0.1f, 0.2f);
        turnTimer = 0f;
    }
  
  private void Update ()
    {
        turnTimer += Time.deltaTime;
        position = transform.position;

        position .y = UpdateHeight();

        if (turnTimer >= 5f)
        {
            newDirection = direction + new Vector3(0f, Random.Range(-120f, 120f), 0f);
            turnTimer = 0f;
            startTurnTime = Time.time;
        }

        if (direction != newDirection)
        {
            float distCovered = Time.time - startTurnTime;
            float fracJourney = distCovered / 2f;

            Vector3 lerpVector = Vector3.Lerp(direction, newDirection, fracJourney);
            
            transform.rotation = Quaternion.Euler(lerpVector);
            direction = lerpVector;
        }

        transform.position = position + transform.forward * speed;
    }

    private float UpdateHeight()
    {
        RaycastHit hitInfo;

        Physics.Raycast(
            position + Vector3.up * 200f,
            Vector3.down * 3f,
            out hitInfo,
            1000f,
            layerMask
            );

        return hitInfo.point.y + 18f;
    }

    public void OnTriggerEnter(Collider other)
    {
        if ((other.gameObject.layer == 9) && isEnemy) // Player
        {
            other.gameObject.GetComponent<ScoreManager>().AddToScore(500, position);

            MeshRenderer[] renderers = gameObject.GetComponentsInChildren<MeshRenderer>();
            for (int i = 0; i < renderers.Length; i++)
            {
                renderers[i].material = convertedMat;
            }
            GetComponent<DropBombs>().StopDroppingBombs();

            isEnemy = false;
        }
    }
}

Powerup

When picking up a powerup you get ten ”grass bombs” to throw, which on impact fills an area with grass.

using UnityEngine;
using UnityEngine.UI;

public class PowerManager : MonoBehaviour {
    private bool activatedPower;
    private int ammo = 0;
    public Camera cam;
    [SerializeField]
    private Text ammoCounter;

    private void Start()
    {
        cam = GetComponent<PlayerMovement>().cam;
        ammoCounter.text = "";
    }

    private void Update ()
    {
        if (activatedPower && Input.GetKeyDown(KeyCode.Joystick1Button2))
        {
            ammo--;
            ammoCounter.text = "";

            for (int i = 0; i < ammo; i++)
            {
                if (i == 4)
                {
                    ammoCounter.text += "\n";
                }
                ammoCounter.text += "o  ";
            }

            GameObject grassBomb = (GameObject)Instantiate(Resources.Load("GrassBomb"), transform.position + new Vector3(0f, 3f, 0f), Quaternion.identity);
            grassBomb.GetComponent<Rigidbody>().AddForce(
                cam.transform.forward * 80f,
                ForceMode.Impulse);

            if (ammo <= 0)
            {
                activatedPower = false;
            }
        }
  }

    public void ActivatePower()
    {
        activatedPower = true;
        ammo = 10;
        ammoCounter.text = "o  o  o  o  o\no  o  o  o  o";
    }
}