Respawning enemies in the RPG course

My “fix” is to

  1. Convert the Awake() to a Start. Same code otherwise.
        private void Start()
        {
            Respawn();
        }
  1. Remove OnValidate()

This fix seems to work but it has some behaviour I am confused by.

When I start a play mode session with no child objects and then exit playmode, the SpawnableEnemy it spawns is retained as a child object even after play mode ends. Why does it not disappear at the end of play mode (like a spawned weapon or spawned fireball?). Again I’m starting off with no child objects in edit mode so I don’t understand why something created during run time persists when I exit play mode?

EDIT: It seems what’s happening is that the Start method is being called when I exit playmode which is triggering a Respawn. Ok but why is start being called? Is it because we’re inheriting from SaveableEntity which has this [ExecuteAlways] at the top?

I’ll note that I still get this error right when I exit play mode even after my “fix” which seems to provide evidence that Start is being called when I exit play mode.

Destroy may not be called from edit mode! Use DestroyImmediate instead.
Destroying an object in edit mode destroys it permanently.
UnityEngine.Object:Destroy (UnityEngine.Object)
RPG.Respawnables.RespawnManager:Respawn () (at Assets/Scripts/Respawnables/RespawnManager.cs:69)
RPG.Respawnables.RespawnManager:Start () (at Assets/Scripts/Respawnables/RespawnManager.cs:33)

I’m now looking into this Procedurally Spawned Characters as a possible solution.

This no longer works in Unity 2022 and later. I learned this the hard way when I moved SpellbornHunter from Unity 2021 to Unity 2022.

The whole trick was designed so that you could change the prefab and then it would automagically spawn the cloned character as a place holder. This addresses pretty much your entire thread of issues. You simply can’t use this OnValidate logic at all.

What you can do is in Start(), you can destroy everything under the spawner and then spawn the character. This means, of course, though, that you’ll have to add the preview character manually.

Thanks. What explains Start getting called after I exit play mode? Is it my guess of having [ExecuteAlways] on the class we inherit from?

Assuming it’s that, then I will resolve this.

I think my path will be to use the “Procedurally Spawned Characters” solution you created. I tried it and added a reply to your post on a few places I got stuck.

See what happens when you pull ExecuteAlways…

There is actually another flaw in the ointment… The character isn’t spawned until Start() but if you call RestoreState() you’ll get an NRE… (Was just covering this with another student on Udemy)

The solution:

bool hasBeenRestored;
void Start()
{
    if(!hasBeenRestored) Respawn();
}

And as the first line in RestoreState()

Respawn();
hasBeenRestored=true;

What did you mean by “pull”. As is use [ExecuteAlways] or remove [ExecuteAlways]. I think you meant the former because I didn’t remove it.

I was uncomfortable with [ExecuteAlways] and how we’re generating GUIDs from the moment I saw it :rofl: :rofl: :rofl: I was totally expecting something like this thread would happen at some point in the not too distant future.

Pull as in remove it… American English for “Pull that out of the code”
I don’t recall putting it in the code in the first place.

OnValidate does not require ExecuteAlways. It’s only really useful for Update() type setups (sort of like what Sam did in SaveableEntity.) It’s actually more efficient to just let OnValidate() generate a UUID.

Ah I misinterpreted your comment because I didn’t have it on my derived class. I didn’t think of modifying the base class. When I tested with it off the base class and it the Start behavior function as expected. But then I have to add it back to make the rest of the game work. I think the solution will be using your RPG.Dynamic stuff – working through your feedback there. Thanks!

Hey again Brian. This is not a problem (believe it or not, my life cycle is slowly coming to an end… but I have a major challenge coming up, kinda like the final boss of all of this), just a quick check-up on whether what I’m doing is right or wrong. I recently started trying to figure out how to re-do this tutorial for my third person integration, and whilst most of the issues were simply solved by changing ‘AIController’ data types to ‘EnemyStateMachine’ (I think… not sure yet, there’s a plethora of testing that I haven’t done yet), one of them stood out as a unique problem

Assigning PatrolPath was a serious pain, mainly because… well… the function is in our deleted ‘AIController.cs’ (which I ended up placing back on my NPCs and deactivating, because of a stored value in there I couldn’t be bothered hunting down). So what I did instead, was before assigning the enemy to his patrolling state, I assigned him a patrol path function that I created in ‘EnemyStateMachine.cs’, and that path is gained from the ‘EnemyStateMachine.cs’ script in the inspector itself, something this simple (in ‘EnemyStateMachine.cs’):

    // Test function
    public void AssignPatrolPath(PatrolPath newPatrolPath) 
    {
        PatrolPath = newPatrolPath;
    }

