Respawning enemies in the RPG course

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