RespawnManager.cs to EnemyRespawnState.cs

heya folks, welcome to another episode of ‘Bahaa and his programming issues’

In today’s problem, we got an interesting one. I’m trying to convert what I learned from this tutorial (it’s something I asked for, over a year ago) to a state machine, to match the new third person state machine system

After my last update of that script well over 6 months ago, this is what I ended up with (P.S: Ignore anything relative to the ‘EnemyStateMachine.cs’, anything with datatype ‘AIController.cs’ was working perfectly fine…):

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

namespace RPG.Respawnables
{

    // Since SaveableEntity relies on the ISaveable information of each character to
    // bundle it into our Save file, there are cases when our enemies' state is non-existant
    // hence we need to create a brand new RespawnManager class, as shown below:

    public class RespawnManager : SaveableEntity, IJsonSaveable //, ISaveable in Saving below, we will have to replace the logic, hence we inherit from 'SaveableEntity.cs'
    
    {

        // This class will handle the following:

        // spawning our enemy to the scene
        // listen to death notifications (i.e: Enemy is dead, proceed to keeping him dead for the following steps)
        // hide the body after 'hideTime'
        // deleting the enemy after 'hideTime' is complete
        // Respawn the enemy after 'respawnTime'
    
        // [SerializeField] AIController spawnableEnemy;   // the enemy to spawn/respawn
        // [HideInInspector] private AIController lastSpawnableEnemy;  // this AI Controller ensures its an enemy we are respawning
        
        // Changing 'AIController.cs' above to State Machines:
        [SerializeField] EnemyStateMachine spawnableEnemy;
        [HideInInspector] private AIController lastSpawnableEnemy;
        
        [SerializeField] private float hideTime = 60;   // time before hiding our dead character
        [SerializeField] private float respawnTime = 90;    // time before respawning our hidden dead character, as an alive 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

        // TEST Variables: Delete if failed
        /* [SerializeField] AggroGroup aggroGroup; // the group of enemies we want attacking our player, if things go bad
        [SerializeField] DialogueAggro dialogueAggro; */

        // private AIController spawnedEnemy; // changed to EnemyStateMachine type below
        private EnemyStateMachine spawnedEnemy;

        // TEST CODE:
        private bool isConversant;
        private PlayerConversant playerConversant;

        /* void Awake() {

            playerConversant = playerConversant.GetComponent<PlayerConversant>();
            isConversant = TryGetComponent(out AIConversant conversant);

        } */

        // --------------------------- NOTE: RestoreState() occurs BEFORE Start(), hence we need to ensure everything works accordingly --------------

