One nice feature of RPG games that we don’t cover in this course is mob respawning. In many RPG games, when you clear an area of mobs, after a short time, the mobs will disappear and eventually be respawned as nice new mobs ready for you to kill again.
This has two advantages
- It gives the player areas to grind for experince.
- It changes content from finite to infinite. Games where everything only lives once eventually run out of enemies to kill, and that’s less interaction between your players and your games.
Before we dive too deep into respawning, let’s discuss the elephant in the room, the Saving System. In our saving system, each character gets it’s own SaveableEntity component, which gathers the ISaveable information off of each component on the character and bundles it for the save file. The problem with this approach when it comes to respawning is that there could be times when the state of the enemy is… non-existant.
The solution for this is to have a RespawnManager class that is, on it’s own a SaveableEntity. This RespawnManager will handle spawning the enemy into the scene, listening for death notifications, hiding the body after a short period of time, then finally deleting the enemy and spawning in a new copy of the enemy. It also has to handle saving, by gathering the ISaveable components on the enemy rather than itself.
Let’s dive in and get started on the Respawn itself:
We’ll need a RespawnManager class:
namespace RPG.Respawnables
{
public class RespawnManager : MonoBehaviour
{
}
}
We’re going to need a few things serialized for the RespawnManager to know what to respawn and how often.
[SerializeField] private AIController spawnableEnemy;
[SerializeField] private float hideTime = 60;
[SerializeField] private float respawnTime = 90;
The spawnableEnemy is the prefab. I’m using an AIController reference to ensure that it is an enemy that we are respawning. We also need a time to hide the character after it dies, and another time for when the character should respawn. I chose fairly quick values here, 60 seconds and 90 seconds. In a real game, you’re probably going to want to make this on the order of 5-10 minutes.
You’ll need a reference to the spawnedEnemy once you spawn it into the game.
private AIController spawnedEnemy;
When the game starts, you’re going to want to spawn the enemy into the game. We’ll do this in Awake()
private void Awake()
{
Respawn();
}
Of course, we haven’t created a Respawn method yet, so we’ll do that now. There are a few things you’ll need to do when we Respawn a character. First, is destroying any old character that may exist. Second is spawning the enemy. Third is subscribing to that character’s Health.onDie() method so you know when it dies.
Let’s start out with removing any existing GameObjects.
private void Respawn()
{
foreach(Transform child in transform)
{
Destroy(child.gameObject);
}
}
We’ll be parenting our spawned characters to the RespawnManager’s Transform, so this first part simply destroys any GameObjects under our RespawnManager.
spawnedEnemy = Instantiate(spawnableEnemy, transform);
This part simply creates our enemy, and childs it to our transform. It will start at the same location as the RespawnManager.
Finally, we need to listen for the death notices.
spawnedEnemy.GetComponent<Health>().onDie.AddListener(OnDeath);
So now our Respawn method looks like this:
private void Respawn()
{
foreach (Transform child in transform)
{
Destroy(child.gameObject);
}
spawnedEnemy = Instantiate(spawnableEnemy, transform);
spawnedEnemy.GetComponent<Health>().onDie.AddListener(OnDeath);
}
Of course, we haven’t created an OnDeath method yet to handle the character dying… Let’s make that method now:
void OnDeath()
{
Invoke(nameof(Respawn), respawnTime);
}
This will call Respawn after respawnTime seconds have elapsed.
Now with just this method, the dead character will disappear at the exact moment that the character respawns, but in most games, the dead character disappears after a short time, and then later is respawned, leaving a brief period of time where no character is there.
void HideCharacter()
{
foreach(Renderer renderer in spawnedEnemy.GetComponentsInChildren<Renderer>())
{
renderer.enabled=false;
}
}
This simply hides all the renderers on the enemy. Let’s add this method to our OnDeath() handler
void OnDeath()
{
Invoke(nameof(HideCharacter), hideTime);
Invoke(nameof(Respawn), respawnTime);
}
So now we’ve cleared away our enemy, spawned in a new enemy, subscribed to the onDie event, and when that event fires, we clear the enemy and start the cycle all over.
So that’s the easy part. Now we have to deal with saving. The solution for this lies in inheriting from SaveableEntity (or JsonSaveableEntity if you’re using my Json Saving System
So first, we need to make a small tweak to SaveableEntity. Open SaveableEntity.cs and add the keyword virtual after the public in the CaptureState and RestoreState()
public virtual object CaptureState()
{
var state = new Dictionary<string, object>();
foreach (var saveable in GetComponents<ISaveable>())
state[saveable.GetType().ToString()] = saveable.CaptureState();
return state;
}
/// <summary>
/// Will restore the state that was captured by `CaptureState`.
/// </summary>
/// <param name="state">
/// The same object that was returned by `CaptureState`.
/// </param>
public virtual void RestoreState(object state)
{
var stateDict = (Dictionary<string, object>)state;
foreach (var saveable in GetComponents<ISaveable>())
{
var typeString = saveable.GetType().ToString();
if (stateDict.ContainsKey(typeString)) saveable.RestoreState(stateDict[typeString]);
}
}
What this does is tells the compiler that if a class inherits from SaveableEntity, then they can override that method and replace the logic with logic of its’ own.
Let’s head up to the header of the RespawnManager and change MonoBehaviour to SaveableEntity
public class RespawnManager : SaveableEntity
Once we’ve done this, we can override the Capture and RestoreState methods. What we’re going to do is instead of getting components on the RespawnManager to save, we’re going to get those ISaveables from the spawnedEnemy.
public override object CaptureState()
{
var state = new Dictionary<string, object>();
foreach (var saveable in spawnedEnemy.GetComponents<ISaveable>())
state[saveable.GetType().ToString()] = saveable.CaptureState();
return state;
}
public override void RestoreState(object state)
{
var stateDict = (Dictionary<string, object>)state;
foreach (var saveable in spawnedEnemy.GetComponents<ISaveable>())
{
var typeString = saveable.GetType().ToString();
if (stateDict.ContainsKey(typeString)) saveable.RestoreState(stateDict[typeString]);
}
if (spawnedEnemy.GetComponent<Health>().IsDead())
{
OnDeath();
}
}
You’ll notice that the actual meat of the methods is the same, except for iterating over spawnedEnemy.GetComponents, not GetComponents. One key difference, though, is at the end of the RestoreState() method, if the enemy is dead when we saved, then we’ll call OnDeath() to set up the next respawn.
One important thing here. On your Enemy prefabs that you’re using in the RespawnManager, you need to remove the SaveableEntity component from that enemy. The RespawnManager will be dealing with that.
Finally, I’m going to add a little validation trick so that whenever we attach a character to the spawnableEnemy (the prefab), it is automagically added to the RespawnManager during edit mode. This allows us to still construct our scene with the character visible, and then when the game starts, that dummy character is removed and replaced with our spawned character.
For this, we need to know if the enemy has changed in the inspector. For this, we’re going to add a hidden serialized property with the [HideInInspector] tag.
[HideInInspector] AIController lastSpawnableEntity;
Now our OnValidate will see if we’ve changed the spawnable character.
private void OnValidate()
{
if (spawnableEnemy != lastSpawnableEnemy)
{
lastSpawnableEnemy = spawnableEnemy;
Respawn();
}
}
OnValidate() is a special method that runs in the Editor any time something is changed in the inspector. This can be used for things like sanity checks, like you might want to reject somebody setting hideTime to greater than respawnTime, because that would cause some significiant bugs.
So here’s the completed class. Don’t forget to remove the SaveableEntity component from any enemy that you wish to make Respawnable. You’ll also need to set the level in the Enemy prefab as well.
Then make the Capture and RestoreState methods in SaveableEntity virtual, and finally, here’s our completed RespawnManager
ReSpawnManager.cs
using System.Collections.Generic;
using GameDevTV.Saving;
using RPG.Attributes;
using RPG.Control;
using UnityEngine;
namespace RPG.Respawnables
{
public class RespawnManager : SaveableEntity
{
[SerializeField] private AIController spawnableEnemy;
[HideInInspector] private AIController lastSpawnableEnemy;
[SerializeField] private float hideTime = 60;
[SerializeField] private float respawnTime = 90;
private AIController spawnedEnemy;
private void Awake()
{
Respawn();
}
private void Respawn()
{
if (spawnedEnemy)
{
spawnedEnemy.GetComponent<Health>().onDie.RemoveListener(OnDeath);
}
foreach (Transform child in transform)
{
Destroy(child.gameObject);
}
spawnedEnemy = Instantiate(spawnableEnemy, transform);
spawnedEnemy.GetComponent<Health>().onDie.AddListener(OnDeath);
}
void HideCharacter()
{
foreach (Renderer renderer in spawnedEnemy.GetComponentsInChildren<Renderer>())
{
renderer.enabled = false;
}
}
void OnDeath()
{
Invoke(nameof(HideCharacter), hideTime);
Invoke(nameof(Respawn), respawnTime);
}
public override object CaptureState()
{
var state = new Dictionary<string, object>();
foreach (var saveable in spawnedEnemy.GetComponents<ISaveable>())
state[saveable.GetType().ToString()] = saveable.CaptureState();
return state;
}
public override void RestoreState(object state)
{
var stateDict = (Dictionary<string, object>)state;
foreach (var saveable in spawnedEnemy.GetComponents<ISaveable>())
{
var typeString = saveable.GetType().ToString();
if (stateDict.ContainsKey(typeString)) saveable.RestoreState(stateDict[typeString]);
}
if (spawnedEnemy.GetComponent<Health>().IsDead())
{
OnDeath();
}
}
private void OnValidate()
{
if (spawnableEnemy != lastSpawnableEnemy)
{
lastSpawnableEnemy = spawnableEnemy;
Respawn();
}
}
}
}