Dynamic Respawning of NPCs

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.

yes I did, just wasn’t sure of the answer

great, time to start investigating what went wrong then :slight_smile:

  1. Get this fixed (but converting the terrain itself to a PickupSpawner seems to be a rough solution, because now I have to convert those pickups into a stored list and what not… I might give this a go as a final attempt)
  2. Move on to the next problem

Fun fact: One way I typically like to go around race conditions when I find them, is through a 0.1 second Coroutine Delay. I had this once when I was in the early stages of developing my OnAnimal state machine, and it never complained since

For a start, the player can save them, but the enemies can’t save their drops…

Can we go through the scripts that have to exist in the game world, one more time, to make sure that this system works? Maybe I deleted something by accident?

Enemy drops from normal enemies… i.e. in the scene heirarchy to begin with should be saving their drops. Review the RandomDropper lesson in the Integration section of Inventory.

Eeep, you’re starting to sound like me and some warnings I may have given quite a while back.

Best practice when integrating new ideas…

  • Ensure everything else is working properly (test it, have a 5 year old play it to break it)
  • Commit project (git/source code)
  • Integrate new idea
  • Test new idea to see if it works
  • Go back and test everything else again to ensure you didn’t break anything.

Outside of environments like Unity, it’s common to use Unit testing. Unity actually HAS Unit testing, but it’s not always practical for runtime (though it’s very good at testing fixed functions that have predictable results). But there are other ways of testing including Debugs and rigorous playtesting.

The important thing is that you focus on one thing at a time… Especially once you’ve ventured far outside of course code territory.

Should have read IJsonSaveables… but the idea is you’re already creatinga JObject, just gather all the data from the IJsonSaveables on the dynamic character you’re saving and store that resulting JObject in your Capture as [“state”]. When you Restore, reverse this. The code for this is quite literally inside of JsonSaveableEntity.

[SOLVED: FEEL FREE TO IGNORE]

[THIS IS A BIT OF A LENGTHY COMMENT, BUT PLEASE BEAR WITH ME. I MANAGED TO TRACK THE SOURCE DOWN, I JUST DON’T KNOW HOW TO SOLVE IT]

@Brian_Trotter (sorry for dragging you into this) Quick hot update:

After a little bit of research, I came to the conclusion that ‘ItemDropper.cs’ has the saving system that is responsible for the drops done by the NPC (and it makes sense, because the RandomDropper, which does the dropping, inherits from it)

and after a little bit of further debugging, I noticed two major flaws in my ‘RestoreFromJToken’, which is this function:

        public void RestoreFromJToken(JToken state) 
        {
            if (state is JArray stateArray) {

                int currentScene = SceneManager.GetActiveScene().buildIndex;
                IList<JToken> stateList = stateArray;
                ClearExistingDrops();

                foreach (var entry in stateList) {

                    if (entry is JObject dropState) {

                        IDictionary<string, JToken> dropStateDict = dropState;
                        int scene = dropStateDict["scene"].ToObject<int>();
                        InventoryItem item = InventoryItem.GetFromID(dropStateDict["id"].ToObject<string>());
                        int number = dropStateDict["number"].ToObject<int>();
                        Vector3 location = dropStateDict["location"].ToVector3();

                        Debug.Log($"(ItemDropper) Restoring item: {item.name}, number: {number}, scene: {scene}, location: {location}");

                        if (scene == currentScene) {

                            SpawnPickup(item, location, number);

                        }

                        else {

                            var otherDrop = new otherSceneDropRecord();
                            otherDrop.id = item.GetItemID();
                            otherDrop.number = number;
                            otherDrop.location = location;
                            otherDrop.scene = scene;
                            otherSceneDrops.Add(otherDrop);

                        }

                    }

                }

            }

        }
  1. The “(ItemDropper)” Debug I threw into Restoring gets called 5 times (assuming it gets called, that is), although the items in the end appear on the ground get called once. I’m not sure why yet, but it’s there. Any idea what this indicates to…?!

  2. Not every NPC gets that restore called, and I have absolutely zero clue why (and the ones that do, it may or may not happen). I also threw a debug in capturing, and I believe it does its job just fine. It’s the restoring that has some issues

