Respawning enemies in the RPG course

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();
            }
        }
    }
}
1 Like

Hi Brian, first of all thank you so much for responding to my question. However, for some reason, everytime I put the script on my enemy and try assign him as the respawnable enemy, I run into a stack overflow and unity crashes completely. What might be the cause for that?

I tried it with my own script, and it failed. I tried it with yours, still the same issue. I also did delete the Saveable Entity (off my enemy holding the script) as you mentioned in the post, and I also made sure ‘SaveableEntity.cs’ has virtual methods, as you said in the post.

The script shouldn’t be on the enemy, it should be on an empty GameObject. Then assign the enemy to the spawnableEnemy field in the inspector. If the script is on the enemy itself, then it will keep spawning new enemies until the stack overflows (this is called recursion).

Hi Brian. I did as you mentioned, but the Enemy does not move now. Yes, he fights back, dies and respawns, but he does not follow the Patrol Path. Overall, if I try make the Patrol Path as a prefab and assign it as my enemy goal in the AIController, he still won’t follow it. He’ll just stand idle, wait for me to approach the distance for him to pursue and fight me, fight and then normally respawn

The function works, but now he’s not patrolling. How do I solve this, and how can we make this as a working prefab for when we want to scatter them around the map?

That wasn’t in the original question… :stuck_out_tongue:

So that will be something that needs to be added to the RespawnManager. Create a serialized field for the Patrol Path in the Respawn Manager, along with a public method in AIController to assign the Patrol Path. When the character is spawned, if there is a Patrol Path in the spawner, then it should call the new AssignPatrolPath method on the AIController.

What are the contents of the AssignPatrolPath() function in the AIController? :smiley:

Can we use the ‘PatrolBehaviour()’ function in ‘AIController.cs’ instead?

No, the PatrolBehaviour acts on the PatrolPath, and is called from AIController.Update()

Your AssignPatrolPath() method should take a PatrolPath as a parameter, and assign that PatrolPath to the AIController’s patrolPath.

Hi Brian. I tried to make some changes, but it’s still not working. Here are the changes I have done to AIController.cs and RespawnManager.cs:

This is my new ‘AssignPatrolPath()’ function in ‘AIController.cs’ (the patrolPath was a serialized variable from before, when I was still in the Core Combat course):

     public void AssignPatrolPath(PatrolPath newPatrolPath) {
        newPatrolPath = patrolPath;
    }

And here is my Respawn() function addon (over the script I wrote along with you above), at the end of the function, right before it finishes:

if (patrolPath != null) {
                spawnableEnemy.AssignPatrolPath(patrolPath);
            }

What went wrong?

Take a closer look at that assignment statement in AssignPatrolPath… remember that our goal is to make the patrolPath take on the value in newPatrolPath…

I went through what you mentioned earlier, and tried to change the return type of my PatrolPath to be a PatrolPath, but it’s still not working. Prior to that, I thought it was a value transfer error, so I tried using ‘==’ instead of ‘=’, but that failed too. Here is my function, please let me know what we can do to fix that:

public PatrolPath AssignPatrolPath(PatrolPath newPatrolPath) {

        return newPatrolPath = patrolPath;

    }

You’re going to laugh when I give you the answer…

public void AssignPatrolPath(PatrolPath newPatrolPath)
{ 
    patrolPath = newPatrolPath;
}

ok honestly, I feel dumb right now for not seeing that, but the function still does not work. I think the issue is in my Spawn Function. I would appreciate if we can look through it together to see what went wrong:

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);

            if (patrolPath != null) {

                spawnableEnemy.AssignPatrolPath(patrolPath);

            }
        }

That actually looks correct.
Let’s throw in some debugs to see what’s going on.
(p.s. I’ll probably not get to the rest of your questions on Udemy tonight…)

We did some debugging. I got a few errors (that probably have nothing to do with the debugging itself), and here are the results I got so far:

Regarding the Udemy Questions, I’m okay with waiting till tomorrow, but we might want to prioritize my loot saving system failure when we switch scenes by then

Do you have six spawners?

You only account for about 1/2 of the Udemy questions tonight… ideally I try to answer all of them within 24.

I do have 4 enemies using the same prefab on the map (and the other two were clones under the Spawner Object Script I was prototyping with, which I just deleted). I am currently testing the system on one enemy though, I think the system is trying to assign the patrolpaths for all 4 of them, but the major issue is with the one I am trying things out with

I see the issue, you need to assign the Patrol Path to the spawnedEnemy, not the spawnableEnemy (the spawnedEnemy is the instance, and should be named Enemy (clone), the spawnableEnemy is the prefab. We don’t want to assign things to that.

Ahh, is it safe to say it’s still not working? I really do apologize if I am being a burden tonight. I did the changes in the if-else statement accordingly:

if (patrolPath != null)
            {
                Debug.Log($"Assigning Patrol Path {patrolPath} to {spawnedEnemy.name}");
                spawnedEnemy.AssignPatrolPath(patrolPath);
            }
            else
            {
                Debug.Log($"No Patrol Path to assign");
            }

So… the enemy doesn’t have a PatrolPath assigned after this? What are the Debugs saying? When you look in the inspector at the Enemy (clone) when the game is running, what’s the value in PatrolPath?

If it helps in anyway, I did once ask for help to randomize the waypoints of our patrolPaths. I’m not sure how that might be of help in our context, but I’m bringing it up just in case. Here is what the Enemy (Clone) says in the Debugging sector (along with the yellow errors mentioned earlier, in case they might be of help), and the enemy does have a PatrolPath Prefab assigned to him:

Privacy & Terms