Push / Pull!
đź“… 2023-04-20
Another day, another procrastinated task. No alter or phase work today, instead I tackled the “Haunt Mother” AI - essentially, a wandering specter AI that antagonizes the player if you get too close. It will eventually be much more than what it currently is, but today I managed to get a ton of work done on it. Originally, it had two states, Lost and Disturbed, and would wander aimlessly while lost from tile to tile, looking for anything to fill the empty void where it’s heart was. We can all relate. If it came into contact with the player, and the player moved, it became disturbed. While disturbed, it had a single behavior it could do - which was just push the player.
But obviously the intention was always to have it do more than that. So the goal became to rebuild the Haunt Mother’s logic, and create something unique. For a while, I’ve thought about the fact that I want the Haunt Mother to draw from a huge array of possible behaviors, depending on it’s past experiences with the player, the current atmosphere of the level, and its own desires. But doing this would require constantly adding new methods and dependencies to the haunt mother script and that sounded unwieldy. If the Horror Kit has taught me anything, fuck off with dependencies. So I needed a proper solution. And if you read my previous blog post, you probably know what solution I came up with.
You got it, scriptables. Ain’t nothing they can’t do baby. I love you, Unity. So we create a new base class called HauntCard. And I figured, what we can do, is include unique Haunt events in a card based system built off the HauntCard base with a simple override.
As you can see all we include really is a name, a level it can be used at, some embedded audio if we want that, which we will, and a reference to the player and the hauntMother that gets filled in and used by its derivitives once we execute the card.
Now, we can build new classes on top of that. So we started by moving the push logic from the HauntMother into a new card called PushHauntCard, and fill in a unique behavior for the Execute Haunt method. This means the logic in our HauntMother can always be the same - draw a card, execute the cards “ExecuteHaunt” and depending on which card it drew, you will get unique behaviors.
And voila, we have our first card. But now we need to figure out how the HauntMother uses these cards. First, we define the card levels - this is where we will plug in our scriptables - and then initialize them by adding them all into a master deck, then run an initialize method on them to shuffle and draw the cards.
Then all we would need to do is decide when to draw a hand and use a card in our state machine. It would be pretty simple to just draw cards randomly, but fuck when have I ever stopped at simple. Let’s complicate it till I’m jamming a fork in my eye. Besides, I’ve wanted the Haunt Mother to be much smarter than that since the start. So I threw some ideas back and forth with ChatGPT, discussing neural nets and genetic algorithms and PPO - and in the end we both decided we couldn't get that complex, but we could borrow some ideas from that sort of system to give our AI some unique behavior.
So we took the reward and punishment system from something like a PPO and simplified it for use with the card system. Essentially, our Haunt Mother will get rewarded with experience if it fires a haunt at the right time and effects the player, and will be given negative experience if it makes a bad decision. Which is why we have that success bool in the haunt cards - we can decide at the end of each cards method if it was a success or not using parameters we haven't plugged in yet.
We then also want to incorporate that punishment and reward system into the cards themselves, so if the Haunt Mother fails, it isn’t just its own fault for firing the card when it did, it’s also potentially the cards fault, a dud card. So what we do is on initialization, we give all the cards in the deck a score of 0, then after we fire a card, based on its success or failure, the Haunt Mother will decide to give score to that kind of card itself. So it begins to like certain cards over others. We also apply a cool-down to each card type so it cant spam it again, even if it has more than one in its hand, and we also count how many times it’s used certain cards, so we can further punish the Haunt Mother for developing dominant card stratagems, specifically so the player doesn't always see the same haunts if the Haunt Mother has decided to preference one card over the rest of the deck.
And this, actually works pretty well. The Haunt Mother will wander around, then when it comes into contact with the player, it will draw a hand, decide on a card, and use it. If it succeeds in affecting the player, however we want to define that, it gets a reward, ditches the card, and teleports to a new location. This is the chart I drew a few hours into it as I was working so I could picture it correctly because it got a little complex in places with the scoring.
The drawcard method is admittedly quite long too, but here is a segment of it that shows the score calculator, how we execute the cards coroutine, then wait for the result, and call it a success or failure, before removing the card.
Once the card system was in, I started to test and iterate on it, fixing it here and there, and also tweaked some of the Haunt Mother’s behaviors in general, what conditions we can see it in, or if it’s invisible entirely. Whether it makes noise or not. How it moves around. It’s rotation and facing controls. It has a lot of weird bugs that needed to be fixed with positional offsets and better code, so I fixed it as I went.
And then it came down to just tweaking the card behaviors themselves. So we started with that one card, push. We then added a new function to it which you could see in the above card script, pull. So that gives us two cards, a push and pull. Uwu. I then wanted a lift - I’ve never understood why we dont see more player manipulation in horror games, its a staple of the genre in other formats like film. One of my favorite scenes in Poltergeist, for example, is when JoBeth Williams gets thrown against a wall, and then dragged up to the ceiling and across it, only to be thrown back down on the ground on the other end. And not just because it’s hot.
Ghosts are hostile in films, they fling you around, so I wanted something sort of like that outside of the push. So I created a Pin card. Initially it was a simple lift in the air and then pull back down. I did something similar in the PSX midnight project I was working on, just a mid air Vader strangle.
However, despite looking a little jank, the rotation made me feel like I was onto something, it just seemed cool to see yourself float up like that in the first person. So I basically spent the rest of the night tweaking it and trying to polish it, and boy did it get messy.
I ended up adding a camera lock and player control lock, tho that may eventually get pulled out as I tweak how the lift occurs in the environment - added camera shake, animation control, and the option to either drop the player, or throw them across the hallway. This was done with simple lerps, it became a pretty girthy card by the end of the night but here is a snapshot of the lerp and some of the other things going on for camera control.
And a preview of how it looks currently, at the end of the night. It’s not quite there yet, but it’s feeling pretty cool in engine, and I’m excited to polish it up and add more cards. I have tons of ideas for these too, you can basically just mine horror films if you want to. They can be jump scares but I prefer behaviors over shock value. Bullying the player, hurting the player chasing the player, dominating the player, uwu. Giving you a reason to have to fight back, instead of just a face popping out behind a wall and then vanishing like it made a horrible mistake.
I also need to flesh out a lot more of the HauntMother’s general state machine. I did end up adding an aggravated state, where it will chase the player and begin spamming cards at you until it’s hand runs dry. It’s pretty brutal right now especially with only four cards, but as I develop cards I’ll create more behavioral use cases in the HauntMother. And also figure out ways to tweak its reward/punishment system and give it more incentive to do well, and ways to use data about the player against the player. For now, I’m pretty happy with todays progress. Every day is a ton of progress, but yet the game is so far out, it’s pretty wild, but step by step, it’ll get done.