and in ‘RespawnManager.cs’, this is what it looks like:

if (patrolPath != null)
            
            {

                Debug.Log($"Assigning Patrol Path {patrolPath} to {spawnedEnemy.name}");

                // spawnedEnemy.AssignPatrolPath(patrolPath); // Swapped to state machine on next line

                // TEST
                spawnedEnemy.AssignPatrolPath(patrolPath);
                // Switching to Patrolling State
                spawnedEnemy.SwitchState(new EnemyPatrolState(spawnedEnemy));

                // Update();
            }

It works, just checking that it won’t cause any unexpected issues… right? (although if we can integrate a random waiting time between patrol points again, that would be nice)

This should work fine, although as long as you’re in the same frame, simply assigning the Patrol Path should cause the IdleState to pick up that there is a patrol path on enter and call the PatrolState (IdleState is sort of a dispatcher state in that regard)

any better alternatives? :stuck_out_tongue_winking_eye:

OK so… I have a bit of a weird issue with this one. Here’s the problem:

In very simple terms, if my enemy respawns when I’m in Time.TimeScale = 0, and then I re-run my game’s time, he waits for a second and then permanently disappears unless I save and quit the game and then return. Why is this the case? Here’s what my code currently looks like:

using System.Collections.Generic;
using GameDevTV.Saving;
using RPG.Control;
using UnityEngine;
using RPG.Combat;
using Newtonsoft.Json.Linq;
using RPG.States.Enemies;
using System.Collections;
using System;

namespace RPG.Respawnables
{
    [RequireComponent(typeof(JSONSaveableEntity))]
    public class RespawnManager : MonoBehaviour, IJsonSaveable
    {
        [SerializeField] EnemyStateMachine spawnableEnemy;  // prefab of the enemy (was 'AIController.cs' type previously)

        [SerializeField] private float hideTime;   // time before hiding our dead character
        [SerializeField] private float respawnTime;    // time before respawning our hidden dead character, as another alive, respawned character
        [SerializeField] PatrolPath patrolPath; // the path our character will follow, from his spawn point
        [SerializeField] AggroGroup aggroGroup; // aggrevated group of guards, based on wrong dialogue player has said
        [SerializeField] bool hasBeenRestored;  // checks if the enemy has been restored before fading in from the main menu, or a load scene, or not

        // private AIController spawnedEnemy; // changed to EnemyStateMachine type below
        // private EnemyStateMachine spawnedEnemy; // in-game instance of the enemy

        // TEST (TimeKeeper)
        private double destroyedTime;
        private TimeKeeper timeKeeper;

        // --------------------------- NOTE: RestoreState() occurs BEFORE Start(), hence we need to change Start() to Awake() --------------
        private void Awake()
        {
            // TEST
            timeKeeper = TimeKeeper.GetTimeKeeper();

            // Check if the Enemy has been restored first or not, prior to Respawning him (ensuring RestoreState(), which occurs first, works properly)
            if (!hasBeenRestored) Respawn();
        }
        // --------------------------------------------------------------------------------------------------------------------------------

        private void Respawn()
        {
            var spawnedEnemy = GetSpawnedEnemy();
            if (spawnedEnemy)
            {
                // Dude is not dead no longer, so delete his previous 'onDeath' record after he's respawned
                spawnedEnemy.Health.onDie.RemoveListener(OnDeath);
            }

            foreach (Transform child in transform)
            {
                // Start the Respawn by deleting any existing gameObjects
                Destroy(child.gameObject);
            }

            // Respawn the enemy, and parent the enemy to our respawnManagers' transform
            spawnedEnemy = Instantiate(spawnableEnemy, transform);

            // Get the spawned/respawned enemies' health, and listen for death notifications
            spawnedEnemy.Health.onDie.AddListener(OnDeath);

            if (patrolPath != null)
            {
                Debug.Log($"Assigning Patrol Path {patrolPath} to {spawnedEnemy.name}");
                // spawnedEnemy.AssignPatrolPath(patrolPath); // Swapped to state machine on next line
                spawnedEnemy.AssignPatrolPath(patrolPath);
                spawnedEnemy.SwitchState(new EnemyPatrolState(spawnedEnemy));
            }
            else
            {
                Debug.Log($"No Patrol Path to assign");
            }
            // --------------------------- Extra Functionality: Setting up Aggro Group + Adding Fighters ---------------
            if (aggroGroup != null)
            {
                aggroGroup.AddFighterToGroup(spawnedEnemy.Fighter);
                if (spawnedEnemy.TryGetComponent(out DialogueAggro dialogueAggro)) //aggrogroup is at this point valid
                {
                    dialogueAggro.SetAggroGroup(aggroGroup);
                }
            }
            // ---------------------------------------------------------------------------------------------------------
        }

