AI Programming
Overview
As lead AI programmer it was up to me to make the enemies of Guncaster. I opted for one general AI system that all enemies could expand from. The end product is a highly customizable and expansive system that can be applied in almost any way possible.
Contributed Breakdowns
AI PROGRAMMING
Features
-
Utilizing the Flyweight pattern using scriptable objects we define well tuned settings that multiple agents can use. Settings cover a variety of behaviors
Pathing Location
Movement
Rotation
Animation
-
All agents utilize the observer pattern to subscribe to update events with custom tick speeds. This cuts down performance and allows for complex behaviors and a good number of agents.
-
AI behavior is organized into states and reactions. This would read like A condition and the various ways to react to the condition being met.
There’s no hard limit on how many states or reactions an agent can have. Since everything is also event-driven it is efficient and has little overhead.
Each state defines when it should be active, and can contain as many reactions as needed.
Because states follow a hierarchical structure, higher-priority states can override lower ones, allowing for fluid, dynamic decision-making.
-
Context-Aware State types that activate based on:
Distance (close or far)
Line of Sight
Health Thresholds
Timed State
-
Expanded reaction types that can be committed to or just play out:
Combat
Pathing
Timed
-
States and reactions have token costs. AI agents are given tokens over time, which they can spend on stronger or rarer behaviors.
This encourages resource-based decision making. Not every option is always available, adding depth and variation.
-
When choosing among available reactions, the system uses weighted randomness. Reactions with higher weights are more likely to be chosen, but less common ones still have a chance.
This makes behavior less predictable but still logical.
-
If an agent takes enough damage in a short period, it can trigger a Poise Break, an event that designers can dictate. This could be a stagger, scream, ragdoll, you name it.
It's a flexible trigger system for memorable and dynamic moments.
-
Agents can enter and exit ragdoll states, adding fun and realism when hit or defeated. Great for humor, variety, or drama.
-
Agents use root motion and animation events to control when attacks fire, particles fly, sounds are played, anything really.
-
Custom animation curves allow you to fine-tune how the AI moves or jumps. This translates to faster movement when close or stopping at a destination, even how high an agent can jump.
-
Additional flare to help the AI units really come to life:
Stop-motion-style snappy movement for stylized feel. Doubles as an efficiency buffer
Dissolving enemies when dying
Distinct silhouettes and poses for personality or combat types
Timeline
The initial AI system for Guncaster was built around a basic finite state machine (FSM). This version featured five fixed states (Attack, Flee, Chase, Pursue and Patrol), shared uniformly across all enemy types. Each agent strictly followed this rigid state structure, limiting variety and adaptability. Combat behavior was binary and agents operated based on this single Boolean flag (isAttacking == true), offering minimal nuance or reaction to player actions.
Despite its simplicity, this version did include a patrol state, though it was eventually deprecated. As the game’s focus shifted more toward direct combat rather than exploration, patrol logic became unnecessary.
Notably, this first iteration also introduced flying enemies. These units were decoupled from the NavMesh system and instead used independent movement logic, ignoring Y-axis constraints. Their behavior was minimal but functional, relying on basic flee and seek states, with supplemental arrive and avoidance mechanics to simulate aerial maneuvering. Sadly, due to complexity these were also deprecated.
Lastly, AI behavior in this version was handled using a fully component-driven architecture. All agent values were tightly bound to Unity components, which limited flexibility and made scalability a challenge in later versions.
Iteration 1
Finite State Machine
The second AI iteration introduced a Hierarchical Finite State Machine (HFSM) with improved structure and modularity. Agents still operated within four core behaviors (Attack, Flee, Chase, Pursue), but could now hold multiple attacks and commit to actions via override functions.
Animation control was vastly improved. Agents could use any Animator parameter type, not just a single Boolean. Parameters were managed through regular expressions, offering quick setup but causing performance issues at scale.
Other upgrades included:
Location-based states enabling world-aware behaviors.
A new state probability system requiring fallback logic.
Enter/exit events for states and reactions (lightly used).
A cleaner, component-based architecture for easier setup and debugging.
This version balanced flexibility and control, laying groundwork for more dynamic agents while exposing bottlenecks to address in future iterations.
Iteration 2
Fixed Hierarchical Finite State Machine
The third iteration prioritized efficiency, scalability, and reuse. A custom Update Manager was implemented to regulate agent tick rates, minimizing unnecessary computations and improving runtime performance across the board.
This phase also introduced the Flyweight design pattern by converting nearly all agent settings, such as movement parameters and animation data, into ScriptableObjects. These lightweight, shared data assets allowed multiple agents to reference common configurations without duplicating memory, streamlining both performance and setup.
A major pain point from the previous iteration, the use of regular expressions to control animator parameters, was eliminated. Instead, hashed IDs enabled direct and efficient access to both animator parameters and states, allowing agents to transition instantly when needed.
Key advancements included:
Full integration of the token system, enabling agents to dynamically "buy" into states and reactions.
ScriptableObject-driven animation commands, enhancing responsiveness and customization.
Early work toward condition based state selection, replacing the rigid four states structure.
This iteration marked a significant leap in both architectural cleanliness and runtime performance, preparing the system for more complex, reactive AI behaviors.
Iteration 3
Fixed Hierarchical Finite State Machine
The final iteration focused on refinement, scalability, and expressive behavior. With dedicated sprint time, I could finally focus entirely on the AI system. This version introduced a truly modular and event-driven architecture, allowing agents to manage any number of states through lightweight Boolean checks instead of expensive distance or health evaluations. Hierarchical prioritization ensured higher-level behaviors could cleanly override lower ones, enabling more dynamic decision-making.
Agent configuration reached peak flexibility with fully separated ScriptableObject settings for movement, rotation, location targeting, and animation. This not only improved data organization but also enhanced fine-tuning per behavior type.
Key enhancements included:
Unlimited event-driven states, each toggled by simple Boolean flags for performance.
Split and specialized SOs for better reusability and customization.
Flair and Personality:
Ragdoll systems for dramatic deaths or recoveries.
Animation curves for jumps and movement speed, adding natural motion.
Poise-based events that triggered new behaviors from prolonged damage.
Improved token and probability systems:
Tokens could be toggled off for efficiency.
State selection now used cumulative weighted probability, recalculating only when needed.
Flexible state types replaced the old four-state model.
Dual-state machine architecture, enabling phase-based AI (think bosses with Phase 1 and Phase 2).
Advanced movement behaviors:
Leap mechanics using OffMeshLinks or dynamic points.
Fleeing using distance-based evaluation and predefined path nodes.
More intelligent and expressive navigation across the vertical plane.
Iteration 4
Flexible Hierarchical Finite State Machine
THe boss spellsniper
A history
Throughout Guncaster's development, the AI system went through four major iterations. The project started with a different team member leading AI development, but life complications forced them to step back partway through. I inherited an incomplete Iteration 1 and spent time trying to salvage the existing Finite State Machine, but the system remained clunky and difficult to extend. In light of the difficulty to expand and the need for more diverse AI units I had to decide how I wanted to move along with the development.
Iteration 2 emerged when I shifted focus to the player controller. Since I was already building the Hierarchical Finite State Machine for movement, I decided to base the AI system on the same architecture. To do this I built the base machine and had both player and AI inherit from the same logic. This alignment was promising, but with player controller work taking priority, AI development stalled and the units themselves were unsatisfactory according to feedback. Despite that, the shift toward the new state machine would help down the line.
Iteration 3 came after Iteration 2’s crunch time. This is because the main focus was the player. While I was successful in rebuilding the AI system from using the HFSM framework, I incorporated several performance-heavy practices. It was still a major improvement over the previous version but meant that the focus was on optimizations. This time it was a lot more streamlined with performance cuts all over and replacements of the defective code areas. After this I had to move onto more pressing matters but a thought was brewing in the back of my head on how this system could flourish and come to completion.
For Iteration 4 I mapped out a sprint where I could work exclusively on the AI system. With my idea from Iteration 3 I worked on fundamentally restructuring the states. This involved shifting the structure from mathematical state-selection logic toward an event-driven architecture. States could now have a toggleable bool that events could call, like health thresholds or OnTriggerEnter proximity. The new system was built this way so that any unit I created could have any number of states. Gone were the days of 4 fixed states, the units could now do so much more. The end product was extremely easy for our designers to make units.
Philosophy
My design philosophy was simple, and came together from examining some of the competition around us. Lots of boomer shooters have some common enemy tropes that I could utilize to make the gameplay impactful. First is the popcorn enemy, someone who is easy to kill and often exists for the player to gratifyingly blow up. Next was enemies with rolls, For this I have my backline or enemies that don’t fight head on. To make these guys a threat we chucked some predictive aiming on their projectiles and they were ready to go. Lastly was the major silhouettes. Our popcorn enemies were small, our backline had emissive particles and stood around player height while our frontline where much larger or carried weapons big enough to outline themselves.
What Went Right
Despite the lengthy development cycle, the end product is something I'm truly proud of. Like most major systems, it really came together once I could dedicate a full sprint to it. The system is packed with features that were genuinely fun to see in action. The ragdoll system was especially satisfying, and additions like Poise and Leaping added depth to combat in ways I didn't anticipate. On top of all the features I was able to fit on these guys I was also able to make the system pretty much entirely event driven. This was truly my biggest triumph as it allowed me to build unique AI characters easily. For reference, I spent a weekend on the roster that exists in the game and many more could have came given more time.The community was essential to the final product. They told me what they wanted, I adapted, and slowly the roster came to life through iteration. I also want to give credit to my mentor Cameron Gullotta, who guided me when I was uncertain about direction. The system wouldn't be half as competent without his input.
What Went Wrong
The AI system went through too many iterations. It consumed more development time than any other system and changed the most. This is partly due to community feedback and my rush to ship something functional. This was a significant burden. Several optimizations and improvements I wanted to implement never made it in. Pathfinding was the biggest offender. Unity's built-in navigation system is expensive, and I cut corners where I could, but units were still recalculating paths too frequently. I wanted to implement a breadcrumb system so units at a distance wouldn't need expensive A* calculations. I also regret not clamping the maximum unit count, 75 simultaneous agents was too aggressive and hurt performance. Had I implemented both of those improvements, the system would have been significantly more robust. My biggest frustration with the final system is that it needed more optimization work than I could give it.