2D Rougelike tutorial

Table of Contents

Project Introduction

Player and Enemy Animations

Creating the Tile Prefabs

Writing the Board Manager

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using Random = UnityEngine.Random;

public class BoardManager : MonoBehaviour {

      [Serializable]
      public class Count
      {
          public int minimum;
          public int maximum;

          public Count (int min, int max)
          {
              minimum = min;
              maximum = max;
          }
      }

      public int columns = 8;
      public int rows = 8;
      public Count wallCount = new Count (5, 9);
      public Count foodCount = new Count(1, 5);
      public GameObject exit;
      public GameObject[] floorTiles;
      public GameObject[] wallTiles;
      public GameObject[] foodTiles;
      public GameObject[] enemyTiles;
      public GameObject[] outerWallTiles;

      private Transform boardHolder;
      private List<Vector3> gridPositions = new List<Vector3>();

      void InitialiseList() {
          gridPositions.Clear ();

          for (int x = 1; x < columns - 1; x++) {
              for (int y = 1; y < rows - 1; y++) {
                  gridPositions.Add (new Vector3 (x, y, 0f));
              }
          }
      }

      void BoardSetup()
      {
          boardHolder = new GameObject ("Board").transform;

          for (int x = -1; x < columns + 1; x++) {
              for (int y = -1; y < rows + 1; y++) {
                  GameObject toInstantiate = floorTiles [Random.Range (0, floorTiles.Length)];
                  if (x == -1 || x == columns || y == -1 || y == rows) {
                      toInstantiate = outerWallTiles [Random.Range (0, outerWallTiles.Length)];
                  }
                  GameObject instance = Instantiate (toInstantiate, new Vector3 (x, y, 0), Quaternion.identity) as GameObject;
                  instance.transform.SetParent (boardHolder);
              }
          }
      }

      Vector3 RandomPosition() {
          int randomIndex = Random.Range (0, gridPositions.Count);
          Vector3 randomPosition = gridPositions (randomIndex);
          gridPositions.RemoveAt (randomIndex);
          return randomPosition;
      }

      void LayoutObjectAtRandom (GameObject[] tileArray, int minimum, int maximum)
      {
          int objectCount = Random.Range (minimum, maximum+1);
          for(int i = 0; i < objectCount; i++)
          {
              Vector3 randomPosition = RandomPosition();
              GameObject tileChoice = tileArray[Random.Range (0, tileArray.Length)];
              Instantiate(tileChoice, randomPosition, Quaternion.identity);
          }
      }

      public void SetupScene (int level)
      {
          BoardSetup ();
          InitialiseList ();
          LayoutObjectAtRandom (wallTiles, wallCount.minimum, wallCount.maximum);
          LayoutObjectAtRandom (foodTiles, foodCount.minimum, foodCount.maximum);

          int enemyCount = (int)Mathf.Log(level, 2f);
          LayoutObjectAtRandom (enemyTiles, enemyCount, enemyCount);
          Instantiate (exit, new Vector3 (columns - 1, rows - 1, 0f), Quaternion.identity);
      }
}

Writing the Game Manager

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

public class GameManager : MonoBehaviour
{
    public static GameManager instance = null;
    private BoardManager boardScript;
    private int level = 3;

    void Awake()
    {
        if (instance == null)
            instance = this;
        else if (instance != this)
            Destroy(gameObject);


        DontDestroyOnLoad(gameObject);
        boardScript = GetComponent<BoardManager>();
        InitGame();
    }

    void InitGame()
    {
        boardScript.SetupScene(level);
    }

    void Update()
    {

    }
}

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

public class GameManager : MonoBehaviour
{
    public static GameManager instance = null;
    private BoardManager boardScript;
    private int level = 3;

    void Awake()
    {
        if (instance == null)
            instance = this;
        else if (instance != this)
            Destroy(gameObject);


        DontDestroyOnLoad(gameObject);
        boardScript = GetComponent<BoardManager>();
        InitGame();
    }

    void InitGame()
    {
        boardScript.SetupScene(level);
    }
}

Moving Object Script

using UnityEngine;
using System.Collections;

public abstract class MovingObject : MonoBehaviour
{
      public float moveTime = 0.1f;
      public LayerMask blockingLayer;

      private BoxCollider2D boxCollider;
      private Rigidbody2D rb2D;
      private float inverseMoveTime;

