2D Roguelike
Unit Mechanics
45 Mins
Unity Technologies
This step of the 2D Roguelike project covers creating the MovingObject script used by both players and enemies in the game, creating the destructible walls system, and setting up the player and enemy controller scripts.
Recommended Unity Versions
Tutorial Materials
Moving Object Script
This is part 6 of 14 of the 2D Roguelike tutorial in which we write the MovingObject script which the Player and Enemy scripts will inherit from to share movement code.

MovingObject ```csharp

using UnityEngine; using System.Collections;
//The abstract keyword enables you to create classes and class members that are incomplete and must be implemented in a derived class. public abstract class MovingObject : MonoBehaviour { public float moveTime = 0.1f; //Time it will take object to move, in seconds. public LayerMask blockingLayer; //Layer on which collision will be checked. private BoxCollider2D boxCollider; //The BoxCollider2D component attached to this object. private Rigidbody2D rb2D; //The Rigidbody2D component attached to this object. private float inverseMoveTime; //Used to make movement more efficient. //Protected, virtual functions can be overridden by inheriting classes. protected virtual void Start () { //Get a component reference to this object's BoxCollider2D boxCollider = GetComponent <BoxCollider2D> (); //Get a component reference to this object's Rigidbody2D rb2D = GetComponent <Rigidbody2D> (); //By storing the reciprocal of the move time we can use it by multiplying instead of dividing, this is more efficient. inverseMoveTime = 1f / moveTime; } //Move returns true if it is able to move and false if not. //Move takes parameters for x direction, y direction and a RaycastHit2D to check collision. protected bool Move (int xDir, int yDir, out RaycastHit2D hit) { //Store start position to move from, based on objects current transform position. Vector2 start = transform.position; // Calculate end position based on the direction parameters passed in when calling Move. Vector2 end = start + new Vector2 (xDir, yDir); //Disable the boxCollider so that linecast doesn't hit this object's own collider. boxCollider.enabled = false; //Cast a line from start point to end point checking collision on blockingLayer. hit = Physics2D.Linecast (start, end, blockingLayer); //Re-enable boxCollider after linecast boxCollider.enabled = true; //Check if anything was hit if(hit.transform == null) { //If nothing was hit, start SmoothMovement co-routine passing in the Vector2 end as destination StartCoroutine (SmoothMovement (end)); //Return true to say that Move was successful return true; } //If something was hit, return false, Move was unsuccesful. return false; } //Co-routine for moving units from one space to next, takes a parameter end to specify where to move to. protected IEnumerator SmoothMovement (Vector3 end) { //Calculate the remaining distance to move based on the square magnitude of the difference between current position and end parameter. //Square magnitude is used instead of magnitude because it's computationally cheaper. float sqrRemainingDistance = (transform.position - end).sqrMagnitude; //While that distance is greater than a very small amount (Epsilon, almost zero): while(sqrRemainingDistance > float.Epsilon) { //Find a new position proportionally closer to the end, based on the moveTime Vector3 newPostion = Vector3.MoveTowards(rb2D.position, end, inverseMoveTime * Time.deltaTime); //Call MovePosition on attached Rigidbody2D and move it to the calculated position. rb2D.MovePosition (newPostion); //Recalculate the remaining distance after moving. sqrRemainingDistance = (transform.position - end).sqrMagnitude; //Return and loop until sqrRemainingDistance is close enough to zero to end the function yield return null; } } //The virtual keyword means AttemptMove can be overridden by inheriting classes using the override keyword. //AttemptMove takes a generic parameter T to specify the type of component we expect our unit to interact with if blocked (Player for Enemies, Wall for Player). protected virtual void AttemptMove <T> (int xDir, int yDir) where T : Component { //Hit will store whatever our linecast hits when Move is called. RaycastHit2D hit; //Set canMove to true if Move was successful, false if failed. bool canMove = Move (xDir, yDir, out hit); //Check if nothing was hit by linecast if(hit.transform == null) //If nothing was hit, return and don't execute further code. return; //Get a component reference to the component of type T attached to the object that was hit T hitComponent = hit.transform.GetComponent <T> (); //If canMove is false and hitComponent is not equal to null, meaning MovingObject is blocked and has hit something it can interact with. if(!canMove && hitComponent != null) //Call the OnCantMove function and pass it hitComponent as a parameter. 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
This is part 7 of 14 of the 2D Roguelike tutorial in which we write the Wall script which will allow the player to damage and destroy the walls.


using UnityEngine; using System.Collections; public class Wall : MonoBehaviour { public AudioClip chopSound1; //1 of 2 audio clips that play when the wall is attacked by the player. public AudioClip chopSound2; //2 of 2 audio clips that play when the wall is attacked by the player. public Sprite dmgSprite; //Alternate sprite to display after Wall has been attacked by player. public int hp = 3; //hit points for the wall. private SpriteRenderer spriteRenderer; //Store a component reference to the attached SpriteRenderer. void Awake () { //Get a component reference to the SpriteRenderer. spriteRenderer = GetComponent<SpriteRenderer> (); } //DamageWall is called when the player attacks a wall. public void DamageWall (int loss) { //Call the RandomizeSfx function of SoundManager to play one of two chop sounds. SoundManager.instance.RandomizeSfx (chopSound1, chopSound2); //Set spriteRenderer to the damaged wall sprite. spriteRenderer.sprite = dmgSprite; //Subtract loss from hit point total. hp -= loss; //If hit points are less than or equal to zero: if(hp <= 0) //Disable the gameObject. gameObject.SetActive (false); } }

Player Animator Controller
This is part 8 of 14 of the 2D Roguelike tutorial in which we set up the Player Animator Controller.

Writing the Player Script
This is part 9 of 14 of the 2D Roguelike tutorial in which we write the Player script which will take input and control the player's movement.
Please note that this lesson has been updated to reflect changes to Unity's API. Please refer to the upgrade guide PDF found in your asset package download or available directly here.


using UnityEngine; using System.Collections; using UnityEngine.SceneManagement; //Allows us to use SceneManager //Player inherits from MovingObject, our base class for objects that can move, Enemy also inherits from this. public class Player : MovingObject { public float restartLevelDelay = 1f; //Delay time in seconds to restart level. public int pointsPerFood = 10; //Number of points to add to player food points when picking up a food object. public int pointsPerSoda = 20; //Number of points to add to player food points when picking up a soda object. public int wallDamage = 1; //How much damage a player does to a wall when chopping it. private Animator animator; //Used to store a reference to the Player's animator component. private int food; //Used to store player food points total during level. //Start overrides the Start function of MovingObject protected override void Start () { //Get a component reference to the Player's animator component animator = GetComponent<Animator>(); //Get the current food point total stored in GameManager.instance between levels. food = GameManager.instance.playerFoodPoints; //Call the Start function of the MovingObject base class. base.Start (); } //This function is called when the behaviour becomes disabled or inactive. private void OnDisable () { //When Player object is disabled, store the current local food total in the GameManager so it can be re-loaded in next level. GameManager.instance.playerFoodPoints = food; } private void Update () { //If it's not the player's turn, exit the function. if(!GameManager.instance.playersTurn) return; int horizontal = 0; //Used to store the horizontal move direction. int vertical = 0; //Used to store the vertical move direction. //Get input from the input manager, round it to an integer and store in horizontal to set x axis move direction horizontal = (int) (Input.GetAxisRaw ("Horizontal")); //Get input from the input manager, round it to an integer and store in vertical to set y axis move direction vertical = (int) (Input.GetAxisRaw ("Vertical")); //Check if moving horizontally, if so set vertical to zero. if(horizontal != 0) { vertical = 0; } //Check if we have a non-zero value for horizontal or vertical if(horizontal != 0 || vertical != 0) { //Call AttemptMove passing in the generic parameter Wall, since that is what Player may interact with if they encounter one (by attacking it) //Pass in horizontal and vertical as parameters to specify the direction to move Player in. AttemptMove<Wall> (horizontal, vertical); } } //AttemptMove overrides the AttemptMove function in the base class MovingObject //AttemptMove takes a generic parameter T which for Player will be of the type Wall, it also takes integers for x and y direction to move in. protected override void AttemptMove <T> (int xDir, int yDir) { //Every time player moves, subtract from food points total. food--; //Call the AttemptMove method of the base class, passing in the component T (in this case Wall) and x and y direction to move. base.AttemptMove <T> (xDir, yDir); //Hit allows us to reference the result of the Linecast done in Move. RaycastHit2D hit; //If Move returns true, meaning Player was able to move into an empty space. if (Move (xDir, yDir, out hit)) { //Call RandomizeSfx of SoundManager to play the move sound, passing in two audio clips to choose from. } //Since the player has moved and lost food points, check if the game has ended. CheckIfGameOver (); //Set the playersTurn boolean of GameManager to false now that players turn is over. GameManager.instance.playersTurn = false; } //OnCantMove overrides the abstract function OnCantMove in MovingObject. //It takes a generic parameter T which in the case of Player is a Wall which the player can attack and destroy. protected override void OnCantMove <T> (T component) { //Set hitWall to equal the component passed in as a parameter. Wall hitWall = component as Wall; //Call the DamageWall function of the Wall we are hitting. hitWall.DamageWall (wallDamage); //Set the attack trigger of the player's animation controller in order to play the player's attack animation. animator.SetTrigger ("playerChop"); } //OnTriggerEnter2D is sent when another object enters a trigger collider attached to this object (2D physics only). private void OnTriggerEnter2D (Collider2D other) { //Check if the tag of the trigger collided with is Exit. if(other.tag == "Exit") { //Invoke the Restart function to start the next level with a delay of restartLevelDelay (default 1 second). Invoke ("Restart", restartLevelDelay); //Disable the player object since level is over. enabled = false; } //Check if the tag of the trigger collided with is Food. else if(other.tag == "Food") { //Add pointsPerFood to the players current food total. food += pointsPerFood; //Disable the food object the player collided with. other.gameObject.SetActive (false); } //Check if the tag of the trigger collided with is Soda. else if(other.tag == "Soda") { //Add pointsPerSoda to players food points total food += pointsPerSoda; //Disable the soda object the player collided with. other.gameObject.SetActive (false); } } //Restart reloads the scene when called. private void Restart () { //Load the last scene loaded, in this case Main, the only scene in the game. SceneManager.LoadScene (0); } //LoseFood is called when an enemy attacks the player. //It takes a parameter loss which specifies how many points to lose. public void LoseFood (int loss) { //Set the trigger for the player animator to transition to the playerHit animation. animator.SetTrigger ("playerHit"); //Subtract lost food points from the players total. food -= loss; //Check to see if game has ended. CheckIfGameOver (); } //CheckIfGameOver checks if the player is out of food points and if so, ends the game. private void CheckIfGameOver () { //Check if food point total is less than or equal to zero. if (food <= 0) { //Call the GameOver function of GameManager. GameManager.instance.GameOver (); } } }

Writing the Enemy Script
This is part 10 of 14 of the 2D Roguelike tutorial in which we write the Enemy script which will control the enemies movement and AI.


using UnityEngine; using System.Collections; //Enemy inherits from MovingObject, our base class for objects that can move, Player also inherits from this. public class Enemy : MovingObject { public int playerDamage; //The amount of food points to subtract from the player when attacking. private Animator animator; //Variable of type Animator to store a reference to the enemy's Animator component. private Transform target; //Transform to attempt to move toward each turn. private bool skipMove; //Boolean to determine whether or not enemy should skip a turn or move this turn. //Start overrides the virtual Start function of the base class. protected override void Start () { //Register this enemy with our instance of GameManager by adding it to a list of Enemy objects. //This allows the GameManager to issue movement commands. GameManager.instance.AddEnemyToList (this); //Get and store a reference to the attached Animator component. animator = GetComponent<Animator> (); //Find the Player GameObject using it's tag and store a reference to its transform component. target = GameObject.FindGameObjectWithTag ("Player").transform; //Call the start function of our base class MovingObject. base.Start (); } //Override the AttemptMove function of MovingObject to include functionality needed for Enemy to skip turns. //See comments in MovingObject for more on how base AttemptMove function works. protected override void AttemptMove <T> (int xDir, int yDir) { //Check if skipMove is true, if so set it to false and skip this turn. if(skipMove) { skipMove = false; return; } //Call the AttemptMove function from MovingObject. base.AttemptMove <T> (xDir, yDir); //Now that Enemy has moved, set skipMove to true to skip next move. skipMove = true; } //MoveEnemy is called by the GameManger each turn to tell each Enemy to try to move towards the player. public void MoveEnemy () { //Declare variables for X and Y axis move directions, these range from -1 to 1. //These values allow us to choose between the cardinal directions: up, down, left and right. int xDir = 0; int yDir = 0; //If the difference in positions is approximately zero (Epsilon) do the following: if(Mathf.Abs (target.position.x - transform.position.x) < float.Epsilon) //If the y coordinate of the target's (player) position is greater than the y coordinate of this enemy's position set y direction 1 (to move up). If not, set it to -1 (to move down). yDir = target.position.y > transform.position.y ? 1 : -1; //If the difference in positions is not approximately zero (Epsilon) do the following: else //Check if target x position is greater than enemy's x position, if so set x direction to 1 (move right), if not set to -1 (move left). xDir = target.position.x > transform.position.x ? 1 : -1; //Call the AttemptMove function and pass in the generic parameter Player, because Enemy is moving and expecting to potentially encounter a Player AttemptMove <Player> (xDir, yDir); } //OnCantMove is called if Enemy attempts to move into a space occupied by a Player, it overrides the OnCantMove function of MovingObject //and takes a generic parameter T which we use to pass in the component we expect to encounter, in this case Player protected override void OnCantMove <T> (T component) { //Declare hitPlayer and set it to equal the encountered component. Player hitPlayer = component as Player; //Call the LoseFood function of hitPlayer passing it playerDamage, the amount of foodpoints to be subtracted. hitPlayer.LoseFood (playerDamage); //Set the attack trigger of animator to trigger Enemy attack animation. animator.SetTrigger ("enemyAttack"); } }

Enemy Animator Controller
This is part 11 of 14 of the 2D Roguelike tutorial in which we set up the Enemy Animation controller and add code to the GameManager to manage the enemies.


using UnityEngine; using System.Collections; using System.Collections.Generic; //Allows us to use Lists. public class GameManager : MonoBehaviour { public float levelStartDelay = 2f; //Time to wait before starting level, in seconds. public float turnDelay = 0.1f; //Delay between each Player turn. public int playerFoodPoints = 100; //Starting value for Player food points. public static GameManager instance = null; //Static instance of GameManager which allows it to be accessed by any other script. [HideInInspector] public bool playersTurn = true; //Boolean to check if it's players turn, hidden in inspector but public. private BoardManager boardScript; //Store a reference to our BoardManager which will set up the level. private int level = 1; //Current level number, expressed in game as "Day 1". private List<Enemy> enemies; //List of all Enemy units, used to issue them move commands. private bool enemiesMoving; //Boolean to check if enemies are moving. //Awake is always called before any Start functions void Awake() { //Check if instance already exists if (instance == null) //if not, set instance to this instance = this; //If instance already exists and it's not this: else if (instance != this) //Then destroy this. This enforces our singleton pattern, meaning there can only ever be one instance of a GameManager. Destroy(gameObject); //Sets this to not be destroyed when reloading scene DontDestroyOnLoad(gameObject); //Assign enemies to a new List of Enemy objects. enemies = new List<Enemy>(); //Get a component reference to the attached BoardManager script boardScript = GetComponent<BoardManager>(); //Call the InitGame function to initialize the first level InitGame(); } //This is called each time a scene is loaded. void OnLevelWasLoaded(int index) { //Add one to our level number. level++; //Call InitGame to initialize our level. InitGame(); } //Initializes the game for each level. void InitGame() { //Clear any Enemy objects in our List to prepare for next level. enemies.Clear(); //Call the SetupScene function of the BoardManager script, pass it current level number. boardScript.SetupScene(level); } //Update is called every frame. void Update() { //Check that playersTurn or enemiesMoving or doingSetup are not currently true. if(playersTurn || enemiesMoving) //If any of these are true, return and do not start MoveEnemies. return; //Start moving enemies. StartCoroutine (MoveEnemies ()); } //Call this to add the passed in Enemy to the List of Enemy objects. public void AddEnemyToList(Enemy script) { //Add Enemy to List enemies. enemies.Add(script); } //GameOver is called when the player reaches 0 food points public void GameOver() { //Enable black background image gameObject. levelImage.SetActive(true); //Disable this GameManager. enabled = false; } //Coroutine to move enemies in sequence. IEnumerator MoveEnemies() { //While enemiesMoving is true player is unable to move. enemiesMoving = true; //Wait for turnDelay seconds, defaults to .1 (100 ms). yield return new WaitForSeconds(turnDelay); //If there are no enemies spawned (IE in first level): if (enemies.Count == 0) { //Wait for turnDelay seconds between moves, replaces delay caused by enemies moving when there are none. yield return new WaitForSeconds(turnDelay); } //Loop through List of Enemy objects. for (int i = 0; i < enemies.Count; i++) { //Call the MoveEnemy function of Enemy at index i in the enemies List. enemies[i].MoveEnemy (); //Wait for Enemy's moveTime before moving next Enemy, yield return new WaitForSeconds(enemies[i].moveTime); } //Once Enemies are done moving, set playersTurn to true so player can move. playersTurn = true; //Enemies are done moving, set enemiesMoving to false. enemiesMoving = false; } }

Woohoo! You nailed this tutorial.
Continue rocking your 2D Roguelike project.
Next Step:
Architecture and Polish
35 Mins
Unity Technologies
Part of: