Dynamic Respawning of NPCs

Alrighty so… here comes another big topic that I’m trying to tackle

I’m trying to develop a Dynamic Respawner in my game. Essentially, the idea is that when the player enters a radius or something, NPCs that feel lively (I’m still working on that lively part since day one… it gets better over time. Let’s put that aside for now, that’s not my goal from this post) should spawn… Those triggers will be really huge, like the size of biomes

Before we start, I believe that it might be important to mention that the hierarchy of my NPCs are currently as follows:

  1. AggroGroup on the top, so if they’re not part of any group, they’re in their own group. It’s essential for my game to function properly at this point in time

  2. RespawnManager. This one is responsible for hiding dead enemies, and respawning enemies when their timer is done. It also holds the current conversation of NPCs that you can invite and expel through dialogue, and the reason why it’s handled here is because it was my easy way out of having to save the conversation and getting this system to work back then, when inviting and expelling NPCs from my groups

  3. The enemy himself

Anyway, here’s what I’m trying to do (and I believe it’s best if @Brian_Trotter sees this whenever he’s feeling better, and possibly @Cat_Hill join in as well (I went through this post of yours, but because our architecture is quite different, I think it’s best I re-write the system from scratch on my own :slight_smile:), because I believe he has experience with this topic, or he can guide me to posts where he discussed about this in more detail, and @bixarrio if he’s interested, xD):

  1. Dynamic Respawn. Essentially, when the player enters the trigger, spawn/instantiate an enemy (I believe it’s best that if we’re going the List path, that we accept “AggroGroup” type of NPCs (for my special case at least), since that’ll be the head of those NPCs), and when he quits the trigger, they vanish (to save resources). Obviously, for every biome in the game, a new set of NPCs will exist, to make sure that there’s progression

  2. Patrol Paths: Needless to say, those NPCs need patrol paths that stay on the NavMesh and follow along with specific paths. I’m not sure if creating random patrol paths for that will be important or not, but… yeah, Patrol Paths that take in from an array of patrol paths will be essential for this

  3. If possible, get the NPCs to spawn and despawn around the player, and everywhere else really doesn’t matter. This is an ideal scenario, but I’m not sure if it’s possible or not

I’m not sure if I need anything else, but I’ll add on this topic if I do

I recently implemented this tutorial to my most advanced character, but I can’t tell if it works or not, but… so far no problems at all

SO… What’s the best way to go around this? (I believe I have code to get started on this, I’m just not sure on what’s the best way to do that, hence why I’m here today :slight_smile:)

If you got any better ideas, please let me know

Safe to say my Dynamic Entity Saving System didn’t work well (at all, whether I’m spawning the enemy with his AggroGroup (which I’ll be doing for my game to run safely) or my EnemyStateMachine as is)… Something is extremely wrong in my setup :sweat_smile:

I created a ‘Dynamic Character Configuration’ of my character, but… the following script which is supposed to be doing the Dynamic Spawning:

using System.Collections.Generic;
using RPG.Control;
using UnityEngine;
using RPG.States.Enemies;
using RPG.Animals;
using RPG.Combat;
using UnityEngine.AI;

namespace RPG.DynamicSpawner
{
    public class DynamicNPCSpawner : MonoBehaviour
    {
        [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);

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

        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 'EnemyStateMachine'
        private void OnValidate()
        {
            for (int i = NPCPrefabs.Count - 1; i >= 0; i--)
            {
                GameObject NPCPrefab = NPCPrefabs[i];
                if (NPCPrefab != null && !NPCPrefab.TryGetComponent(out AggroGroup _ /* don't care about the name */) && !NPCPrefab.TryGetComponent(out Animal _ /* don't care about the name */))
                {
                    Debug.LogError($"Invalid NPC Prefab: {NPCPrefab.name} does not have an EnemyStateMachine or an Animal Component on it", NPCPrefab);
                    NPCPrefabs.RemoveAt(i);
                }
            }
        }
    }
}