      protected virtual void Start ()
      {
          boxCollider = GetComponent <BoxCollider2D> ();
          rb2D = GetComponent <Rigidbody2D> ();
          inverseMoveTime = 1f / moveTime;
      }

      protected bool Move (int xDir, int yDir, out RaycastHit2D hit)
      {
          Vector2 start = transform.position;
          Vector2 end = start + new Vector2 (xDir, yDir);
          boxCollider.enabled = false;
          hit = Physics2D.Linecast (start, end, blockingLayer);
          boxCollider.enabled = true;
          if(hit.transform == null)
          {
              StartCoroutine (SmoothMovement (end));
              return true;
          }
          return false;
      }

      protected IEnumerator SmoothMovement (Vector3 end)
      {
          float sqrRemainingDistance = (transform.position - end).sqrMagnitude;
          while(sqrRemainingDistance > float.Epsilon)
          {
              Vector3 newPostion = Vector3.MoveTowards(rb2D.position, end, inverseMoveTime * Time.deltaTime);
              rb2D.MovePosition (newPostion);
              sqrRemainingDistance = (transform.position - end).sqrMagnitude;
              yield return null;
          }
      }

      protected virtual void AttemptMove <T> (int xDir, int yDir)
          where T : Component
      {
          RaycastHit2D hit;
          bool canMove = Move (xDir, yDir, out hit);

          if(hit.transform == null)
              return;

          T hitComponent = hit.transform.GetComponent <T> ();
          if(!canMove && hitComponent != null)
              OnCantMove (hitComponent);
      }


      //The abstract modifier indicates that the thing being modified has a missing or incomplete implementation.
      //OnCantMove will be overriden by functions in the inheriting classes.
      protected abstract void OnCantMove <T> (T component)
          where T : Component;
}

Creating Destructible Walls

using UnityEngine;
using System.Collections;

public class Wall : MonoBehaviour
{
    public AudioClip chopSound1;
    public AudioClip chopSound2;
    public Sprite dmgSprite;
    public int hp = 3;

    private SpriteRenderer spriteRenderer;

    void Awake ()
    {
        spriteRenderer = GetComponent<SpriteRenderer> ();
    }

    public void DamageWall (int loss)
    {
        spriteRenderer.sprite = dmgSprite;
        hp -= loss;
        if(hp <= 0)
            gameObject.SetActive (false);
    }
}

Player Animator Controller

Writing the Player Script

using UnityEngine;
using System.Collections;
using UnityEngine.SceneManagement;

public class Player : MovingObject
{
      public float restartLevelDelay = 1f;
      public int pointsPerFood = 10;
      public int pointsPerSoda = 20;
      public int wallDamage = 1;

      private Animator animator;
      private int food;

      protected override void Start ()
      {
          animator = GetComponent<Animator>();
          food = GameManager.instance.playerFoodPoints;

          //Call the Start function of the MovingObject base class.
          base.Start ();
      }

      private void OnDisable ()
      {
          GameManager.instance.playerFoodPoints = food;
      }

      private void Update ()
      {
          if(!GameManager.instance.playersTurn) return;

          int horizontal = 0;
          int vertical = 0;

          horizontal = (int) (Input.GetAxisRaw ("Horizontal"));
          vertical = (int) (Input.GetAxisRaw ("Vertical"));
          if(horizontal != 0)
          {
              vertical = 0;
          }

          if(horizontal != 0 || vertical != 0)
          {
              AttemptMove<Wall> (horizontal, vertical);
          }
      }

      protected override void AttemptMove <T> (int xDir, int yDir)
      {
          food--;
          base.AttemptMove <T> (xDir, yDir);
          RaycastHit2D hit;
          if (Move (xDir, yDir, out hit))
          {
              //Call RandomizeSfx of SoundManager to play the move sound, passing in two audio clips to choose from.
          }

          CheckIfGameOver ();
          GameManager.instance.playersTurn = false;
      }


      protected override void OnCantMove <T> (T component)
      {
          Wall hitWall = component as Wall;
          hitWall.DamageWall (wallDamage);
          animator.SetTrigger ("playerChop");
      }

      private void OnTriggerEnter2D (Collider2D other)
      {
          if(other.tag == "Exit")
          {
              Invoke ("Restart", restartLevelDelay);
              enabled = false;
          }
          else if(other.tag == "Food")
          {
              food += pointsPerFood;
              other.gameObject.SetActive (false);
          }
          else if(other.tag == "Soda")
          {
              food += pointsPerSoda;
              other.gameObject.SetActive (false);
          }
      }