If it helps, I do also have to mention that this ONLY occurs with Enemy NPCs, not with the player

I read a little further from the link @Cat_Hill provided earlier, and I noticed that there could be a race condition introduced somewhere, responsible for this. I’m honestly not very sure though

From a glimpse, is this a bug from the course code, or did I probably do something dumb elsewhere down the line? Because looking at it, especially that the restore only happens on a JObject called ‘dropState’, which only has been named this way in this script, out of my entire codebase), I don’t think I ever touched that code ever again down the line to be honest


Edit 1: 3 hours later, and here’s what I noticed. This is exactly where the code fails:

foreach (var entry in stateList)

the foreach loop, at the code above, is exactly where the restoring may or may not work, and I have absolutely no idea why!

Edit 2: the foreach loop doesn’t get entered because my ‘stateArray’ is empty, before it even gets fed to the ‘stateList’ JToken List. What does this mean, and how do I solve it?

Edit 3: I did a little further research, and threw in some debugs in my ‘CaptureAsJToken()’:

public JToken CaptureAsJToken() {

            RemoveDestroyedDrops();
            var drops = MergeDroppedItemsWithOtherSceneDrops();
            JArray state = new JArray();
            IList<JToken> stateList = state;

            Debug.Log($"(ItemDropper) Number of drops to capture: {drops.Count}");

            foreach (var drop in drops) {

                JObject dropState = new JObject();
                IDictionary<string, JToken> dropStateDict = dropState;
                dropStateDict["id"] = JToken.FromObject(drop.id);
                dropStateDict["number"] = drop.number;
                dropStateDict["location"] = drop.location.ToToken();
                dropStateDict["scene"] = drop.scene;
                stateList.Add(dropState);

                Debug.Log($"(ItemDropper) Capturing Item: {drop.id}, number: {drop.number}, scene: {drop.scene}, location: {drop.location}");

            }

            Debug.Log($"(ItemDropper) Captured State: {state.ToString()}");
            return state;

        }

and, as it turns out, this Debug line (right before returning the state):

            Debug.Log($"(ItemDropper) Captured State: {state.ToString()}");
            return state;

returns as an empty array, which means it does not even get saved to begin with, eventually leading to the disasters down the line…

Edit 4: I went to the core of the problem (I think so), and it turns out that ‘MergeDroppedItemsWithOtherSceneDrops()’ does add the results, as seen in this function:

        List<otherSceneDropRecord> MergeDroppedItemsWithOtherSceneDrops() {

            List<otherSceneDropRecord> result = new List<otherSceneDropRecord>();
            result.AddRange(otherSceneDrops);

            foreach (var item in droppedItems) {

                otherSceneDropRecord drop = new otherSceneDropRecord();
                drop.id = item.GetItem().GetItemID();
                drop.number = item.GetNumber();
                drop.location = item.transform.position;
                drop.scene = SceneManager.GetActiveScene().buildIndex;
                result.Add(drop);

                Debug.Log($"(ItemDropper) Adding Drop: {drop.id}, number: {drop.number}, scene: {drop.scene}, location: {drop.location}");
            }

            Debug.Log($"(ItemDropper) Total Drops: {result.Count}");
            return result;

        }

through this debug:

                Debug.Log($"(ItemDropper) Adding Drop: {drop.id}, number: {drop.number}, scene: {drop.scene}, location: {drop.location}");

(This debug works perfectly fine)

HOWEVER, This debug, at the end of the function:

            Debug.Log($"(ItemDropper) Total Drops: {result.Count}");
            return result;

returns a total drop count of zero

SO… the ‘Merge’ function has a false result, which leads to a false result in capturing, eventually leading to a false result in restoring

and that’s where my problem is currently at… How do I fix this? (I wish I knew about the ‘new System.Diagnostics.StackTrace()’ function earlier…!)

If needed, here is my entire ‘ItemDropper.cs’ script:

using System.Collections.Generic;
using UnityEngine;
using GameDevTV.Saving;
using UnityEngine.SceneManagement;
using Newtonsoft.Json.Linq;
using Unity.VisualScripting;