Does not accept ‘DynamicCharacterConfiguration’ Scriptable Objects as an input. How do I go around this?

Essentially, I just want to be able to save those dynamically spawned NPCs, but something is wrong with my setup.

Can we please review, one more time, exactly how the setup is supposed to happen? I know I should’ve referred the ‘DynamicCharacterConfiguration’ here, but… the code above can’t accept it because it’s an SO

I followed all the steps in this tutorial, but to no avail

Also a heads-up of the steps I took, on a sample character of my Dynamic NPCs:

  1. Dynamic Character Saving setup in the scene:

  2. Dynamic Entity replacing my JSON Saveable Entity:

  3. (And I have no clue how to properly use this), the “DynamicCharacterConfiguration.cs” Scriptable Object Setup:

Well you have a lot here so I’m not sure I can address all of it. I’ll offer one general bit of advice and also say that my previously posted code will probably do more than you expect. Much of it won’t be applicable and other parts will need an overhaul.

The trick is in how you break down your problem

Break down your desired end result into a series of smaller problems. Start with something basic that gives you a tiny foundational piece of what you want. Each feature should build on that.
Now… It’s one thing to say this and another thing to actually do it. You will need some degree of engineering savvy to construct your code in such a way to leave easy hooks for those future features.

  • In my case it was easy for me to break down my problems into logical chunks but I lacked the engineering experience to efficiently turn everything into working code. The more experience I had the easier it was to know where to begin and what code/architecture dead ends to avoid.

Regardless - Based on what you described here is one possible sequence of steps:

  • When the player enters a DynamicSpawnZone, output a Debug.Log statement.
  • When the player enters a DynamicSpawnZone, spawn the enemies. No aggrogroup. No patrol paths. Just spawn them. Keep it as simple as possible
    • Auto-assign enemies into an aggrogroup of 1 if your system depends on the existence of aggrogroups.
  • Add some short term hack for saving of state to make sure you are not continuously spawning every single frame.
  • Add whatever conditional triggering logic you want to have. You can use the IDynamicSpawnTrigger interface to simplify this for you.
    • Note that even though I used Quests, the interface makes no requirement to use Quests for your trigger. It’s generic enough that the DynamicSpawnZone can be triggered based on whatever logic you want.
  • Remove the hack above. Save your “hasSpawned” state somewhere appropriate so that enemies are not continuously spawned / not spawned when you don’t want them to be. Note that I put this in QuestStatus (as it already is designed for the purpose of saving state). You will need to save it somewhere applicable for your use case.
  • Add whatever respawn logic you want to have.
  • Add whatever cleanup logic you want to have to remove previously spawned enemies
  • add support for spawning other entities like NPCs.
  • add support for random patrol paths (depending on your level design this could be easier than associating each enemy with the applicable static path)
    • I started with random paths computed on the fly. I eventually added heuristics to make the paths not fully random. It is biased. IOW enemies are more likely look in certain directions and less likely to look in other directions. But I added these heuristics only after getting random to work.
  • add support for multi-entity aggrogroups.

What you can see from the above is that I’m not trying to solve everything in one go. Depending on your skillset, constraints on your existing architecture, and what you want to accomplish a different sequencing of the features may be more applicable. I posted the above for inspiration. It doesn’t mean it’s right.

Also my post you linked to contains a lot of sample code again for inspiration.

Perhaps to summarize: come up with a mini-project plan. write it down. Then go down the plan step by step until you get stuck. If you get stuck, try skipping that step and going to the next item on the list.

my degree is in Engineering, so there’s that :stuck_out_tongue_winking_eye:

same here :slight_smile:

For me personally, my approach is a little different than yours. I don’t want to waste so much time on this topic, because I got a few final large topics to try and tackle in the near future, so I decided to make NPCs that keep the game world alive dynamic (but they also must be saveable) and the important NPCs, the ones that give quests, bosses, etc… Fixed entities, so as to make sure that they don’t get blended in with the mess of the crowd

As for how will I work around this, I decided to take a different approach than yours (no offence meant, it’s just easier for me to understand with my own approach), where I create a small, empty gameObject around the player, and use that as a trigger point. From there, fire a radius around the player, and every 3 seconds or so, fire the spawn and despawn functions if the player is not around. You can use a Coroutine or an InvokeRepeating function for that. Personally, I went with the latter

We will also keep massive triggers, the sizes of biomes (will break them down in the future), on each biome, to ensure that the list of spawnable NPCs will only spawn specific NPCs at specific areas, no more and no less

The list of spawnable entities will accept either Enemy NPCs, or Animals. I’ll deal with the frequency of each one later

That’s… kinda the plan I’m trying to go with, but first I need to make sure that these Dynamic Entities are saveable, and right now, for some reason, it’s not working for me, and I’m waiting for Brian to return and see if he can tell us why :slight_smile:

@Brian_Trotter I realize this is quite a long thread so please respond when you feel up to it.
Its one that escaped my radar

1 Like

Thank you Marc!

I’m prototyping in the meanwhile (but I left my house for the day… I needed a day off before I mentally collapse :sweat_smile:), and it was an absolute disaster before I left

I really do need Brian to hop on board with this one whenever he can, because this one is quite a tough topic

No offense taken. I really just wanted to offer some inspiration for how to break down the problem. Just note my approach is strikingly similar to yours. I also have a triggering collider around the player and if I understand your terminology correctly I also have “massive biomes” in the scenes as well. I think the only difference is that I use an Update call for the MB associated with each “biome” whereas you use InvokeRepeating probably on some player-centered MB. Especially if you have multiple biomes in a scene, your approach makes a lot of sense. The rest of it sounds very similar.

In general, having multiple different questions (especially different types of questions) in a post make it VERY hard for anyone to respond to. Assuming my answer below doesn’t address it, I would keep this part of your question separate from your post.

Well for one you want to replace GameObject in [SerializeField] List<GameObject> NPCPrefabs with DynamicCharacterConfiguration

You also don’t want to do this GameObject spawnedNPC = Instantiate(NPCPrefab, spawnPosition, Quaternion.identity);

Instead you should be using something like
dynamicSavingComponent.CreateDynamicEntity(config, spawnPosition, new Vector3(0, 0, 0));

Take a look at my code for DynamicSpawnZone to see how I interfaced with Brian’s code. You will be referencing primarily DynamicEntity and DynamicCharacterConfiguration and DynamicSaving and NOT GameObject. If you need the gameobject you will need to do something like entity.gameobject.

If Brian doesn’t return today, I’ll most likely go through your approach again tomorrow. I’m outside the city where my laptop is at for the night :grinning_face_with_smiling_eyes: (man needs a break too)

But yes, I replaced my original code with a ‘dynamicSaving.CreateDynamicEntity’, and as far as I can recall, they pile up vertically as they spawn on the exact same point with my new coin. Still haven’t figured out how to connect my AggroGroup dynamic entities (my hierarchy is a little complex) to my patrol paths in dynamicSaving just yet

For now, I know it’s a buggy mess, but I can’t do much about it at this moment (I’m away from home. No spider man movie puns intended here :stuck_out_tongue_winking_eye:)

Ignore my earlier response here as I just looked at your code and it looks like you have some measures in place to prevent infinite spawning.

What do you mean however by “exact same point with my new coin?” What happens if you try to isolate the problem by getting rid of the association to a patrol path? Or if that doesn’t work just Debug.Log the location where you attempted to spawn to further isolate the root cause.

Comment out/simplify your code until you have isolated the piece that is causing problems.

Yup. That’s normal!

OK this part is really important.

You’ll notice one thing Sam/Rick do is each video implements some bit of functionality. But at the end of the video you can tell there are some REALLY obvious bugs. Sam/Rick address the bugs one by one video by video. (As opposed to ending each and every video with bug free code). And sometimes bugs are hard to spot so he’ll have a video dedicated just to fixing accumulated bugs. And sometimes they’re even harder to spot and found after the course is done so then there’s Brian to the rescue.

