Dynamic Respawning of NPCs

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

I know I’ll sound like a complete idiot for not figuring that one out earlier, but I noticed that the Unique ID on all my NPCs on the JSON Saveable Entity on my hierarchy WERE THE EXACT SAME… NO WONDER IT WON’T WORK

My bad… :sweat_smile:

Here’s what I added to fix it, in ‘JSONSaveableEntity.cs’:

        // TEST (Delete if failed)
        private static HashSet<string> usedIdentifiers = new HashSet<string>(); // I copied that from ChatGPT


        // TEST (Delete if failed)
        void OnValidate() 
        {
            if (string.IsNullOrEmpty(uniqueIdentifier) || usedIdentifiers.Contains(uniqueIdentifier)) // if I was blindly copying, I would've left the false exclamation mark before 'usedIdentifiers', xD
            {
                uniqueIdentifier = Guid.NewGuid().ToString();
                usedIdentifiers.Add(uniqueIdentifier);
                Debug.Log($"New Unique Identifier generated");
            }
        }

Earlier, I created my own unique ID (like, fill up that slot with words I want, lol), and that failed… At first, I disqualified that idea (until eventually I decided to return and try Microsoft’s Guid.NewGuid().ToString() solution, which eventually resolved the issue)

(I’m not sure if that’s robust enough or not, but I hope it is)

Then, I went and deleted them and let ‘OnValidate()’ do it’s magic. How did I get to that kind of problem, I hear you ask? Well… I was copy-pasting my NPCs around, and not checking their Unique IDs properly

LSS (Long Story Short): my Unique Identifiers weren’t so “Unique” after all, that’s it

There was something very similar to this in my original JSONSaveableEntity, just using Sam’s original code from the old SaveableEntity…

That’s where I got the idea from :stuck_out_tongue_winking_eye:

I’ll turn this into a talk for the time being, until the UI issues are all clear. When we get back to it, I’ll reverse it to an ask (or open a new topic if I lose this one. Hopefully I don’t)

@Brian_Trotter since this was turned to a talk, I’m tagging you here

OK apart from the Purse (I’m not sure about this one yet), I think I got my UI issues all fixed, so let’s get back to this topic:

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>(); // Each time you spawn an NPC into the game, this private list contains it
        private Transform playerTransform; // Transform position of the player

        // FOR CASES WHEN THE PLAYER EXITS A TRIGGER, AND DOESN'T KNOW WHAT TO DO NEXT
        [SerializeField] List<GameObject> defaultNPCPrefabs;
        [SerializeField] List<PatrolPath> defaultPatrolPaths;

        // PRIVATE
        private PatrolPath patrolPathToAssign;

        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:
                patrolPathToAssign = patrolPaths[Random.Range(0, patrolPaths.Count)]; // Scene-based
                EnemyStateMachine enemyStateMachine = spawnedNPC.GetComponentInChildren<EnemyStateMachine>(); // Prefab-based
                enemyStateMachine.AssignPatrolPath(patrolPathToAssign);

                // For Patrol Paths, "EnemyIdleState" also has a mechanic to ensure they're
                // all assigned one, if they got missed out here

                // 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");
                    Destroy(npc);
                    spawnedNPCs.RemoveAt(i);
                    continue;
                }

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

                // IF THEY'RE DEAD, DON'T TRY TO DESPAWN THEM
                if (healthComponent == null)
                {
                    Debug.Log($"Health Component is null");
                    Destroy(npc);
                    spawnedNPCs.RemoveAt(i);
                    continue;
                }

                // IF THEY'RE NOT AN ENEMY, DON'T TRY TO DESPAWN THEM EITHER
                if (enemyStateMachineComponent == null) 
                {
                    Debug.Log($"Enemy State Machine Component is null");
                    Destroy(npc);
                    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 dead, let RespawnManager's HideCharacter function take control of the situation
                if (healthComponent != null && healthComponent.IsDead()) return; // once you despawned, the Dynamic NPC Respawner will take care of replacing you

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

        public PatrolPath GetPatrolPathToAssign() 
        {
            return patrolPathToAssign;
        }

        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, WITH THE LAYER TERRAIN:
                if (NavMesh.SamplePosition(point, out NavMeshHit hit, radius, NavMesh.AllAreas))
                {
                    randomPoint = hit.position;
                    break; // Exit loop if a valid position is found
                }
            }
            return randomPoint;
        }

        public void SetPatrolPaths(List<PatrolPath> patrolPaths)
        {
            // Used in 'AreaDynamicSpawnerInformationSwapper.cs'
            this.patrolPaths = patrolPaths;
        }

        public void SetNPCPrefabs(List<GameObject> NPCPrefabs)
        {
            // Used in 'AreaDynamicSpawnerInformationSwapper.cs'
            this.NPCPrefabs = NPCPrefabs;
        }

        public void ResetToDefaultSettings() 
        {
            NPCPrefabs = defaultNPCPrefabs;
            patrolPaths = defaultPatrolPaths;
        }

        // 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 (Resources-folder based approach)
            foreach (var npc in NPCPrefabs) 
            {
                JObject npcState = new JObject();
                npcState["name"] = npc.name;

                npcArray.Add(npcState);
            }

            // Patrol Paths (Scene-based approach)
            foreach (var patrolPath in patrolPaths) 
            {
                JObject patrolPathState = new JObject();
                patrolPathState["name"] = patrolPath.name;

                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 NPC Prefabs and PatrolPath Lists
            NPCPrefabs.Clear();
            patrolPaths.Clear();

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

            // Restore NPC Prefabs (Asset-based, hence we're using the 'Resources' folder)
            foreach (var npcState in npcsArray) 
            {
                string npcName = npcState["name"].ToString();

                AggroGroup npcPrefab = npcPrefabs.FirstOrDefault(npc => npc.name == npcName);
                if (npcPrefab != null) 
                {
                    NPCPrefabs.Add(npcPrefab.gameObject);
                }
            }

            // Restore Patrol Paths (Scene-based approach)
            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;
        }
    }
}

