DUNGEON HEROES

What Is it?


Dungeon Heroes is a 2D roguelike, dungeon crawler made in unity. You play as a hero that explore dungeons, collect loot and fight bosses. Purchase more heroes and switch between them to be as efficient as possible. The player can later on upgrade the heroes to progress even further though the game. Beware though; since this is a roguelike, there is no such thing as respawn. The player has to survive through the dungeons without dying and if a hero were to die, then that hero is gone forever.

Platform: PC

Tools Used: Unity, Photoshop/Gimp 2, Git

Duration: July 2016 - Ongoing

Team Size: Solo

Role: Designer, Scripter & 2D artist

Responsibilities: Gameplay Design, Gameplay Scripting, AI Scripting, 2D Art

Player


The player can swap freely between all collected heroes, altough the max amount of heroes that the player can have is 4

The selected hero is represented by the heroes head on the UI. The head is slightly bigger than the other ones to indicate which one is selected.

  1. public const int MaxHeroes = 4;
  2. public static Hero currentHero;
  3. public static List<Hero> heroes = new List<Hero>();
  4. public static bool FullHeroes { get { return heroes.Count == MaxHeroes; } }
  5.     
  6.     public static void NextHero() {
  7. var index = heroes.IndexOf(currentHero);
  8. if (index + 1 >= heroes.Count)
  9. SetCurrentHero(heroes[0]);
  10. else
  11. SetCurrentHero(heroes[index + 1]);
  12. }
  13. public static void AddHero(Hero hero) {
  14. heroes.Add(hero);
  15. UIManager.CreateHeroIcon(hero.headIcon, hero);
  16. }
  17. public static Hero SetCurrentHero(Hero hero) {
  18. var returnThis = currentHero;
  19. currentHero = hero;
  20. UIManager.SetActiveHeroIcon(currentHero);
  21. return returnThis;
  22. }
  23. public static void RemoveHero(Hero hero) {
  24. var hi = UIManager.heroIcons[hero];
  25. GameEvents.Destroy(hi.gameObject);
  26. heroes.Remove(hero);
  27. GameSaver.SaveGame();
  28. }

Each hero has two abilities: one offensive and one defensive which gives them all a different playstyle. They also have different amount of health, mana and stamina. When a player swaps hero, the health percentage carries over, which means that the player tactically has to swap heroes to, for instance reduce damage taken or increase mana regeneration.

Heroes

Wizard

The wizard is a classic magic user that throws fireballs and can teleport. Teleport goes through walls if the target location is walkable.

Assassin

The assassin specializes in quick melee combat. He moves faster than the other heroes and has a backstab and a teleport that places the player behind an enemy.

Goblin

The goblin throws bones that bounces at his enemies. This can be really powerful in cramped spaces. The goblin can also become immune to damage, but takes damage over time.

Knight

The knight has a classic attack that deals heavy damage in melee. He also has a shield that he can put out that blocks projectiles and attacks from enemies.

Angel

The angel has a disc that she can throw out. After a short distance, the disc will return like a boomerang. The disc can deal damage on both ways. The angel also has a shield that protects the heroes from one source of damage. The angel has no stamina and can fly.

Ninja

The ninja throws a high amount of throwing stars with low damage. He can also dash and has additional stamina to air jump with.

World Generation

The world generation in Dungeon Heroes consist of creating a set amount of rooms in the world and then adding corridors in between them.

