2D Rougelike tutorial
Table of Contents
- Project Introduction
- Player and Enemy Animations
- Creating the Tile Prefabs
- Writing the Board Manager
- Writing the Game Manager
- Moving Object Script
- Creating Destructible Walls
- Player Animator Controller
- Writing the Player Script
- Writing the Enemy Script
- Enemy Animator Controller
- Adding UI & Level Transitions
- Audio and Sound Manager
- Adding Mobile Controls
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