        void HideCharacter()
        {
            var spawnedEnemy = GetSpawnedEnemy();
            if (spawnedEnemy == null) return;
            spawnedEnemy.transform.SetParent(null);
            Destroy(spawnedEnemy.gameObject);
        }

        void OnDeath()
        {
            var spawnedEnemy = GetSpawnedEnemy();
            spawnedEnemy.Health.onDie.RemoveListener(OnDeath);

            // TEST (Uncomment hide and respawn functions below if failed):
            Invoke(nameof(HideCharacter), hideTime); // don't need to convert 'HideCharacter' to an IEnumerable, it won't take long for it to work anyway...

            destroyedTime = timeKeeper.GetGlobalTime();
            StartCoroutine(WaitAndRespawn());

            if (aggroGroup != null)
            {
                aggroGroup.RemoveFighterFromGroup(spawnedEnemy.Fighter);
            }
        }

        // TEST Function
        private IEnumerator WaitAndRespawn()
        {
            var elapsedTime = (float)(timeKeeper.GetGlobalTime() - destroyedTime);
            yield return new WaitForSecondsRealtime(respawnTime - elapsedTime);
            Respawn();
        }

        // TEST Boolean
        private bool IsDead()
        {
            var spawnedEnemy = GetSpawnedEnemy();
            return spawnedEnemy == null || spawnedEnemy.Health.IsDead();
        }

        // TEST Enemy getter
        private EnemyStateMachine GetSpawnedEnemy()
        {
            return GetComponentInChildren<EnemyStateMachine>();
        }

        public JToken CaptureAsJToken()
        {
            JObject state = new JObject();
            IDictionary<string, JToken> stateDict = state;

            // TEST (Adding data to the JObject Dictionary):
            var isDead = IsDead();
            var data = new RespawnData(destroyedTime, isDead);
            stateDict["RespawnData"] = JToken.FromObject(data);

            // we only care about data of alive enemies
            if (!isDead)
            {
                var spawnedEnemy = GetSpawnedEnemy();
                foreach (IJsonSaveable JSONSaveable in spawnedEnemy.GetComponents<IJsonSaveable>())
                {
                    JToken token = JSONSaveable.CaptureAsJToken();
                    string component = JSONSaveable.GetType().ToString();
                    Debug.Log($"{name} Capture {component} = {token.ToString()}");
                    stateDict[component] = token;
                }
            }
            return state;
        }

        public void RestoreFromJToken(JToken s)
        {
            JObject state = s.ToObject<JObject>();
            IDictionary<string, JToken> stateDict = state;

            var data = default(RespawnData);
            if (stateDict.TryGetValue("RespawnData", out var dataToken))
            {
                data = dataToken.ToObject<RespawnData>();
            }

            var isDead = data.IsDead;
            destroyedTime = data.DestroyedTime;

            // Should be dead
            if (isDead && !IsDead())
            {
                Debug.Log("Should be dead, but isn't...");
                var spawnedEnemy = GetSpawnedEnemy();
                Debug.Log($"Listeners before: {spawnedEnemy.Health.onDie.GetPersistentEventCount()}");
                spawnedEnemy.Health.onDie.RemoveListener(OnDeath);
                Debug.Log($"Listeners after: {spawnedEnemy.Health.onDie.GetPersistentEventCount()}");
                Debug.Log($"Health Before: {spawnedEnemy.Health.GetHealthPoints()}");
                spawnedEnemy.Health.Kill();
                Debug.Log($"Health After: {spawnedEnemy.Health.GetHealthPoints()}");
                StartCoroutine(WaitAndRespawn());
                if (aggroGroup != null)
                {
                    aggroGroup.RemoveFighterFromGroup(spawnedEnemy.Fighter);
                }
                HideCharacter();
                Debug.Log($"Spawned Enemy: {GetSpawnedEnemy()}");
            }
            else if (isDead && IsDead())
            {
                Debug.Log("Should be dead, and is indeed dead...");
                StopAllCoroutines();
                StartCoroutine(WaitAndRespawn());
                HideCharacter();
            }
            // Should be alive
            else if (!isDead && IsDead())
            {
                Debug.Log("Shouldn't be dead, but is dead...");
                Respawn();
                LoadEnemyState(stateDict);
            }
            else
            {
                Debug.Log("Shouldn't be dead, and isn't dead...");
                LoadEnemyState(stateDict);
            }
        }