The theory behind the dungeons are based off of binary trees, where each node is a room and the edges are corridors. Each room has a location in world space, a width and a height as well as the ability to create new child nodes.

  1. public class Room {
  2. public float x, y; // world position
  3. public int width, height;
  4. public Room rightRoom, leftRoom;
  5. public TreeRoomInstance instance;
  6. TreeDungeon dungeon;
  7. public Room(float x, float y, Vector2 roomSizeMin, Vector2 roomSizeMax, TreeDungeon dungeon) {
  8. this.x = x;
  9. this.y = y;
  10. this.width = Random.Range((int)roomSizeMin.x, (int)roomSizeMax.x);
  11. this.height = Random.Range((int)roomSizeMin.y, (int)roomSizeMax.y);
  12. this.dungeon = dungeon;
  13. }
  14. public Room(float x, float y, int width, int height, TreeDungeon dungeon) {
  15. this.x = x;
  16. this.y = y;
  17. this.width = width;
  18. this.height = height;
  19. this.dungeon = dungeon;
  20. }
  21. public Room MakeNeighbours(int minRoomWidth, int minRoomHeight, int maxRoomWidth, int maxRoomHeight, int distanceMax, int distanceMin) {
  22. while (rightRoom == null || leftRoom == null) {
  23. Vector2 direction = RandomPointInEllipse(1, 0.1f);
  24. float distanceAwayX = Random.Range(distanceMin, distanceMax);
  25. float distanceAwayY = Random.Range(distanceMin, distanceMax);
  26. int roomWidth = Random.Range(minRoomWidth, maxRoomWidth);
  27. int roomHeight = Random.Range(minRoomHeight, maxRoomHeight);
  28. Vector2 topLeft = new Vector2(x + distanceAwayX * direction.x, y + distanceAwayY * direction.y);
  29. Vector2 bottomRight = new Vector2(x + distanceAwayX * direction.x + roomWidth, y + distanceAwayY * direction.y + roomHeight);
  30. if (Physics2D.OverlapArea(topLeft, bottomRight) != null) {
  31. continue;
  32. }
  33. if (rightRoom == null) {
  34. rightRoom = new Room(Mathf.FloorToInt(x + distanceAwayX * direction.x), Mathf.FloorToInt(y + distanceAwayY * direction.y), roomWidth, roomHeight, dungeon);
  35. return rightRoom;
  36. } else {
  37. leftRoom = new Room(Mathf.FloorToInt(x + distanceAwayX * direction.x), Mathf.FloorToInt(y + distanceAwayY * direction.y), roomWidth, roomHeight, dungeon);
  38. return leftRoom;
  39. }
  40. }
  41. return null;
  42. }
  43. public override string ToString() {
  44. return "Room " + x + "," + y;
  45. }
  46. private Vector2 RandomPointInEllipse(float distanceX, float distanceY) {
  47. var x = Random.Range(-distanceX, distanceX);
  48. var y = Random.Range(-distanceY, distanceY);
  49. Vector2 ellipse = new Vector2(x, y).normalized;
  50. return ellipse;
  51. }
  52. }

The dungeon is populated with a recursive method called "PopulateDungeon()" and then stored in a list so that it can be referenced when the dungeon is converted to world tiles.

  1. private void PopulateDungeon(ref int counter, Room room) {
  2. counter += 1;
  3. if (counter > iterations) {
  4. return;
  5. }
  6. Room newRoom = AddToList(room, counter);
  7. Room newRoom2 = AddToList(room, counter);
  8. if (newRoom != null)
  9. PopulateDungeon(ref counter, newRoom);
  10. if (newRoom2 != null)
  11. PopulateDungeon(ref counter, newRoom2);
  12. }
  13. private Room AddToList(Room r, int counter) {
  14. Room newRoom = r.MakeNeighbours((int)roomMinSize.x, (int)roomMinSize.y, (int)roomMaxSize.x, (int)roomMaxSize.y, maxDistance, minDistance);
  15. if (newRoom != null) {
  16. roomList.Add(newRoom);
  17. var go = new GameObject(newRoom.ToString() + ":" + counter);
  18. go.transform.position = newRoom.position;
  19. TreeRoomInstance instance = go.AddComponent<TreeRoomInstance>();
  20. instance.room = newRoom;
  21. RoomToWorld(newRoom, go);
  22. newRoom.instance = instance;
  23. SetInstanceOptions(ref instance);
  24. return newRoom;
  25. }
  26. return null;
  27. }

