Dynamic Respawning of NPCs

Not sure what you’re meaning… when they’re restored, they should all appear at the same time (because they were there when you captured)
The location should be the NPC’s location… are you saying they’re spawning on top of each other?

Is your Health component not an IJsonSaveable?

I have no way of testing the script, since it relies on things I don’t have. I’m sure from here, the issue is just tweaks…

when it used to work (I’m not sure what went wrong, but I believe something else happened that won’t even let me save and quit to begin with :sweat_smile:), they’d all respawn at the exact same spot as one of them

yup

it is an IJsonSaveable:

    public JToken CaptureAsJToken() 
    {
        return JToken.FromObject(healthPoints.value);
    }

    public void RestoreFromJToken(JToken state) 
    {
        healthPoints.value = state.ToObject<float>();
        UpdateState();
    }

it broke things that have nothing to do with this… like a death glitch for example, where it won’t even get the player to respawn for some reason (don’t ask me why. I’m clueless), or where it added 2 null spots in the enemy AggroGroup (that one was fixed by restarting Unity… the traditional Egyptian way of dealing with anything in life). Here’s where I handle all the respawn logic:

        private void RespawnPlayer()
        {

            // This function will deal with the Cinemachine respawn location, and the Player respawn mechanisms

            // The distance the CineMachine camera will move when respawning our player
            Vector3 positionDelta = respawnLocation.position - transform.position;
            
            // This function includes with the mechanics that respawn the player

            // (FOR STEP 1 BELOW, we are eliminating the NavMeshAgent, so we eliminated that line for the 'transform.position' below):
            // 1. placing (Warping) the player back to his original respawn location:
            // (it's done in a Coroutine so we can give the player some time rather than instantly respawning)
            // GetComponent<NavMeshAgent>().Warp(respawnLocation.position);

            // if you had a NavMeshAgent, you'd have to disable that too before teleportation, and re-enable it after:
            characterController.enabled = false;
            transform.position = respawnLocation.position;
            transform.rotation = Quaternion.identity; // when dying off the back of an animal, this will be necessary for correct respawn rotation
            characterController.enabled = true;

            // STATIC Variable, added by Bahaa (so that MalbersAnimations.MountTrigger is accessible when the player has resurrected):
            HealthAndMalbersMountTriggersScriptRelationshipManager.isPlayerDead = false;

            // 2. Restore the Player Health (specific Percentage):
            Health health = GetComponent<Health>();
            health.Heal(health.GetMaxHealthPoints() * healthRegenPercentage / 100);

            // 3. Fixing the Respawn Issue of our Cinemachine Camera:
            ICinemachineCamera activeVirtualCamera = FindObjectOfType<CinemachineBrain>().ActiveVirtualCamera;
            
            // 4. throw the camera to the players' position, and halt it there:
            if (activeVirtualCamera.Follow == transform) {
                activeVirtualCamera.OnTargetObjectWarped(transform, positionDelta);
            }
        }
    }

I heard about Unity breaking irrelevant stuff, mainly when reading about Quaternions, and frankly speaking… it scares me :sweat_smile:

For the time being, I deleted the entire saving system from that script because it was severely acting weird… and the last thing I want right now, is stuff breaking because something else was modified :sweat_smile: - until we have a better idea

Little bit of an update. Remember when i told you that sometimes my player never respawns from his death, and he may never even be able to save the game when I hit “Save and Quit”? I just found out the reason why, through an NRE.

If you recall a while ago, I programmed a solution to save my ‘AggroGroup.cs’, as follows:

        public JToken CaptureAsJToken()
        {
            JObject state = new JObject();
            JArray enemiesArray = new JArray();

            foreach (var enemy in enemies) 
            {
                enemiesArray.Add(enemy.name);
            }

            state["enemies"] = enemiesArray;
            return state;
        }

        public void RestoreFromJToken(JToken state)
        {
            JObject stateObject = (JObject)state;
            JArray enemiesArray = (JArray)stateObject["enemies"];

            enemies.Clear();
            foreach (var enemyName in enemiesArray)
            {
                GameObject enemy = GameObject.Find((string)enemyName);
                if (enemy != null)
                {
                    enemies.Add(enemy.GetComponent<EnemyStateMachine>());
                    // Connecting RespawnManager's AggroGroup variable to the correct AggroGroup the enemy is in:
                    if (enemy.GetComponentInParent<RespawnManager>() != null)
                    {
                        enemy.GetComponentInParent<RespawnManager>().GetAggroGroup();
                        if (enemy.GetComponentInParent<RespawnManager>().GetAggroGroup() != null)
                        {
                            enemy.GetComponentInParent<RespawnManager>().SetAggroGroup(this);
                        }
                    }
                    // ------------------------------------------------------------------------------------------
                }
            }
        }

Both errors lead to this foreach loop in ‘CaptureAsJToken()’:

            foreach (var enemy in enemies) 
            {
                enemiesArray.Add(enemy.name);
            }

Apparently for god knows what reason, sometimes my NPCs are not in their own AggroGroup, and I have no idea why

I know this is completely off topic, but any clue how to fix this?

Again, here’s my DynamicNPCSpawner.cs script for the moment:

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

namespace RPG.DynamicSpawner
{
    [RequireComponent(typeof(JSONSaveableEntity))]
    public class DynamicNPCSpawner : MonoBehaviour, IJsonSaveable
    {
        private struct NPCRecord
        {
            public GameObject NPC;
            public int PrefabIndex;
            public int PatrolPathIndex;
        }

        [SerializeField] float spawnRadius = 50f;
        [SerializeField] float checkInterval = 3f;
        [SerializeField] int maxNPCCount = 4;

        [SerializeField] List<GameObject> NPCPrefabs;
        [SerializeField] List<PatrolPath> patrolPaths;

        private List<NPCRecord> spawnedNPCs = new List<NPCRecord>();
        private Transform playerTransform;

        private void Start()
        {
            playerTransform = GameObject.FindGameObjectWithTag("Player").transform;
            InvokeRepeating(nameof(CheckSpawningAndDespawning), 0f, checkInterval);
        }

        private void CheckSpawningAndDespawning()
        {
            SpawnNPCs();
            DespawnNPCs();
        }

        private void SpawnNPCs()
        {
            if (spawnedNPCs.Count < maxNPCCount)
            {
                Vector3 spawnPosition = GetRandomPointWithinRadius(playerTransform.position, spawnRadius);
                int prefabIndex = Random.Range(0, NPCPrefabs.Count);
                GameObject NPCPrefab = NPCPrefabs[prefabIndex];
                GameObject spawnedNPC = Instantiate(NPCPrefab, spawnPosition, Quaternion.identity);

                int patrolPathIndex = Random.Range(0, patrolPaths.Count);
                PatrolPath patrolPath = patrolPaths[patrolPathIndex];
                EnemyStateMachine enemyStateMachine = spawnedNPC.GetComponentInChildren<EnemyStateMachine>();
                enemyStateMachine.AssignPatrolPath(patrolPath);

                NPCRecord record = new NPCRecord()
                {
                    NPC = spawnedNPC,
                    PrefabIndex = prefabIndex,
                    PatrolPathIndex = patrolPathIndex
                };

                spawnedNPCs.Add(record);
            }
        }

        private void DespawnNPCs()
        {
            for (int i = spawnedNPCs.Count - 1; i >= 0; i--)
            {
                if (Vector3.Distance(playerTransform.position, spawnedNPCs[i].NPC.transform.position) > spawnRadius)
                {
                    Destroy(spawnedNPCs[i].NPC);
                    spawnedNPCs.RemoveAt(i);
                }
            }
        }

        private Vector3 GetRandomPointWithinRadius(Vector3 center, float radius)
        {
            Vector3 randomPoint = Vector3.zero;
            int maxAttempts = 30;

            for (int i = 0; i < maxAttempts; i++)
            {
                Vector3 point = Random.insideUnitSphere * radius + center;
                point.y = center.y;

                if (NavMesh.SamplePosition(point, out NavMeshHit hit, radius, NavMesh.AllAreas))
                {
                    randomPoint = hit.position;
                    break;
                }
            }
            return randomPoint;
        }

        private void OnValidate()
        {
            for (int i = NPCPrefabs.Count - 1; i >= 0; i--)
            {
                GameObject NPCPrefab = NPCPrefabs[i];
                if (NPCPrefab != null && !NPCPrefab.TryGetComponent(out AggroGroup _) && !NPCPrefab.TryGetComponent(out Animal _))
                {
                    Debug.LogError($"Invalid NPC Prefab: {NPCPrefab.name} does not have an AggroGroup or an Animal Component on it", NPCPrefab);
                    NPCPrefabs.RemoveAt(i);
                }
            }
        }