        private void LoadEnemyState(IDictionary<string, JToken> stateDict)
        {
            var spawnedEnemy = GetSpawnedEnemy();
            foreach (IJsonSaveable jsonSaveable in spawnedEnemy.GetComponents<IJsonSaveable>())
            {
                string component = jsonSaveable.GetType().ToString();
                if (stateDict.ContainsKey(component))
                {
                    // NORMAL CODE (Don't delete if test failed):
                    Debug.Log($"{name} Restore {component} => {stateDict[component].ToString()}");
                    jsonSaveable.RestoreFromJToken(stateDict[component]);
                }
            }
        }
    }

    [Serializable]
    public struct RespawnData
    {
        public bool IsDead;
        public double DestroyedTime;

        public RespawnData(double destroyedTime, bool isDead)
        {
            IsDead = isDead;
            DestroyedTime = destroyedTime;
        }
    }
}

Sorry about this again :slight_smile: (@bixarrio you also may want to have a look at this)

Time.TimeScale=0 and WaitForSecondsRealtime don’t mix very well…

No guarantees, but try this:

        private IEnumerator WaitAndRespawn()
        {
            var elapsedTime = (float)(timeKeeper.GetGlobalTime() - destroyedTime);
            yield return new WaitForSecondsRealtime(respawnTime - elapsedTime);
            while(Time.TimeScale==0)
            {
                 yield return null;
            }
            Respawn();
        }

mmm… nope, he still vanishes if he reloads and I’m pausing my game, and this one has been a bug for quite a bit :confused: - any other ideas? Maybe we can eliminate the RealTime solution, it was something I quickly thought of back then for a hot fix, unknowing its issues

Yesterday I tampered with unscaled time to try fix this and failed again. Let’s just say… I need help with this one :sweat_smile: (it’s an obvious bug unfortunately, happens 100% of the time)

That would generally be my suggestion… Unless you’re running a client-server based game (i.e. multiplayer), you’re generally better off not messing with Realtime.

The only problem I currently have is that if I use ‘WaitForSeconds’ instead, the respawner works on local time, then that means the enemy timer won’t countdown, and he won’t respawn unless the player is not paused… not exactly how I want things to work for me :sweat_smile: - if you have any updates about this, please let me know :slight_smile:

But respawning while the player is paused is breaking the character… so…

so make the time so big that nobody notices this flaw exists :stuck_out_tongue: (hopefully they don’t)

Anyway, with this out the way for now, please check the comments under “Merging the Third Person to RPG Course”. I can’t seem to properly throttle the player, and I’d like to control how much time he has between each attack :slight_smile:

oop and before I almost forget, can we at least get the timer for the hideTime to keep on counting regardless of whether the game is paused or not? like, hide him as normal, and if the timer runs out, don’t respawn until the player is back

You could use a DateTime…

Here’s an example:

            DateTime dateTime = DateTime.Now.AddMinutes(5);
            if (dateTime > DateTime.Now)
            {
                DoSomething;
            }

This means you’d need to refactor from a coroutine to checking within an Update loop, using a bool to determine that you’re waiting to respawn (and comparing when that bool is true). Performance-wise, this is abysmal compared to a coroutine, but it is what it is. Nope, not writing it, this is your challenge.

OK OK I get it, it’s my challenge for the day (this will take me a while to fully grasp before I know what I’m doing…)

In the meanwhile, can we please address the issue of the player being able to throttle his attacks? We’ll come back to this issue tomorrow :slight_smile: - this is where I’m stuck (and what’s next on the roadmap?)

Privacy & Terms