So one of my main problems with this script, is that it can spawn NPCs literally anywhere that has no NavMesh Blocker, and this includes on top of a tower, the width of a stick, xD

Now, I have 2 approaches to fix this:

  1. Either I fill my map with NavMesh blockers literally everywhere, and god knows what kind of performance issues this will lead to

OR

  1. Use NavMesh Blockers intelligently, and make sure the Dynamic NPCs only land on stuff with the Layer called “Terrain” (i.e: Only on land), and only use NavMesh blockers when I want to block entrance at certain areas

Personally, I want the second approach, but I tried it once and the code didn’t work at all. So, how do I ensure that the NPCs only spawn on anything with LayerMask “Terrain”?

And this Dynamic NPC Spawner is really REALLY breaking my Enemy Chasing State for some reason, and I can’t tell why would they sometimes freeze in position and point at the player. It’s like it’s exposing a bug just because the demands of that script are quite high, I’m not sure, but I’ll get back to this bug later

Tower should have a NavMeshModifier. Anything that winds up with NavMesh on a roof should get a blocker, especially if you’re going to be using SamplePosition.

That being said, ideally we want this to always land on a Terrain position, so…

[SerializeField] LayerMask terrainMask; //Set to terrain's layer in Inspector

if(NavMesh.SamplePosition(point, out NavMeshHit hit, radius, terrainMask))

should do the trick.

Wouldn’t that hurt performance? I mean if we use it a lot, it will hurt performance… right?

you mean there’s better ways?

tried it before, nobody spawns. Tried it again now, still nobody spawns (and yes, I set it up in the inspector and made sure all systems go)

Guess I gotta be smart about the Nav Mesh Modifier Volumes then, although honestly I’m not a huge fan of this approach

No, because when the NavMeshSurface bakes, it’s already looking at the object, deciding whether or not it needs to be walkable.

Not for what you’re doing. The only way to make sure you’re spawning a character dynamically onto a NavMesh is the same as we do it with RandomDropper. Sample the NavMesh and get a valid location.

As a rule, I prefer Modifier over ModifierVolume, as you can wind up setting areas you didn’t intend.

A NavMeshModifier only reacts to that GameObject’s collider or geometry.

Handy tip: I put the NavMeshModifiers in the prefabs themselves. Easier to set up one building than the 10 copies of same building in the scene.

Privacy & Terms