Don't Talk To Me
đź“… 2023-04-19
Today is a good example of how I set out to work on one thing and then when I wake up just fucking decide to work on something entirely different. Instead of what I said I would work on, today I tried to consolidate the NPC systems across the project so I could have some proper control and cohesion.
The NPC system has gone through 3 iterations, initially, an NPC had a fairly simple interaction script on it that simply filled a single dialogue line for the player, then emptied it, and moved the character across the environment based on a location array that checked the TimeManager. This is the same interaction script that I used for other objects as well - door teleports, object movement, and sleep/bathe/pray interactions, so it was unwieldy.
Then when I built the proper node based Dialogue system, with its wild intricacies, including global bool checking, mood checking, clue use, branching options, and mood effects - I moved the movement behavior into the Dialogue scripts, because it was using the Time Manager more effectively and I needed to remove the NPC system from the original interaction script because I knew I always would anyway.
But the NPC’s still had no complicated behavior outside of the Dialogue System controlling their movement based on the time of day, and the Dialogue system itself. So today I decided to tackle the actual NPC system.
Our first problem was that we already have a simple mood system in the dialogue system that our player can effect by choosing different dialogue options, these are trust, affection, loyalty, and fear - and they are relative to the player. AKA, how much the NPC trusts, fears, is loyal to, or is affectionate too, the player, based on their behaviors. But I wanted a more robust mood system for the NPCs beyond the four dialogue effectors, since I want the NPC’s to have moods that are affected by things outside of just player behavior or dialogue - aka, the world around them.
So we created a new class, InternalMood - with 8 new mood types; sad, happy, bored, excited, angry, relaxed, tired, and energetic. These scale from -10 to 10, and the NPC’s mood is essentially chosen from the dominant scale. I had already built something similar for the Orphan system, and will be building something similar again for the Haunt mother, but regardless the ability to add, remove and change mood is now in. I havent created anything that actually affects the new mood system yet, but the idea is that once we have more NPC behaviors to flip through, that will be fairly easy to do.
Next, and what took up most of the day, was rebuilding the movement system. We were always moving the NPC locally in a single scene, and that isn’t ever what the intended behavior was. Ideally, the NPC’s should move around the entire Library, from scene to scene, even if the player isn’t present in the scene - they have places to be, and things to do - akin to the Skyrim NPC management, they do what they have to do whether the player is there or not.
And in order to get that behavior, we had to delete the movement stuff that was in the Dialogue system, trash the old scene dependent NPC manager, and build a new system for handling NPC persistence across the game. I decided I would now handle the bulk of this in our Globals script, since it is persistent across the game and is never destroyed.
So first things first, we built a new class scriptables for handling some of this, the NPCState class, which holds both the dialogue moods, and the general moods, as well as a name, how long the player has spent with the NPC, and a dictionary of locations based on their timeslot. We also made sure to include a LevelLocation class just to hold the scene name, position, and rotation that every single one of these dictionary elements would need to hold.
I then created a second scriptable so I could fill these NPCState locations via the inspector, by creating location nodes based on character name. So I didnt have to add all the vector positions and rotations by hand, GPT created a simple Editor extension that let me drag my character around in the right position, press a button on the node to add the current transform, so both position and rotation is added based on what timeslot I wanted.
So now the idea is when a player loads up a scene, the Globals script checks where the NPCs are based on their states and their location nodes. We created a simple new NPC Manager that just holds our NPCs at an offscreen position in a scene, it then calls each NPC’s script (which we called “SisterBehavior” and will be handling all the NPC behaviors outside of dialogue and the A* pathing) to check our globals to see if the NPCState exists in its list (if its been loaded from a save, lets say), if it doesnt exist, in creates a new NPC State.
Finally, and most importantly, we Update the NPC states based on if they should be in the currently loaded scene, or are present in another scene. This was by far the hardest method to get right, and it was back and forward with GPT for hours with it. Since I’m new to some of this code (specifically Linq) specific errors or unexpected behaviors were a little hard for me to troubleshoot without a million Debug logs, and to be honest the system was a little confusing because of how complicated it was to deal with essentially 3 different scriptables that are handling different aspects of how this works.
But we got it working, what we do is check our NPCState for its newly loaded targetlocations, filtering them by the current time, and then if we have more than one location for that time and scene - we randomize which one to choose. We then check if the scene and intended scene is correct, grab our NPCs, do a name comparison to make sure we have the right NPC, then apply the position and update the NPC.
If the NPC shouldnt be present in a scene, the script will move them offscreen and when the time advances to the next time chunk (morning to late morning lets say) the check runs again, and all the NPCs are shuffled around to where they should be, if relevant to the scene.
Of course there is the obvious pitfall here. We don’t really have persistent NPC’s yet. While the new system means I can move between all the different scenes and NPCs can be in different locations across different scenes depending on the time of day, which is what I wanted - they arent exactly executing behavior in the other scenes when the player isn’t present, which is what I will eventually want. Whether the player is doing a run or in the bathhouse, the NPC should be moving from one location to the other, and doing what they do at that set time, in that set location, regardless of whether we are in the scene or not. I also want to have what they do be different depending on their mood, much like the Orphan programming, which is so unpredictable due to moods you can start the game and get a bottle to the head 3 minutes into a session, or never interact with it for the first half hour.
This means we have to add some more in depth NPC tracking, but I’ll probably tackle that once we have A* in, and to be honest, right now with a maximum of 7 NPC’s, and only 4 unique locations as of the current build, I dont really need persistence at the level of Skyrim. What I have now is probably sufficient for our needs.
In building out the new system I needed to wire up a second NPC (Selina) in order to see if everything was working, because she hadn’t been updated since the first iteration pass of NPC systems. So I wrote some simple dialogue, threw on the new systems, and started debugging her. Oddly enough, her face tracking is way off compared to the Emily NPC. I added a new neck bone mask and set the new neck bone, but she just looks down at you with such disgust. This is oddly fitting for Selina, but I will have to dive in and tweak the NPC Neck Rotation script to see why this is, and attempt to make it more universal.
A weird bug also cropped up in the dialogue system - if you have only 1 dialogue node, which I’ve never had before outside of the test dialogue for Selina - after talking to the NPC Unity will just fucking hang forever. It looks like a while loop issue, the bane of my existance, but it only crops up if you use a single dialogue line with no choices so for now we can just work around it until I get time or need to expand on the dialogue system.
Next step in terms of NPCs is A* - GPT suggested some A* middleware but fucking hell it was $100 and so robust - I dont need that much flexability and I figure why not just build some basic A* from scratch, I’ve done it several times now so it won’t hurt, but it is going to be a fucking hurdle. I won’t tackle it next next though, thats for another iteration pass, really we need to work on the phase system like I wanted to today, so that’s probably next.