Procedurally Spawned Characters

Within the RPG course, our saving system deals very well with characters that are already in the scene when the game starts.

What it doesn’t deal with well is characters who are spawned into the scene dynamically. These may be characters spawned by a trigger, say walking into the town square… or characters who only appear after a quest is accepted or any number of other conditions.

What I’ve put together here is a framework for tracking and saving procedurally spawned characters. This will not deal with the initial spawn of the character, except to provide a function that can be called to spawn a character.

Note that through this post, I will include both the classic saving system presented in the course, as well as the JsonSavingSystem intended as a replacement for the classic saving system.

DynamicEntity

Our normal saving system gathers all SaveableEntities in the scene, then each SaveableEntity is responsible for saving it's own characters. It restores those characters by looking for every SaveableEntity and restoring them from what was gathered in CaptureState. The problem with this approach is that for characters that are procedurally generated, while their save data will be captured, when they are re-incorporated into the scene, they may not be there to be restored.

For this, we need a new entity type, the DynamicEntity. This script should be on your dynamic character’s prefab instead of SaveableEntity. (If you intend to use the same character for both fixed entities and dynamic ones, create a Prefab variant of your fixed entity, and in the Variant remove the SaveableEntity and add this one.

DynamicEntity.cs
using System.Collections.Generic;
using GameDevTV.Saving;
using Newtonsoft.Json.Linq;
using UnityEngine;

namespace RPG.Dynamic
{
    public class DynamicEntity : MonoBehaviour
    {
        // Called when entity is destroyed, allowing cleanup of entities list
        public event System.Action<DynamicEntity> entityDestroyed;
        
        public object CaptureState()
        {
            Dictionary<string, object> state = new Dictionary<string, object>();
            foreach (ISaveable saveable in GetComponents<ISaveable>())
            {
                state[saveable.GetType().ToString()] = saveable.CaptureState();
            }
            return state;
        }
        
        public void RestoreState(object state)
        {
            Dictionary<string, object> stateDict = (Dictionary<string, object>)state;
            foreach (ISaveable saveable in GetComponents<ISaveable>())
            {
                string typeString = saveable.GetType().ToString();
                if (stateDict.ContainsKey(typeString))
                {
                    saveable.RestoreState(stateDict[typeString]);
                }
            }
        }

        public JToken CaptureAsJToken()
        {
            JObject state = new JObject();
            foreach (IJsonSaveable saveable in GetComponents<IJsonSaveable>())
            {
                state[saveable.GetType().ToString()] = saveable.CaptureAsJToken();
            }
            return state;
        }

        public void RestoreFromJToken(JToken state)
        {
            if (state is JObject stateDict)
            {
                foreach (IJsonSaveable saveable in GetComponents<IJsonSaveable>())
                {
                    string typeString = saveable.GetType().ToString();
                    if (stateDict.ContainsKey(typeString))
                    {
                        saveable.RestoreFromJToken(stateDict[typeString]);
                    }
                }
            }
        }
        
        private void OnDestroy()
        {
            entityDestroyed?.Invoke(this);
        }
    }
}

There’s not a great deal to this script, and it was quite literally taken from the SaveableEntity and JsonSaveableEntity classes.

DynamicCharacterConfiguration

Another tricky aspect of dynamic characters is knowing what characters to spawn in when RestoreState() has been called. The easiest way to manage this is to use a setup similar to our InventoryItem, using a ScriptableObject with an ID which we can retrieve from the resources folder.
DynamicCharacterConfiguration.cs
using System.Collections.Generic;
using UnityEngine;

namespace RPG.Dynamic
{
    [CreateAssetMenu(fileName = "Dynamic Character Config", menuName = "Dynamic/Character Configuration", order = 0)]
    public class DynamicCharacterConfiguration : ScriptableObject
    {
        [SerializeField] private string characterID = "";
        [SerializeField] private GameObject characterPrefab;

        /// <summary>
        /// Spawns in the characterPrefab with the supplied transform as the owner.  The calling object
        /// is responsible for placing the character and restoring it's state if applicable.
        /// </summary>
        /// <param name="owner">Owner of the resulting entity, generally the calling class.</param>
        /// <returns>null if no prefab or prefab does not contain a DynamicEntity</returns>
        public DynamicEntity Spawn(Transform owner)
        {
            if (characterPrefab == null) return null;
            GameObject go = Instantiate(characterPrefab, owner);
            DynamicEntity entity = go.GetComponent<DynamicEntity>();
            return entity;
        }


        public string CharacterID => characterID;
        
        
        private void OnValidate()
        {
            // prevent prefabs from being assigned that do not have a DynamicEntity on them.
            if (characterPrefab != null && characterPrefab.TryGetComponent(out DynamicEntity dynamicEntity))
            {
                characterPrefab = null;
            }
            //Ensures there is always a characterID
            if (string.IsNullOrEmpty(characterID))
            {
                characterID = System.Guid.NewGuid().ToString();
                lookUp = null;
            }
        }

        private static Dictionary<string, DynamicCharacterConfiguration> lookUp;
        /// <summary>
        /// Gets the configuration from Resources with the given CharacterID. In order to be retrieved,
        /// the DynamicConfiguration must be in a folder named Resources (or a subfolder in a Resources folder).
        /// </summary>
        /// <param name="id">CharacterID to be retrieved</param>
        /// <returns>null if string is empty or id is not found</returns>
        public static DynamicCharacterConfiguration GetFromID(string id)
        {
            if (string.IsNullOrEmpty(id)) return null;
            if (lookUp == null)
            {
                lookUp = new Dictionary<string, DynamicCharacterConfiguration>();
                foreach (var configuration in Resources.LoadAll<DynamicCharacterConfiguration>(""))
                {
                    if (lookUp.ContainsKey(configuration.characterID))
                    {
                        Debug.LogWarning($"{configuration} and {lookUp[configuration.characterID]} have the same characterID");
                    }
                    else
                    {
                        lookUp[configuration.characterID] = configuration;
                    }
                }
            }
            if (lookUp.ContainsKey(id)) return lookUp[id];
            return null;
        }
    }
}

OnValidate ensures that we only have DynamicEntities as our prefabs, rejecting an attempt to put any other type of prefab in the location (but still allowing us to use the object picker, which won’t work if you make the field a DynamicEntity). It also ensures that the ID field is filled in.

Spawn instantiates the prefab, returning a link to the instantiated instance. This is used both by whatever method you use to spawn in a character, and the Dynamic Saving component.

For each type of character you want to be spawned dynamically, you’ll need to create a DynamicCharacterConfiguration, set it up, and put it in a Resources Folder, much like you would with an Inventory Item.

Dynamic Saving

The final piece of the puzzle is in the DynamicSaving component. This component will go on a GameObject in your scene.
DynamicSaving.cs
using System.Collections.Generic;
using GameDevTV.Saving;
using Newtonsoft.Json.Linq;
using UnityEngine;

namespace RPG.Dynamic
{
    public class DynamicSaving : MonoBehaviour, ISaveable, IJsonSaveable
    {
        private Dictionary<DynamicEntity, DynamicCharacterConfiguration> entities = new ();

        /// <summary>
        /// Registers an entity with the DynamicSaving component.  Will automatically subscribe to the
        /// DynamicEntity's OnDestroy if the Entity is destroyed.  
        /// </summary>
        /// <param name="entity">An instantiated entity within the scene</param>
        /// <param name="configuration">DynamicCharacterConfiguration assigned to thsi character</param>
        public void RegisterDynamicEntity(DynamicEntity entity, DynamicCharacterConfiguration configuration)
        {
            entities[entity] = configuration;
            entity.entityDestroyed += dynamicEntity =>
            {
                if (entities.ContainsKey(dynamicEntity))
                {
                    entities.Remove(dynamicEntity);
                }
            };
        }

        /// <summary>
        /// Spawns a Dynamic Entity using the supplied DynamicCharacterConfiguration and registers it.
        /// </summary>
        /// <param name="configuration">Configuration to use.</param>
        /// <param name="position"></param>
        /// <param name="eulerAngles"></param>
        /// <returns>null if entity cannot be spawned.</returns>
        public DynamicEntity CreateDynamicEntity(DynamicCharacterConfiguration configuration, Vector3 position, Vector3 eulerAngles)
        {
            if (configuration == null) return null;
            DynamicEntity entity = configuration.Spawn(transform);
            if (entity == null) return null;
            entity.transform.position = position;
            entity.transform.eulerAngles = eulerAngles;
            RegisterDynamicEntity(entity, configuration);
            return entity;
        }
        
        
        public object CaptureState()
        {
            Dictionary<string, object> state = new();
            foreach (KeyValuePair<DynamicEntity,DynamicCharacterConfiguration> pair in entities)
            {
                if(pair.Key==null) continue; //Should be impossible
                state[pair.Value.CharacterID] = pair.Key.CaptureState();
            }
            return state;
        }

        public void RestoreState(object state)
        {
            if (state is Dictionary<string, object> stateDict)
            {
                foreach (KeyValuePair<string,object> pair in stateDict)
                {
                    DynamicCharacterConfiguration config = DynamicCharacterConfiguration.GetFromID(pair.Key);
                    if (config == null) continue;
                    var character = CreateDynamicEntity(config, transform.position, transform.eulerAngles);
                    if (character)
                    {
                        character.RestoreState(pair.Key);
                    }
                }
            }
        }

        public JToken CaptureAsJToken()
        {
            JObject state = new();
            foreach (KeyValuePair<DynamicEntity,DynamicCharacterConfiguration> pair in entities)
            {
                if(pair.Key==null) continue; //Should be impossible
                state[pair.Value.CharacterID] = pair.Key.CaptureAsJToken();
            }
            return state;
        }

        public void RestoreFromJToken(JToken state)
        {
            if (state is JObject stateDict)
            {
                foreach (KeyValuePair<string,JToken> pair in stateDict)
                {
                    DynamicCharacterConfiguration config = DynamicCharacterConfiguration.GetFromID(pair.Key);
                    if (config == null) continue;
                    var character = CreateDynamicEntity(config, transform.position, transform.eulerAngles);
                    if (character)
                    {
                        character.RestoreFromJToken(pair.Key);
                    }
                }
            }
        }
    }
}

This component should be paired with a SaveableEntity (or JsonSaveableEntity), which will be found by the Saving System. The entity finds the DynamicSaving as an ISaveable, and it captures and restores the states of all DynamicEntities.

When you want to spawn a dynamic character, you can do it through the DynamicCharacterConfiguration and register it with the DynamicSaving manually with RegisterDynamicEntity(), or you can simply call DynamicSaving’s CreateDynamicEntity with the DynamicCharacterConfiguration and the position/rotation you wish to have applied to the new character.

When the SavingSystem gets to the DynamicSaving, it’s CaptureState (or CaptureAsJToken) gathers all of the entities and saves them by the string id of the DynamicCharacterConfiguration and gets the CaptureState from that entity. When restoring, it simply calls CreateDynamicEntity for each configuration in it’s Dictionary and passes the saved state on to the newly created entity.

This system isn’t exclusively tied to the RPG course. As long as you’re using the SavingSystem (or preferably the JsonSavingSystem), you can use these three classes to spawn procedural elements into your scene and save/restore them.

One caveat: This system assumes a Restore from a clean version of the scene. It will produce bugs like duplicating the dynamic characters if you L) oad using the L key in the RPG game.