Sam and Rick provide a fantastic example for how you could approach a major project or a major feature set (there are alternate ways as well). They don’t articulate this approach explicitly however (it’s just implicit in the course). Before recording these videos the instructors I have to imagine they did some project planning in advance. You as the student don’t see that explicit planning step and that’s really my only major critique of these course. It’s possible to get an impression that you can just start coding just like Sam does but very few people can do that in the real world.

With the list of bullet points I gave you I was attempting to give you a running start on putting a plan together.

Exact same point with my new code* - that was a typo on my. Behalf. My bad :sweat_smile:

We need to duplicate him. He’s too valuable to leave us alone :stuck_out_tongue_winking_eye:

I’ll read the rest in the morning. It’s 3 AM here, and I’m still not at home. Will probably be home in 4 hours, but I still am convinced having Brian around can potentially simplify this problem

His instructions are usually really direct and make starting to work on these systems significantly easier

Not to mention that the saving system is his creation, so nobody really understands this better than him

As soon as I’m home as well, I’ll show you a copy of the code that I currently have. It’s significantly different than the one above, but it doesn’t work yet :sweat_smile:

The script itself is set to accept GameObjects, not ScriptableObjects. Simply no way around that, especially because you want to be able to instantiate it. You’ve doubled down on this with the OnValidate() enforcing it having either the AggroGroup or Animal component on it.

Your DynamicCharacterConfiguration should have the prefab to spawn, and you should be able to adjust this to also accept your animal characters… and then instead of keeping a List NPCPrefabs, you could keep a List, and when you spawn, get the prefab from the CharacterConfiguration.

I see you’ve already gotten that far…

Since this is the script that is managing the Dynamic characters, if you need saving, This scripts GameObject will need the JsonSaveableEntity, and this script will be responsible for capturing and restoring the indivisual characters.

This means that in CaptureState, you’ll need an array with the character’s config (use the same methodology we use with an Inventory Item), and an index into the list of PatrolPaths to represent the patrol path. You’ll also have to capture anything on the individual character.

Then in RestoreState, you’ll have to get the correct configuration, spawn the character, and assign the correct Patrol Path by looking up it’s index in your list of patrol paths.