        private void Start()
        {
            // 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()
        {

            if (spawnedEnemy)
            {

                // Dude is not dead no longer, so delete his previous 'onDeath' record after he's respawned
                spawnedEnemy.GetComponent<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.GetComponent<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.SwitchState(new EnemyPatrolState(spawnedEnemy));

                // Update();
            }

            else
            
            {
                Debug.Log($"No Patrol Path to assign");
            }

            // --------------------------- Extra Functionality: Setting up Aggro Group + Adding Fighters ---------------

            // First add the fighters, and then the Dialogue Guard (if there's one), because doing the opposite
            // means that we will not be adding fighters if there' no dialogue guard (which is not something we
            // want because we need to prioritize fighters over dialogue)

            if (aggroGroup != null)
            {
                aggroGroup.AddFighterToGroup(spawnedEnemy.GetComponent<Fighter>());
                if (spawnedEnemy.TryGetComponent(out DialogueAggro dialogueAggro)) //aggrogroup is at this point valid
                {
                    dialogueAggro.SetAggroGroup(aggroGroup);
                }
            }

            // ---------------------------------------------------------------------------------------------------------

        }

        void HideCharacter()
        {
            // Hide the dead character
            foreach (Renderer renderer in spawnedEnemy.GetComponentsInChildren<Renderer>())
            {
                renderer.enabled = false;
            }
        }

        void OnDeath() {

            // hide the character after 'hideTime', and then respawn him after 'respawnTime'
            Invoke(nameof(HideCharacter), hideTime);
            Invoke(nameof(Respawn), respawnTime);
            
            // Test (Delete if failed): disable the spawnableEnemy, until he comes back to life
            // spawnableEnemy.enabled = false;

            // TEST: Delete if failed
            if (aggroGroup != null) {

                aggroGroup.RemoveFighterFromGroup(spawnedEnemy.GetComponent<Fighter>());

            }

            // if he quit the game and returned, deactivate the dead dude until he can respawn

        }

        public JToken CaptureAsJToken()
        {

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

            foreach (IJsonSaveable JSONSaveable in spawnedEnemy.GetComponents<IJsonSaveable>())
            {
                JToken token = JSONSaveable.CaptureAsJToken();
                string component = JSONSaveable.GetType().ToString();
                Debug.Log($"{name} Capture {component} = {token.ToString()}");
                stateDict[JSONSaveable.GetType().ToString()] = token;
            }

            return state;

        }

        public void RestoreFromJToken(JToken s)
        {

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

            foreach (IJsonSaveable jsonSaveable in spawnedEnemy.GetComponents<IJsonSaveable>())
            {

                string component = jsonSaveable.GetType().ToString();
                if (stateDict.ContainsKey(component))
                {

                    Debug.Log($"{name} Restore {component} => {stateDict[component].ToString()}");
                    jsonSaveable.RestoreFromJToken(stateDict[component]);

                }

            }

        }

        /* private void OnValidate()
        {

            // This function checks if we changed the Spawnable Character
            // (ensuring any character we attach to the spawnableEnemy is automatically added
            // to the RespawnManager when editing the game, 
            // hence we dont accidentally spawn the wrong dude at the right spot)

            if (spawnableEnemy != lastSpawnableEnemy)
            {
                lastSpawnableEnemy = spawnableEnemy;
                
                foreach (Transform child in transform) {

                    Destroy(child.gameObject);

                }

                Instantiate(spawnableEnemy, transform);

            }
        } */
    }
}



// THE ARCHER AND ENEMY 2 ARE BOTH CHILDREN OF THE 'CHARACTER' PREFAB. DISCONNECT THEM FROM THAT, OTHERWISE THEY'LL KEEP INSTANTIATING
// FROM UNDER THE QUEST GIVER, WHO IS ALSO A PREFAB FROM THE CHARACTER PREFAB!!!

Over the past few hours, I tried converting it to a State Machine, but because it’s inheriting from an EnemyBaseState instead of a State Machine, this is… a bit of a headache:

using RPG.Attributes;
using RPG.Core;
using UnityEngine;

namespace RPG.States.Enemies {

public class EnemyRespawnState : EnemyBaseState
{

    private EnemyStateMachine spawnedEnemy;

    public float hideTime;  // time before the character hides off the game scene
    public float respawnTime;   // time before the character respawns as a new alive enemy to fight with
    // we have a patrol path in 'EnemyStateMachine.cs'


    public EnemyRespawnState(EnemyStateMachine stateMachine, float hideTime, float respawnTime) : base(stateMachine)
    {
        this.hideTime = hideTime;
        this.respawnTime = respawnTime;
    }

    public override void Enter() {}

    public override void Tick(float deltaTime)
    {
        hideTime -= deltaTime;

        if (hideTime <= 0f) 
        {
            HideCharacter();
            stateMachine.SwitchState(new EnemyDeathState(stateMachine));
            respawnTime -= deltaTime;
        }

        if (respawnTime <= 0f) 
        {
            Respawn();
            stateMachine.SwitchState(new EnemyPatrolState(stateMachine));
        }
        else respawnTime -= deltaTime;

    }

    public override void Exit() {}

    private void HideCharacter() 
    {
        foreach (Renderer renderer in stateMachine.gameObject.GetComponentsInChildren<Renderer>()) 
        {
            renderer.enabled = false;
        }
    }

