Companion AI
An AI produced in the game Blacksite Deep Horizon
What makes an AI stand out to me?
In my school project 7, we aimed to focus on areas that align with our future career goals. For me, that was AI development. I was first introduced to AI creation in one of our courses and became fascinated by the logical thinking, mathematical challenges, and the ability to shape behavior, making the AI feel more alive beyond simply moving from point A to point B.
Creation of a companion AI
Designing a companion AI that strikes the right balance between being engaging yet not overwhelming, helpful yet not overpowered, and ultimately serves a meaningful purpose within the game presented a significant challenge.
However, after thorough planning and iteration, the puzzle pieces gradually fell into place. The objective was to create an AI for our wave-based defense game capable of healing the player, engaging in combat, and enhancing the overall experience through dynamic and entertaining dialogue.
Click here to see my git repositorie for Companion AI
Behavior tree
For this Project all of our AI was using a Behaviour tree that i created. The design provides a generic way to build behavior trees, with support for both composite nodes which manage child nodes and leaf nodes which perform specific actions.
The Behaviour Tree class is the central component, acting as the container for the root node. It is initialized either with a null root or with a specific root node, which is passed through a constructor. The Update method triggers the behavior of the root node by calling its Tick method, which in turn can execute the node's logic and evaluate its status.
Nodes
The Node class is the base for all behavior tree nodes, handling execution. Specific logic is defined in its subclasses.
Composite
The Composite class, a subclass of Node, acts as a container for other nodes and supports a hierarchical structure. It allows adding child nodes with AddChild and ensures they are managed and updated consistently. By treating both individual and grouped nodes the same way, it follows the Composite Design Pattern, making it easier to handle complex structures.
Sequence and Selector
Sequence and Selector are specific types of composite nodes that manage a sequence or selection of child nodes, respectively.
A Sequence node will execute its children in order, returning Success only if all its children return Success. If any child returns a status other than Success, the Sequence will immediately return that status.
A Selector node will execute its children in order, returning the first non-Failure status encountered. If all children return Failure, the Selector will return Failure.
These node types implement their Update method by iterating over their child nodes, checking their status, and making decisions based on the result of each child's execution.
Leaf
Leaf nodes represent the final actions in the behavior tree. These nodes are not composed of other nodes and typically contain specific logic, such as performing a task or evaluating a condition. The Update method is abstract, requiring derived classes to implement their own specific behavior (e.g., performing an action or checking a condition)
Result
When creating the behavior tree, it might look something like the following picture. I start by making the behavior tree a shared pointer and adding the composites and leaves in the order of what should go first to last in the loop.
To provide an example of the Leaf ShootEnemy Update function, you can see the usage of success and running to determine when the leaf is active or inactive. In this example, the logic controls when the AI is allowed to shoot, and it also triggers corresponding audio when the action is supposed to occur.
To see an example of how a Sequence can look, I will demonstrate my turret function. When the player presses "V," the AI is called to the player's position at the moment the button is pressed and enters turret mode, where it begins shooting at enemies. While in this mode, the companion also deals double damage.
The logic is primarily handled through a post master system, which sends messages to both the projectile class and the HUD to indicate when turret mode is active. If the turret cooldown has not been reached, the companion remains in FollowPlayer mode. However, once the cooldown is ready, the companion moves to the player’s position and remains in turret mode until the timer expires.
Once the turret timer reaches its threshold, the AI exits turret mode, resets its cooldown, and sends messages to update the HUD and projectiles accordingly. If none of the timers have finished, the companion continues moving toward its designated turret position.
Steering behaviour
For the steering behavior of this companion, we chose not to implement a 3D navmesh or a grid system, relying instead on physics-based collision detection. This was a big challenge, particularly in making a drone to follow a player who parkours, has a grappling hook, and moves at high speeds. The development of this process involved both learning while doing and some amount of trial and error. However, after much iteration, I was satisfied with the resulting movement mechanics.
Movement and Steering
While working with a steering class, I wanted the update function to be the main way to calculate velocity, keeping the base class as clean as possible and preventing it from becoming a blob.
Arrival force
The arrival force is implementing an arrival steering behavior witch is used to make an agent slow down smoothly as it approaches a target instead of moving full speed and either getting an abrupt stop or an movement by going back and forth.
Flee force
The three functions FleeForce, CollisionCheck and DirectionAvoidance handles the companion AI's movement by avoiding obstacles and determining a safe direction to flee. I use raycasting in PhysX to check for nearby collisions and adjusts the fleeing direction based on detected obstacles. If the AI detects a potential collision ahead, it tries to move in a safer direction. The fleeing force is then calculated to steer the AI away from danger while maintaining a smooth movement.
Seek force
Seek force is graduatly moving closer to the target getting faster the futher away it is but also doing so graduatly so it still is showing some acceleration.
Turncate
Truncate ensures that the AI does not exceed its maximum speed.
Calculate Weights
When applying all these forces, I noticed that I couldn't use them all at the same time because the AI either didn't flee enough or fled too much. The seek behavior didn't always appear correctly and started reacting in strange ways. To fix this, I had to create a function that calculates the weights properly.
For the flee behavior, I wanted it to react more strongly the closer it was to a collision, but only if the distance was smaller than the ray length used for collision detection.
The arrival weight needed to be heavier when the target was within the slowing radius, gradually increasing as the AI got closer to the target.
Lastly, seek was the behavior that should have the highest weight when the other forces had little to no influence.
Since I didn’t want the total weight to exceed 100%, I normalized them, ensuring their sum always remains within a valid range.
Results
When combinding all of these we get a really cute companion thats following you around. the following gifs and videos are some highlights from the companion work.
Follow player
When designing the companion's movement, I wanted it to feel more fluid, with a swaying motion that gives a sense of weight and freedom. However, as someone who plays a lot of different FPS games, I knew I didn’t want the companion to obstruct the crosshair. To prevent this, I gave it two possible movement positions one offset to the right and one to the left by using the camera's forward and right matrices.
To determine which offset position to use without moving in front of the crosshair, I implemented a simple distance check. The companion moves to the closer of the two positions and, upon arrival, stops to face the player.
Once the companion knows which position to move to, the bigger challenge is deciding when it should start considering the offset and applying its arrival logic. I solved this by introducing a float variable for a slowdown radius. When the drone enters this radius around the player, it begins calculating the offset; otherwise, it follows the player’s position.
Fetch
The idea of having a swarm of enemies attacking the player raised some concerns, leading us to explore ways to counter it. One idea was to introduce a healing ability, where the companion could actively assist by retrieving health packs. The companion would travel to a health station, grab a healing pack, and bring it to the player. This would be especially useful in situations where the player is too far away to heal themselves. Making the companion helpful beyond just combat would also enhance its overall role and make it feel more complete.
To implement this, the system simply pings all available health stations, performs a quick calculation to determine the closest one, and then directs the companion to travel there. Upon arrival, the companion sends a message to the player, prompting them to heal to full health.
Turret
In combat, the companion fires while staying close to the player, as long as it's within range to shoot an enemy. However, to give the player more variety, the companion also has a turret mode. When you press "C," the companion locks onto your current position, stations itself there, and begins firing at enemies. While its firing speed remains the same, its damage output is doubled in this mode.