I’ll leave my most recent code here for the time being whilst I go and prototype with your suggestions on my original code (which is in post #2 on this topic):

using System.Collections.Generic;
using UnityEngine;
using RPG.Control;
using RPG.States.Enemies;
using RPG.Animals;
using UnityEngine.AI;
using RPG.Dynamic;
using RPG.Combat;

namespace RPG.DynamicSpawner
{
    public class DynamicNPCSpawner : MonoBehaviour
    {
        [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

        [SerializeField] List<DynamicCharacterConfiguration> npcConfigurations; // List of NPC configurations to spawn
        [SerializeField] List<PatrolPath> patrolPaths; // List of patrol paths to follow

        private List<DynamicEntity> spawnedNPCs = new List<DynamicEntity>();
        private Transform playerTransform;
        private DynamicSaving dynamicSaving;

        private void Start()
        {
            playerTransform = GameObject.FindGameObjectWithTag("Player").transform;
            dynamicSaving = FindObjectOfType<DynamicSaving>();
            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);
                if (spawnPosition == Vector3.zero) return;

                DynamicCharacterConfiguration npcConfig = npcConfigurations[Random.Range(0, npcConfigurations.Count)];
                DynamicEntity spawnedNPC = dynamicSaving.CreateDynamicEntity(npcConfig, spawnPosition, Quaternion.identity.eulerAngles);

                if (spawnedNPC != null)
                {
                    // NPC PATROL PATH ASSIGNING:
                    PatrolPath patrolPath = patrolPaths[Random.Range(0, patrolPaths.Count)];
                    EnemyStateMachine enemyStateMachine = spawnedNPC.GetComponentInChildren<EnemyStateMachine>();
                    if (enemyStateMachine != null)
                    {
                        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 (spawnedNPCs[i] != null && Vector3.Distance(playerTransform.position, spawnedNPCs[i].transform.position) > spawnRadius)
                {
                    Destroy(spawnedNPCs[i].gameObject);
                    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 'EnemyStateMachine'
        //private void OnValidate()
        //{
        //    for (int i = npcConfigurations.Count - 1; i >= 0; i--)
        //    {
        //        DynamicCharacterConfiguration npcConfig = npcConfigurations[i];
        //        if (npcConfig.GetCharacterPrefab() != null &&
        //            !npcConfig.GetCharacterPrefab().TryGetComponent(out AggroGroup _ /* don't care about the name */) &&
        //            !npcConfig.GetCharacterPrefab().TryGetComponent(out Animal _ /* don't care about the name */))
        //        {
        //            Debug.LogError($"Invalid NPC Prefab: {npcConfig.GetCharacterPrefab().name} does not have an EnemyStateMachine or an Animal Component on it", npcConfig.GetCharacterPrefab());
        //            npcConfigurations.RemoveAt(i);
        //        }
        //    }
        //}
    }
}

Temporary placeholder :slight_smile: (this one is an absolute mess tbh)

OK so… a bit of an update. Here’s my latest script, Ignore the previous one:

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

namespace RPG.DynamicSpawner
{
    [RequireComponent(typeof(JSONSaveableEntity))]
    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);

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

        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 'EnemyStateMachine'
        private void OnValidate()
        {
            for (int i = NPCPrefabs.Count - 1; i >= 0; i--)
            {
                GameObject NPCPrefab = NPCPrefabs[i];
                if (NPCPrefab != null && !NPCPrefab.TryGetComponent(out AggroGroup _ /* don't care about the name */) && !NPCPrefab.TryGetComponent(out Animal _ /* don't care about the name */))
                {
                    Debug.LogError($"Invalid NPC Prefab: {NPCPrefab.name} does not have an AggroGroup or an Animal Component on it", NPCPrefab);
                    NPCPrefabs.RemoveAt(i);
                }
            }
        }

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

            foreach (var npc in spawnedNPCs) 
            {
                JObject npcState = new JObject();
                npcState["prefabIndex"] = NPCPrefabs.IndexOf(npc);
                npcState["position"] = new JArray(npc.transform.position.x, npc.transform.position.y, npc.transform.position.z);

                var enemyStateMachine = npc.GetComponentInChildren<EnemyStateMachine>();
                npcState["patrolPathIndex"] = patrolPaths.IndexOf(enemyStateMachine.GetAssignedPatrolPath());

                stateArray.Add(npcState);
            }
            Debug.Log($"Capturing Completed");
            return stateArray;
        }

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

            foreach (JObject npcState in stateArray) 
            {
                int prefabIndex = npcState["prefabIndex"].ToObject<int>();
                
                /* if (prefabIndex < 0 || prefabIndex >= NPCPrefabs.Count) 
                {
                    Debug.LogError($"Invalid Prefab Index: {prefabIndex}");
                    continue;
                } */

                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>();

                /* if (patrolPathIndex < 0 || patrolPathIndex >= patrolPaths.Count) 
                {
                    Debug.LogError($"Invalid Patrol Path Index: {patrolPathIndex}");
                    continue;
                } */

                GameObject npcPrefab = NPCPrefabs[prefabIndex];
                GameObject spawnedNPC = Instantiate(npcPrefab, position, Quaternion.identity);

                PatrolPath patrolPath = patrolPaths[patrolPathIndex];
                EnemyStateMachine enemyStateMachine = spawnedNPC.GetComponentInChildren<EnemyStateMachine>();
                enemyStateMachine.AssignPatrolPath(patrolPath);

                spawnedNPCs.Add(spawnedNPC);
            }

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

To avoid confusing myself, I decided I want to stick to my own version of dynamically spawning NPCs.

The problem I have is, is that whilst this saving system does not give me any sort of runtime errors (whenever I keep the ‘if’ statements that I commented out, because they’re the source of a logical error, and deleting them gives me an ‘ArgumentOutOfRangeIndex’), it does not restore them to their precise precisions or what not (@Brian_Trotter please help me out :slight_smile:)

I know this is a bit of a vague question, but… what should I do to get this system to restore the health, position and all of that stuff with the correct values? (These will be the important variables for me: their health, the position, the patrol paths and the type of enemy on that list of ‘NPCPrefabs’)… I’m not sure if I want dialogues in this or not)

And like I said earlier, I’ll stick to programming mine from scratch. Not to sound too arrogant, but I just find setting up the other one a bit confusing :sweat_smile:)

At this point in time, I can’t really think of a lot of advantages of saving and restoring dynamically spawned NPCs, but I’m doing it JUST IN CASE in the future something happens that warrants this system to be functional

My plan, assuming Brian doesn’t come up with something amazing in the future, is that Quest Givers, Team mates and other important NPCs in my game shall be Fixed Entities (which can respawn. Essentially, they’ll be the type of characters that my current guards are at), and other NPCs that keep the game alive and fun will be dynamic.

The idea behind those dynamic characters will be, in the future, that whenever you enter a different biome, your list of enemies, animals and patrol paths will be changed through an ‘OnTriggerEnter’ that triggers when a biome is entered, so that creatures from each biome don’t accidentally spawn in another one, and they’ll be done on a much larger scale than the current prototype scale

Personally, I think having dynamic spawning occur suddenly and disappear suddenly can remind you that you’re in a video game. I don’t know to be honest if there should be fading, keeping it when the player isn’t looking and what not, etc…

AT THE VERY VERY LEAST I WANT TO SAVE WHAT THE BIOME CURRENTLY HAS IN DATA, SO THAT MY CREATURES KNOW WHAT TO SPAWN, AND WHERE TO GO

It looks like you’re absolutely on the right track with this saving setup. You want to save each character that this DynamicNPCSpawner is responsible for… Since these are fairly simple characters, there really isn’t a need to get too far in depth with what we need to save them…

I would recommend using the if(thing is otherthing) construction within the RestoreState, as a good RestoreFromJToken should do it’s best to handle any errors it encounters in the mix…

Are you saying the position the enemy was in doesn’t match up between when it was Captured and when it was Restored? That seems in doubt to me, as you are correctly serializing and deserializing a Vector3. It’s not the way I do it, but my method (a static extension method) was written more for readability than efficiency. In short, the character absolutely should be standing where it was, but not necessarilly facing where it was.

Saving the Health is as easy as getting the current health value in CaptureAsJToken and assigning it to state[“Health”], and setting it in RestoreAsJToken…

I am seeing an issue with this line:

npcState["prefabIndex"] = NPCPrefabs.IndexOf(npc);

This shouldn’t actually work because the instantiated npc is not equal in any assignable way to the NPCPrefab.

There may need to be a slight reworking of the list of NPCs to make this work properly…

Give me a few minutes to make some minor changes, and to show you how you can save all the ISaveable information on the character (which would include the Heath, and anything else important)

Honestly, yes, and it was a bit so weird that I suspected that the restore was even working to begin with :sweat_smile:

It doesn’t get anymore advanced than this, does it? We’re at a point where we’re trying to save and load characters that, essentially, vanish and pop up out of nowhere :sweat_smile:

By all means, please take your time. You’ll have to excuse me for about 30 minutes though, I need to get some food otherwise I might faint

I’ll be back as soon as possible :slight_smile:

There’s no fainting in coding!

Ok, here’s what I came up with… When a character is dynamically created, we not only need to store the NPC, but we also need our hooks in the index into the NPC prefabs and patrol paths… So I created a struct for this and changed spawnedNPCs into a List of that new struct.

Then when spawning a new character, we add that record to the list.

I reworked the CaptureAsJToken to get the indexes and location, as well as capturing any data in the dynamic character’s IJsonSaveables. That takes care of Health.

I reworked RestoreFromJToken to handle this new setup.

using System.Collections.Generic;
using RPG.Control;
using UnityEngine;
using RPG.States.Enemies;
using RPG.Animals;
using RPG.Combat;
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; // 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<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)
            {
                // 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);

                // NPC PATROL PATH ASSIGNING:
                int patrolPathIndex = Random.Range(0, patrolPaths.Count);
                PatrolPath patrolPath = patrolPaths[patrolPathIndex];
                EnemyStateMachine enemyStateMachine = spawnedNPC.GetComponentInChildren<EnemyStateMachine>();
                enemyStateMachine.AssignPatrolPath(patrolPath);
                NPCRecord record = new NPCRecord()
                {
                    NPC = spawnedNPC,
                    PrefabIndex = prefabIndex,
                    PatrolPathIndex = patrolPathIndex;
                };
                // ADDING THE NPC TO THE 'spawnedNPCs' LIST:
                spawnedNPCs.Add(record);
            }
        }

        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].NPC.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 'EnemyStateMachine'
        private void OnValidate()
        {
            for (int i = NPCPrefabs.Count - 1; i >= 0; i--)
            {
                GameObject NPCPrefab = NPCPrefabs[i];
                if (NPCPrefab != null && !NPCPrefab.TryGetComponent(out AggroGroup _ /* don't care about the name */) && !NPCPrefab.TryGetComponent(out Animal _ /* don't care about the name */))
                {
                    Debug.LogError($"Invalid NPC Prefab: {NPCPrefab.name} does not have an AggroGroup or an Animal Component on it", NPCPrefab);
                    NPCPrefabs.RemoveAt(i);
                }
            }
        }

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

        // RESTORING
        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 npc.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 need to survive for another day lah :stuck_out_tongue_winking_eye:

By all means, I’ll give this new script a test run, and keep you updated on what happened :slight_smile:

OK so, for the record, I believe there’s 4 syntax mistakes:

  1. in ‘DespawnNPC()’:

this line goes from this:

Destroy(spawnedNPCs[i]);

to this:

Destroy(spawnedNPCs[i].NPC);
  1. in ‘RestoreFromJToken()’:

This line, goes from this

                foreach (IJsonSaveable jsonSaveable in npc.GetComponents<IJsonSaveable>()) 

to this:

                foreach (IJsonSaveable jsonSaveable in npcPrefab.GetComponents<IJsonSaveable>()) 

and two semicolons in constructors :stuck_out_tongue_winking_eye:

Test time :slight_smile:

Well that went terribly wrong… :sweat_smile:

I’m not sure what happened, but apart from the fact that somehow, right now they all spawn at the exact same point over the ‘checkInterval’ time (I was hoping to randomize them, which is what I initially had), their health still doesn’t get saved :confused:

Can we please give it another try? :slight_smile:

At this point in time, my end goal is to just save the patrol paths and the boss list, especially that they will be changed again down the line

Here’s the modified code from my side, if it helps:

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;

namespace RPG.DynamicSpawner
{
    [RequireComponent(typeof(JSONSaveableEntity))]
    public class DynamicNPCSpawner : MonoBehaviour, IJsonSaveable
    {

        // NPC RECORD STRUCT
        private struct NPCRecord 
        {
            public GameObject NPC;
            public int PrefabIndex;
            public int PatrolPathIndex;
        };

        [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<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)
            {
                // 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);

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

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

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

        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].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; // 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);
                }
            }
        }

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

        // RESTORING
        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 npcPrefab.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");
        }

    }
}

and he also glitches on death now for some reason, after the recent modifications, where the screen won’t even fade out to begin with

Privacy & Terms