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

Guncaster Homepage
Level Design
Movement System
Audio System

AI PROGRAMMING

Backend
Development History
Gameplay Showcase
Project Pipeline

Features

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.