Corridors in between the rooms  are created and some platforms are added so that the player can move upwards if the corridor goes upwards.

  1. private void MakeCorridors(Room room) {
  2.     if (room == null) {
  3.         return;
  4.     }
  5.     MakeCorridorBetween(room, room.leftRoom);
  6.     MakeCorridorBetween(room, room.rightRoom);
  7.     MakeCorridors(room.rightRoom);
  8.     MakeCorridors(room.leftRoom);
  9. }
  10. private void MakeCorridorBetween(Room a, Room b) {
  11.     if (a == null || b == null)
  12.         return;
  13.     var direction = (b.position - a.position);
  14.     var multip = direction.x >= 0 ? 1 : -1;
  15.     for (int x = 0; x < Mathf.Abs(direction.x) + corridorSize + 1; x++) {
  16.         for (int y = -corridorSize; y <= corridorSize; y++) {
  17.             if (y == -corridorSize || y == corridorSize) {
  18.             } else {
  19.                 var point = a.position + new Vector2(x, y) * multip;
  20.                 Collider2D hit = Physics2D.OverlapPoint(point);
  21.                 if (hit != null && !hit.CompareTag("Object"))
  22.                     Destroy(hit.gameObject);
  23.             }
  24.         }
  25.     }
  26.     multip = direction.y >= 0 ? 1 : -1;
  27.     for (int y = 0; y < Mathf.Abs(direction.y); y++) {
  28.         for (int x = -corridorSize; x <= corridorSize; x++) {
  29.             if (x == -corridorSize || x == corridorSize) {
  30.                 continue;
  31.             } else {
  32.                 var point = a.position + new Vector2(x + direction.x * multip, y) * multip;
  33.                 Collider2D hit = Physics2D.OverlapPoint(point);
  34.                 if (hit != null)
  35.                     Destroy(hit.gameObject);
  36.             }
  37.             if (y > 0 && y < Mathf.Abs(direction.y) - 1){
  38.                 var location = a.position + new Vector2(x + direction.x * multip, y) * multip;
  39.                 Instantiate(jumpBoard,location , Quaternion.identity);
  40.             }
  41.         }
  42.     }
  43. }

The rooms and the corridors are cleaned up by going through each room and cleaning up overlapping tiles. This will ensure that there is always space to run in.


I also add some monsters, some random props in the background and some objects around the world to make it feel more alive, instead of having just empty rooms.

  1. private void ClearRooms() {
  2. foreach (Room room in roomList) {
  3. var start = room.bottomLeft + Vector2.one;
  4. var end = room.topRight - Vector2.one * 2;
  5. Collider2D[] hits = Physics2D.OverlapAreaAll(start, end);
  6. foreach (Collider2D hit in hits) {
  7. if (!hit.gameObject.GetComponent<HeroClass>())
  8. Destroy(hit.gameObject);
  9. }
  10. }
  11. foreach (Room r in roomList) {
  12. CleanupCorridors(r, r.rightRoom);
  13. CleanupCorridors(r, r.leftRoom);
  14. }
  15. }
  16. private void CleanupCorridors(Room a, Room b) {
  17. if (a == null || b == null) {
  18. return;
  19. }
  20. var direction = (b.position - a.position);
  21. var cSize = Random.Range(1, corridorSize + 1);
  22. var multip = direction.x >= 0 ? 1 : -1;
  23. for (int x = 0; x < Mathf.Abs(direction.x); x++) {
  24. for (int y = -cSize; y <= cSize; y++) {
  25. if (y == -cSize || y == cSize) { } else {
  26. var point = a.position + new Vector2(x, y) * multip;
  27. Collider2D[] hits = Physics2D.OverlapPointAll(point);
  28. foreach (Collider2D hit in hits)
  29. if (hit != null && !hit.CompareTag("Object"))
  30. Destroy(hit.gameObject);
  31. }
  32. }
  33. }
  34. multip = direction.y >= 0 ? 1 : -1;
  35. for (int y = 0; y < Mathf.Abs(direction.y); y++) {
  36. for (int x = -cSize; x <= cSize; x++) {
  37. if (x == -cSize || x == cSize) {
  38. } else {
  39. var point = a.position + new Vector2(x + direction.x * multip, y) * multip;
  40. Collider2D hit = Physics2D.OverlapPoint(point);
  41. if (hit != null && !hit.CompareTag("Object"))
  42. Destroy(hit.gameObject);
  43. }
  44. }
  45. }
  46. }

fredrik.tumlin@Gmail.com

+46 76 191 96 57