Roguelike 2D Game (Unity C#)

This game combines the Roguelike genre with card game elements to create a challenging and replayable game. The Reincarnation mechanic, aswell as the different enemy types makes the Player think in creative ways to get their health to 0. Procedural level generation means you never get the same layout twice.


Movement

Movement

The Character is controlled with WASD, and able to aim and use their weapon using the mouse. They can also roll using Spacebar, giving them a brief period of invincibility against enemy projectiles. The Player also faces left or right depending on which side the Mouse cursor is pointing.

The Player Controller script deals with all of the above, aswell as dealing with the Player health and other stats. I created animations for the states that the Player can be in, such as running, idling and rolling, plus there are different sprites used to indicate when the Player is damaged or healed.

  • class PlayerController : MonoBehaviour
    {
    	public static PlayerController instance;
    
    	public Rigidbody2D theRb;
    
    	public SpriteRenderer playerSR, bodySR, body2SR;
    
    	public Sprite player, playerHealed, body, bodyHealed, playerDamaged, bodyDamaged;
    
    	public float moveSpeed, activeMoveSpeed;
    
    	public Transform gunHand;
    
    	private Vector2 moveInput;
    
    	public Animator anim;
    
    	public float maxHealth, currentHealth, onReincarnateHealth;
    	private float reincarnateHealthToTakeAway = 30f;
    
    	public GameObject rock, firePoint, zEffect;
    
    	public bool isDeincarnated = false;
    
    	public bool canMove = true;
    
    	public float InvincibilityTimer, timeOfInvincibility;
    
    	public bool isInvincible = false;
    
    	public float dashSpeed, dashCooldown, dashInvincibilityTime, dashLength;
    
    	private float damageLength = 0.1f;
    	public int extraHealToTake = 0;
    
    	[Header("Card Effects")]
    	public bool isDoubleHeal;
    
    	[HideInInspector]
    	public int damageTakenIncrease = 0;
    
    	private float dashTimer, dashCooldownTimer, damageTimer;
    
    	public GameObject audioManager;
    
    	private void Awake()
    	{
    		instance = this;
    	}
    	// Start is called before the first frame update
    	void Start()
    	{
    		audioManager = GameObject.Find("Audio Manager");
    		activeMoveSpeed = moveSpeed;
    
    		//Start the game with the health given after reincarnating
    		onReincarnateHealth = maxHealth - reincarnateHealthToTakeAway;
    		currentHealth = onReincarnateHealth;
    	}
    
    	// Update is called once per frame
    	void Update()
    	{
    		//Handle movement
    		if (!GameManager.instance.isPausedReincarnate && !GameManager.instance.isPausedMenu)
    		{
    			if (damageTimer > 0)
    			{
    				damageTimer -= Time.deltaTime;
    			}
    			else
    			{
    				playerSR.sprite = player;
    				bodySR.sprite = body;
    				body2SR.sprite = body;
    			}
    
    			if (canMove)
    			{
    				{
    					if (isInvincible)
    					{
    						playerSR.color = new Color(1.0f, 1.0f, 1.0f, 0.5f);
    						if (InvincibilityTimer > 0)
    						{
    							InvincibilityTimer -= Time.deltaTime;
    						}
    						else
    						{
    							isInvincible = false;
    							playerSR.color = new Color(1.0f, 1.0f, 1.0f, 1.0f);
    						}
    					}
    
    					moveInput.x = Input.GetAxisRaw("Horizontal");
    					moveInput.y = Input.GetAxisRaw("Vertical");
    
    					moveInput.Normalize();
    
    					theRb.velocity = moveInput * activeMoveSpeed;
    
    					Vector3 mousePos = Input.mousePosition;
    					Vector3 screenPoint = CameraController.instance.mainCamera.WorldToScreenPoint(transform.localPosition);
    
    					if (mousePos.x < screenPoint.x)
    					{
    						transform.localScale = new Vector3(-1f, 1f, 1f);
    						gunHand.localScale = new Vector3(-1.5f, -1.5f, 1f);
    					}
    					else
    					{
    						transform.localScale = new Vector3(1f, 1f, 1f);
    						gunHand.localScale = new Vector3(1.5f, 1.5f, 1f);
    					}
    
    					Vector2 offset = new Vector2(mousePos.x - screenPoint.x, mousePos.y - screenPoint.y);
    					float angle = Mathf.Atan2(offset.y, offset.x) * Mathf.Rad2Deg;
    
    					gunHand.rotation = Quaternion.Euler(0, 0, angle);
    
    					if (moveInput != Vector2.zero)
    					{
    						anim.SetBool("isMoving", true);
    					}
    					else
    					{
    						anim.SetBool("isMoving", false);
    					}
    
    					if (Input.GetKeyDown(KeyCode.Space) && dashTimer <= 0 && dashCooldownTimer <= 0)
    					{
    						audioManager.GetComponent().PlaySFX(6);
    						activeMoveSpeed = dashSpeed;
    						ActivateInvincibility(dashInvincibilityTime);
    						dashTimer = dashLength;
    						anim.SetTrigger("Dash");
    					}
    
    					if (dashTimer > 0)
    					{
    						dashTimer -= Time.deltaTime;
    						if (dashTimer <= 0)
    						{
    							activeMoveSpeed = moveSpeed;
    							dashCooldownTimer = dashCooldown;
    						}
    					}
    
    					if (dashCooldownTimer > 0)
    					{
    						dashCooldownTimer -= Time.deltaTime;
    					}
    				}
    			}
    
    			else
    			{
    				theRb.velocity = Vector3.zero;
    				anim.SetBool("isMoving", false);
    				activeMoveSpeed = moveSpeed;
    				dashCooldownTimer = 0;
    				dashTimer = 0;
    
    				if (isInvincible)
    				{
    					isInvincible = false;
    					playerSR.color = new Color(1.0f, 1.0f, 1.0f, 1.0f);
    					InvincibilityTimer = 0;
    				}
    			}
    		}
    
    		//If player is healed, Deincarnate 
    		if (currentHealth >= maxHealth && !isDeincarnated)
    		{
    			currentHealth = maxHealth;
    			isDeincarnated = true;
    
    			GameManager.instance.PauseGame();
    			CardManager.instance.Unequip();
    			//GameManager.instance.UnpauseGame();
    			//ActivateInvincibility(2f);
    		}
    	}
    
    	//This function covers being damaged by items and healed by enemies
    	public void TakeDamage(int damageToTake)
    	{
    		if (!isInvincible || damageToTake > 0)
    		{
    			if (damageToTake > currentHealth)
    			{
    				currentHealth = 0;
    			}
    			else
    			{
    				if (isDoubleHeal && damageToTake < 0)
    				{
    					currentHealth -= damageToTake * 2;
    				}
    				else
    				{
    					currentHealth -= damageToTake;
    				}
    			}
    		}
    	}
    
    	//One of the card abilities
    	public void IncreaseMaxHealth(int healthToIncrease)
    	{
    		maxHealth += healthToIncrease;
    		onReincarnateHealth = maxHealth - reincarnateHealthToTakeAway;
    		currentHealth = onReincarnateHealth;
    	}
    
    	//This is called when the Player rolls
    	public void ActivateInvincibility(float timeOfInvincibility)
    	{
    		isInvincible = true;
    		InvincibilityTimer = timeOfInvincibility;
    	}
    
    	//Trigger the visual effect for when the Player is damaged or healed
    	public void DamagedByEnemy(bool isEnemy)
    	{
    		if (isEnemy)
    		{
    			if (!isInvincible)
    			{
    				damageTimer = damageLength;
    				damageTimer = damageLength;
    				playerSR.sprite = playerHealed;
    				bodySR.sprite = bodyHealed;
    				body2SR.sprite = bodyHealed;
    			}
    		}
    
    		if (!isEnemy)
    		{
    			damageTimer = damageLength;
    			playerSR.sprite = playerDamaged;
    			bodySR.sprite = bodyDamaged;
    			body2SR.sprite = bodyDamaged;
    		}
    	}
    
    	//One of the card abilities
    	public void IncreaseDamageTaken(int damageIncrease)
    	{
    		damageTakenIncrease += damageIncrease;
    	}
    
    	//This occurs when a Hammer enemy knocks the Player out
    	public void KnockOut()
    	{
    		Instantiate(zEffect, new Vector3(transform.position.x, transform.position.y + 0.5f), transform.rotation, gameObject.transform);
    	}
    
    	//One of the card abilities
    	public void DecreaseHealthOnReincarnation(int healthAmount)
    	{
    		reincarnateHealthToTakeAway += healthAmount;
    	}
    
    	//One of the card abilities
    	public void IncreaseHealToTake(int heal)
    	{
    		extraHealToTake += heal;
    	}
    }

Procedural Level Generation

Different rooms every time

The rooms are made up out of 2 pre-made parts, the outline and the center. The outline contains the exits, and is generated to connect each room together, while the center contains the enemies, objects and floor tiles.

There are lists for varieties of both: the outlines list containing every combination of exits possible, and the centers lists containing pre-placed elements to make for purposeful gameplay encounters. The centers lists are split up into easy and hard levels. Building the varieties of outlines and centers was easy as I created a tileset.

Procedural level layout

The "Generation Point" object starts at 0,0 and an rectangle icon is created for the start room. The Point then translates randomly in one of 4 directions, creates an icon, and repeats until the specified number of icons have been created. This is to prepare for creating the room outlines, and these icons are also used to visualise the mini-map. If the Point happens to overlap an existing room, it chooses a direction and translates in that direction until an empty space is found.

To create the room outlines, for every room, an overlap circle checks in each direction to see which rooms are surrounding it, and chooses from the list of pre-made room outlines to perfectly connect each room with exits.

The room centers are then created from a list of easy and hard room centers, which are placed so that the levels are harder closer towards the end of the level. The start and end room centers are pre-determined. These steps ensure that there is a high chance that no two rooms will be the same, and that the layout of rooms will be unique to every run.

  • public class LevelGenerator : MonoBehaviour
    {
    	public GameObject layoutRoom;
    	public Color startColour, endColour;
    
    	public int distanceToEnd;
    
    	public Transform generatorPoint;
    
    	public enum Direction { up, right, down, left};
    	public Direction selectedDirection;
    
    	public float xOffset = 18f, yOffset = 10f;
    
    	public LayerMask whatisRoom;
    
    	private GameObject endRoom;
    
    	private List layoutRoomObjects = new List();
    
    	public List roomPrefabs = new List();
    
    	public RoomCenter centerStart, centerEnd;
    	public RoomCenter[] potentialCentersEasy;
    	public RoomCenter[] potentialCentersHard;
    
    	private List generatedOutlines = new List();
    
    	// Start is called before the first frame update
    	void Start()
    	{   //Create start room
    		Instantiate(layoutRoom, generatorPoint.position, generatorPoint.rotation).GetComponent().color = startColour;
    
    		selectedDirection = (Direction)Random.Range(0, 4);
    		MoveGenerationPoint();
    
    		//Create a room, choose where the next room will be and repeat
    		for (int i = 0; i < distanceToEnd; i++)
    		{
    			GameObject newRoom = Instantiate(layoutRoom, generatorPoint.position, generatorPoint.rotation);
    
    			layoutRoomObjects.Add(newRoom);
    
    			if (i + 1 == distanceToEnd)
    			{
    				newRoom.GetComponent().color = endColour;
    				layoutRoomObjects.RemoveAt(layoutRoomObjects.Count - 1);
    				endRoom = newRoom;
    			}
    			selectedDirection = (Direction)Random.Range(0, 4);
    			MoveGenerationPoint();
    
    			while (Physics2D.OverlapCircle(generatorPoint.position, .2f, whatisRoom))
    			{
    				MoveGenerationPoint();
    			}
    		}
    
    		//create room outlines
    		CreateRoomOutline(Vector3.zero, 0);
    		foreach(GameObject room in layoutRoomObjects)
    		{
    			int position = layoutRoomObjects.IndexOf(room);
    			CreateRoomOutline(room.transform.position, position);
    		}
    		CreateRoomOutline(endRoom.transform.position, distanceToEnd - 1);
    
    		//Create the room centers that will go inside the room outlines
    		foreach(GameObject outline in generatedOutlines)
    		{
    			bool generateCenter = true;
    
    			//Create specific start room center
    			if (outline.transform.position == Vector3.zero)
    			{
    				Instantiate(centerStart, outline.transform.position, transform.rotation).theRoom = outline.GetComponent();
    				generateCenter = false;
    			}
    			//Create specific end room center
    			if (outline.transform.position == endRoom.transform.position)
    			{
    				Instantiate(centerEnd, outline.transform.position, transform.rotation).theRoom = outline.GetComponent();
    				generateCenter = false;
    			}
    			//Decide difficulty of room center based on how far to the end of the level it is
    			if (generateCenter)
    			{
    				if (outline.GetComponent().roomNumber / distanceToEnd <= 0.25f)
    				{
    					int centerSelect = Random.Range(0, potentialCentersEasy.Length);
    
    					Instantiate(potentialCentersEasy[centerSelect], outline.transform.position, transform.rotation).theRoom = outline.GetComponent();
    				}
    				else
    				{
    					int centerSelect = Random.Range(0, potentialCentersHard.Length);
    
    					Instantiate(potentialCentersHard[centerSelect], outline.transform.position, transform.rotation).theRoom = outline.GetComponent();
    				}
    				
    			}
    		}
    	}
    
    	// Update is called once per frame
    	void Update()
    	{
    #if UNITY_EDITOR
    		if (Input.GetKey(KeyCode.R))
    		{
    			Scene scene = SceneManager.GetActiveScene(); SceneManager.LoadScene(scene.name);
    		}
    
    #endif
    	}
    
    	//Choose where the rooms will spawn
    	public void MoveGenerationPoint()
    	{
    		switch (selectedDirection)
    		{
    			case Direction.up:
    				generatorPoint.position += new Vector3(0f, yOffset, 0f);
    				break;
    
    			case Direction.right:
    				generatorPoint.position += new Vector3(xOffset, 0f, 0f);
    				break;
    
    			case Direction.down:
    				generatorPoint.position += new Vector3(0f, -yOffset, 0f);
    				break;
    
    			case Direction.left:
    				generatorPoint.position += new Vector3(-xOffset, 0f, 0f);
    				break;
    		}
    	}
    
    	//Decide which Room outline will spawn so the rooms connect
    	private RoomPrefab PickRoom(bool exitUp, bool exitRight, bool exitDown, bool exitLeft)
    	{
    		foreach (RoomPrefab rp in roomPrefabs)
    		{
    			if (rp.exitUp == exitUp && rp.exitRight == exitRight && rp.exitDown == exitDown && rp.exitLeft == exitLeft)
    			{
    				return rp;
    			}
    		}
    		return null;
    	}
    
    	//Create the room outline
    	public void CreateRoomOutline(Vector3 position, int roomNumber)
    	{
    		bool exitUp = Physics2D.OverlapCircle(position + new Vector3(0, yOffset), .2f, whatisRoom);
    		bool exitDown = Physics2D.OverlapCircle(position + new Vector3(0, -yOffset), .2f, whatisRoom);
    		bool exitRight = Physics2D.OverlapCircle(position + new Vector3(xOffset, 0), .2f, whatisRoom);
    		bool exitLeft = Physics2D.OverlapCircle(position + new Vector3(-xOffset, 0), .2f, whatisRoom);
    
    		var roomPrefab = PickRoom(exitUp, exitRight, exitDown, exitLeft);
    		if (roomPrefab != null)
    		{
    			var room = Instantiate(roomPrefab.prefab, position, Quaternion.identity, transform);
    			room.GetComponent().roomNumber = roomNumber;
    			generatedOutlines.Add(room);
    		}
    	}
    }
    
    [System.Serializable]
    public class RoomPrefab
    {
    	public bool exitUp;
    	public bool exitRight;
    	public bool exitDown;
    	public bool exitLeft;
    	public GameObject prefab;
    } 

Reincarnation mechanic and card elements

Reincarnation

The reincarnation mechanic is what the game is mostly revolves around: when the Player's health becomes 0, instead of a game over, you reincarnate and become stronger. In contrast, when the Player is fully healed they get weaker and can lead to a game over. This means the Player has to find ways of damaging themselves while the enemies attempt to heal them.

There are multiple objects the Player can find in the rooms to damage themselves, such as triggering arrows to fire from a statue, to coaxing enemies to fire at uneven structures and causing debris to fall from the ceiling.

Getting stronger with cards

Upon reincarnating, the Player picks between 3 cards which give the Player benefits, such as higher damage output or increased firerate. Better card rarities offer bonuses to these stats. Being fully healed by enemies causes the last aquired card to be destroyed. Being fully healed with no cards causes a game over.

The Card Manager takes care of the list of cards aqcuired and cards that appear for the Player to pick from. This includes the Inventory, where you can hover over cards aqcuired to see their stats. The script for the Card itself deals with the information it holds, such as the stats, rarity and ability.

  • public class ReincarnationCard : MonoBehaviour
    {
    	public Animator anim;
    
    	public bool isLeft, isRight, isChosen, isCurseCard;
    
    	public RectTransform rectTransform;
    
    	public Text goodEffectText, badEffectText, titleText, cursedText, cursedTextTwo;
    
    	public int damageAmount, healthAmount, healthOnReincarnateAmount, damageTakenIncrease, healAmount, cardRarity;
    	public float fireRateIncrease;
    
    	public Sprite[] cardRarities, cardRaritiesHighlighted;
    	public Sprite cardRarityHighlighted;
    	public GameObject audioManager;
    
    	public enum cardType { damageGivenUpHealthUp, healthDownDamageGivenDown, damageTakenIncreaseHealthUp, fireRateUpDamageDown};
    	public enum curseCardType { takeExtraHeal};
    	public cardType selectedType;
    	public curseCardType selectedCurse;
    
    	// Start is called before the first frame update
    	void Start()
    	{
    		audioManager = GameObject.Find("Audio Manager");
    
    		if (!isCurseCard)
    		{
    			cursedText.gameObject.SetActive(false);
    			cursedTextTwo.gameObject.SetActive(false);
    			cardRarity = Random.Range(0, 4);
    		}
    		else
    		{
    			goodEffectText.gameObject.SetActive(false);
    			badEffectText.gameObject.SetActive(false);
    			cardRarity = 4;
    		}
    
    		cardRarityHighlighted = cardRaritiesHighlighted[cardRarity];
    
    		gameObject.GetComponent().sprite = cardRarities[cardRarity];
    
    		if (!isCurseCard)
    		{
    			selectedType = (cardType)Random.Range(0, 4);
    		}
    		else
    		{
    			selectedCurse = (curseCardType)(0);
    		}
    
    		//Set the card rarity
    		if (cardRarity == 0)
    		{
    			titleText.text = "Common";
    		}
    		if (cardRarity == 1)
    		{
    			titleText.text = "Rare";
    		}
    		if (cardRarity == 2)
    		{
    			titleText.text = "Epic";
    		}
    		if (cardRarity == 3)
    		{
    			titleText.text = "Legendary";
    		}
    		if (cardRarity == 4)
    		{
    			titleText.text = "Curse";
    		}
    
    		//Set the functionality and text for the various card effects
    		switch (selectedType)
    		{
    			case cardType.damageGivenUpHealthUp:
    				damageAmount = Random.Range(6, 9) + cardRarity;
    				healthAmount = Random.Range(6, 9) - cardRarity;
    				goodEffectText.text = "+" + damageAmount + " damage";
    				badEffectText.text = "+" + healthAmount + " max health";
    				break;
    
    			case cardType.healthDownDamageGivenDown:
    				damageAmount = Random.Range(8, 11) - cardRarity;
    				healthOnReincarnateAmount = Random.Range(8, 11) + cardRarity;
    				goodEffectText.text = "-" + healthOnReincarnateAmount + " health after reincarnate";
    				badEffectText.text = "-" + damageAmount + " damage";
    				break;
    
    			case cardType.damageTakenIncreaseHealthUp:
    				damageTakenIncrease = Random.Range(9, 12) + cardRarity;
    				healthAmount = Random.Range(7, 10) - cardRarity;
    				goodEffectText.text = "+" + damageTakenIncrease + " damage taken from objects";
    				badEffectText.text = "+" + healthAmount + " max health";
    				break;
    
    			case cardType.fireRateUpDamageDown:
    				fireRateIncrease = Random.Range(4, 5) * 0.1f + (cardRarity * 0.1f);
    				damageAmount = Random.Range(7, 10) - cardRarity;
    				goodEffectText.text = "+" + fireRateIncrease + " shots per second";
    				badEffectText.text = "-" + damageAmount + " damage";
    				break;
    		}
    
    		switch (selectedCurse)
    		{
    			case curseCardType.takeExtraHeal:
    				healAmount = 4;
    				cursedText.text = "+" + healAmount + " heal given by enemies";
    				cursedTextTwo.text = "Curse cards cannot be removed";
    				break;
    		}
    	}
    
    	// Update is called once per frame
    	void Update()
    	{
    		if (UIController.instance.isReincarnationCardClicked && !isChosen)
    		{
    			anim.SetBool("isDiscarded", true);
    		}
    	}
    
    	public void Hoverable()
    	{
    		anim.SetBool("isHoverable", true);
    	}
    
    	public void OnMouseHover()
    	{
    		if (anim.GetBool("isHoverable"))
    		{
    			anim.SetBool("isMouseHover", true);
    			//audioManager.GetComponent().PlaySFX(2);
    		}
    	}
    
    	public void OffMouseHover()
    	{
    		if (anim.GetBool("isMouseHover"))
    		{
    			anim.SetBool("isMouseHover", false);
    		}
    	}
    
    	//When the Player chooses a card, apply the effects 
    	public void OnMouseClick()
    	{
    		if (anim.GetBool("isClickable") && anim != null)
    		{
    			audioManager.GetComponent().PlaySFX(3);
    			UIController.instance.isReincarnationCardClicked = true;
    			isChosen = true;
    			anim.SetBool("isMouseClick", true);
    			anim.SetBool("isHoverable", false);
    
    			switch (selectedType)
    			{
    				case cardType.damageGivenUpHealthUp:
    					PlayerController.instance.IncreaseMaxHealth(healthAmount);
    					Gun.instance.IncreaseDamageToGive(damageAmount);
    					break;
    
    				case cardType.healthDownDamageGivenDown:
    					PlayerController.instance.DecreaseHealthOnReincarnation(healthOnReincarnateAmount);
    					Gun.instance.IncreaseDamageToGive(-damageAmount);
    					PlayerController.instance.IncreaseMaxHealth(0);
    					break;
    
    				case cardType.damageTakenIncreaseHealthUp:
    					PlayerController.instance.IncreaseDamageTaken(damageTakenIncrease);
    					PlayerController.instance.IncreaseMaxHealth(healthAmount);
    					break;
    
    				case cardType.fireRateUpDamageDown:
    					Gun.instance.IncreaseFireRate(fireRateIncrease);
    					Gun.instance.IncreaseDamageToGive(-damageAmount);
    					PlayerController.instance.IncreaseMaxHealth(0);
    					break;
    			}
    
    			switch (selectedCurse)
    			{
    				case curseCardType.takeExtraHeal:
    					PlayerController.instance.IncreaseHealToTake(healAmount);
    					PlayerController.instance.IncreaseMaxHealth(0);
    					break;
    			}
    
    			CardManager.instance.Equip(this);
    
    			anim.SetBool("isClickable", false); 
    
    			Debug.Log("Click!");
    		}
    	}
    
    	//Reverse the card effects if destroyed
    	public void OnRemove()
    	{
    		audioManager.GetComponent().LoudenMusic();
    
    		//Remove the stats and set current health to OnReinarncateHealth and Unpause the game
    		switch (selectedType)
    		{
    			case cardType.damageGivenUpHealthUp:
    				PlayerController.instance.IncreaseMaxHealth(-healthAmount);
    				Gun.instance.IncreaseDamageToGive(-damageAmount);
    				break;
    
    			case cardType.healthDownDamageGivenDown:
    				PlayerController.instance.DecreaseHealthOnReincarnation(-healthOnReincarnateAmount);
    				Gun.instance.IncreaseDamageToGive(damageAmount);
    				PlayerController.instance.IncreaseMaxHealth(0);
    				break;
    
    			case cardType.damageTakenIncreaseHealthUp:
    				PlayerController.instance.IncreaseDamageTaken(-damageTakenIncrease);
    				PlayerController.instance.IncreaseMaxHealth(-healthAmount);
    				break;
    
    			case cardType.fireRateUpDamageDown:
    				Gun.instance.IncreaseFireRate(-fireRateIncrease);
    				Gun.instance.IncreaseDamageToGive(damageAmount);
    				PlayerController.instance.IncreaseMaxHealth(0);
    				break;
    		}
    
    		switch (selectedCurse)
    		{
    			case curseCardType.takeExtraHeal:
    				PlayerController.instance.IncreaseHealToTake(-healAmount);
    				PlayerController.instance.IncreaseMaxHealth(0);
    				break;
    		}
    
    		PlayerController.instance.currentHealth = PlayerController.instance.onReincarnateHealth;
    		GameManager.instance.UnpauseGame();
    		PlayerController.instance.isDeincarnated = false;
    		PlayerController.instance.ActivateInvincibility(2f);
    	}
    
    	public void PlaySound()
    	{
    		audioManager.GetComponent().PlaySFX(2);
    	}
    }
  • public class CardManager : MonoBehaviour
    {
    	public static CardManager instance;
    
    	#region Singleton
    	void Awake()
    	{
    		instance = this;
    	}
    
    	#endregion
    
    	public List currentCards = new List();
    
    	public List cardIcons = new List();
    	public List curseCardIcons = new List();
    	private float xOffset = 60f;
    
    	public GameObject cardIconImage;
    	public Transform cardIconHolder, curseCardIconHolder;
    
    	[HideInInspector]
    	public bool inventoryOpen = false;
    
    	public RectTransform cardIconBackground, cursedCardIconBackground;
    
    	public Image fadeScreen;
    
    	public Text inventoryText;
    
    	public GameObject audioManager;
    
    	public GameObject inventoryTutorial;
    
    	public bool gameOver;
    
    	private void Start()
    	{
    		instance = this;
    		audioManager = GameObject.Find("Audio Manager");
    	}
    
    	private void Update()
    	{
    		if (Input.GetKeyDown(KeyCode.I))
    		{
    			if (inventoryOpen)
    			{
    				CloseInventory();
    			}
    			else if (!GameManager.instance.isPausedMenu && !GameManager.instance.isPausedReincarnate && cardIcons.Count + curseCardIcons.Count > 0)
    			{
    				OpenInventory();
    			}
    		}
    	}
    
    	//When the Player chooses a card, equip it 
    	public void Equip (ReincarnationCard newCard)
    	{
    		ReincarnationCard createdCard = newCard;
    
    		if (!newCard.isCurseCard)
    		{
    			currentCards.Add(newCard);
    			cardIconBackground.gameObject.SetActive(true);
    		}
    		else
    		{
    			cursedCardIconBackground.gameObject.SetActive(true);
    			cardIconBackground.gameObject.SetActive(true);
    		}
    
    		AddCardIcon(createdCard);
    	}
    
    	//Remove the most recently aqcuired card
    	public void Unequip()
    	{
    		if (currentCards.Count > 0)
    		{
    			audioManager.GetComponent().QuietenMusic();
    			audioManager.GetComponent().PlaySFX(13);
    			audioManager.GetComponent().StopSFX(19);
    
    			//Start the "Remove" animation for the last card in the list, set the position to be centered and remove from the list
    			currentCards[currentCards.Count - 1].anim.SetBool("isRemoved", true);
    			currentCards[currentCards.Count - 1].gameObject.transform.SetParent(UIController.instance.reincarnationCardHolder.transform);
    			currentCards.RemoveAt(currentCards.Count - 1);
    
    			if (!PlayerController.instance.canMove)
    			{
    				PlayerController.instance.canMove = true;
    			}
    
    			RemoveCardIcon();
    
    			if (cardIcons.Count + curseCardIcons.Count == 0)
    			{
    				cardIconBackground.gameObject.SetActive(false);
    			}
    		}
    		else
    		{
    			gameOver = true;
    			Debug.Log("Game Over!");
    			GameManager.instance.GameOver();
    		}
    	}
    
    	//Adds the icon to the Inventory
    	public void AddCardIcon(ReincarnationCard newCard)
    	{
    		if (newCard.isCurseCard)
    		{
    			GameObject card = Instantiate(cardIconImage, Vector3.zero, transform.rotation, curseCardIconHolder.transform);
    			card.transform.localPosition = new Vector3(xOffset * (curseCardIcons.Count + 1), 0);
    			card.GetComponent().theCard = newCard;
    			card.GetComponent().sprite = newCard.cardRarities[newCard.cardRarity];
    			curseCardIcons.Add(card);
    			cursedCardIconBackground.sizeDelta = new Vector3(cursedCardIconBackground.sizeDelta.x + 30f, cardIconBackground.sizeDelta.y);
    		}
    		else if (!newCard.isCurseCard)
    		{
    			GameObject card = Instantiate(cardIconImage, Vector3.zero, transform.rotation, cardIconHolder.transform);
    			card.transform.localPosition = new Vector3(xOffset * (cardIcons.Count + 1), 0);
    			card.GetComponent().theCard = newCard;
    			card.GetComponent().sprite = newCard.cardRarities[newCard.cardRarity];
    			cardIcons.Add(card);
    			cardIconBackground.sizeDelta = new Vector3(cardIconBackground.sizeDelta.x + 30f, cardIconBackground.sizeDelta.y);
    		}
    		
    	}
    
    	//Removes the icon from the Inventory
    	public void RemoveCardIcon()
    	{
    		GameObject card = cardIcons[cardIcons.Count - 1];
    		cardIcons.RemoveAt(cardIcons.Count - 1);
    		Destroy(card.gameObject);
    		cardIconBackground.sizeDelta = new Vector3(cardIconBackground.sizeDelta.x - 30f, cardIconBackground.sizeDelta.y);
    	}
    
    	public void OpenInventory()
    	{
    		if (!TutorialManager.instance.isInventoryTutorialFinished)
    		{
    			inventoryTutorial.gameObject.SetActive(true);
    		}
    
    		inventoryOpen = true;
    		GameManager.instance.PauseGame();
    		fadeScreen.color = new Color(fadeScreen.color.r, fadeScreen.color.g, fadeScreen.color.b, Mathf.MoveTowards(fadeScreen.color.a, 0.64f, UIController.instance.fadeSpeed));
    		inventoryText.text = "Close Inventory";
    	}
    
    	public void CloseInventory()
    	{
    		inventoryOpen = false;
    		inventoryTutorial.gameObject.SetActive(false);
    
    		foreach (GameObject card in cardIcons)
    		{
    			card.GetComponent().OffMouseHover();
    		}
    
    		if (curseCardIcons.Count > 0)
    		{
    			foreach (GameObject card in curseCardIcons)
    			{
    				card.GetComponent().OffMouseHover();
    			}
    		}
    
    		fadeScreen.color = new Color(fadeScreen.color.r, fadeScreen.color.g, fadeScreen.color.b, Mathf.MoveTowards(fadeScreen.color.a, 0f, UIController.instance.fadeSpeed));
    
    		GameManager.instance.UnpauseGame();
    
    		inventoryText.text = "Open Inventory";
    	}
    }

Enemies

Enemy behaviour

Each Enemy class has a different set of behaviours that vary based on the conditions in gameplay. For example, Hammer enemies will destroy objects that the Player can use to damage themselves. If there are no objects to destroy, they will chase the Player instead and attempt to stun them, allowing other enemies to heal the Player.

Another example is the Maid enemy, who chases the Player to heal them when they collide. However, if the Player sets off an arrow to fly, the Maid will chase that instead, and dive into it if within close enough range, depriving the Player from potentially reincarnating. This makes surveying the room quickly a necessity, as performing an action in the wrong order can lead to big mistakes.

  • public class EnemyController : MonoBehaviour
    	{
    		[Header("Chase Player")]
    		public bool shouldChase;
    		public float chasePlayerRange;
    	
    		[Header("Sacrifice")]
    		public bool shouldSacrifice;
    		public float chaseProjectileRange, diveRange, diveSpeed;
    	
    		[Header("Shoot Projectiles")]
    		public bool shouldShoot;
    		private bool shootBreak = false;
    		public float shootSpeed, shootRange,  timeBetweenShots, shootLength, shootBreakLength;
    		private float shootTimer, shootBreakTimer, shootLengthTimer;
    	
    		public float moveSpeed, normalSpeed;
    	
    		[Header("Dismantle traps")]
    		public bool shouldDismantle;
    		public bool isMovingToDismantle, isDismantling, isSteppingBack;
    		public float dismantleLength, dismantleTimer;
    		public Animator hammerAnim;
    	
    		public Rigidbody2D theRB;
    	
    		private Vector3 moveDirection;
    	
    		public Animator anim;
    	
    		private bool isLookingRight;
    	
    		public bool isDead = false;
    	
    		public bool isSacrificing = false;
    	
    		public bool failedDive = false;
    	
    		public int health, maxHealth;
    	
    		public SpriteRenderer theSR;
    	
    		public Sprite arrowKill;
    	
    		public GameObject bullet, heartWarningEffect;
    	
    		public RoomCenter room;
    	
    		public DamageItems target;
    	
    		public Coroutine dismantleCoroutine, holdPlayerCoroutine;
    	
    		public int damageToPlayer = -10;
    	
    		public GameObject audioManager;
    	
    		public float cameraXPos, cameraTargetXPos, cameraYPos, cameraTargetYPos;
    	
    	
    		// Start is called before the first frame update
    		void Start()
    		{
    			audioManager = GameObject.Find("Audio Manager");
    			health = maxHealth;
    			shootBreakTimer = shootBreakLength;
    	
    			cameraXPos = CameraController.instance.transform.position.x;
    			cameraTargetXPos = CameraController.instance.target.transform.position.x;
    			cameraYPos = CameraController.instance.transform.position.y;
    			cameraTargetYPos = CameraController.instance.target.transform.position.y;
    		}
    	
    		// Update is called once per frame
    		void Update()
    		{
    			if (!isDead)
    			{
    				if (theSR.isVisible)
    				{
    					if (GameObject.FindObjectOfType() != null || cameraXPos == cameraTargetXPos && cameraYPos == cameraTargetYPos)
    					{
    						if (PlayerController.instance.canMove && !isDismantling)
    						{
    							moveDirection = Vector3.zero;
    						}
    	
    						//Do something based on distance from another object
    						if (shouldChase && !isSacrificing && !failedDive)
    						{
    							if (Vector3.Distance(PlayerController.instance.transform.position, transform.position) < chasePlayerRange)
    							{
    								if (shouldDismantle)
    								{
    									hammerAnim.SetBool("isChasing", true);
    								}
    	
    								moveDirection = PlayerController.instance.transform.position - transform.position;
    								moveSpeed = normalSpeed;
    							}
    							else if (shouldDismantle)
    							{
    								hammerAnim.SetBool("isChasing", false);
    							}
    						}
    	
    						if (shouldSacrifice)
    						{
    							if (GameObject.FindGameObjectWithTag("DamageProjectile") != null)
    							{
    								Projectiles arrow = GameObject.FindGameObjectWithTag("DamageProjectile").GetComponent();
    	
    								if (Vector3.Distance(arrow.sacrificeTarget.transform.position, transform.position) < chaseProjectileRange)
    								{
    									isSacrificing = true;
    	
    									if (Vector3.Distance(arrow.sacrificeTarget.transform.position, transform.position) <= diveRange)
    									{
    										anim.SetBool("isDiving", true);
    										moveSpeed = diveSpeed;
    										moveDirection = arrow.sacrificeTarget.transform.position - transform.position;
    									}
    									else
    									{
    										moveDirection = arrow.sacrificeTarget.transform.position - transform.position;
    										anim.SetBool("isDiving", false);
    										moveSpeed = normalSpeed;
    									}
    								}
    							}
    							else
    							{
    								isSacrificing = false;
    								anim.SetBool("isDiving", false);
    	
    								if (anim.GetCurrentAnimatorStateInfo(0).IsName("Enemy_Dive"))
    								{
    									failedDive = true;
    								}
    								else
    								{
    									failedDive = false;
    								}
    							}
    						}
    						else
    						{
    							theRB.velocity = Vector3.zero;
    							anim.SetBool("isDiving", false);
    							moveSpeed = normalSpeed;
    						}
    	
    						if (shouldShoot)
    						{
    							if (Vector3.Distance(PlayerController.instance.transform.position, transform.position) <= shootRange)
    							{
    								if (!shootBreak)
    								{
    									shootLengthTimer -= Time.deltaTime;
    	
    									if (shootLengthTimer <= 0)
    									{
    										shootBreak = true;
    										shootBreakTimer = shootBreakLength;
    										Instantiate(heartWarningEffect, transform.position, transform.rotation, gameObject.transform);
    										audioManager.GetComponent().PlaySFX(19);
    									}
    	
    									if (shootTimer > 0)
    									{
    										shootTimer -= Time.deltaTime;
    									}
    									else if (!shootBreak)
    									{
    										Instantiate(bullet, transform.position, transform.rotation);
    										audioManager.GetComponent().PlaySFX(17);
    	
    	
    										shootTimer = timeBetweenShots;
    									}
    								}
    								else
    								{
    									shootBreakTimer -= Time.deltaTime;
    	
    									if (shootBreakTimer <= 0)
    									{
    										shootBreak = false;
    										shootLengthTimer = shootLength;
    									}
    								}
    							}
    						}
    	
    						if (shouldDismantle)
    						{
    							if (room == null)
    							{
    								if (PlayerController.instance.canMove)
    								{
    									shouldChase = true;
    								}
    								else
    								{
    									shouldChase = false;
    									hammerAnim.SetBool("isHitting", false);
    								}
    							}
    	
    							if (target != null)
    							{
    								if (target.GetComponent().isDead)
    								{
    									int position = room.damageItems.IndexOf(target);
    									room.damageItems.RemoveAt(position);
    									Destroy(target.gameObject);
    									isMovingToDismantle = false;
    									isDismantling = false;
    	
    									if (dismantleCoroutine != null)
    									{
    										StopCoroutine(dismantleCoroutine);
    										hammerAnim.SetBool("isHitting", false);
    									}
    								}
    							}
    	
    							if (!isMovingToDismantle && !isDismantling && room != null)
    							{
    								if (room.damageItems.Count > 0)
    								{
    									foreach (DamageItems damageItem in room.damageItems)
    									{
    										if (!damageItem.isTargeted)
    										{
    											target = damageItem;
    											target.isTargeted = true;
    											isMovingToDismantle = true;
    	
    											break;
    										}
    									}
    	
    									if (!shouldChase && !isSteppingBack)
    									{
    										moveDirection = Vector3.zero;
    									}
    	
    									if (!isMovingToDismantle)
    									{
    										if (PlayerController.instance.canMove)
    										{
    											shouldChase = true;
    										}
    										else
    										{
    											shouldChase = false;
    											hammerAnim.SetBool("isHitting", false);
    										}
    									}
    								}
    								else
    								{
    									if (PlayerController.instance.canMove)
    									{
    										shouldChase = true;
    									}
    									else
    									{
    										shouldChase = false;
    										hammerAnim.SetBool("isHitting", false);
    	
    										if (!isSteppingBack)
    										{
    											moveDirection = Vector3.zero;
    										}
    									}
    								}
    							}
    							else
    							{
    								if (target != null && !isDismantling)
    								{
    									moveDirection = target.GetComponent().target.transform.position - transform.position;
    								}
    	
    								if (!isMovingToDismantle && !isDismantling && !PlayerController.instance.canMove)
    								{
    									shouldChase = false;
    									hammerAnim.SetBool("isHitting", false);
    	
    									if (!isSteppingBack)
    									{
    										moveDirection = Vector3.zero;
    									}
    								}
    							}
    	
    							if (target != null && target.GetComponent() != null)
    							{
    								if (target.GetComponent().hasShot == true)
    								{
    									target.GetComponent().isTargeted = false;
    									int position = room.damageItems.IndexOf(target);
    									room.damageItems.RemoveAt(position);
    									target = null;
    									isMovingToDismantle = false;
    									isDismantling = false;
    	
    									Debug.Log("Removed");
    	
    									if (dismantleCoroutine != null)
    									{
    										StopCoroutine(dismantleCoroutine);
    										hammerAnim.SetBool("isHitting", false);
    									}
    								}
    							}
    						}
    					}
    				}
    	
    				moveDirection.Normalize();
    	
    				theRB.velocity = moveDirection * moveSpeed;
    	
    				if (moveDirection != Vector3.zero)
    				{
    					anim.SetBool("isMoving", true);
    				}
    				else
    				{
    					anim.SetBool("isMoving", false);
    				}
    	
    				if (moveDirection.x > 0)
    				{
    					transform.localScale = new Vector3(-1f, 1f, 1f);
    					isLookingRight = true;
    				}
    				else if (moveDirection.x == 0 && isLookingRight == true)
    				{
    					transform.localScale = new Vector3(-1f, 1f, 1f);
    				}
    				else
    				{
    					transform.localScale = Vector3.one;
    					isLookingRight = false;
    				}
    			}
    			else
    			{
    				theRB.velocity = Vector3.zero;
    	
    				if (dismantleCoroutine != null)
    				{
    					StopCoroutine(dismantleCoroutine);
    					hammerAnim.SetBool("isHitting", false);
    				}
    	
    				if (holdPlayerCoroutine != null)
    				{
    					StopCoroutine(holdPlayerCoroutine);
    	
    					if (!PlayerController.instance.canMove)
    					{
    						PlayerController.instance.canMove = true;
    					}
    				}
    			}
    		}
    	
    		public IEnumerator Dismantle(float dismantleLength)
    		{
    			hammerAnim.SetBool("isHitting", true);
    			shouldChase = false;
    			moveDirection = Vector3.zero;
    			yield return new WaitForSeconds(dismantleLength);
    			hammerAnim.SetBool("isHitting", false);
    			int position = room.damageItems.IndexOf(target);
    			room.damageItems.RemoveAt(position);
    	
    			if (target.gameObject.GetComponent() != null)
    			{
    				audioManager.GetComponent().PlaySFX(21);
    			}
    	
    			if (target.gameObject.GetComponent() != null)
    			{
    				audioManager.GetComponent().PlaySFX(22);
    			}
    	
    			Destroy(target.gameObject);
    			isMovingToDismantle = false;
    			isDismantling = false;
    		}
    		public IEnumerator HoldPlayer()
    		{
    			audioManager.GetComponent().PlaySFX(24);
    			shouldChase = false;
    			hammerAnim.SetBool("isHitting", false);
    			isSteppingBack = true;
    			PlayerController.instance.canMove = false;
    			moveDirection = transform.position - PlayerController.instance.transform.position;
    			PlayerController.instance.KnockOut();
    			yield return new WaitForSeconds(1);
    			moveDirection = Vector3.zero;
    			yield return new WaitForSeconds(1);
    			PlayerController.instance.canMove = true;
    			isSteppingBack = false;
    			moveDirection = PlayerController.instance.transform.position - transform.position;
    	
    		}
    	
    		private void OnCollisionEnter2D(Collision2D other)
    		{
    			if (other.gameObject.CompareTag("Player") && !isDead)
    			{
    				if (shouldDismantle && shouldChase && !isMovingToDismantle && !other.gameObject.GetComponent().isInvincible)
    				{
    					//moveDirection = transform.position - PlayerController.instance.transform.position;
    					holdPlayerCoroutine = StartCoroutine(HoldPlayer());
    				}
    				else if (!shouldDismantle)
    				{
    					PlayerController.instance.TakeDamage(damageToPlayer - PlayerController.instance.extraHealToTake);
    					other.gameObject.GetComponent().DamagedByEnemy(true);
    				}
    			}     
    		}
    	
    		private void OnTriggerEnter2D(Collider2D other)
    		{
    			if (other.gameObject.CompareTag("DamageItem") && shouldDismantle)
    			{
    				if (target != null)
    				{
    					if (Vector3.Distance(target.transform.position, transform.position) <= 2f)
    					{
    						isDismantling = true;
    						dismantleCoroutine = StartCoroutine(Dismantle(target.dismantleTime));
    					}
    				}
    			}
    		}
    	
    		public void DamageEnemy(int damage)
    		{
    			health -= damage;
    	
    			if (health <= 0)
    			{
    				if (target != null)
    				{
    					target.GetComponent().isTargeted = false;
    				}
    				anim.SetBool("isDead", true);
    				isDead = true;
    	
    				if (shouldDismantle)
    				{
    					hammerAnim.SetBool("isHitting", false);
    					hammerAnim.SetBool("isChasing", false);
    				}
    	
    				StartCoroutine(DestroyEnemy());
    			}
    		}
    	
    		IEnumerator DestroyEnemy()
    		{
    			yield return new WaitForSeconds(1);
    			Destroy(gameObject);
    		}
    	}