Voice of the Ocean - NPC AI - Part 1
The last few months, my team and I have been hard at work, creating an underwater world. Touched by the lack of humans. Reclaimed by nature.
We're making a game with a narrative and unique gameplay, using the player's voice to interact with the world. One of the things the player can interact with, are sea animals. Mainly pufferfish and sharks.
I've been given the task of programming the AI of these underwater creatures. I'd like to go over my thought process and how I am implementing these behaviors.
Some context
Before diving into how I'm implementing the AI, I'd like to go over the development environment and some terminology that's linked to the AI systems.
This is the first part of a multi-part blogpost.
The Engine
We're using Unreal Engine 5.4 to develop our game, known for its rendering capabilities and triple-A nature it was a good fit for our project where visuals and narrative are key.
Unreal's AI Tools
Unreal comes pre packaged with tools that make programming and setting up NPC AI easier. I'll be listing the ones I use and add what I have since learned about them.
AI Controller
This is a subclass of the Controller class that enables the AI systems to "Possess" the controlled pawn/character and take control of their movement.
Behavior Tree
Behavior Trees are assets that define the actions an AI takes. The AI's behavior is composed by the developer by placing composite nodes and tasks in the order that they should fire, from top to bottom and from left to right.
Composite Nodes
Composite nodes are nodes that don't execute any logic themselves. They are used to structure the behavior tree and can control the flow of execution depending on the success or failure of the tasks the composite contains.
There are two main composite nodes:
- The Selector: This composite executes the nodes below it until one of them returns a success. Think of this like selecting the most suitable task. When the nodes below a selector are arranged correctly (higher priority to the left), the selector will go through all of them until it finds one that returns a success and if it can't find one, will return a failure.
- The Sequence: This one executes all the sub nodes, again from left to right, but only returns successful if all the sub nodes returns a success. If even one fails, the whole sequence fails. As the name suggests, this composite is used to define a set of actions that need to happen in a sequence that can't be changed.
Besides returning "Succeeded" or "Failed", task nodes can also return "Aborted" and "InProgress". The last one being useful when a task needs a certain condition to be met before the behavior tree's execution moves along.
Blackboard
The blackboard is a data container that is linked to a Behavior Tree. It allows the developer to set "Keys" at runtime and retrieve their values to make the AI aware of various things in the game world.
Behavior Tree Tasks
The behavior tree that unreal provides does not come with a whole lot of variety in tasks. So it's up to the developer to create custom tasks, either by Blueprints or by native C++. The latter being harder to develop, but more performant.
These tasks derive from a base BTTask class that defines the set of functions the developer is allowed to override. The most basic one being "ExecuteTask".
While tasks are easy to create and use, it's best to think modularly when creating them. Create them with reusability in mind. Leverage the fact that Tasks can access the Blackboard.
Environment Query System
This is an additional system Unreal Engine provides that allows AI agents to query their surroundings for a target location or actor.
The Environment Query
This is the asset that defines what the query actually contains. The query consists of a Generator and Tests, where the Generator generates an item and the tests can filter and/or score them.
Generators
Using Generators we can generate:
- Actors of a specific Class
- the current location of the Query Context (more on this later) that is querying
- a set of points around a Context in various shapes:
- Circle
- Cone
- Donut
- Grid
- Pathing Grid
If these are not enough, Unreal also provides us with a way to make custom generators using Blueprints or native C++, just like with behavior tree tasks. These then derive from EnvQueryGenerator_BlueprintBase
and EnvQueryGenerator
respectively.
Query Contexts
Query Contexts define a frame of reference from which a generator or test will operate. A simple example of this is the Querier
context that returns the actor that is performing the Environment Query.
Contexts can provide generators and tests with one of the following:
- A single actor
- A set of actors
- A single location
- A set of locations
Of course, Unreal has some Contexts ready to use for us:
- EnvQueryContext_Item: This is the item generated by a generator, could be a location or an actor.
- EnvQueryContext_Querier: This is the Pawn that is currently possessed by the AI Controller that is executing the Behavior Tree this Environment Query was fired from.
Contexts are very useful if we have certain logic that needs to be performed around a certain actor or location. Creating our own Contexts is as easy as creating a Blueprint derived from EnvQueryContext_BlueprintBase
or if C++ is preferred EnvQueryContext
.
Tests
Tests are used to score and/or filter items produced by generators. Filtering outright removes an item that doesn't pass a test, while scoring determines the "best" item out of all of them.
Each node can be filtered based on a min & max value or if a boolean value matches. This depends on the test type its returned value type.
Scoring has a similar setup. Raw test values can:
- be clamped between a min & max
- have a weight multiplier applied to them
- be normalized using either 0 as the base value (Absolute Normalization) or the lowest of the test scores (Relative Normalization)
Test values are scored based on a scoring equation that is also up to the developer:
- Constant
- Linear
- Square
- Inverse Linear
- Square root
The equation maps the raw scores to the chosen equation, making the scoring more intuitive and easier to work with.
Unreal Engine comes with a list of tests that can be very useful in a variety of situations and again, if you need more or these test just don't cut it for you, you can create your own through C++ code. Currently, there seems to be no support for creating tests through Blueprints.
The built-in tests are:
- Distance: Returns the direct distance between the Item and the chosen Context.
- Dot: Returns the dot product of two vectors, useful when we need an item at an angle from the Context.
- Gameplay Tags: This allows you to find specific actors that math the tag you enter
- Overlap: This test allows you to define a shape, a collision channel and an offset from the item this test runs on. If the shape overlaps anything in the collision channel, the test fails/succeeds. Depending on whether the "bool match" option is set or not.
- Pathfinding & Pathfinding Batch: This test is used to determine if a path exists to the item in question, you can get the path's cost and also it's length to test against. (this requires a navmesh to be present)
- Project: Will project the resulting item (that may be blocked or inside a wall) back onto the navmesh.
- Trace: Will use a line/shape trace and return if the trace hit something. Useful for determining what the AI agent can/cannot see in the game world.
End of part 1
Thanks for reading! This was an overview of what I know and have learned thus far, which allowed me to implement the behaviors I'll specify in the next posts.
Hope to see you there!
References:
Unreal Documentation - EQS
Unreal Documentation - EQS - Tests
Unreal Documentation - EQS - Generators
Unreal Documentation - EQS - Query Contexts
The Games Dev - All about BTTasks in C++
Github - Unreal AI Module