namespace GameDevTV.Inventories
{
    /// <summary>
    /// To be placed on anything that wishes to drop pickups into the world.
    /// Tracks the drops for saving and restoring.
    /// </summary>
    public class ItemDropper : MonoBehaviour, IJsonSaveable //, ISaveable
    {
        // STATE
        private List<Pickup> droppedItems = new List<Pickup>();
        private List<DropRecord> otherSceneDroppedItems = new List<DropRecord>();

        // PUBLIC

        /// <summary>
        /// Create a pickup at the current position.
        /// </summary>
        /// <param name="item">The item type for the pickup.</param>
        /// <param name="number">
        /// The number of items contained in the pickup. Only used if the item
        /// is stackable.
        /// </param>
        public void DropItem(InventoryItem item, int number)
        {
            SpawnPickup(item, GetDropLocation(), number);
            Debug.Log($"DropItem being called from {(new System.Diagnostics.StackTrace()).GetFrame(1).GetMethod().Name}");
        }

        /// <summary>
        /// Create a pickup at the current position.
        /// </summary>
        /// <param name="item">The item type for the pickup.</param>
        public void DropItem(InventoryItem item)
        {
            SpawnPickup(item, GetDropLocation(), 1);
            Debug.Log($"DropItem being called from {(new System.Diagnostics.StackTrace()).GetFrame(1).GetMethod().Name}");
        }

        // PROTECTED

        /// <summary>
        /// Override to set a custom method for locating a drop.
        /// </summary>
        /// <returns>The location the drop should be spawned.</returns>
        protected virtual Vector3 GetDropLocation()
        {
            return transform.position;
        }

        // PRIVATE

        public void SpawnPickup(InventoryItem item, Vector3 spawnLocation, int number)
        {
            var pickup = item.SpawnPickup(spawnLocation, number);
            droppedItems.Add(pickup);
        }

        [System.Serializable]
        private struct DropRecord
        {
            public string itemID;
            public SerializableVector3 position;
            public int number;
            public int sceneBuildIndex;
        }

        /* object ISaveable.CaptureState()
        {
            RemoveDestroyedDrops();
            var droppedItemsList = new List<DropRecord>();
            int buildIndex = SceneManager.GetActiveScene().buildIndex;
            foreach (Pickup pickup in droppedItems)
            {
                var droppedItem = new DropRecord();
                droppedItem.itemID = pickup.GetItem().GetItemID();
                droppedItem.position = new SerializableVector3(pickup.transform.position);
                droppedItem.number = pickup.GetNumber();
                droppedItem.sceneBuildIndex = buildIndex;
                droppedItemsList.Add(droppedItem);
            }
            droppedItemsList.AddRange(otherSceneDroppedItems);
            return droppedItemsList;
        }

        void ISaveable.RestoreState(object state)
        {
            var droppedItemsList = (List<DropRecord>)state;
            int buildIndex = SceneManager.GetActiveScene().buildIndex;
            otherSceneDroppedItems.Clear();
            foreach (var item in droppedItemsList)
            {
                if (item.sceneBuildIndex != buildIndex)
                {
                    otherSceneDroppedItems.Add(item);
                    continue;
                }
                var pickupItem = InventoryItem.GetFromID(item.itemID);
                Vector3 position = item.position.ToVector();
                int number = item.number;
                SpawnPickup(pickupItem, position, number);
            }
        } */

        class otherSceneDropRecord {

            public string id;
            public int number;
            public Vector3 location;
            public int scene;

        }

        private List<otherSceneDropRecord> otherSceneDrops = new List<otherSceneDropRecord>();

