Game Architecture: Characters and AI

Posted 3/12/2024
by TheHocken

post title image
I needed to make it easy to build interesting enemies, so I spent some time refining the architecture around my characters and AI. This is how arranged my code so I can move faster during content creation.

My first year of unity was spent learning the basics of the engine. I framed everything in terms of the first project I have envisioned. Working from a list of the systems required to deliver that vision, and a list of the mechanics to support those systems, I would script a mechanic somewhere in my codebase, test, and iterate until I felt good about the way it felt then move on to the next mechanic/system. This has been a great way to learn the basics of things like 2d movement, collision detection, leveling, pathing, etc. and to learn them in a way that directly benefit the specific goals I am working towards in my 2d Action RPG.

All was well and good with this workflow as it took me to my first playable prototype. Some choice victims got selected to do a playtest on a test map (made with a purchased tile set) and 1 enemy type on infinite spawn - 'Your presence has awoken the vengeful dead' ;p. Feedback came in from these helpful folks, a spread of gamer to game-acquainted, and the general consensus was that my main system felt good, but the areas I knew were a little suspect were indeed suspect.

To me, this was a massive win. It showed that I had a solid foundation and my internal compass was pointing me in the right direction.

At this point I took a second to step back and plan again. I asked myself a few questions:

  • - What did my players not enjoy?
  • - What did my players want to see more of?
  • - What do I want to give my players next?
  • - How do I make my game more interesting from here?

My answer to these questions was MORE ENEMIES. They need reasons to use the mechanics I have built and a few tweaks to the mechanics themselves. The best way to do this would be to make new enemies and situations that would challenge the player into using the mechanics at their disposal. So I sat down to make a new enemy and this is when I realized I had a problem. My code was set up roughly like this:

spaghetti-architecture.png

All of my code related to enemys was in 1 big wad of spaghetti called Enemy Controller that was over 1000 lines long. It included a state machine that drove both behaviors and animations, stats, current resources, movement, etc. etc. To make a new, unique, enemy I basically had to write a whole new Enemy Controller for each enemy. If your not a programmer, let me tell you, this is very bad. It's lots of error prone work that is likely to introduce bugs unique to any new enemy. At this point I decided to go heavy into architectural design and refactoring.

My process here was examine the player and enemy controller code to find the individual components that made up each and list them out in some boxes in a diagraming tool. I started by looking at things that were shared between the enemy and player, stats and animations stood out. So I turned stats into a new class supporting a component and just pulled the state/animator a separate component. I also added an interface to the player and enemy controller modules so I could start consolidating the methods in which attacks and interactions were handled between the two entities.

architecture-Controllers-R1.drawio.svg

Doing this indeed created a stronger separation of concerns which would make it easier to create new enemies with similar animation states. I could also now set up unique stats for each enemy type with very little effort. I know enemy behavior is still a big part of my controller, but at this point I can't see a way to break it out effectively. To keep moving, I decided to continue to focus on low hanging fruit in the controllers. Things that were immediately obvious to me at this point were the equipment/inventory and the player input.

architecture-Controllers-R2.drawio.svg

Another good step. My player controller saw massive reductions in amount of code and its complexity, but my enemy controller was still weighted down by large amounts of behavior code. If I wanted to create new enemies I would still need to write a new controller every time... it was time to dig in and get creative.

I started by writing out a list of concepts that I felt defined enemy behavior.

  • - Behavior is unique to each enemy type
  • - Decisions are unique to each enemy at that specific point in time
  • - Activities determine possible actions
  • - Actions taken are determined by current activity, nav considerations, and available resources
  • - Actions taken determine the state and animation

I'm a very visual person, so despite writing this out I still didn't have a clear picture of how to break out what I needed. It wasn't until I was drawing the diagram and on the whiteboard in our kitchen, for my wife who is supportive, but doesn't care about software architecture, that I saw it. As I was explaining my current design in gloriously nerdy detail, I realized that my enemy controller was basically a mirror of my player controller at this point, except with behavior logic in it. The player controller has input, but all the information processing and decision making is in the player's head, the input is just the interface to make it happen. I just needed to create the enemy's brain and let it do the work.

architecture-Controllers.drawio.svg

My solution was to feed the same information a player uses to the enemy decision making modules and let them decide which inputs to return to the controller. The controller is telling the activity management system what it knows and what it is capable of, but the activity manager is calling all the shots.

In this design, activities can be largely built into their own modules and used to dynamically compose behaviors on a per enemy basis using the activity manager. Now Activity manager is the only thing that is certain to be unique to a given enemy type and I have cut out probably thousands of lines of code from my future! This won't be the last refactor, but it's hopefully a big enough step to keep me focused on content and world building for a while.

Until next time,

-TheHocken