A 3D Endless Runner Game

My first experience using the Unity3D Game Engine.

Brief Introduction

This semester at college, I took an Advanced Programming class. The class was mainly an introduction to the C# programming language and the main concepts of Object Oriented Programming (OOP) such as encapsulation, abstraction, inheritance and polymorphism.

The class was not terribly difficult, and so my friends and I decided to kick it up a notch for our end of course project. Therefore, in a team of three, we built a simple endless runner/shooter game for MacOS/Windows using the Unity3D game engine. We called it Magic Mushroom.

Due to various reasons we had very little time to learn how to work with the game engine as none of had used it before. We started out by watching a few of the official Unity tutorials and reading the official documentation, which was particularly helpful.

The character and environment assets used are not ours and were obtained from the Unity Asset Store and Mark Price’s Unity Game Academy tutorials.


Short preview of the gameplay.

About Magic Mushroom

The game is about a little kid who is high on psychedelic mushrooms that caused him to hallucinate badly, believing that he is at war with his stuffed animals; a gigantic elephant and an army of bunnies, in a hell-like environment. Armed with a laser gun, he sets about killing as much enemies as possible until his health bar runs out.

Magic Mushroom menu screen.

This is the code used for the menu screen:

LevelManager.cs

using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class LevelManager : MonoBehaviour {
public Transform mainMenu, aboutSection, instructions;

Play button function to start game scene.

static public bool isPlaying = false;
public void LoadScene(string name) {
isPlaying = true;
Application.LoadLevel (name);
}

Exit button function to quit game.

public void QuitGame() {
Application.Quit ();
}

Function to switch between main menu and about section when ‘About’ is clicked.

public void AboutSection(bool clicked){
if (clicked == true){
aboutSection.gameObject.SetActive (clicked);
mainMenu.gameObject.SetActive (false);
} else {
aboutSection.gameObject.SetActive (clicked);
mainMenu.gameObject.SetActive (true);
}
}
public void Instructions(bool clicked){
if (clicked == true){
instructions.gameObject.SetActive (clicked);
mainMenu.gameObject.SetActive (false);
} else {
instructions.gameObject.SetActive (clicked);
mainMenu.gameObject.SetActive (true);
}
}
}

RepeatedBridge.cs

Screenshot of the bridge platform.
using UnityEngine;
using System.Collections;

public class RepeatedBridge : MonoBehaviour {

[SerializeField] private float BridgeObjectSpeed = 10;
[SerializeField] private float StartPosition = 68.26f;
[SerializeField] private float ResetPosition = -77.5f;

// Update is called once per frame

protected virtual void Update() {

if (Player.isDead == true)
BridgeObjectSpeed = 0;

transform.Translate(Vector3.left * (BridgeObjectSpeed * Time.deltaTime));

if (transform.localPosition.x <= ResetPosition)
{
Vector3 newPos = new Vector3(StartPosition, transform.position.y, transform.position.z);

transform.position = newPos;
}
}
}

This code basically keeps the bridge parts moving in a loop, giving the impression that it is endlessly scrolling to the side.

FallingRocks.cs

using UnityEngine;
using System.Collections;

public class FallingRocks : RepeatedBridge{

[SerializeField] float Speed;
[SerializeField] Vector3 topPos;
[SerializeField] Vector3 bottomPos;

// Use this for initialization
void Start() {
StartCoroutine(Move(bottomPos));
}

A Coroutine is a function that is executed partially and, presuming suitable conditions are met, will be resumed at some point in the future until its work is done. 
For instance, they can be used for:

  • Writing routines that need to happen over time.
  • Writing routines that have to wait for another operation to complete.

Read more about Coroutines here

IEnumerator Move(Vector3 target)
{
while (Mathf.Abs((target - transform.localPosition).y) > 0.6f)
{
Vector3 direction = target.y == topPos.y ? Vector3.up : Vector3.down;
if (direction == Vector3.up)
transform.localPosition += direction * Time.deltaTime * Speed;
else
transform.localPosition += direction * Time.deltaTime * Speed * 2.5f;
yield return null;
}

yield return new WaitForSeconds(0.5f);
Vector3 newTarget = target.y == topPos.y ? bottomPos : topPos;
StartCoroutine(Move(newTarget));
}

protected override void Update()
{
base.Update();
if (Player.isDead == true)
{
Speed = 0;
}
}
}