        List<otherSceneDropRecord> MergeDroppedItemsWithOtherSceneDrops() {

            List<otherSceneDropRecord> result = new List<otherSceneDropRecord>();
            result.AddRange(otherSceneDrops);

            foreach (var item in droppedItems) 
            {
                otherSceneDropRecord drop = new otherSceneDropRecord();
                drop.id = item.GetItem().GetItemID();
                drop.number = item.GetNumber();
                drop.location = item.transform.position;
                drop.scene = SceneManager.GetActiveScene().buildIndex;
                result.Add(drop);

                Debug.Log($"(ItemDropper) Adding Drop: {drop.id}, number: {drop.number}, scene: {drop.scene}, location: {drop.location}");
            }

            Debug.Log($"(ItemDropper) Total Drops: {result.Count}");
            return result;

        }

        public JToken CaptureAsJToken() {

            RemoveDestroyedDrops();
            var drops = MergeDroppedItemsWithOtherSceneDrops();
            JArray state = new JArray();
            IList<JToken> stateList = state;

            Debug.Log($"(ItemDropper) Number of drops to capture: {drops.Count}");

            foreach (var drop in drops) {

                JObject dropState = new JObject();
                IDictionary<string, JToken> dropStateDict = dropState;
                dropStateDict["id"] = JToken.FromObject(drop.id);
                dropStateDict["number"] = drop.number;
                dropStateDict["location"] = drop.location.ToToken();
                dropStateDict["scene"] = drop.scene;
                stateList.Add(dropState);

                Debug.Log($"(ItemDropper) Capturing Item: {drop.id}, number: {drop.number}, scene: {drop.scene}, location: {drop.location}");

            }

            Debug.Log($"(ItemDropper) Captured State: {state.ToString()}");
            return state;

        }

        private void ClearExistingDrops() {

            foreach (var oldDrop in droppedItems) {

                if (oldDrop != null) Destroy(oldDrop.gameObject);

            }

            otherSceneDrops.Clear();

        }

        public void RestoreFromJToken(JToken state) 
        {
            Debug.Log($"(ItemDropper) Restoring State started"); // SUCCESS

            if (state == null) 
            {
                Debug.Log($"(ItemDropper) state is null");
                return;
            }

            if (state is JArray stateArray) 
            {
                Debug.Log($"(ItemDropper) stateArray entered with {stateArray.Count} entries"); // SUCCESS
                int currentScene = SceneManager.GetActiveScene().buildIndex;
                IList<JToken> stateList = stateArray;

                Debug.Log($"(ItemDropper) stateArray: {stateArray.ToString()}");

                ClearExistingDrops();

                if (stateArray.Count == 0) 
                {
                    Debug.Log($"(ItemDropper) stateArray is empty"); // THIS IS THE REASON WHY THE STATELIST IS EMPTY, AND NO RESTORING OCCURS
                    return;
                }

                foreach (var entry in stateList) {
                    Debug.Log($"(ItemDropper) stateList entry: {entry.ToString()}"); // FAILED (it's why the Restore of the pickups fails)
                    if (entry is JObject dropState) {

                        IDictionary<string, JToken> dropStateDict = dropState;
                        int scene = dropStateDict["scene"].ToObject<int>();
                        InventoryItem item = InventoryItem.GetFromID(dropStateDict["id"].ToObject<string>());
                        int number = dropStateDict["number"].ToObject<int>();
                        Vector3 location = dropStateDict["location"].ToVector3();

                        Debug.Log($"(ItemDropper) Restoring item: {item.name}, number: {number}, scene: {scene}, location: {location}");

                        if (scene == currentScene) {

                            SpawnPickup(item, location, number);

                        }

                        else 
                        {
                            var otherDrop = new otherSceneDropRecord();
                            otherDrop.id = item.GetItemID();
                            otherDrop.number = number;
                            otherDrop.location = location;
                            otherDrop.scene = scene;
                            otherSceneDrops.Add(otherDrop);
                        }

                    }
                    else Debug.Log($"(ItemDropper) Unexpected entry type: {entry.GetType().ToString()}");
                }

            }

        }

        /// <summary>
        /// Remove any drops in the world that have subsequently been picked up.
        /// </summary>
        private void RemoveDestroyedDrops()
        {
            var newList = new List<Pickup>();
            foreach (var item in droppedItems)
            {
                if (item != null)
                {
                    newList.Add(item);
                }
            }
            droppedItems = newList;
        }
    }
}

Privacy & Terms