Procedurally Spawned Characters

“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.

Ok I figured out why it fails on restore. The RestoreState method (below) gives it the transform of the parent, which is overwritten by the Mover. But the “bad” guard position stays. It seems now the correct fix is to modify the AIController to implement ISaveable

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

The mover should only be modifying the transform.position property, not the transform itself, and the Mover should have saved the position of the character at the time it was saved… so I’m not sure what’s going on with that.
If maiking AIController as an ISaveable/IJsonSaveble is doing the trick, though, I’d stick with it.

The mover is modifying the transform.position correctly, but what is happening is in this line of the code on RestoreState.

var character = CreateDynamicEntity(config, transform.position, transform.eulerAngles);

The position is being set to the DynamicSaving’s associated gameobject.
Then what happens is this code in AIController.Awake executes and sets the guardPosition to be the same as the DynamicSaving’s transform. This was a change late in the SAB course.

            guardPosition = new LazyValue<Vector3>(GetGuardPosition);
            guardPosition.ForceInit();

So when the game loads, the Dynamic Entities are in their correct starting locations thanks to the Mover script but they end up drifting towards a guard position that is the location of the Dynamic Saving’s gameobject.

Yeah I think this is the fix. It was in this commit that I think broke it. Sam in the video mentions he’s putting in a hack that he would have preferred if it was implementing ISaveable. So in short, I think the RPG.Dynamic is fine, it’s just exhibiting weird behavior as a result of a late change in SAB. Moving the ForceInit to Start() fixes the issue (but breaks respawn).

Interesting. This is one of the trickier aspects of programming. Sometimes, what seems like an insignificant change in one place turns out to break something else. I think what Sam did made sense at the time, but it was set up for a different purpose (actually, sort of a way to manage respawns, if you’re creative with it, but I actually prefer NOT to use Reset. I prefer restoring to clean objects with a known state).

Yes. It seemed like an innocuous change at the time. Fighting the bug was also an awesome way to hammer home the point of transforms get set on instantiated objects.

Is this the relevant discussion? How to instantiate a new & unique ScriptableObject during run-time

Does Instantiate on an SO at runtime still work as expected in the Unity 2022.3 LTS? If so, this would massively simplify some stuff I’ve been working on.

I’m using them in the 2022.3.8 for Spellborne Hunter. I would be screaming bloody murder in the Unity Support Forum if they did away with that functionality.

Here’s the code repo for Spellborne Hunter. Check out RandomDropper and Shop for creation, and the Inventory, Equipment, and ActionStore for saving/restoring the instantiated stats. In the StasEquipableItem itself is the code where the random stats are generated.

1 Like

Spellborn GitLab

1 Like

You have no idea how useful this is. I was in the middle of creating parallel objects (non SO) to my SOs to store instance-level state. It started to turn into a mess. Then I figured I would come back to this thread and your repo. Glad I did.

SOs are a wonderful tool in the Unity arsenal. Many people just think of them as pure data containers, but they are so much more than that, and even what I do with them in Spellborne Hunter is just a drop in the bucket to their true power.

1 Like

I just wanted to say, that I got procedurally generated with quests with quest objectives and rewards that are also procedurally determined AND that also save their own state working in less than a day after you responded to me. Thank you so much.

I tried to do this back in August and forget about saving state, I couldn’t even figure out the proper data structure for playing a live game. I spent days and I kept confusing myself. Using this guidance with instantiating scriptable objects at run time and marking them as “instances” was a HUGE help. Again - done AND with state saving in less than a day when prior attempts took days with nothing to show for it.

This builds on top of the procedurally spawned enemies from RPG.Dynamic. They are spawned as part of these procedural quests. I also have enemy levels that are procedurally determined and based off a delta of the player’s current level.

Since my game is educational in nature, I will now using these techniques to dynamically create educational content (not just quests/enemies) based on how well the player is doing. The strategy pattern I learned in SAB is very helpful here.

In the course of figuring this out, I noticed something interesting about how you are serializing and deserializing in JSON and posted a separate question.

For anyone reading this and new to Unity/C#, my recommendation is to take all 4 RPG courses and also do a few of Brian’s tips and tricks before trying to go too far in customizing your game. The stuff you learn doing that will make later customizations so much easier and faster.

Again - thank you so much!

1 Like

Privacy & Terms