Similar to the RepeatedBridge class, the FallingRocks class endlessly moves to the side while also moving vertically up and down.


Enemy.cs

Screenshot of enemies spawning on the bridge.
using UnityEngine;
using System.Collections;

public class Enemy : MonoBehaviour {

static public bool Colliding = false;
static public bool TookDamage = false;
public float EnemyHP = 10;
bool isDead = false;
NavMeshAgent nav;
Transform player;

Animator enemyAnimator;
AudioSource enemyAudio;

void Start() {
player = GameObject.FindGameObjectWithTag("Player").transform;
nav = GetComponent<NavMeshAgent>();
enemyAnimator = GetComponent<Animator>();
enemyAudio = GetComponent<AudioSource>();
}

IEnumerator takeDamage()
{
yield return new WaitForEndOfFrame();
TookDamage = false;
yield return new WaitForSeconds(1.0f);
Colliding = false;
}
nav.SetDestination(player.position);

if (EnemyHP <= 0 && isDead == false)
Death();
}

private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.name == "Player" && Colliding == false && Player.HP >= 0 && EnemyHP >= 0 && isDead == false)
{
TookDamage = true;
Colliding = true;
Player.HP -= 10;
StartCoroutine(takeDamage());
}

else if (collision.gameObject.tag == "Bullet" && Player.HP >= 0 && EnemyHP >= 0 && isDead == false)
EnemyHP -= Shooting.bulletDamage;
}

private void Death()
{
Score.score += 10;
isDead = true;
Colliding = false;
if (LevelManager.isPlaying == true)
enemyAudio.Play();
enemyAnimator.Play("Death");
Destroy(gameObject, 1.5f);
}
}

For the enemies (bunnies), we needed to create a Navigation Mesh in order for the enemies to intelligently move around the game world (the bridge) and restrict their movement such that they are not able to walk on the lava underneath. To do so, we added a NavMesh Agent to the enemies and baked the NavMesh according to our environment. Read more about Navigation and Pathfinding here.

Spawner.cs

using UnityEngine;
using System.Collections;

public class Spawner : MonoBehaviour {

public GameObject Enemy;
float spawnTime = 1.5f;
public Transform spawnPoint;

void Start () {
InvokeRepeating("Spawning", spawnTime, spawnTime);
}

void Spawning () {

Instantiate(Enemy, spawnPoint.position, spawnPoint.rotation);
}
}

The enemies are spawned continuously from the spawning point shown in the picture.


Gameplay scene.

Elephant.cs

using UnityEngine;
using System.Collections;

public class Elephant : MonoBehaviour {

AudioSource hellSound;
Animator Anim;

void Start() {
InvokeRepeating("hellScream", 5.0f, 5.0f);
hellSound = GetComponent<AudioSource>();
Anim = GetComponent<Animator>();
}

IEnumerator triggerAnimation()
{
yield return new WaitForSeconds(1.0f);
Anim.Play("Idle");
}

void hellScream()
{
if (LevelManager.isPlaying == true)
hellSound.Play();
}

The elephant stays in a fixed position onscreen, and if the kid gets too close to it then he is instantly killed.

void Update () {
transform.Translate(0f, 0f, 0.2f * Time.deltaTime);
if (Player.isDead == true)
StartCoroutine(triggerAnimation());
}
}