        public JToken CaptureAsJToken()
        {
            JArray stateArray = new JArray();
            Debug.Log($"Capturing NPC States...");

            foreach (var record in spawnedNPCs)
            {
                JObject npcState = new JObject();
                npcState["prefabIndex"] = record.PrefabIndex;
                GameObject npc = record.NPC;
                npcState["position"] = new JArray(npc.transform.position.x, npc.transform.position.y, npc.transform.position.z);
                npcState["patrolPathIndex"] = record.PatrolPathIndex;
                JObject state = new JObject();
                foreach (IJsonSaveable jsonSaveable in npc.GetComponents<IJsonSaveable>())
                {
                    JToken token = jsonSaveable.CaptureAsJToken();
                    string component = jsonSaveable.GetType().ToString();
                    state[jsonSaveable.GetType().ToString()] = token;
                }
                npcState["state"] = state;
                stateArray.Add(npcState);
            }
            Debug.Log($"Capturing Completed");
            return stateArray;
        }

        public void RestoreFromJToken(JToken state)
        {
            spawnedNPCs.Clear();
            JArray stateArray = (JArray)state;
            Debug.Log($"Restoring NPC State...");

            foreach (JObject npcState in stateArray)
            {
                int prefabIndex = npcState["prefabIndex"].ToObject<int>();
                JArray positionArray = (JArray)npcState["position"];
                Vector3 position = new Vector3(
                    positionArray[0].ToObject<float>(),
                    positionArray[1].ToObject<float>(),
                    positionArray[2].ToObject<float>()
                );

                int patrolPathIndex = npcState["patrolPathIndex"].ToObject<int>();
                GameObject npcPrefab = NPCPrefabs[prefabIndex];
                GameObject spawnedNPC = Instantiate(npcPrefab, position, Quaternion.identity);

                PatrolPath patrolPath = patrolPaths[patrolPathIndex];
                EnemyStateMachine enemyStateMachine = spawnedNPC.GetComponentInChildren<EnemyStateMachine>();
                enemyStateMachine.AssignPatrolPath(patrolPath);
                JObject stateDict = npcState["state"] as JObject;
                foreach (IJsonSaveable jsonSaveable in spawnedNPC.GetComponents<IJsonSaveable>())
                {
                    string component = jsonSaveable.GetType().ToString();
                    if (stateDict.ContainsKey(component))
                    {
                        jsonSaveable.RestoreFromJToken(stateDict[component]);
                    }
                }
                spawnedNPCs.Add(new NPCRecord()
                {
                    NPC = spawnedNPC,
                    PrefabIndex = prefabIndex,
                    PatrolPathIndex = patrolPathIndex
                });
            }

            Debug.Log($"Restore Completed");
        }
    }
}

(my AggroGroup script is a little over 500 lines long, so unless necessary, I think it’d be best that I don’t share it and overwhelm you :sweat_smile:)

if I were to take a wild guess, I’d say it’s a race condition. If that’s the case, can we add a timer, maybe 0.05 seconds or something, just to give one of them the time to actually get the information before it’s used?

(and one of the many ways to get them out of their AggroGroup, is to not have strict rules on when despawning happens…!)

Just a reminder from your friendly neighborhood spiderman, you should always hunt down villianous NREs as soon as possible before working on new features or the bugs are likely to multiply…

If it was between late May and two weeks ago, I don’t remember much of anything… :stuck_out_tongue:

Out of curiosity… why?
It looks like you’re saving the membership roster of the Aggrogroup, but your respawn manager already knows how to have it’s character join the corred Aggrogroup. What the Aggrogroup really needs to save is the status of the Aggrogroup. Are you currently hostile, or are you currently neutral/friendly.

Are you saying that the NRE is within that foreach loop? (highly unlikely)

I would tend to agree with you. Let the Respawn Managers assign the Aggrogroups.

[AFTER READING THIS COMMENT, SKIP TILL THE END. THE NEXT TWO COMMENTS WERE A PROGRAMMING DISASTER]

trying to keep up, xD. There’s a reason why my game’s environment still looks horrible :sweat_smile:

because for fixed entities, sometimes you can invite and expel NPCs out of your AggroGroup, and unfortunately that one needs saving from multiple dimensions, including the conversation based on whether they’re on the team or not, and the AggroGroup itself

Essentially, the RespawnManager saves and restores the correct dialogue based on their status, and the AggroGroup saves which group these fixed entities currently belong to. If you save the correct conversation but the NPC does not go to the correct AggroGroup where it was last saved at, this will lead to some serious logical problems in my game.

Also, considering that my NPCs have AggroGroups at the core of it all right now, this really really avoids some serious bugs down the line

Oh, and the PlayerStateMachine saves a boolean that I introduced which determines whether they’re on the player’s team or not (my Aim Reticle needs that piece of information, to make sure it doesn’t get the wrong color)

Unfortunately this part has to stay (if it was up to me, I’d delete it, but unfortunately it’s not… it needs to know exactly where to go from the get go). I can’t think of any other way around it for now :sweat_smile:

true, but this error becomes present when the new saving and restoring code that we placed in the ‘DynamicNPCSpawner.cs’ comes to life, hence why I wanted to keep it simple:

I don’t want to save and restore their exact positions and health (for now). I just want to save the lists of the NPCs and the Patrol Paths, so the game knows what to spawn, and where, and let the dynamic spawning take care of the rest (as long as they don’t despawn whilst they’re fighting, which can be checked for by checking ‘LastAttacker’, because somehow… the 'DespawnNPCs still despawns them when I’m dangerously close. It gets a little suspicious (Imagine having a really long fight, and then they vanish into thin air… I’m sure you’d be bothered, xD))

Any ideas how we can go around this, though? Without trying to modify my AggroGroup’s Save and Restore Systems (I don’t want to break more systems :sweat_smile:)

Alright so… to compensate for my problem where sometimes I can’t save, I decided to destroy the entire AggroGroup after ‘hideTime’ in ‘RespawnManager.cs’ is done, through event subscriptions and unsubscriptions (although the unsub is a little sus). Here’s what I came up with:

        private void HandleNPCDeath(GameObject deadNPC, Health healthComponent) 
        {
            StartCoroutine(DelayedNPCRemoval(deadNPC, healthComponent));
        }

        private IEnumerator DelayedNPCRemoval(GameObject deadNPC, Health healthComponent) 
        {
            float hideTime = deadNPC.GetComponentInChildren<RespawnManager>().GetHideTime();
            yield return new WaitForSecondsRealtime(hideTime); // to match the 'WaitForSecondsRealtime' in 'RespawnManager.HideCharacter()'

            for (int i = spawnedNPCs.Count - 1; i >= 0; i--) 
            {
                if (spawnedNPCs[i].NPC == deadNPC) 
                {
                    if (healthComponent != null) 
                    {
                        healthComponent.onDie.RemoveListener(() => HandleNPCDeath(deadNPC, healthComponent));
                    }

                    spawnedNPCs.RemoveAt(i);
                    Destroy(deadNPC);
                    break;
                }
            }
        }

and then in ‘SpawnNPC’, I subscribe to it:

                // SUBSCRIBE TO 'Health.onDie'
                Health healthComponent = spawnedNPC.GetComponentInChildren<Health>();
                if (healthComponent != null) 
                {
                    healthComponent.onDie.AddListener(() => HandleNPCDeath(spawnedNPC, healthComponent));
                }

and in ‘DespawnNPC’, I unsubscribe to it:

                    // UNSUBSCRIBE TO 'Health.onDie'
                    Health healthComponent = spawnedNPCs[i].NPC.GetComponentInChildren<Health>();
                    if (healthComponent != null) 
                    {
                        healthComponent.onDie.RemoveListener(() => HandleNPCDeath(spawnedNPCs[i].NPC, healthComponent));
                    }

I’ll also add an unsubscription in ‘OnDisable’, in case the game gets called quits before they disappear

That way, I don’t have to deal with any missing stuff, and hopefully the AggroGroup’s saving system works well again

although I am suspicious a little bit of what’s going on

I still have a few severe bugs with this system to be dealt with, including them vanishing when I’m in range, whilst they’re still targets, and this does lead to an NRE (temporary solution would be to cancel the ‘CurrentTarget’ by brute force in ‘PlayerFreeLookState.Enter()’).

and the pickup saving system… Apparently this Dynamic System is a bit of a disaster, and I’m just reversing stuff like a madman now :sweat_smile:

That last approach was a complete failure…

Anyway, for the time being, here’s my current code, because I’m about to try out something REALLY, REALLY, REALLY STUPID:

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

namespace RPG.DynamicSpawner
{
    [RequireComponent(typeof(JSONSaveableEntity))]
    public class DynamicNPCSpawner : MonoBehaviour, IJsonSaveable
    {
        private struct NPCRecord
        {
            public GameObject NPC;
            public int PrefabIndex;
            public int PatrolPathIndex;
        }

        [SerializeField] float spawnRadius = 50f;
        [SerializeField] float checkInterval = 3f;
        [SerializeField] int maxNPCCount = 4;

        [SerializeField] List<GameObject> NPCPrefabs;
        [SerializeField] List<PatrolPath> patrolPaths;

        private List<NPCRecord> spawnedNPCs = new List<NPCRecord>();
        private Transform playerTransform;

        private void Start()
        {
            playerTransform = GameObject.FindGameObjectWithTag("Player").transform;
            InvokeRepeating(nameof(CheckSpawningAndDespawning), 0f, checkInterval);
        }

        private void CheckSpawningAndDespawning()
        {
            SpawnNPCs();
            DespawnNPCs();
        }

        private void SpawnNPCs()
        {
            if (spawnedNPCs.Count < maxNPCCount)
            {
                // DYNAMIC NPC SPAWNING
                Vector3 spawnPosition = GetRandomPointWithinRadius(playerTransform.position, spawnRadius);
                int prefabIndex = Random.Range(0, NPCPrefabs.Count);
                GameObject NPCPrefab = NPCPrefabs[prefabIndex];
                GameObject spawnedNPC = Instantiate(NPCPrefab, spawnPosition, Quaternion.identity);

                // PATROL PATH ASSIGNMENT
                int patrolPathIndex = Random.Range(0, patrolPaths.Count);
                PatrolPath patrolPath = patrolPaths[patrolPathIndex];
                EnemyStateMachine enemyStateMachine = spawnedNPC.GetComponentInChildren<EnemyStateMachine>();
                enemyStateMachine.AssignPatrolPath(patrolPath);

                // NPC RECORD
                NPCRecord record = new NPCRecord()
                {
                    NPC = spawnedNPC,
                    PrefabIndex = prefabIndex,
                    PatrolPathIndex = patrolPathIndex
                };

                spawnedNPCs.Add(record);
            }
        }

        private void DespawnNPCs()
        {
            for (int i = spawnedNPCs.Count - 1; i >= 0; i--)
            {
                if (Vector3.Distance(playerTransform.position, spawnedNPCs[i].NPC.transform.position) > spawnRadius)
                {
                    Destroy(spawnedNPCs[i].NPC);
                    spawnedNPCs.RemoveAt(i);
                }
            }
        }

        private Vector3 GetRandomPointWithinRadius(Vector3 center, float radius)
        {
            Vector3 randomPoint = Vector3.zero;
            int maxAttempts = 30;

            for (int i = 0; i < maxAttempts; i++)
            {
                Vector3 point = Random.insideUnitSphere * radius + center;
                point.y = center.y;

                if (NavMesh.SamplePosition(point, out NavMeshHit hit, radius, NavMesh.AllAreas))
                {
                    randomPoint = hit.position;
                    break;
                }
            }
            return randomPoint;
        }

        private void OnValidate()
        {
            for (int i = NPCPrefabs.Count - 1; i >= 0; i--)
            {
                GameObject NPCPrefab = NPCPrefabs[i];
                if (NPCPrefab != null && !NPCPrefab.TryGetComponent(out AggroGroup _) && !NPCPrefab.TryGetComponent(out Animal _))
                {
                    Debug.LogError($"Invalid NPC Prefab: {NPCPrefab.name} does not have an AggroGroup or an Animal Component on it", NPCPrefab);
                    NPCPrefabs.RemoveAt(i);
                }
            }
        }

        public JToken CaptureAsJToken()
        {
            JArray stateArray = new JArray();
            Debug.Log($"Capturing NPC States...");

            foreach (var record in spawnedNPCs)
            {
                JObject npcState = new JObject();
                npcState["prefabIndex"] = record.PrefabIndex;
                GameObject npc = record.NPC;
                npcState["position"] = new JArray(npc.transform.position.x, npc.transform.position.y, npc.transform.position.z);
                npcState["patrolPathIndex"] = record.PatrolPathIndex;
                JObject state = new JObject();
                foreach (IJsonSaveable jsonSaveable in npc.GetComponents<IJsonSaveable>())
                {
                    JToken token = jsonSaveable.CaptureAsJToken();
                    string component = jsonSaveable.GetType().ToString();
                    state[jsonSaveable.GetType().ToString()] = token;
                }
                npcState["state"] = state;
                stateArray.Add(npcState);
            }
            Debug.Log($"Capturing Completed");
            return stateArray;
        }

        public void RestoreFromJToken(JToken state)
        {
            spawnedNPCs.Clear();
            JArray stateArray = (JArray)state;
            Debug.Log($"Restoring NPC State...");

            foreach (JObject npcState in stateArray)
            {
                int prefabIndex = npcState["prefabIndex"].ToObject<int>();
                JArray positionArray = (JArray)npcState["position"];
                Vector3 position = new Vector3
                (
                    positionArray[0].ToObject<float>(),
                    positionArray[1].ToObject<float>(),
                    positionArray[2].ToObject<float>()
                );

                int patrolPathIndex = npcState["patrolPathIndex"].ToObject<int>();
                GameObject npcPrefab = NPCPrefabs[prefabIndex];
                GameObject spawnedNPC = Instantiate(npcPrefab, position, Quaternion.identity);

                PatrolPath patrolPath = patrolPaths[patrolPathIndex];
                EnemyStateMachine enemyStateMachine = spawnedNPC.GetComponentInChildren<EnemyStateMachine>();
                enemyStateMachine.AssignPatrolPath(patrolPath);
                JObject stateDict = npcState["state"] as JObject;
                foreach (IJsonSaveable jsonSaveable in spawnedNPC.GetComponents<IJsonSaveable>())
                {
                    string component = jsonSaveable.GetType().ToString();
                    if (stateDict.ContainsKey(component))
                    {
                        jsonSaveable.RestoreFromJToken(stateDict[component]);
                    }
                }
                spawnedNPCs.Add(new NPCRecord()
                {
                    NPC = spawnedNPC,
                    PrefabIndex = prefabIndex,
                    PatrolPathIndex = patrolPathIndex
                });
            }
            Debug.Log($"Restore Completed");
        }
    }
}

(I’ll restart rewriting the entire system from scratch, because I genuinely don’t even know where I’m going wrong right now)

Edit: That approach failed too… OK now I’m genuinely stomped. I just need to find a way to delete the ‘NPCPrefabs’ when the NPC dies, to avoid issues with the AggroGroup’s saving system, that’s all I want to do (and fix my attempt at the saving system with this Dynamic Spawner)

and avoid him from vanishing if he’s in combat, because that’s another severe problem that exists which really messes with the Targeter Key, because all of a sudden the NPC is gone ‘missing’

OK so, a little update (again)… I found a way to actually eliminate my AggroGroup and get rid of the problem I have alltogether, where missing enemies blocks my AggroGroup’s Capture and Restore states, by simply ensuring that my ‘RespawnManager’ does not Set the Parent of the dead enemies to null (this wasn’t a nuisance, until now. I just hope I didn’t break anything as a result), and using that info to get the parent AggroGroup, and hiding it after ‘hideTime’, as follows:

        private void SpawnNPCs()
        {
            if (spawnedNPCs.Count < maxNPCCount)
            {
                // NPC SPAWNING:
                Vector3 spawnPosition = GetRandomPointWithinRadius(playerTransform.position, spawnRadius);
                GameObject NPCPrefab = NPCPrefabs[Random.Range(0, NPCPrefabs.Count)];
                GameObject spawnedNPC = Instantiate(NPCPrefab, spawnPosition, Quaternion.identity);

                // NPC PATROL PATH ASSIGNING:
                PatrolPath patrolPath = patrolPaths[Random.Range(0, patrolPaths.Count)];
                EnemyStateMachine enemyStateMachine = spawnedNPC.GetComponentInChildren<EnemyStateMachine>();
                enemyStateMachine.AssignPatrolPath(patrolPath);

                // TEST:
                Health health = enemyStateMachine.Health;
                if (health != null) 
                {
                    health.onDie.AddListener(() => 
                    {
                        if (health.IsDead()) 
                        {
                        Debug.Log($"{health.gameObject.name} Died, starting delayed AggroGroup destroy");
                        RespawnManager respawnManager = enemyStateMachine.GetComponentInParent<RespawnManager>();

                        if (respawnManager != null) 
                        {
                            Debug.Log($"RespawnManager name: {respawnManager.gameObject.name}");
                        }
                        else 
                        {
                            Debug.Log($"RespawnManager not found");
                        }

                        AggroGroup aggroGroup = enemyStateMachine.GetComponentInParent<AggroGroup>();

                        if (aggroGroup != null) 
                        {
                            Debug.Log($"AggroGroup name: {aggroGroup.gameObject.name}");
                        }
                        else 
                        {
                            Debug.Log($"AggroGroup not found");
                        }

                        if (respawnManager != null && aggroGroup != null)
                        {
                            Debug.Log($"AggroGroup and RespawnManager found");
                            float hideTime = respawnManager.GetHideTime();
                            StartCoroutine(DelayedAggroGroupDestroy(hideTime, aggroGroup));
                        }
                        else 
                        {
                            Debug.Log($"RespawnManager and/or AggroGroup not found");
                        }
                        }
                    });
                }
                // END OF TEST

                // ADDING THE NPC TO THE 'spawnedNPCs' LIST:
                spawnedNPCs.Add(spawnedNPC);
            }
        }

        private void DespawnNPCs()
        {
            // DELETE NPCs WHO ARE FAR AWAY FROM THE PLAYER (PERFORMANCE SAVIOR):
            for (int i = spawnedNPCs.Count - 1; i >= 0; i--)
            {
                if (Vector3.Distance(playerTransform.position, spawnedNPCs[i].transform.position) > spawnRadius)
                {
                    Destroy(spawnedNPCs[i]);
                    spawnedNPCs.RemoveAt(i);
                }
            }
        }

        // TEST FUNCTION:
        private IEnumerator DelayedAggroGroupDestroy(float delay, AggroGroup enemyAggroGroup)
        {
            yield return new WaitForSecondsRealtime(delay);
            Debug.Log($"Destroying {enemyAggroGroup.gameObject.name} in {delay} seconds");
            Destroy(enemyAggroGroup.gameObject);
        }

moving on, I tried re-writing the saving system to something that I can understand (no offence @Brian_Trotter , just trying to get things to work properly :sweat_smile:):

        public JToken CaptureAsJToken() 
        {
            JObject state = new JObject();
            JArray npcArray = new JArray();
            JArray patrolPathArray = new JArray();

            // NPCs:
            foreach (var npc in NPCPrefabs) 
            {
                JObject npcState = new JObject();
                npcState["name"] = npc.name;

                npcState["position"] = new JObject 
                {
                    ["x"] = npc.transform.position.x,
                    ["y"] = npc.transform.position.y,
                    ["z"] = npc.transform.position.z
                };

                npcState["rotation"] = new JObject 
                {
                    ["x"] = npc.transform.rotation.x,
                    ["y"] = npc.transform.rotation.y,
                    ["z"] = npc.transform.rotation.z,
                    ["w"] = npc.transform.rotation.w
                };

                Health health = npc.GetComponentInChildren<Health>();
                if (health != null) 
                {
                    npcState["health"] = health.GetHealthPoints();
                }
                npcArray.Add(npcState);
            }

            // Patrol Paths:
            foreach (var patrolPath in patrolPaths) 
            {
                JObject patrolPathState = new JObject();
                patrolPathState["name"] = patrolPath.name;
                // Other PatrolPath properties here, if any exist and matter
                patrolPathArray.Add(patrolPathState);
            }

            state["NPCs"] = npcArray;
            state["patrolPaths"] = patrolPathArray;

            return state;
        }

        public void RestoreFromJToken(JToken state) 
        {
            JObject stateObject = (JObject)state;
            JArray npcsArray = (JArray)stateObject["NPCs"];
            JArray patrolPathArray = (JArray)stateObject["patrolPaths"];

            // Clear up the current NPCPrefabs and PatrolPath lists
            spawnedNPCs.Clear();
            patrolPaths.Clear();

            // Load all NPC AggroGroup Prefabs from the Resources Folder:
            AggroGroup[] npcPrefabs = Resources.LoadAll<AggroGroup>("NPCs");

            // Resource folder-based restoration search:
            foreach (var npcState in npcsArray) 
            {
                string npcName = npcState["name"].ToString();

                JObject positionObject = (JObject)npcState["position"];
                Vector3 position = new Vector3
                (
                    positionObject["x"].ToObject<float>(),
                    positionObject["y"].ToObject<float>(),
                    positionObject["z"].ToObject<float>()
                );

                JObject rotationObject = (JObject)npcState["rotation"];
                Quaternion rotation = new Quaternion
                (
                    rotationObject["x"].ToObject<float>(),
                    rotationObject["y"].ToObject<float>(),
                    rotationObject["z"].ToObject<float>(),
                    rotationObject["w"].ToObject<float>()
                );

                float healthValue = npcState["health"].ToObject<float>();

                AggroGroup npcPrefab = npcPrefabs.FirstOrDefault(npc => npc.name == npcName);
                if (npcPrefab != null) 
                {
                    GameObject spawnedNPC = Instantiate(npcPrefab.gameObject, position, rotation);
                    Health health = spawnedNPC.GetComponentInChildren<Health>();
                    if (health != null) 
                    {
                        health.SetHealthPoints(healthValue); // <-- find a way to write a 'SetHealth' function in 'Health.cs'
                    }
                    spawnedNPCs.Add(spawnedNPC);
                }
            }

            // Scene-based PatrolPath restoration search:
            foreach (var patrolPathState in patrolPathArray) 
            {
                string patrolPathName = patrolPathState["name"].ToString();
                PatrolPath patrolPath = FindPatrolPathByName(patrolPathName);
                if (patrolPath != null) 
                {
                    patrolPaths.Add(patrolPath);
                }
            }
        }

        private PatrolPath FindPatrolPathByName(string name)
        {
            PatrolPath[] allPatrolPaths = FindObjectsOfType<PatrolPath>();
            foreach (PatrolPath patrolPath in allPatrolPaths)
            {
                if (patrolPath.name == name)
                {
                    // if you found a patrol path
                    return patrolPath;
                }
            }
            // if you can't find a patrol path
            return null;
        }

now, for me, I tried to keep the Patrol Path capturing and restoring scene-based, as they will stay on the game scene, but for the NPCs I tried a Resources-folder approach, by creating a new “Resources” folder for them, and placing them in a subfolder named “NPCs” for them, as follows:

but still… something is a little wrong, and I can’t identify what it is. For now, here’s my most (semi-functional) advanced script:

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

namespace RPG.DynamicSpawner
{
    public class DynamicNPCSpawner : MonoBehaviour, IJsonSaveable
    {
        [SerializeField] float spawnRadius = 50f; // Radius around the player, to spawn NPCs
        [SerializeField] float checkInterval = 3f; // Interval, in seconds, to check for spawning/Despawning
        [SerializeField] int maxNPCCount = 4; // Maximum number of NPCs to spawn, in 'checkInterval' seconds (if this value is maxed out, don't respawn anymore)

        [SerializeField] List<GameObject> NPCPrefabs; // List of NPC Prefabs to Spawn (Type 'GameObject' because even Animals can get involved down the line)
        [SerializeField] List<PatrolPath> patrolPaths; // List of Patrol Paths to follow

        private List<GameObject> spawnedNPCs = new List<GameObject>();
        private Transform playerTransform;

        private void Start()
        {
            playerTransform = GameObject.FindGameObjectWithTag("Player").transform;
            InvokeRepeating(nameof(CheckSpawningAndDespawning), 0f, checkInterval);
        }

        private void CheckSpawningAndDespawning()
        {
            SpawnNPCs();
            DespawnNPCs();
        }

        private void SpawnNPCs()
        {
            if (spawnedNPCs.Count < maxNPCCount)
            {
                // NPC SPAWNING:
                Vector3 spawnPosition = GetRandomPointWithinRadius(playerTransform.position, spawnRadius);
                GameObject NPCPrefab = NPCPrefabs[Random.Range(0, NPCPrefabs.Count)];
                GameObject spawnedNPC = Instantiate(NPCPrefab, spawnPosition, Quaternion.identity);

                // NPC PATROL PATH ASSIGNING:
                PatrolPath patrolPath = patrolPaths[Random.Range(0, patrolPaths.Count)];
                EnemyStateMachine enemyStateMachine = spawnedNPC.GetComponentInChildren<EnemyStateMachine>();
                enemyStateMachine.AssignPatrolPath(patrolPath);

                // TEST:
                Health health = enemyStateMachine.Health;
                if (health != null) 
                {
                    health.onDie.AddListener(() => 
                    {
                        if (health.IsDead()) 
                        {
                        Debug.Log($"{health.gameObject.name} Died, starting delayed AggroGroup destroy");
                        RespawnManager respawnManager = enemyStateMachine.GetComponentInParent<RespawnManager>();

                        if (respawnManager != null) 
                        {
                            Debug.Log($"RespawnManager name: {respawnManager.gameObject.name}");
                        }
                        else 
                        {
                            Debug.Log($"RespawnManager not found");
                        }

                        AggroGroup aggroGroup = enemyStateMachine.GetComponentInParent<AggroGroup>();

                        if (aggroGroup != null) 
                        {
                            Debug.Log($"AggroGroup name: {aggroGroup.gameObject.name}");
                        }
                        else 
                        {
                            Debug.Log($"AggroGroup not found");
                        }

                        if (respawnManager != null && aggroGroup != null)
                        {
                            Debug.Log($"AggroGroup and RespawnManager found");
                            float hideTime = respawnManager.GetHideTime();
                            StartCoroutine(DelayedAggroGroupDestroy(hideTime, aggroGroup));
                        }
                        else 
                        {
                            Debug.Log($"RespawnManager and/or AggroGroup not found");
                        }
                        }
                    });
                }
                // END OF TEST

                // ADDING THE NPC TO THE 'spawnedNPCs' LIST:
                spawnedNPCs.Add(spawnedNPC);
            }
        }

        private void DespawnNPCs()
        {
            // DELETE NPCs WHO ARE FAR AWAY FROM THE PLAYER (PERFORMANCE SAVIOR):
            for (int i = spawnedNPCs.Count - 1; i >= 0; i--)
            {
                if (Vector3.Distance(playerTransform.position, spawnedNPCs[i].transform.position) > spawnRadius)
                {
                    Destroy(spawnedNPCs[i]);
                    spawnedNPCs.RemoveAt(i);
                }
            }
        }

        // TEST FUNCTION:
        private IEnumerator DelayedAggroGroupDestroy(float delay, AggroGroup enemyAggroGroup)
        {
            yield return new WaitForSecondsRealtime(delay);
            Debug.Log($"Destroying {enemyAggroGroup.gameObject.name} in {delay} seconds");
            Destroy(enemyAggroGroup.gameObject);
        }

        private Vector3 GetRandomPointWithinRadius(Vector3 center, float radius)
        {
            Vector3 randomPoint = Vector3.zero;
            int maxAttempts = 30; // 30 Attempts to find a valid position, at most (Give up otherwise)

            for (int i = 0; i < maxAttempts; i++)
            {
                Vector3 point = Random.insideUnitSphere * radius + center;
                point.y = center.y; // spawned NPC Height = player height

                // Make sure you don't Instantiate the character on a Rock, tree, 
                // or even underground. ONLY ON THE NAVMESHAGENT:
                if (NavMesh.SamplePosition(point, out NavMeshHit hit, radius, NavMesh.AllAreas))
                {
                    randomPoint = hit.position;
                    break; // Exit loop if a valid position is found
                }
            }
            return randomPoint;
        }

        // ONLY ACCEPT DATATYPES OF 'Animal' OR 'AggroGroup' (ensures that everything below works as expected)
        private void OnValidate()
        {
            for (int i = NPCPrefabs.Count - 1; i >= 0; i--)
            {
                GameObject NPCPrefab = NPCPrefabs[i];
                if (NPCPrefab != null && !NPCPrefab.TryGetComponent(out AggroGroup _) && !NPCPrefab.TryGetComponent(out Animal _))
                {
                    Debug.LogError($"Invalid NPC Prefab: {NPCPrefab.name} does not have an AggroGroup or an Animal Component on it", NPCPrefab);
                    NPCPrefabs.RemoveAt(i);
                }
            }
        }

        public JToken CaptureAsJToken() 
        {
            JObject state = new JObject();
            JArray npcArray = new JArray();
            JArray patrolPathArray = new JArray();

            // NPCs:
            foreach (var npc in NPCPrefabs) 
            {
                JObject npcState = new JObject();
                npcState["name"] = npc.name;

                npcState["position"] = new JObject 
                {
                    ["x"] = npc.transform.position.x,
                    ["y"] = npc.transform.position.y,
                    ["z"] = npc.transform.position.z
                };

                npcState["rotation"] = new JObject 
                {
                    ["x"] = npc.transform.rotation.x,
                    ["y"] = npc.transform.rotation.y,
                    ["z"] = npc.transform.rotation.z,
                    ["w"] = npc.transform.rotation.w
                };

                Health health = npc.GetComponentInChildren<Health>();
                if (health != null) 
                {
                    npcState["health"] = health.GetHealthPoints();
                }
                npcArray.Add(npcState);
            }

            // Patrol Paths:
            foreach (var patrolPath in patrolPaths) 
            {
                JObject patrolPathState = new JObject();
                patrolPathState["name"] = patrolPath.name;
                // Other PatrolPath properties here, if any exist and matter
                patrolPathArray.Add(patrolPathState);
            }

            state["NPCs"] = npcArray;
            state["patrolPaths"] = patrolPathArray;

            return state;
        }

        public void RestoreFromJToken(JToken state) 
        {
            JObject stateObject = (JObject)state;
            JArray npcsArray = (JArray)stateObject["NPCs"];
            JArray patrolPathArray = (JArray)stateObject["patrolPaths"];

            // Clear up the current NPCPrefabs and PatrolPath lists
            spawnedNPCs.Clear();
            patrolPaths.Clear();

            // Load all NPC AggroGroup Prefabs from the Resources Folder:
            AggroGroup[] npcPrefabs = Resources.LoadAll<AggroGroup>("NPCs");

            // Resource folder-based restoration search:
            foreach (var npcState in npcsArray) 
            {
                string npcName = npcState["name"].ToString();

                JObject positionObject = (JObject)npcState["position"];
                Vector3 position = new Vector3
                (
                    positionObject["x"].ToObject<float>(),
                    positionObject["y"].ToObject<float>(),
                    positionObject["z"].ToObject<float>()
                );

                JObject rotationObject = (JObject)npcState["rotation"];
                Quaternion rotation = new Quaternion
                (
                    rotationObject["x"].ToObject<float>(),
                    rotationObject["y"].ToObject<float>(),
                    rotationObject["z"].ToObject<float>(),
                    rotationObject["w"].ToObject<float>()
                );

                float healthValue = npcState["health"].ToObject<float>();

                AggroGroup npcPrefab = npcPrefabs.FirstOrDefault(npc => npc.name == npcName);
                if (npcPrefab != null) 
                {
                    GameObject spawnedNPC = Instantiate(npcPrefab.gameObject, position, rotation);
                    Health health = spawnedNPC.GetComponentInChildren<Health>();
                    if (health != null) 
                    {
                        health.SetHealthPoints(healthValue); // <-- find a way to write a 'SetHealth' function in 'Health.cs'
                    }
                    spawnedNPCs.Add(spawnedNPC);
                }
            }

            // Scene-based PatrolPath restoration search:
            foreach (var patrolPathState in patrolPathArray) 
            {
                string patrolPathName = patrolPathState["name"].ToString();
                PatrolPath patrolPath = FindPatrolPathByName(patrolPathName);
                if (patrolPath != null) 
                {
                    patrolPaths.Add(patrolPath);
                }
            }
        }

        private PatrolPath FindPatrolPathByName(string name)
        {
            PatrolPath[] allPatrolPaths = FindObjectsOfType<PatrolPath>();
            foreach (PatrolPath patrolPath in allPatrolPaths)
            {
                if (patrolPath.name == name)
                {
                    // if you found a patrol path
                    return patrolPath;
                }
            }
            // if you can't find a patrol path
            return null;
        }
    }
}

@Brian_Trotter please have a look and see if this Capture and Restore attempt works or not. I’m still trying to make it work (again, Resources-folder based NPC search, and scene-based Patrol Path search) :slight_smile:

I also placed this Setter in ‘Health.cs’, just for this saving system part:

    // Used ONLY IN 'DynamicNPCSpawner.cs', to restore DYNAMIC NPCs properly:
    public void SetHealthPoints(float healthPoints) 
    {
        this.healthPoints.value = healthPoints;
    }

(but because it uses a ‘.value’ (which I’m not very familiar with where it comes from and what not), I’m not sure if it’s right or wrong)

Restoring the health as we speak right now is an absolute disaster. It ALWAYS ALWAYS returns an NRE for some reason

(And one last favour… how do I ‘RemoveListener()’ what I did in ‘SpawnNPC’? I don’t think I ever encountered a scenario where I have to remove the listener of something this big, it was usually just a small function name)


So, what I want to store in the saving system, is the following (this is all that matters to me right now):

  1. The correct health of each NPC (optional)
  2. The correct position of each NPC (optional)
  3. The correct ‘NPCPrefabs’ list of gameObjects (mandatory)
  4. The correct ‘patrolPaths’ list of gameObject (mandatory)
  5. The correct ‘PickupSpawner’ information (mandatory). In simple terms, save and restore our pickups, just like normal fixed entity NPCs would do it

The reason the first two are ‘optional’, is because I genuinely don’t care about the status of the Spawned NPCs themselves, since they’ll be replaced anyway by a respawn when the game restarts from a clean save

And 3 and 4 will be replaced when we enter new biomes, so knowing what they are, with respect to our biomes, will be essential

Wow this whole post is hard to follow :slight_smile: There was one specific problem (spawning on top of each other) you had that I may have a suggestion for.

Please read this post and see if it addresses your problem.

— More broadly —

@Bahaa I think the root cause of many of your problems is you’re trying to solve too many things at once. I quoted Brian below and I agree 100%. I recommend approaching this whole feature you’re working on the same way Rick/Sam approach any feature in the course. One tiny bit of functionality at a time.

This could be promising. Since you are dynamically and continuously spawning and despawning the question is why save state at all? Upon restore can you rely on your code to just create the required state?

Saving what they drop after death will be essential. Imagine killing an NPC and quitting the game instantly, and when you return, you don’t find your loot. You’d be quite mad, I’m sure of that :slight_smile:

on top of all, when you enter a new biome, you definitely want the correct NPC Prefabs and the correct Patrol Paths to be restored. The last thing you want, is the wrong NPCs showing up at the wrong place

I do understand that this post is a little hard to follow up with, but keep in mind that I’m still changing and moving code around, all with the aim to get this system to work seamlessly with the game, and maybe even better than the fixed entities (I should’ve tackled this topic earlier…!)

I still don’t understand the purpose of this, as these things should already be serialized within the Aggrogroup. I understand the idea that within different regions/biomes, you want different patrol paths and enemies, but for this, generally an Aggrogroup should be local to a specific region/biome in general. It seems like these are the things that are giving you the most trouble.

Should be the easiest things to capture, especially if you simply use the method I showed you above to grab all of the ISaveables off of the spawned characters.

Quite literally handled in the Inventory course.

At this point, your code is so far from any code base that it’s very difficult to even figure out what’s going on in many cases…

That’s actually pretty common in a lot of games… grab your loot or the kobolds will pick them up when you’re off in the next scene. :stuck_out_tongue:

I have no way of telling if it works or not. It looks right, but ultimately the only way to know for sure is through rigorous testing.

I mean… you did specify the purpose of what I’m trying to do :sweat_smile: - the goal is to save, on the list of ‘NPCPrefabs’, and ‘PatrolPaths’, what goes where, so the dynamic spawner knows what to save, and where, based on the position of the player. That’s why they are crucial to save

For access purposes, again, my Dynamic NPCs are in a Resources Folder, and my Patrol Paths are scene-based. I need to find a way to make sure that these are correct and not played with

Can you please elaborate to me further on this? I’m a little confused on what you mean

and somehow, to my big surprise, it’s not working :sweat_smile: - I’m getting a little suspicious as of why.

Does it have to do with me instantiating an AggroGroup instead of an ‘EnemyStateMachine’ directly?

Ehh I just want to future-proof my code with this one, just to make sure in the future I got a point of view if things go wrong

but does it work? If not, something is wrong :stuck_out_tongue_winking_eye: - and in my case, it doesn’t exactly work :sweat_smile:

Can you please reference me to this again? I must have accidentally missed it out, or the name doesn’t sound too familiar to me

@Brian_Trotter @Cat_Hill or anyone else who sees this, I just have one last question, because I seem to be incredibly confused here… were our pickups from NPCs ALWAYS Non-Saveable, or did I mess something up by accident? I’m not sure of what’s going on, but my enemy drops (the fixed entities we are talking about right now… the regular enemies, away from this system) are not saveable, although my player’s are. I’m not sure if this is new or I messed something up, but I want to reconfirm

The fixed entity drops which always existed on the floor when the game starts are safe, I’m just not sure if I’m starting to see weird things for now or not… :sweat_smile:

For now, here’s my ‘DynamicNPCSpawner.cs’ script:

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

namespace RPG.DynamicSpawner
{
    public class DynamicNPCSpawner : MonoBehaviour//, IJsonSaveable
    {
        [SerializeField] float spawnRadius; // Radius around the player, to spawn NPCs
        [SerializeField] float checkInterval; // Interval, in seconds, to check for spawning/Despawning
        [SerializeField] int maxNPCCount; // Maximum number of NPCs to spawn, in 'checkInterval' seconds (if this value is maxed out, don't respawn anymore)

        [SerializeField] List<GameObject> NPCPrefabs; // List of NPC Prefabs to Spawn (Type 'GameObject' because even Animals will probably get involved down the line)
        [SerializeField] List<PatrolPath> patrolPaths; // List of Patrol Paths to follow

        private List<GameObject> spawnedNPCs = new List<GameObject>();
        private Transform playerTransform;

        private void Start()
        {
            playerTransform = GameObject.FindGameObjectWithTag("Player").transform;
            InvokeRepeating(nameof(CheckSpawningAndDespawning), 0f, checkInterval);
        }

        private void CheckSpawningAndDespawning()
        {
            SpawnNPCs();
            DespawnNPCs();
        }

        private void SpawnNPCs()
        {
            if (spawnedNPCs.Count < maxNPCCount)
            {
                // NPC SPAWNING:
                Vector3 spawnPosition = GetRandomPointWithinRadius(playerTransform.position, spawnRadius);
                GameObject NPCPrefab = NPCPrefabs[Random.Range(0, NPCPrefabs.Count)];
                GameObject spawnedNPC = Instantiate(NPCPrefab, spawnPosition, Quaternion.identity);

                // NPC PATROL PATH ASSIGNING:
                PatrolPath patrolPath = patrolPaths[Random.Range(0, patrolPaths.Count)];
                EnemyStateMachine enemyStateMachine = spawnedNPC.GetComponentInChildren<EnemyStateMachine>();
                enemyStateMachine.AssignPatrolPath(patrolPath);

                // ADDING THE NPC TO THE 'spawnedNPCs' LIST:
                spawnedNPCs.Add(spawnedNPC);
            }
        }

        private void DespawnNPCs()
        {
            // DELETE NPCs WHO ARE FAR AWAY FROM THE PLAYER (PERFORMANCE SAVIOR):
            for (int i = spawnedNPCs.Count - 1; i >= 0; i--)
            {
                // If you're in combat, you can't despawn
                if (spawnedNPCs[i].GetComponentInChildren<EnemyStateMachine>().GetLastAttacker() != null) return;

                if (Vector3.Distance(playerTransform.position, spawnedNPCs[i].GetComponentInChildren<EnemyStateMachine>().transform.position) > spawnRadius)
                {
                    Destroy(spawnedNPCs[i]);
                    spawnedNPCs.RemoveAt(i);
                }
            }
        }

        private Vector3 GetRandomPointWithinRadius(Vector3 center, float radius)
        {
            Vector3 randomPoint = Vector3.zero;
            int maxAttempts = 30; // 30 Attempts to find a valid position, at most (Give up otherwise)

            for (int i = 0; i < maxAttempts; i++)
            {
                Vector3 point = Random.insideUnitSphere * radius + center;
                point.y = center.y; // spawned NPC Height = player height

                // Make sure you don't Instantiate the character on a Rock, tree, 
                // or even underground. ONLY ON THE NAVMESHAGENT:
                if (NavMesh.SamplePosition(point, out NavMeshHit hit, radius, NavMesh.AllAreas))
                {
                    randomPoint = hit.position;
                    break; // Exit loop if a valid position is found
                }
            }
            return randomPoint;
        }

        // ONLY ACCEPT DATATYPES OF 'Animal' OR 'AggroGroup' (ensures that everything below works as expected)
        private void OnValidate()
        {
            for (int i = NPCPrefabs.Count - 1; i >= 0; i--)
            {
                GameObject NPCPrefab = NPCPrefabs[i];
                if (NPCPrefab != null && !NPCPrefab.TryGetComponent(out AggroGroup _) && !NPCPrefab.TryGetComponent(out Animal _))
                {
                    Debug.LogError($"Invalid NPC Prefab: {NPCPrefab.name} does not have an AggroGroup or an Animal Component on it", NPCPrefab);
                    NPCPrefabs.RemoveAt(i);
                }
            }
        }

        /* public JToken CaptureAsJToken() 
        {
            JObject state = new JObject();
            JArray npcArray = new JArray();
            JArray patrolPathArray = new JArray();

            // NPCs:
            foreach (var npc in NPCPrefabs) 
            {
                JObject npcState = new JObject();
                npcState["name"] = npc.name;

                npcState["position"] = new JObject 
                {
                    ["x"] = npc.transform.position.x,
                    ["y"] = npc.transform.position.y,
                    ["z"] = npc.transform.position.z
                };

                npcState["rotation"] = new JObject 
                {
                    ["x"] = npc.transform.rotation.x,
                    ["y"] = npc.transform.rotation.y,
                    ["z"] = npc.transform.rotation.z,
                    ["w"] = npc.transform.rotation.w
                };

                Health health = npc.GetComponentInChildren<Health>();
                if (health != null) 
                {
                    npcState["health"] = health.GetHealthPoints();
                }
                npcArray.Add(npcState);
            }

            // Patrol Paths:
            foreach (var patrolPath in patrolPaths) 
            {
                JObject patrolPathState = new JObject();
                patrolPathState["name"] = patrolPath.name;
                // Other PatrolPath properties here, if any exist and matter
                patrolPathArray.Add(patrolPathState);
            }

            state["NPCs"] = npcArray;
            state["patrolPaths"] = patrolPathArray;

            return state;
        }

        public void RestoreFromJToken(JToken state) 
        {
            JObject stateObject = (JObject)state;
            JArray npcsArray = (JArray)stateObject["NPCs"];
            JArray patrolPathArray = (JArray)stateObject["patrolPaths"];

            // Clear up the current NPCPrefabs and PatrolPath lists
            spawnedNPCs.Clear();
            patrolPaths.Clear();

            // Load all NPC AggroGroup Prefabs from the Resources Folder:
            AggroGroup[] npcPrefabs = Resources.LoadAll<AggroGroup>("NPCs");

            // Resource folder-based restoration search:
            foreach (var npcState in npcsArray) 
            {
                string npcName = npcState["name"].ToString();

                JObject positionObject = (JObject)npcState["position"];
                Vector3 position = new Vector3
                (
                    positionObject["x"].ToObject<float>(),
                    positionObject["y"].ToObject<float>(),
                    positionObject["z"].ToObject<float>()
                );

                JObject rotationObject = (JObject)npcState["rotation"];
                Quaternion rotation = new Quaternion
                (
                    rotationObject["x"].ToObject<float>(),
                    rotationObject["y"].ToObject<float>(),
                    rotationObject["z"].ToObject<float>(),
                    rotationObject["w"].ToObject<float>()
                );

                // float healthValue = npcState["health"].ToObject<float>();

                AggroGroup npcPrefab = npcPrefabs.FirstOrDefault(npc => npc.name == npcName);
                if (npcPrefab != null) 
                {
                    GameObject spawnedNPC = Instantiate(npcPrefab.gameObject, position, rotation);
                    // Health health = spawnedNPC.GetComponentInChildren<Health>();
                    // if (health != null) 
                    // {
                    //     health.SetHealthPoints(healthValue); // <-- find a way to write a 'SetHealth' function in 'Health.cs'
                    // }
                    spawnedNPCs.Add(spawnedNPC);
                }
            }

            // Scene-based PatrolPath restoration search:
            foreach (var patrolPathState in patrolPathArray) 
            {
                string patrolPathName = patrolPathState["name"].ToString();
                PatrolPath patrolPath = FindPatrolPathByName(patrolPathName);
                if (patrolPath != null) 
                {
                    patrolPaths.Add(patrolPath);
                }
            }
        }

        private PatrolPath FindPatrolPathByName(string name)
        {
            PatrolPath[] allPatrolPaths = FindObjectsOfType<PatrolPath>();
            foreach (PatrolPath patrolPath in allPatrolPaths)
            {
                if (patrolPath.name == name)
                {
                    // if you found a patrol path
                    return patrolPath;
                }
            }
            // if you can't find a patrol path
            return null;
        } */
    }
}

This topic has produced a significant amount of bugs in my code, because of it’s architecture, that I think it’s not worth losing my entire project over.

You fix something = you break another, and it’s an endless cycle (and I admit, my current architecture for my NPCs is quite complex, it’ll be a dealbreaker for me to break it now and try re-engineer it on my own)

I may address it another day, but for now it has to go, because I’m not risking my entire project over this. Fixed entities seem to be doing the job just fine. This was an accessory that was about to go further than the given space for it

If anyone wants to play around with it, here’s my code so far:

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

namespace RPG.DynamicSpawner
{
    public class DynamicNPCSpawner : MonoBehaviour//, IJsonSaveable
    {
        [SerializeField] float spawnRadius; // Radius around the player, to spawn NPCs
        [SerializeField] float checkInterval; // Interval, in seconds, to check for spawning/Despawning
        [SerializeField] int maxNPCCount; // Maximum number of NPCs to spawn, in 'checkInterval' seconds (if this value is maxed out, don't respawn anymore)

        [SerializeField] List<GameObject> NPCPrefabs; // List of NPC Prefabs to Spawn (Type 'GameObject' because even Animals can get involved down the line)
        [SerializeField] List<PatrolPath> patrolPaths; // List of Patrol Paths to follow

        private List<GameObject> spawnedNPCs = new List<GameObject>();
        private Transform playerTransform;

        private void Start()
        {
            playerTransform = GameObject.FindGameObjectWithTag("Player").transform;
            InvokeRepeating(nameof(CheckSpawningAndDespawning), 0f, checkInterval);
        }

        private void CheckSpawningAndDespawning()
        {
            SpawnNPCs();
            DespawnNPCs();
        }

        private void SpawnNPCs()
        {
            if (spawnedNPCs.Count < maxNPCCount)
            {
                // NPC SPAWNING:
                Vector3 spawnPosition = GetRandomPointWithinRadius(playerTransform.position, spawnRadius);
                GameObject NPCPrefab = NPCPrefabs[Random.Range(0, NPCPrefabs.Count)];
                GameObject spawnedNPC = Instantiate(NPCPrefab, spawnPosition, Quaternion.identity);

                // NPC PATROL PATH ASSIGNING:
                PatrolPath patrolPath = patrolPaths[Random.Range(0, patrolPaths.Count)];
                EnemyStateMachine enemyStateMachine = spawnedNPC.GetComponentInChildren<EnemyStateMachine>();
                enemyStateMachine.AssignPatrolPath(patrolPath);

                // ADDING THE NPC TO THE 'spawnedNPCs' LIST:
                spawnedNPCs.Add(spawnedNPC);
            }
        }

        private void DespawnNPCs()
        {
            // DELETE NPCs WHO ARE FAR AWAY FROM THE PLAYER (PERFORMANCE SAVIOR):
            for (int i = spawnedNPCs.Count - 1; i >= 0; i--)
            {
                var npc = spawnedNPCs[i];
                if (npc == null)
                {
                    Debug.Log($"NPC is null");
                    spawnedNPCs.RemoveAt(i);
                    continue;
                }

                var healthComponent = npc.GetComponentInChildren<Health>();
                var enemyStateMachineComponent = npc.GetComponentInChildren<EnemyStateMachine>();

                if (healthComponent == null)
                {
                    Debug.Log($"Health Component is null");
                    spawnedNPCs.RemoveAt(i);
                    continue;
                }

                if (enemyStateMachineComponent == null) 
                {
                    Debug.Log($"Enemy State Machine Component is null");
                    spawnedNPCs.RemoveAt(i);
                    continue;
                }

                // If you're not dead, and you're in combat, you can't despawn
                if (!healthComponent.IsDead() && enemyStateMachineComponent.GetLastAttacker() != null) return;

                // If you're far away from the player, you can despawn
                if (Vector3.Distance(playerTransform.position, enemyStateMachineComponent.transform.position) > spawnRadius)
                {
                    Destroy(npc);
                    spawnedNPCs.RemoveAt(i);
                }
            }
        }

        private Vector3 GetRandomPointWithinRadius(Vector3 center, float radius)
        {
            Vector3 randomPoint = Vector3.zero;
            int maxAttempts = 30; // 30 Attempts to find a valid position, at most (Give up otherwise)

            for (int i = 0; i < maxAttempts; i++)
            {
                Vector3 point = Random.insideUnitSphere * radius + center;
                point.y = center.y; // spawned NPC Height = player height

                // Make sure you don't Instantiate the character on a Rock, tree, 
                // or even underground. ONLY ON THE NAVMESHAGENT:
                if (NavMesh.SamplePosition(point, out NavMeshHit hit, radius, NavMesh.AllAreas))
                {
                    randomPoint = hit.position;
                    break; // Exit loop if a valid position is found
                }
            }
            return randomPoint;
        }

        // ONLY ACCEPT DATATYPES OF 'Animal' OR 'AggroGroup' (ensures that everything below works as expected)
        private void OnValidate()
        {
            for (int i = NPCPrefabs.Count - 1; i >= 0; i--)
            {
                GameObject NPCPrefab = NPCPrefabs[i];
                if (NPCPrefab != null && !NPCPrefab.TryGetComponent(out AggroGroup _) && !NPCPrefab.TryGetComponent(out Animal _))
                {
                    Debug.LogError($"Invalid NPC Prefab: {NPCPrefab.name} does not have an AggroGroup or an Animal Component on it", NPCPrefab);
                    NPCPrefabs.RemoveAt(i);
                }
            }
        }

        public JToken CaptureAsJToken() 
        {
            JObject state = new JObject();
            JArray npcArray = new JArray();
            JArray patrolPathArray = new JArray();

            // NPCs:
            foreach (var npc in NPCPrefabs) 
            {
                JObject npcState = new JObject();
                npcState["name"] = npc.name;

                npcState["position"] = new JObject 
                {
                    ["x"] = npc.transform.position.x,
                    ["y"] = npc.transform.position.y,
                    ["z"] = npc.transform.position.z
                };

                npcState["rotation"] = new JObject 
                {
                    ["x"] = npc.transform.rotation.x,
                    ["y"] = npc.transform.rotation.y,
                    ["z"] = npc.transform.rotation.z,
                    ["w"] = npc.transform.rotation.w
                };

                Health health = npc.GetComponentInChildren<Health>();
                if (health != null) 
                {
                    npcState["health"] = health.GetHealthPoints();
                }
                npcArray.Add(npcState);
            }

            // Patrol Paths:
            foreach (var patrolPath in patrolPaths) 
            {
                JObject patrolPathState = new JObject();
                patrolPathState["name"] = patrolPath.name;
                // Other PatrolPath properties here, if any exist and matter
                patrolPathArray.Add(patrolPathState);
            }

            state["NPCs"] = npcArray;
            state["patrolPaths"] = patrolPathArray;

            return state;
        }

        public void RestoreFromJToken(JToken state) 
        {
            JObject stateObject = (JObject)state;
            JArray npcsArray = (JArray)stateObject["NPCs"];
            JArray patrolPathArray = (JArray)stateObject["patrolPaths"];

            // Clear up the current NPCPrefabs and PatrolPath lists
            spawnedNPCs.Clear();
            patrolPaths.Clear();

            // Load all NPC AggroGroup Prefabs from the Resources Folder:
            AggroGroup[] npcPrefabs = Resources.LoadAll<AggroGroup>("NPCs");

            // Resource folder-based restoration search:
            foreach (var npcState in npcsArray) 
            {
                string npcName = npcState["name"].ToString();

                JObject positionObject = (JObject)npcState["position"];
                Vector3 position = new Vector3
                (
                    positionObject["x"].ToObject<float>(),
                    positionObject["y"].ToObject<float>(),
                    positionObject["z"].ToObject<float>()
                );

                JObject rotationObject = (JObject)npcState["rotation"];
                Quaternion rotation = new Quaternion
                (
                    rotationObject["x"].ToObject<float>(),
                    rotationObject["y"].ToObject<float>(),
                    rotationObject["z"].ToObject<float>(),
                    rotationObject["w"].ToObject<float>()
                );

                // float healthValue = npcState["health"].ToObject<float>();

                AggroGroup npcPrefab = npcPrefabs.FirstOrDefault(npc => npc.name == npcName);
                if (npcPrefab != null) 
                {
                    GameObject spawnedNPC = Instantiate(npcPrefab.gameObject, position, rotation);
                    // Health health = spawnedNPC.GetComponentInChildren<Health>();
                    // if (health != null) 
                    // {
                    //     health.SetHealthPoints(healthValue); // <-- find a way to write a 'SetHealth' function in 'Health.cs'
                    // }
                    spawnedNPCs.Add(spawnedNPC);
                }
            }

            // Scene-based PatrolPath restoration search:
            foreach (var patrolPathState in patrolPathArray) 
            {
                string patrolPathName = patrolPathState["name"].ToString();
                PatrolPath patrolPath = FindPatrolPathByName(patrolPathName);
                if (patrolPath != null) 
                {
                    patrolPaths.Add(patrolPath);
                }
            }
        }

        private PatrolPath FindPatrolPathByName(string name)
        {
            PatrolPath[] allPatrolPaths = FindObjectsOfType<PatrolPath>();
            foreach (PatrolPath patrolPath in allPatrolPaths)
            {
                if (patrolPath.name == name)
                {
                    // if you found a patrol path
                    return patrolPath;
                }
            }
            // if you can't find a patrol path
            return null;
        }
    }
}

(You might want to delete that ‘OnValidate’. It’s for my special cases)

To start with, I’m not even sure if my NPC drops are supposed to be saved or not, but they no longer are… so that’s probably one system that I accidentally lost down the line, and I have no idea how to recover it, and that’s more than enough for me to realize the risk of this system (and reversing my backups didn’t help either, so this is probably an ancient problem, which I never noticed until now)

To use it, just create an empty gameObject underneath your player, attach a rigidbody and a sphere collider on it (marked ‘isTrigger’ to true. If you’re following Brian’s third person approach, change the layer to ‘RangeFinder’ or something that won’t mess with your projectiles layer as well), and this script. Enjoy

The course code has a few bugs and race conditions relating to pickups. But the fixes usually require only small modifications (not rewrites). All documented but potentially hard to find. Here’s what I would do.

  1. Check this tag specifically. Look through each post.
  2. If previous step doesn’t work, start drafting your own question (but don’t submit) and as you type the forum system will try to find a matching existing post.
  3. Worst case post a separate question but try to do it in the appropriate topic (so it gets tagged appropriately). Describe your repro steps. E.g. was it after a save/load or after a portal or something else. Explain what you did to debug it yourself first.

If you feel you have architectural issues that result in major rewrites to the provided code course or Brian’s tips and tricks, consider these alternative explanations:

  1. You found a bug in the provided code. e.g. a missing call to Warp() Many of these are identified and solved in forum posts.
  2. You have an integration bug.

Based on what I’ve read I think you have a mix of both of the above. The way to solve both is the same (below). Following this kind of approach will serve you well even if you abandon this feature.

  1. Integrate just one small piece at a time.
  2. Then test.
  3. Add the next small piece and repeat.

Leave in test code until all aspects of a feature are fully integrated and tested. One trick I use is to prefix every Debug.Log* statement with a set of characters unique for that feature. You can then comment them out when done or uncomment them if you feel a bug has resurfaced. Unit Tests / Integration Tests are a pain in Unity (and usually a pain in any similar engine/framework). But there are some instances where they are easy to write and are the right call.

Agree with Brian here. Take a look at Diablo III for example which used procedurally generated levels and enemies quite extensively. If you save and leave the game in the middle of these levels, the restore doesn’t bring back the state of enemies or pickups. For that matter even the scene gets regenerated.

Ok so one option you have here is to make the scene terrain be the “PickupSpawner” instead. I recall a post of someone suggesting this bug I don’t know if any code was provided. I think it’s a smart idea but I haven’t tried it myself. I unfortunately can’t find the post anymore.

Let’s talk about the positives at least… yesterday I cleared a ‘Targeter’ problem that I had when I introduced this system, because vanishing whilst targeting would produce an NRE

And after a little bit of code manipulation, I think I got things to work for the time being as expected, relatively bug-free. I’ll take a break off this project for a short while and do something else, before I lose my sanity

I’m giving Subnautica (the game) a try (definitely not tryna steal some ideas here… wink wink) - seems chilly and fits my personality :stuck_out_tongue_winking_eye: - their entire bundle is currently up for less than $20

I don’t think that’s the case here, because here it’s about 90% failure rate… Something is so wrong here (and funnily enough, I’m starting to genuinely embrace this system because it adds a layer of difficulty. I don’t know though, I’m just not used to that yet)

This will probably require we tune the code to contain a list then… sounds doable, but will probably take a while of prototyping first

Anyway, I got the (Dynamic Respawning) system to a stable position (NOT THE PICKUPS… THEY’RE STILL IN SHAMBLES). The next step will be to find a way to save and restore these entities and then I believe it’ll be ready for more advanced functions

OK so… Apart from the Fixed Entity (and Dynamic Entity) NPC drops not being saved anymore for some reason (please help me with any ideas that can be the reason for this, or at least tell me if that’s the case or not, because I truly don’t remember), I got the entire Dynamic Respawning System to work exactly as I want

In other words, here’s what it does:

  1. Switch NPCs and Patrol Path Lists when a trigger is entered (this way, big biomes will have their individual creatures and what not)
  2. Spawn and Despawn, on a 3 second timer, on the NavMesh, around the player
  3. Save and Restore the NPC (using the Resources Folder approach) and Patrol Path (Scene-based name searching approach) Lists properly

And I’m quite happy with what I solved so far, regardless of how annoying it was

The next step will be to create something similar, but for the police or something… You kill too many guys, and you got cops. Stay in pursuit for too long, and that list just might get harder on you. I don’t know yet, it’s all still food for thought (but you know I’m that psychopath who might give it a go… :stuck_out_tongue_winking_eye: )

In the end, if it’s enemies who keep the game world fun and running (pedestrians you can attack, wild animals you can hunt, etc), I’ll use this Dynamic Spawning System. For important characters (like Quest Givers, Allies, etc), I’ll use the Fixed Entity

[If it’s in caps lock, it means I’m a little excited, not angry :smiley:] I JUST WANT TO KNOW IF THE PICKUPS DROPPED FROM ENEMIES SHOULD BE SAVED OR NOT, BECAUSE IF THEY DO, THEN I GOT SOME INVESTIGATING TO DO :sweat_smile:

Did you see my post above? I referenced a few forum posts you might want to look at. You are not the first person to find issues with pickups so chances are the answer exists in a prior post.

[edit] yes they should be. One thing I noticed you attempted is you tried to roll your own code to save/restore state rather than use the saving system. In doing so you may have created a completely different bug. The way the saving system works is it looks for all relevant components and calls their save/restore method. If you did your own thing it’s possible that’s not being called anymore and so you lost this capability.

Sometimes a one or two line change of code can have consequential impact. I’ve been through the whole course and integrated much of Brian’s tips-and-tricks. I can say from experience that almost everything is resolved with a small scale fix.

Privacy & Terms