      private void Restart ()
      {
          SceneManager.LoadScene (0);
      }

      public void LoseFood (int loss)
      {
          animator.SetTrigger ("playerHit");
          food -= loss;
          CheckIfGameOver ();
      }

      private void CheckIfGameOver ()
      {
          if (food <= 0)
          {
              // GameManager.instance.GameOver ();
          }
      }
}

Writing the Enemy Script

using UnityEngine;
using System.Collections;

public class Enemy : MovingObject
{
      public int playerDamage;

      private Animator animator;
      private Transform target;
      private bool skipMove;

      protected override void Start ()
      {
          // GameManager.instance.AddEnemyToList (this);
          animator = GetComponent<Animator> ();
          target = GameObject.FindGameObjectWithTag ("Player").transform;
          base.Start ();
      }

      protected override void AttemptMove <T> (int xDir, int yDir)
      {
          if(skipMove)
          {
              skipMove = false;
              return;
          }

          base.AttemptMove <T> (xDir, yDir);
          skipMove = true;
      }

      public void MoveEnemy ()
      {
          int xDir = 0;
          int yDir = 0;
          if(Mathf.Abs (target.position.x - transform.position.x) < float.Epsilon)
              yDir = target.position.y > transform.position.y ? 1 : -1;
          else
              xDir = target.position.x > transform.position.x ? 1 : -1;
          AttemptMove <Player> (xDir, yDir);
      }

      protected override void OnCantMove <T> (T component)
      {
          Player hitPlayer = component as Player;
          hitPlayer.LoseFood (playerDamage);
          animator.SetTrigger ("enemyAttack");
      }
}

Enemy Animator Controller

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

public class GameManager : MonoBehaviour
{
      public float levelStartDelay = 2f;
      public float turnDelay = 0.1f;
      public int playerFoodPoints = 100;
      public static GameManager instance = null;
      [HideInInspector] public bool playersTurn = true;

      private BoardManager boardScript;
      private int level = 1;
      private List<Enemy> enemies;
      private bool enemiesMoving;

      void Awake()
      {
          if (instance == null)
              instance = this;
          else if (instance != this)
              Destroy(gameObject);

          DontDestroyOnLoad(gameObject);
          enemies = new List<Enemy>();
          boardScript = GetComponent<BoardManager>();
          InitGame();
      }

      void OnLevelWasLoaded(int index)
      {
          level++;
          InitGame();
      }

      void InitGame()
      {
          enemies.Clear();
          boardScript.SetupScene(level);

      }

      void Update()
      {
          if(playersTurn || enemiesMoving)
              return;
          StartCoroutine (MoveEnemies ());
      }

      //Call this to add the passed in Enemy to the List of Enemy objects.
      public void AddEnemyToList(Enemy script)
      {
          enemies.Add(script);
      }

      public void GameOver()
      {
          // levelImage.SetActive(true);
          enabled = false;
      }

      IEnumerator MoveEnemies()
      {
          enemiesMoving = true;
          yield return new WaitForSeconds(turnDelay);

          if (enemies.Count == 0)
          {
              yield return new WaitForSeconds(turnDelay);
          }

          for (int i = 0; i < enemies.Count; i++)
          {
              enemies[i].MoveEnemy ();
              yield return new WaitForSeconds(enemies[i].moveTime);
          }
          playersTurn = true;
          enemiesMoving = false;
      }
}

I don't know why these had not worked. But It worked after restarting it.

Adding UI & Level Transitions

using UnityEngine;
using System.Collections;


using System.Collections.Generic;       //Allows us to use Lists.
using UnityEngine.UI;                   //Allows us to use UI.

public class GameManager : MonoBehaviour
{
      public float levelStartDelay = 2f;
      public float turnDelay = 0.1f;
      public int playerFoodPoints = 100;
      public static GameManager instance = null;
      [HideInInspector] public bool playersTurn = true;

      private Text levelText;
      private GameObject levelImage;
      private BoardManager boardScript;
      private int level = 1;
      private List<Enemy> enemies;
      private bool enemiesMoving;
      private bool doingSetup = true;

      void Awake()
      {
          if (instance == null)
              instance = this;
          else if (instance != this)
              Destroy(gameObject);

          DontDestroyOnLoad(gameObject);
          enemies = new List<Enemy>();
          boardScript = GetComponent<BoardManager>();
          InitGame();
      }