    private void Respawn() 
    {
        if (spawnedEnemy != null) 
        {
            spawnedEnemy.GetComponent<Health>().onDie.RemoveListener(OnDeath); // he's dead, don't need to listen for his death anymore...
        }

        foreach (Transform child in stateMachine.transform) 
        {
            // find a way to destroy the dead enemy without having to use the 'Destroy()' function
            GameObject.Destroy(child.gameObject);
        }

        // Respawn the enemy, and parent it to the respawnManager's transform:
        // spawnedEnemy = GameObject.Instantiate(stateMachine.SpawnableEnemy, stateMachine.transform)
        // spawnedEnemy = spawnedEnemy.GetComponent<EnemyStateMachine>();

            // He's alive again, start listening for his death all over again...
            spawnedEnemy.GetComponent<Health>().onDie.AddListener(OnDeath);

        // Assign possible Patrol Path
        if (stateMachine.PatrolPath != null) 
        {
            spawnedEnemy.SwitchState(new EnemyPatrolState(spawnedEnemy));
        }

        // AggroGroup logic here... (will need reworking down the line, so it's empty for now)
    
    }

    private void OnDeath() 
    {
        // Find a way to invoke both 'HideCharacter' and 'Respawn' functions here,
        // and then remove your fighter from 'AggroGroup.cs' (below is usually how it would go):
        // Invoke(nameof(HideCharacter), hideTime);
        // Invoke(nameof(Respawn), respawnTime);
    }

}

}

Can someone kindly have a look at this, and update me on how we can get that missing script to work? (Don’t need the code, but at least some guidance of how this should work would be nice, and I’ll do my best to try figure out programming it myself… above all else, can you integrate the saving system into a State Machine? The original script has it, don’t want to mess that up)

Bahaa, I don’t think you should make a State Machine out of RespawnManager. Reread the intro to the post you linked. The respawn manager needs to be saveable by its self. Think about the player. It is a savable entity, not the player state machine. State machines handle the frame by fame action. When you pause and save, the value of variables is what is saved. Health.cs does not know if you were in free look state or targeting when the save button is pressed.

1 Like

so… they don’t have saveable entities, which means that we can’t re-write that into a State Machine… Got it

I’ll probably try figure out, based on what Brian last told me, a solution to integrate state machines then into my current ‘RespawnManager.cs’… This sounds like the best way to move forward

Edit: apparently just swapping out ‘AIController.cs’ to ‘EnemyStateMachine.cs’ somehow was all I needed to do… but now I have a new NRE relevant to pickups (I think me and bixarrio dealt with something similar yesterday, but that was for parented objects):

MissingReferenceException: The object of type 'PickupTarget' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
RPG.Core.RangeFinder`1[T].FindNearestTarget () (at Assets/Project Backup/Scripts/Core/RangeFinder.cs:56)
RPG.States.Player.PlayerFreeLookState.InputReader_HandlePickupEvent () (at Assets/Project Backup/Scripts/State Machines/Player/PlayerFreeLookState.cs:100)
RPG.InputReading.InputReader.OnPickup (UnityEngine.InputSystem.InputAction+CallbackContext context) (at Assets/Project Backup/Scripts/Input Controls/InputReader.cs:145)
UnityEngine.InputSystem.Utilities.DelegateHelpers.InvokeCallbacksSafe[TValue] (UnityEngine.InputSystem.Utilities.CallbackArray`1[System.Action`1[TValue]]& callbacks, TValue argument, System.String callbackName, System.Object context) (at Library/PackageCache/com.unity.inputsystem@1.7.0/InputSystem/Utilities/DelegateHelpers.cs:46)
UnityEngine.InputSystem.LowLevel.<>c__DisplayClass7_0:<set_onUpdate>b__0(NativeInputUpdateType, NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate(NativeInputUpdateType, IntPtr)

MissingReferenceException while executing 'performed' callbacks of 'Player/Pickup[/Keyboard/p]'
UnityEngine.InputSystem.LowLevel.NativeInputRuntime/<>c__DisplayClass7_0:<set_onUpdate>b__0 (UnityEngineInternal.Input.NativeInputUpdateType,UnityEngineInternal.Input.NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate (UnityEngineInternal.Input.NativeInputUpdateType,intptr)

It’s an occasional bug (I could’ve sworn I saw something incredibly similar when working on the resource gathering 2 days ago with Brian… and it has a lot to do with added and removed targets for the range finder), but I’ll slowly investigate where this is coming from

Edit: I found the solution here, but that was already implemented, as shown below (in ‘PickupTarget.cs’ and ‘PickupFinder.cs’):

PickupTarget.cs:

using GameDevTV.Inventories;
using RPG.Core;
using UnityEngine;

namespace RPG.Inventories 
{

    public class PickupTarget : MonoBehaviour, ITarget 
    {

        Pickup pickup;
        private Inventory inventory;

        public event System.Action<PickupTarget> OnPickedUp;

        private void Awake() 
        {
            pickup = GetComponent<Pickup>();
            inventory = Inventory.GetPlayerInventory();
        }

        public bool IsValid() 
        {
            return inventory.HasSpaceFor(pickup.GetItem());
        }

        public void PickupItem() 
        {
            OnPickedUp?.Invoke(this);
            pickup.PickupItem();
        }

    }

}

PickupFinder.cs:

using System.Linq;
using RPG.Core;
using UnityEngine;

namespace RPG.Inventories 
{
    public class PickupFinder : RangeFinder<PickupTarget> 
    {

        public PickupTarget GetNearestPickup() 
        {
            CurrentTarget = Targets.OrderBy(t => Vector3.Distance(transform.position, t.transform.position)).FirstOrDefault();
            return CurrentTarget;
        }

        protected override void AddTarget(PickupTarget target)
        {
            base.AddTarget(target);
            target.OnPickedUp += RemoveTarget;
            Debug.Log($"Pickup Finder: Adding {target.name}");
        }

        protected override void RemoveTarget(PickupTarget target)
        {
            base.RemoveTarget(target);
            target.OnPickedUp -= RemoveTarget;
            Debug.Log($"Pickup Finder: Removing {target.name}");
        }

    }

}

something else is wrong (THAT, and considering that this is a local timer and I got a timekeeper, may as well use that to go global timer as well for the respawn times, so they respawn the enemies indepdendent of the game time (my first challenge right now is even finding where the enumerable responsible for waiting the time out before respawning even is… I think I have an idea of what to do right after that)… ANOTHER programming challenge :slight_smile:)

The only thing you should have to do in the RespawnManager is to set the PatrolPath on the newly spawned character’s statemachine. IdleState will detect the PatrolPath and switch appropriately. Everything else should be the same.

In terms of saving information from the states, this should largely be unnecessary, but I intentionally made the Blackboards easily convertible to and from JTokens, so you could make the EnemyStateMachine an IJsonSaveable and return the Blackboard.GetData(), and using new Blackboard(state) to restore the Blackboard.

1 Like

so this works…? I mean I found it to be working quite well tbh (sorry I’m jumping around everywhere)

If it does, I will skip to the part where I convert the timer to a global one (apparently being able to save and quit the game and return to have an enemy respawn before its time is a terrible idea for me, and I want it to spawn dependent on realTime (hence the use of ‘WaitForSecondsRealtime()’ in ‘ResourceRespawner.WaitAndRespawn()’ below), but because I am referring a lot to what bixarrio helped me create as a ‘ResourceRespawner.cs’ (check the script below), re-writing that system sounds is quite the challenge for me:

using System;
using System.Collections;
using GameDevTV.Saving;
using Newtonsoft.Json.Linq;
using UnityEngine;

// Advanced Resource Respawning System, for the Third Person Implementation

namespace RPG.ResourceManager {

[RequireComponent(typeof(JSONSaveableEntity))]
public class ResourceRespawner : MonoBehaviour, IJsonSaveable
{
    [SerializeField] ResourceGathering resourceToSpawn;
    [SerializeField] int hideTime;

    private double destroyedTime;
    private TimeKeeper timeKeeper;

    private void Awake()
    {
        timeKeeper = TimeKeeper.GetTimeKeeper();
        SpawnResource();
    }

    public ResourceGathering GetResource()
    {
        return GetComponentInChildren<ResourceGathering>();
    }

    public bool IsDestroyed()
    {
        return GetResource() == null;
    }

    public int GetQuantityLeft() 
    {
        if (GetResource() == null) return 0;
        return GetResource().GetQuantityLeft();
    }

    private void SpawnResource()
    {
        var resourceObject = Instantiate(resourceToSpawn, transform.position, Quaternion.identity);
        resourceObject.transform.SetParent(transform);
        resourceObject.OnResourceDestroyed += OnResourceDestroyed;
    }

    private IEnumerator WaitAndRespawn()
    {
        var elapsedTime = (float)(timeKeeper.GetGlobalTime() - destroyedTime);
        // yield return new WaitForSeconds(hideTime - elapsedTime);
        yield return new WaitForSecondsRealtime(hideTime - elapsedTime);
        SpawnResource();
    }

    private void OnResourceDestroyed(ResourceGathering resourceNode)
    {
        resourceNode.OnResourceDestroyed -= OnResourceDestroyed;
        destroyedTime = timeKeeper.GetGlobalTime();
        StartCoroutine(WaitAndRespawn());
    }

    public JToken CaptureAsJToken()
    {
        var data = new ResourceData(destroyedTime, IsDestroyed(), GetQuantityLeft());
        return JToken.FromObject(data);
    }

    public void RestoreFromJToken(JToken state)
    {
        var data = state.ToObject<ResourceData>();
        destroyedTime = data.DestroyedTime;

        var shouldBeDestroyed = data.ShouldBeDestroyed;
        var quantityLeft = data.QuantityLeft;

        if (shouldBeDestroyed && !IsDestroyed())
        {
            // Should be destroyed, but isn't... destroy it
            var resourceObject = GetResource();
            resourceObject.OnResourceDestroyed -= OnResourceDestroyed;
            resourceObject.DestroyResource();
            StartCoroutine(WaitAndRespawn());
        }
        else if (!shouldBeDestroyed && IsDestroyed())
        {
            // Shouldn't be destroyed, but it is... spawn it
            SpawnResource();
            GetResource().quantityLeft = quantityLeft;
        }
        else if (shouldBeDestroyed && IsDestroyed()) 
        {
            // Should be destroyed, and is indeed destroyed... reset the timer
            StopAllCoroutines();
            StartCoroutine(WaitAndRespawn());
        }
        else 
        {
            // Shouldn't be destroyed, and isn't destroyed... reset the quantity
            var resourceObject = GetResource();
            GetResource().quantityLeft = quantityLeft;
        }
    }
}

[Serializable]
public struct ResourceData
{
    public double DestroyedTime;
    public bool ShouldBeDestroyed;
    public int QuantityLeft;

    public ResourceData(double destroyedTime, bool shouldBeDestroyed, int quantityLeft)
    {
        DestroyedTime = destroyedTime;
        ShouldBeDestroyed = shouldBeDestroyed;
        QuantityLeft = quantityLeft;
    }
}

}

once done, that system can then be called off

Hint: a DateTime can easily be converted to and from a JToken

    private static void TestDateTime()
    {
        JToken jValue = JToken.FromObject(DateTime.Now);
        var value = jValue.ToObject<DateTime>();
        Debug.Log($"JValue {jValue.ToString()}  | datetime value = {value}");
    }

is this a function I’m supposed to be placing somewhere…? I’m genuinely confused :sweat_smile:

(P.S: I tried extracting the important lines of code and placing them to their respectful capture and restore functions, but that didn’t work, for the obvious reason that the restore didn’t recognize the variable in the capture function).

(I also tried something else, but I won’t tell you what it is, because it’s a coding crime :stuck_out_tongue_winking_eye: )

It’s just an example of converting a DateTime to and from a JToken…

If you’re looking to manage things by date and time instead of seconds delay, you’ll need to use a DateTime() and use .AddMinutes(), AddHours(), AddDays(), etc.

I’m not trying to manage things through date and time instead of second delays. In fact, I’d rather keep it all in seconds to avoid overwhelming myself in the design stage… I think there’s a misunderstanding here.

All I’m trying to do is to make the timer work based on the system timer instead of the game timer, so if the player has to wait… idk… 30 minutes for example, for an enemy to respawn, he can’t just quit the game and return in 10 seconds to find the enemy suddenly respawn (which is what’s happening right now). If it says 30 minutes, he has to actually wait for 30 minutes of real time before the enemy respawns. That’s all what I’m trying to do :slight_smile:

In order to accomplish that, you need a DateTime. That’s how it works.

OK I’m not 100% sure how to accomplish that, but I think I have an idea in mind. I’ll give it a try, and update you on how it goes :slight_smile:

heya Brian, hope you’re doing well

OK so far my implementation is… going well I suppose, based on how I want this to work (I made some code changes, and they worked well so far). But I have one major problem:

In the saving and restoring states, the state when capturing the data is of type ‘JObject’. Based on my limited knowledge, I’m trying to replicate what I recently learned in ‘ResourceRespawner.cs’ to integrate a new type of data known as ‘ResourceData’, that was supposedly going to return at first side by side with JObject (if you guessed I’m about to be bombed with the same issue when restoring the data, you’re absolutely correct)

but… I can’t return more than one type of data. How do I go around that? (to be very clear, ‘ResourceData’ is a serializable class I created just for data storage, but I’ll rename it for the Respawn Manager)

It’s a dictionary. Just add it (I’m not sure about the FromObject and ToObject bits, but it’s something like this)

// To Save
stateDict["RespawnData"] = JToken.FromObject(data);

// To Load
var data = stateDict["RespawnData"].ToObject<ResourceData>();

Don’t use the ResourceData object, though, because this is not a resource.

1 Like

hey Brian, a bit of an update:

me and bixarrio spent the past 4 hours trying to make it work, but somehow we both failed at it… can you please have a look at my ‘RespawnManager.cs’ and see if you can help us get it to work? (all lines of code labelled as ‘TEST’ is what we added):

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

namespace RPG.Respawnables
{
    public class RespawnManager : SaveableEntity, 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;
        private bool isDead;
        private Health health;

        // --------------------------- NOTE: RestoreState() occurs BEFORE Start(), hence we need to ensure everything works accordingly --------------
        
        private void Start()
        {
            // TEST
            timeKeeper = TimeKeeper.GetTimeKeeper();
            health = GetComponentInChildren<Health>();

            // 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()
        {
            if (spawnedEnemy)
            {
                // Dude is not dead no longer, so delete his previous 'onDeath' record after he's respawned
                spawnedEnemy.GetComponent<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);

            // TEST Boolean (just one line below):
            isDead = false;

            // Get the spawned/respawned enemies' health, and listen for death notifications
            spawnedEnemy.GetComponent<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.GetComponent<Fighter>());
                if (spawnedEnemy.TryGetComponent(out DialogueAggro dialogueAggro)) //aggrogroup is at this point valid
                {
                    dialogueAggro.SetAggroGroup(aggroGroup);
                }
            }
            // ---------------------------------------------------------------------------------------------------------
        }

        void HideCharacter()
        {
            // Hide the dead character
            foreach (Renderer renderer in spawnedEnemy.GetComponentsInChildren<Renderer>())
            {
                renderer.enabled = false;
            }
        }

        void OnDeath()
        {
            // TEST Boolean (just the next line):
            isDead = true;
            
            // 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());
            
            // hide the character after 'hideTime', and then respawn him after 'respawnTime'
            // Invoke(nameof(HideCharacter), hideTime);
            // Invoke(nameof(Respawn), respawnTime);
            
            if (aggroGroup != null) {
                aggroGroup.RemoveFighterFromGroup(spawnedEnemy.GetComponent<Fighter>());
            }
        }

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

        // TEST Boolean
        private bool IsDead() 
        {
            return isDead;
        }

        // TEST Enemy getter
        private EnemyStateMachine GetSpawnedEnemy() 
        {
            return spawnedEnemy;
        }

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

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

            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;

            // TEST (just the line below):
            var data = stateDict["RespawnData"].ToObject<RespawnData>();

            foreach (IJsonSaveable jsonSaveable in spawnedEnemy.GetComponents<IJsonSaveable>())
            {
                string component = jsonSaveable.GetType().ToString();
                if (stateDict.ContainsKey(component))
                {
                    // TEST (Getting the timer regardless of conditions):
                    var isDead = data.IsDead;
                    destroyedTime = data.DestroyedTime;

                    // IF STATEMENT TESTING:
                    if (isDead && !IsDead()) 
                    {
                        var spawnedEnemy = GetSpawnedEnemy();
                        spawnedEnemy.GetComponent<Health>().onDie.RemoveListener(OnDeath);
                        OnDeath();
                        StartCoroutine(WaitAndRespawn());
                    }
                    else if (!isDead && IsDead()) 
                    {
                        Respawn();
                    }
                    else if (isDead && IsDead()) 
                    {
                        StopAllCoroutines();
                        StartCoroutine(WaitAndRespawn());
                    }
                        
                    spawnedEnemy = GetSpawnedEnemy();

                    // 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;
        }
    }
}

along with the class at the bottom… that’s also something we added

The end goal is that the respawn Timer goes global, so if he takes 30 minutes for example to respawn, that’s 30 real-time minutes, not just in game, so if we switch between scenes it’s still working, not only in the main game scene

Edit: Apparently it took me 4 hours to decode that I didn’t have the JSONSaveableEntity.cs script attached to the Respawn Manager empty gameObject… (that was my first problem, idk what’s next…)

Edit 2: We fixed 99% of it… it was a nightmare, but this man just didn’t give up on me :stuck_out_tongue: (and we accidentally found out that over the past year of me owning this script, the saving system never actually worked…)

Edit 3: what took me and @bixarrio a whole day to re-program parts of it, the final ‘RespawnManager.cs’:

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

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

It works globally, independent of the game timer, so you can have enemies spawn, hide and respawn completely independently of whether you’re playing the game or offline (however I have to highlight that because the hide time for my game will be a constant 5 seconds regardless of the enemy, I did not bother much about it being a global timer as much as I was concerned about the ‘respawnTime’). Extremely beneficial for enemies that you want to make valuable by spawning them once every few hours.

Place it on an empty gameObject, and set it up in the inspector :smiley: (P.S: It currently only works with state machines)

I haven’t failed at anything

I did, my bad :smiley:

I wouldn’t bother saving hide time, either.

1 Like

ahh well… if it works, don’t touch it :stuck_out_tongue_winking_eye: (off to the next topic, xD (TOMORROW, TOMORROW))

I still would have done a DateTime architecture (and if you ever make a mobile game, I strongly recommend doing so), but the way you used works quite well also.

frankly speaking, when I first went with low poly for my game design, apart from the fact that I was unable (back then) to find any good support for a consistent style, I had intentions to go mobile one day, but for now my main goal is to actually develop a story and release something to steam first before I can even think of mobile games… :slight_smile:

Do have a look sometime at my new question by the way, whenever you got some time please :smiley: (and no, I didn’t end up playing Palworld for long before going to educate myself about cutting terrain into chunks in Unity)

Privacy & Terms