Ok I think I got this almost working.
Issue 1: I think this line of OnValidate in DynamicCharacterConfiguration should have a ! in front of the second operand
Should this:

if (characterPrefab != null && characterPrefab.TryGetComponent(out DynamicEntity dynamicEntity))

be this instead?

if (characterPrefab != null && !characterPrefab.TryGetComponent(out DynamicEntity dynamicEntity))

Issue 2: I am running into a bug on loading a saved set of spawned characters which I think is due to me misinterpreting this line and probably some other mistake I made.

I don’t know you meant by “restore from a clean version.” Also - if we cannot restore a saved game (like through “L”) then are we really saving it?

The error I’m getting is the following whenever the game loads either from initially entering play mode or from the “L” key. What results is only a single object gets loaded but I had 5 spawned prior to saving.

InvalidCastException: Specified cast is not valid.
RPG.Dynamic.DynamicEntity.RestoreState (System.Object state) (at Assets/Scripts/Dynamic/DynamicEntity.cs:25)
RPG.Dynamic.DynamicSaving.RestoreState (System.Object state) (at Assets/Scripts/Dynamic/DynamicSaving.cs:71)
GameDevTV.Saving.SaveableEntity.RestoreState (System.Object state) (at Assets/Asset Packs/GameDev.tv Assets/Scripts/Saving/SaveableEntity.cs:64)
GameDevTV.Saving.SavingSystem.RestoreState (System.Collections.Generic.Dictionary`2[TKey,TValue] state) (at Assets/Asset Packs/GameDev.tv Assets/Scripts/Saving/SavingSystem.cs:104)
GameDevTV.Saving.SavingSystem+<LoadLastScene>d__0.MoveNext () (at Assets/Asset Packs/GameDev.tv Assets/Scripts/Saving/SavingSystem.cs:34)
UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) (at <56073df97ede4f769c2cc45d546d986d>:0)

If I attempt to respawn another 5, I end up with 6 total. But since I started with 1 then having 6 makes complete sense to me! I don’t view that as duplicates. My problem is that I am starting with only 1 and getting that InvalidCastException.

If I get this problem solved, my way of avoiding duplicates will be easy… I’ll create some logic to track how many have spawned and how many I need to spawn and always spawn the delta.

Here is how I set up my scene and my test object

Here is my code:

using UnityEngine;
using UnityEngine.AI;

namespace RPG.Dynamic
{
    public class DynamicSpawningTester : MonoBehaviour
    {
        [SerializeField] DynamicCharacterConfiguration config;
        [SerializeField] private float scatterDistance = 1;
        [SerializeField] private float navMeshSamplingRange = 0.1f;
        [SerializeField] int numberToSpawn = 5;

        DynamicSaving dynamicSavingEntity;


        // CONSTANTS
        const int MAX_ATTEMPTS_TO_SPAWN_SINGLE_OBJECT = 30;

        private void Awake()
        {
            dynamicSavingEntity = GetComponent<DynamicSaving>();
        }

        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                for (int i = 0; i < numberToSpawn; i++)
                {
                    RandomSpawn();
                }
            }
        }

        /// <summary>
        /// yes this is hacky.  e.g. randompoint.y = 1.58  I'll fix it later.  
        /// Just want to get something working
        /// </summary>
        void RandomSpawn()
        {
            for (int i = 0; i < MAX_ATTEMPTS_TO_SPAWN_SINGLE_OBJECT; i++)
            {
                Vector3 randomPoint = transform.position + Random.insideUnitSphere * scatterDistance;
                randomPoint.y = 1.58f;
                NavMeshHit hit;
                if (NavMesh.SamplePosition(randomPoint, out hit, navMeshSamplingRange, NavMesh.AllAreas))
                {
                    dynamicSavingEntity.CreateDynamicEntity(config, hit.position, new Vector3(0, 0, 0));
                    return;
                } 
            }
        }
    }
}

I’ll have to get back to you on this later…

1 Like

Oops

The architecture of the SavingWrapper has this extremely annoying section in Update()

if(Input.GetKeyDown(KeyCode.L))
{
    Load();
}

The problem with this is that most of the things we restore tend to assume from a virgin state. With “Load”, you get issues like dead characters still being dead but following the player around like a puppy.
The solution to this is to replace “Load()” with

StartCoroutine(LoadLastScene());

Which reloads the scene and then runs RestoreState, preventing all manner of duplication bugs and zombies.

Odd… there shouldn’t be an invalid cast there, but let’s make it safe:

public void RestoreState(object state)
{
    if(state is Dictionary<string, object> stateDict)
    {
          foreach(ISaveable saveable in GetComponents<ISaveable>())
          {
                string typeString = saveable.GetType().ToString();
                if(stateDict.ContainsKey(typeString))
                {
                       saveable.RestoreState(stateDict[typeString]);
                 }
          }
    }
}

Ah. OK. I had to rewatch a few of the older videos a couple times for this to make sense. I initially didn’t do this because I thought this would re-introduce the bug that Sam fixed here: Fixing A Saving Bug | GameDev.tv But now I see what you are doing is a slight variation and perhaps what Sam intended to do all along.

OK. I think I know what might be going on. So this removed the Invalid cast but it is now only restoring a single dynamic entity. In my code I am attempting to spawn multiple. I traced through the code and your instructions and I can’t quite figure out where or how the unique keys are being generated for each dynamically spawned enemy. Take a look at my debug statements below.

        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                for (int i = 0; i < numberToSpawn; i++)
                {
                    RandomSpawn();
                }
            }
        }

I put some debug statements in the DynamicSaving like so:

        public object CaptureState()
        {
            int i = 1;
            Dictionary<string, object> state = new();
            foreach (KeyValuePair<DynamicEntity, DynamicCharacterConfiguration> pair in entities)
            {
                if (pair.Key == null) continue; //Should be impossible
                state[pair.Value.CharacterID] = pair.Key.CaptureState();
                Debug.Log($"Saving dynamic entity number {i} with key equal to {pair.Value.CharacterID}");
                i++;
            }
            Debug.Log("After save state dict contains: " + state.Count);
            return state;
        }

My output when saving was

Saving dynamic entity number 1 with key equal to b80501b9-732b-44f9-b49b-9ab53bb600fd
Saving dynamic entity number 2 with key equal to b80501b9-732b-44f9-b49b-9ab53bb600fd
Saving dynamic entity number 3 with key equal to b80501b9-732b-44f9-b49b-9ab53bb600fd
Saving dynamic entity number 4 with key equal to b80501b9-732b-44f9-b49b-9ab53bb600fd
Saving dynamic entity number 5 with key equal to b80501b9-732b-44f9-b49b-9ab53bb600fd
After save state dict contains: 1

I also had these debug statements in the OnValidate of DynamicCharacterConfiguration

        private void OnValidate()
        {
            Debug.Log("On Validate called");
            // prevent prefabs from being assigned that do not have a DynamicEntity on them.
            if (characterPrefab != null && !characterPrefab.TryGetComponent(out DynamicEntity dynamicEntity))
            {
                Debug.Log($"Rejecting {characterPrefab.name}");
                characterPrefab = null;
            }
            //Ensures there is always a characterID
            if (string.IsNullOrEmpty(characterID))
            {
                characterID = System.Guid.NewGuid().ToString();
                Debug.Log($"Newly generated ID is {characterID}");  //TODO: Remove after testing
                lookUp = null;
            }
        }

Per my debug statements, OnValidate only gets called once when loading the game but doesn’t get called when I am spawning multiple. In fact every attempt to spawn a new enemy results in every one of them having the same exact characterID. I am imagining the fix is to generate a new ID on each spawned Dynamic entity here:

        public DynamicEntity Spawn(Transform owner)
        {
            if (characterPrefab == null) return null;
            GameObject go = Instantiate(characterPrefab, owner);
            DynamicEntity entity = go.GetComponent<DynamicEntity>();
            // entity.AssignUniqueID(); ???
            return entity;
        }

But for that to work you need a unique ID for each DynamicEntity but we don’t have one – it only exists on the scriptable object. At this point I figured I must have misunderstood something.

If I remember right, that lecture actually introduced a WORSE bug.

I’ll address this later, I’m at work, but I know what the issue is.

1 Like

This (Integration Introduction | GameDev.tv) is the one that introduced the really bad bug. The endless loop on portals.

Then this one (Fixing A Saving Bug | GameDev.tv) reverted that change but left the original problem.

Then finally by just changing what the input key triggers, I think it fixed it. I wonder how many people like me ignored the advice to change how the L key works thinking it may either cause a regression or thinking they already solved it.

No, I made a flaw in my code for saving…
Serves me right for testing this with one Dynamic character instead of 7
What we want is a structure that looks like this:

public List<KeyValuePair<string, JObject>> savedCharacters.

What I did instead was a Dictionary<string, JObject> savedCharacters…

Here’s a replacement for DynamicSaving’s ISaveable and JsonSaveables:

        public object CaptureState()
        {
            List<KeyValuePair<string, object>> state = new();
            foreach (KeyValuePair<DynamicEntity,DynamicCharacterConfiguration> pair in entities)
            {
                if(pair.Key==null) continue; //Should be impossible
                state.Add(new KeyValuePair<string, object>(pair.Value.CharacterID, pair.Key.CaptureState()));
            }
            return state;
        }

        public void RestoreState(object state)
        {
            if (state is List<KeyValuePair<string, object>> stateDict)
            {
                foreach (KeyValuePair<string,object> pair in stateDict)
                {
                    DynamicCharacterConfiguration config = DynamicCharacterConfiguration.GetFromID(pair.Key);
                    if (config == null) continue;
                    var character = CreateDynamicEntity(config, transform.position, transform.eulerAngles);
                    if (character)
                    {
                        character.RestoreState(pair.Value);
                    }
                }
            }
        }

        public JToken CaptureAsJToken()
        {
            JArray state = new();
            foreach (KeyValuePair<DynamicEntity,DynamicCharacterConfiguration> pair in entities)
            {
                if(pair.Key==null) continue; //Should be impossible
                JObject entry = new();
                entry[pair.Value.CharacterID] = pair.Key.CaptureAsJToken();
                state.Add(entry);
            }
            return state;
        }

        public void RestoreFromJToken(JToken state)
        {
            if (state is JArray stateArray)
            {
                foreach (JToken token in stateArray)
                {
                    if(token is JObject entry)
                    {
                        foreach (KeyValuePair<string,JToken> pair in entry)
                        {
                            DynamicCharacterConfiguration config = DynamicCharacterConfiguration.GetFromID(pair.Key);
                            if (config == null) continue;
                            var character = CreateDynamicEntity(config, transform.position, transform.eulerAngles);
                            if (character)
                            {
                                character.RestoreFromJToken(pair.Value);
                            }
                        }
                    }
                }
            }
        }

Now, instead of looking up the state by the DynamicEntity’s ID, we’re creating pairs of IDs and states. So if we have two characters with the same DynamicEntity, they’ll just both be entries in a list. The list is comprised of a KeyValuePair (or JObject) with the key being the id and the value as the state.

1 Like

Awesome. It passed a good round of testing. I tried spawning a few, saving, portaling, loading, exiting play mode.

This part below seemed surprising. First reaction - we are swapping the key and value around during CaptureState. And we’re using the same key a number of times.

state.Add(new KeyValuePair<string, object>(pair.Value.CharacterID, pair.Key.CaptureState()));

But then I can see why a Dictionary didn’t work for the output of capture state. Wow. It didn’t make sense why you tried to do CaptureState as a KVP until I saw what’s happening in RestoreState. I would have done this very differently but it seems your way is more efficient than what I would have envisioned.

I was envisioning the key in capture/restore state is a tuple of a GUID and CharacterID and the value is the fully serialized object. But your way doesn’t require a GUID per DynamicEntity.

This is actually similar to an approach I use for Equipable Items with procedurally generated stats… For this, you need two things… the ID of the Equipable Item and a Captured state. Then I instantiate the SO of the item and restorestate on it.

“This” here refers to what you did for what’s in RPG.Dynamic? (as opposed to my initial idea)

Thanks so much for mentioning this. I will be using procedural generation in a number of ways because it is much less work as a designer and I think if done right, it is better for players.

So the captured state contains the procedurally generated stats that modify or enhance the base item. The base item being the thing that is specified by the ID of the Equipable Item. Did I get that right?


Unrelated to items / but related to how OnValidate works
I ran into a curious pair of Debug messages. I seem to recall something of the sort happening in the course too I think for folks on a later version of Unity. It seems whenever I generate a new scriptable object that has an “OnValidate” in it, the OnValidate gets called and runs inside a worker but against what seems to be a phantom object. I say phantom object because that characterID is not persisted anywhere. It doesn’t even show me the associated line of the code in the console window.

[Worker0] Newly generated ID is 7ba6f98c-0d7f-4225-b4c7-302a87755000
Newly generated ID is 2c6cff5a-bcda-4e85-8055-b9e1744c17b3

Before the Scriptable Object is named (when it’s got the default name highlighted ready for you to christen it with a sensible name, it technically doesn’t exist. This doesn’t stop the jobs system from running OnValidate() on it and giving us a very nasty headache for our trouble. Once it has a name, all the information from OnValidate is thrown out the window and a new “real” SO is created. This can often lead to one off null reference errors.

I’ve tried six ways from Tuesday to get around this bug, and I can’t. Unity just says that it doesn’t exist so don’t worry about it. It does, after all, work normally once you give it a name.

1 Like

Does the DynamicSaving keep track of which scene the objects belong to? If this were to be used as a Core Instance to keep track of game wide things, if you are in a scene, and it spawns some entities, then you proceed to go to a different scene, and the DynamicSaving is trying to reload all the entities, what will happen to them?
A scenario would be a BagManager that extends dynamic saving, and is a singleton that will handle dynamic container spawning.
If an enemy from scene 1 drops a container which gets spawned by the bag manager and is assigned to the entity list, then i proceed to leave the scene, what would happen to those entities when i go to scene 2?

You have to pair it with a SaveableEntity and then drop both components on a GameObject in the scene. Then it will save stuff on a per scene basis.

The thing is, a singleton manager will always have the same ID in the saveable entity. I’ve extended the normal dynamic saving to a bunch of other things, I’m taking care of how i will handle “managers” that will be persistent using this system

I’m sorry I don’t understand the concern. I implemented this without using singletons. You can have any many spawn points in as many scenes as you want. For each spawn point just pair up a DynamicSaving component and a SaveableEntity. In my implementation I also have a “Dynamic Spawn Zone” Monobehavior that handles some additional logic and config for me such as randomly distributing the enemies in a zone. So I have a 3-tuple of components for every single spawn zone.

I just posted this a few hours ago: Quest Triggered, Procedurally Spawned Characters, in an Arbitrary Scene

In that case, the Singleton would have to also be responsible for blending the existing information from other scenes (which it would have to cache in RestoreState, and then fold the current scene information into the CaptureState when called.

This will add a significant layer of complexity to your saving setup. I strongly recommend against singletons, especially when they are ISaveable/IJsonSaveables.

The saving system is most efficient when you keep the scope of a given class to it’s single responsibility (The Single Responsibility Principle). As you add more things for a single class to “manage”, more and more opportunities for untestable errors creep into your code. In fact, I go out of my way to never name a class “Manager” for a very specific reason. It’s easy to see the term Manager as part of the name of a class and then when you have a new feature to add, you see Manager in your class list, and you (quite understandably) think, oh, DynamicManager, it can handle this new thing, when in fact it’s generally more appropriate to add a new class for this.

True, i tend to stay away as well, but when it comes to managing player specific things, i like to use them, since they would be present everywhere, in any scene.
I resolved my “issue” by having different versions of my class, one that is specifically for the player, and the rest that would be used individually.

Brian I think I found a bug in this part of the code relative to how it interacts with the guard position on AI Controller. I am working on “Final Moment” on Shops and Abilities and caught it because I just passed the part where we mess with the guard position lazy value.

        public DynamicEntity CreateDynamicEntity(DynamicCharacterConfiguration configuration, Vector3 position, Vector3 eulerAngles)
        {
            if (configuration == null) return null;
            DynamicEntity entity = configuration.Spawn(transform);
            if (entity == null) return null;
            entity.transform.position = position;
            entity.transform.eulerAngles = eulerAngles;
            RegisterDynamicEntity(entity, configuration);
            return entity;
        }

I believe the fix would look something like this where the transform is still passed in to Spawn() to make sure the parent is set up at the same time as the object is instantiated with the intended position and rotation. Right?

        public DynamicEntity CreateDynamicEntity(DynamicCharacterConfiguration configuration, Vector3 position, Vector3 eulerAngles)
        {
            if (configuration == null) return null;
            DynamicEntity entity = configuration.Spawn(position,
                                                       eulerAngles,
                                                       transform);
            if (entity == null) return null;
            RegisterDynamicEntity(entity, configuration);
            return entity;
        }

EDIT: Code above works on initial spawn but for some reason it fails when the scene is reloaded. All the dynamic entities want to come back to the parent transform.

Privacy & Terms