      void OnLevelWasLoaded(int index)
      {
          level++;
          InitGame();
      }

      void InitGame()
      {
          doingSetup = true;
          levelImage = GameObject.Find("LevelImage");
          levelText = GameObject.Find("LevelText").GetComponent<Text>();
          levelText.text = "Day " + level;
          levelImage.SetActive(true);
          Invoke("HideLevelImage", levelStartDelay);
          enemies.Clear();
          boardScript.SetupScene(level);
      }

      void HideLevelImage()
      {
          levelImage.SetActive(false);
          doingSetup = false;
      }

      void Update()
      {
          if(playersTurn || enemiesMoving || doingSetup)
              return;
          StartCoroutine (MoveEnemies ());
      }

      public void AddEnemyToList(Enemy script)
      {
          enemies.Add(script);
      }

      public void GameOver()
      {
          levelText.text = "After " + level + " days, you starved.";
          levelImage.SetActive(true);
          enabled = false;
      }

      IEnumerator MoveEnemies()
      {
          enemiesMoving = true;
          yield return new WaitForSeconds(turnDelay);
          if (enemies.Count == 0)
          {
              yield return new WaitForSeconds(turnDelay);
          }

          for (int i = 0; i < enemies.Count; i++)
          {
              enemies[i].MoveEnemy ();
              yield return new WaitForSeconds(enemies[i].moveTime);
          }

          playersTurn = true;
          enemiesMoving = false;
      }
}
public Text foodText;

private void OnTriggerEnter2D (Collider2D other)
{
          if(other.tag == "Exit")
          {
        Invoke ("Restart", restartLevelDelay);
        enabled = false;
          }
          else if(other.tag == "Food")
          {
        food += pointsPerFood;
        foodText.text = "+" + pointsPerFood + " Food: " + food;
        other.gameObject.SetActive (false);
          }
          else if(other.tag == "Soda")
          {
        food += pointsPerSoda;
        foodText.text = "+" + pointsPerSoda + " Food: " + food;
        other.gameObject.SetActive (false);
          }
      }

Audio and Sound Manager

using UnityEngine;
using System.Collections;

public class SoundManager : MonoBehaviour
{
    public AudioSource efxSource;
    public AudioSource musicSource;
    public static SoundManager instance = null;
    public float lowPitchRange = .95f;
    public float highPitchRange = 1.05f;

    void Awake ()
    {
        if (instance == null)
            instance = this;
        else if (instance != this)
            Destroy (gameObject);
        DontDestroyOnLoad (gameObject);
    }

    public void PlaySingle(AudioClip clip)
    {
        efxSource.clip = clip;
        efxSource.Play ();
    }

    public void RandomizeSfx (params AudioClip[] clips)
    {
        int randomIndex = Random.Range(0, clips.Length);
        float randomPitch = Random.Range(lowPitchRange, highPitchRange);
        efxSource.pitch = randomPitch;
        efxSource.clip = clips[randomIndex];
        efxSource.Play();
    }
}

public AudioClip moveSound1;
public AudioClip moveSound2;

protected override void AttemptMove <T> (int xDir, int yDir)
{
          food--;
          base.AttemptMove <T> (xDir, yDir);
          RaycastHit2D hit;
          if (Move (xDir, yDir, out hit))
          {
        //Call RandomizeSfx of SoundManager to play the move sound, passing in two audio clips to choose from.
        SoundManager.instance.RandomizeSfx(moveSound1, moveSound2);
          }

          CheckIfGameOver ();
          GameManager.instance.playersTurn = false;
}

Adding Mobile Controls

#elif UNITY_IOS || UNITY_ANDROID || UNITY_WP8 || UNITY_IPHONE

if (Input.touchCount > 0)
 {
     Touch myTouch = Input.touches[0];
     if (myTouch.phase == TouchPhase.Began)
     {
         touchOrigin = myTouch.position;
     }
     else if (myTouch.phase == TouchPhase.Ended && touchOrigin.x >= 0)
     {
         Vector2 touchEnd = myTouch.position;
         float x = touchEnd.x - touchOrigin.x;
         float y = touchEnd.y - touchOrigin.y;
         touchOrigin.x = -1;
         if (Mathf.Abs(x) > Mathf.Abs(y))
             horizontal = x > 0 ? 1 : -1;
         else
             vertical = y > 0 ? 1 : -1;
     }
 }

#endif