Player.cs

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class Player : MonoBehaviour {
float speed = 5f;
Rigidbody playerRigidBody;
Animator anim;
AudioSource hurt;
AudioClip PlayerDeath;
public Image damageImage;
public Slider healthSlider;
public float flashSpeed = 5f;
public Color flashColor = new Color(1f, 0f, 0f, 0.1f);
public static bool isDead = false;
public static int HP = 100;
public static bool gameOver = false;

void Start() {
anim = GetComponent<Animator>();
hurt = GetComponent<AudioSource>();
}

IEnumerator triggerAnimation()
{
yield return new WaitForSeconds(2.0f);
gameOver = true;
}

private void Awake()
{
playerRigidBody = GetComponent<Rigidbody>();
}

If the player is not dead yet, if he is attacked by bunnies, decrease his health points. If he is attacked by the elephant, he is immediately killed and the death animation is played using a Coroutine.

void Update() {

if (Enemy.TookDamage == true && Player.isDead == false)
{
if( LevelManager.isPlaying == true)
hurt.Play();
damageImage.color = flashColor;
healthSlider.value = HP;
}
else if (Enemy.TookDamage == false)
damageImage.color = Color.Lerp(damageImage.color, Color.clear, flashSpeed * Time.deltaTime);

if (HP <= 0 && isDead == false)
Death();
if (Input.GetKey(KeyCode.RightArrow))
transform.Translate(0f, 0f, speed * Time.deltaTime);
if (Input.GetKey(KeyCode.LeftArrow))
transform.Translate(0f, 0f, -speed * Time.deltaTime);
}

private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.name == "Elephant" && isDead == false)
{
HP = 0;
healthSlider.value = HP;
Death();
}
}

Death animation function.

void Death()
{
isDead = true;
Time.timeScale = 0.3f;
anim.Play("Death");
StartCoroutine(triggerAnimation());
}
}

Shooting.cs

using UnityEngine;
using System.Collections;

public class Shooting : MonoBehaviour
{
AudioSource bulletSound;
float effectsTime = 0.2f;
float Timer;
public GameObject gunCap;
public GameObject Bullet;
public float bulletForce = 5000f;
static public float bulletDamage = 10f;
public float gunCooldown = 0.75f;

void Start()
{
bulletSound = GetComponent<AudioSource>();
}

void Update()
{
Timer += Time.deltaTime;

if (Input.GetButton("Fire") && Timer >= gunCooldown)
{
Shoot();
}
}

Shooting function:

void Shoot()
{
if (LevelManager.isPlaying == true)
bulletSound.Play ();

GameObject tempCap;
Rigidbody tempBody;
tempCap = Instantiate(Bullet, gunCap.transform.position, gunCap.transform.rotation) as GameObject;
tempBody = tempCap.GetComponent<Rigidbody>();
tempBody.AddForce(transform.forward * bulletForce);
Destroy(tempCap, 0.5f);

if (Timer >= gunCooldown)
Timer = 0f;
}
}

The function instantiates new bullets every time the space button is pressed and the gun cool down time is exceeded. Gun cool down was implemented to increase difficulty and in order to prevent the player from holding down the space button and firing continuously.

Score, GameOver & Pause…

Score calculation:

public class Score : MonoBehaviour {

public static int score;
Text text;

void Start () {
text = GetComponent<Text>();
score = 0;
}

void Update () {
text.text = "Score: " + score;
}
}

‘Pause’ text overlay on screen when esc button is pressed:

public class Pausing : MonoBehaviour {
public Image pauseImage;
public Text pauseText;

void Update () {

if (Input.GetKeyDown(KeyCode.Escape))
{
if (Time.timeScale == 0)
{
Time.timeScale = 1;
pauseImage.enabled = false;
pauseText.enabled = false;
}
else
{
Time.timeScale = 0;
pauseImage.enabled = true;
pauseText.enabled = true;
}
}
}
}

GameOver:

public class GameOver : MonoBehaviour {
public Image overlayImage;
public Text gameoverText;
public Text scoreText;

void Update () {
if (Player.isDead == true) {
overlayImage.enabled = true;
gameoverText.enabled = true;
scoreText.enabled = true;
Camera.current.enabled = false;
}
}
}

Final Thoughts...

Overall, I think is was a great learning experience building this game and although it was frustrating at times, I genuinely enjoyed working with my team on this project.

We have thought about working on the game later on in the future in order to fix a few various bugs and possibly make some enhancements including the addition of power ups, Facebook leaderboards and extending the game such that when the running phase is successfully completed, the character progresses in to an arena-like environment where he fights other enemies where the game transforms into a 3D isometric style shooter.

Hopefully this is just the beginning and I very much look forward to working more with the Unity game engine and maybe try building games for iOS and Android in the near future.