Learning Game AI Programming With Lua Sample Chapter
Learning Game AI Programming With Lua Sample Chapter
David Young
Chapter No. 6
"Decision Making"
Learning Game AI
Programming with Lua
Game AI is a combination of decision making and animation playback. Although classic
or academic AI is focused solely on finding the right decision to make, game AI is
responsible for acting on numerous decisions over time. Treating game AI independent
from animation is a classic mistake that this book avoids by integrating animation and
locomotion systems immediately into the AI systems. This subtle difference of decision
making and execution changes many of the aspects that game AI programmers have to
focus on.
The other large issue with game AI is regarding the specific needs and implementation
strategies that are genre-specific. In order to prevent a watered-down approach, this book
focuses on one specific genre, which is the first- and third-person action genre. Limiting
the AI to this decision allows for an in-depth, tutorial-based approach of creating a full
AI system. The overall goal of this book is to create an AI sandbox composed of
professional level C and C++ open source libraries exposed to an AI system scripted
in Lua.
Chapter 5, Navigation, builds up from local movement and moves on to long distance
movement and planning. Navigation mesh generation provided by the Recast library will
be integrated into the AI sandbox in order to allow A* pathfinding provided by the
Detour library.
Chapter 6, Decision Making, adds intelligence to the choices the AI agents make.
Different data structures and approaches to creating modular and reusable decision logic
are covered through Lua scripts. Decision trees, finite state machines, and behavior trees
are integrated into the sandbox.
Chapter 7, Knowledge Representation, adds the ability to store long-term and short-term
information for individual agents. A centralized approach to storing and propagating
agent knowledge about the world is exposed to Lua.
Chapter 8, Perception, exposes the services that are available to agents for them to query
information about the world. Approaches toward visual- and communication-based
information is integrated into the sandbox.
Chapter 9, Tactics, exposes a high-level spatial knowledge of the environment to the
sandbox. Through a grid-based representation of the world, different knowledge sources
are combined in order to give you an accurate tactical view of the environment for
decision making.
Decision Making
In this chapter, we will cover the following topics:
Now that we have agents that can animate and maneuver through their environments,
we'll add high-level decision making to our agents. These data structures will finally
give our agents autonomy in how they interact with the world as well as other agents.
Creating userdata
So far we've been using global data to store information about our agents. As we're
going to create decision structures that require information about our agents, we'll
create a local userdata table variable that contains our specific agent data as well as
the agent controller in order to manage animation handling:
local userData =
{
alive, -- terminal flag
agent, -- Sandbox agent
ammo, -- current ammo
controller, -- Agent animation controller
enemy, -- current enemy, can be nil
health, -- current health
maxHealth -- max Health
};
Decision Making
Moving forward, we will encapsulate more and more data as a means of isolating
our systems from global variables. A userData table is perfect for storing any
arbitrary piece of agent data that the agent doesn't already possess and provides a
common storage area for data structures to manipulate agent data. So far, the listed
data members are some common pieces of information we'll be storing; when we
start creating individual behaviors, we'll access and modify this data.
Agent actions
Ultimately, any decision logic or structure we create for our agents comes down to
deciding what action our agent should perform. Actions themselves are isolated
structures that will be constructed from three distinct states:
Uninitialized
Running
Terminated
The typical lifespan of an action begins in uninitialized state and will then become
initialized through a onetime initialization, and then, it is considered to be running.
After an action completes the running phase, it moves to a terminated state where
cleanup is performed. Once the cleanup of an action has been completed, actions are
once again set to uninitialized until they wait to be reactivated.
We'll start defining an action by declaring the three different states in which actions
can be as well as a type specifier, so our data structures will know that a specific Lua
table should be treated as an action.
Remember, even though we use Lua in an object-oriented manner, Lua
itself merely creates each instance of an object as a primitive table. It is
up to the code we write to correctly interpret different tables as different
objects. The use of a Type variable that is moving forward will be used
to distinguish one class type from another.
Action.lua:
Action = {};
Action.Status = {
RUNNING = "RUNNING",
TERMINATED = "TERMINATED",
UNINITIALIZED = "UNINITIALIZED"
};
Action.Type = "Action";
[ 174 ]
Chapter 6
Whenever a callback function for the action is executed, the same userData table
passed in during the construction time will be passed into each function. The update
callback will receive an additional deltaTimeInMillis parameter in order to
perform any time specific update logic.
To flush out the Action class' constructor function, we'll store each of the callback
functions as well as initialize some common data members:
Action.lua:
function Action.new(name, initializeFunction, updateFunction,
cleanUpFunction, userData)
local action = {};
-- The Action's data members.
action.cleanUpFunction_ = cleanUpFunction;
action.initializeFunction_ = initializeFunction;
action.updateFunction_ = updateFunction;
action.name_ = name or "";
action.status_ = Action.Status.UNINITIALIZED;
action.type_ = Action.Type;
action.userData_ = userData;
return action;
end
[ 175 ]
Decision Making
Initializing an action
Initializing an action begins by calling the action's initialize callback and then
immediately sets the action into a running state. This transitions the action into
a standard update loop that is moving forward:
Action.lua:
function Action.Initialize(self)
-- Run the initialize function if one is specified.
if (self.status_ == Action.Status.UNINITIALIZED) then
if (self.initializeFunction_) then
self.initializeFunction_(self.userData_);
end
end
-- Set the action to running after initializing.
self.status_ = Action.Status.RUNNING;
end
Updating an action
Once an action has transitioned to a running state, it will receive callbacks to the
update function every time the agent itself is updated, until the action decides
to terminate. To avoid an infinite loop case, the update function must return a
terminated status when a condition is met; otherwise, our agents will never be
able to finish the running action.
An update function isn't a hard requirement for our actions, as actions
terminate immediately by default if no callback function is present.
Action.lua:
function Action.Update(self, deltaTimeInMillis)
if (self.status_ == Action.Status.TERMINATED) then
-- Immediately return if the Action has already
-- terminated.
return Action.Status.TERMINATED;
elseif (self.status_ == Action.Status.RUNNING) then
if (self.updateFunction_) then
-- Run the update function if one is specified.
self.status_ = self.updateFunction_(
deltaTimeInMillis, self.userData_);
-- Ensure that a status was returned by the update
-- function.
assert(self.status_);
else
[ 176 ]
Chapter 6
-- If no update function is present move the action
-- into a terminated state.
self.status_ = Action.Status.TERMINATED;
end
end
return self.status_;
end
Action cleanup
Terminating an action is very similar to initializing an action, and it sets the status
of the action to uninitialized once the cleanup callback has an opportunity to finish
any processing of the action.
If a cleanup callback function isn't defined, the action will
immediately move to an uninitialized state upon cleanup.
During action cleanup, we'll check to make sure the action has fully terminated,
and then run a cleanup function if one is specified.
Action.lua:
function Action.CleanUp(self)
if (self.status_ == Action.Status.TERMINATED) then
if (self.cleanUpFunction_) then
self.cleanUpFunction_(self.userData_);
end
end
self.status_ = Action.Status.UNINITIALIZED;
end
Decision Making
action.Initialize = Action.Initialize;
action.Update = Action.Update;
return action;
end
Creating actions
With a basic action class out of the way, we can start implementing specific
action logic that our agents can use. Each action will consist of three callback
functionsinitialization, update, and cleanupthat we'll use when we
instantiate our action instances.
Updating our action requires that we check how much time has passed; if the 2
seconds have gone by, we terminate the action by returning the terminated state;
otherwise, we return that the action is still running:
SoldierActions.lua:
function SoldierActions_IdleUpdate(deltaTimeInMillis, userData)
local sandboxTimeInMillis = Sandbox.GetTimeInMillis(
[ 178 ]
Chapter 6
userData.agent:GetSandbox());
if (sandboxTimeInMillis >= userData.idleEndTime) then
userData.idleEndTime = nil;
return Action.Status.TERMINATED;
end
return Action.Status.RUNNING;
end
As we'll be using our idle action numerous times, we'll create a wrapper around
initializing our action based on our three functions:
SoldierLogic.lua:
local function IdleAction(userData)
return Action.new(
"idle",
SoldierActions_IdleInitialize,
SoldierActions_IdleUpdate,
SoldierActions_IdleCleanUp,
userData);
end
Decision Making
end
function SoldierActions_DieUpdate(deltaTimeInMillis, userData)
return Action.Status.TERMINATED;
end
Creating a wrapper function to instantiate a death action is identical to our idle action:
SoldierLogic.lua:
local function DieAction(userData)
return Action.new(
"die",
SoldierActions_DieInitialize,
SoldierActions_DieUpdate,
SoldierActions_DieCleanUp,
userData);
end
Chapter 6
end
return Action.Status.TERMINATED;
end
SoldierLogic.lua:
local function ReloadAction(userData)
return Action.new(
"reload",
SoldierActions_ReloadInitialize,
SoldierActions_ReloadUpdate,
SoldierActions_ReloadCleanUp,
userData);
end
[ 181 ]
Decision Making
Creating the shooting action requires more than just queuing up a shoot command
to the animation controller. As the SHOOT command loops, we'll queue an IDLE
command immediately afterward so that the shoot action will terminate after a single
bullet is fired. To have a chance at actually shooting an enemy agent, though, we first
need to orient our agent to face toward its enemy. During the normal update loop of
the action, we will forcefully set the agent to point in the enemy's direction.
Forcefully setting the agent's forward direction during an action will
allow our soldier to shoot but creates a visual artifact where the agent
will pop to the correct forward direction. See whether you can modify
the shoot action's update to interpolate to the correct forward direction
for better visual results.
SoldierActions.lua:
function SoldierActions_ShootCleanUp(userData)
-- No cleanup is required for shooting.
end
function SoldierActions_ShootInitialize(userData)
userData.controller:QueueCommand(
userData.agent,
SoldierController.Commands.SHOOT);
userData.controller:QueueCommand(
userData.agent,
SoldierController.Commands.IDLE);
return Action.Status.RUNNING;
end
function SoldierActions_ShootUpdate(deltaTimeInMillis, userData)
-- Point toward the enemy so the Agent's rifle will shoot
-- correctly.
local forwardToEnemy = userData.enemy:GetPosition()
userData.agent:GetPosition();
Agent.SetForward(userData.agent, forwardToEnemy);
if (userData.controller:QueueLength() > 0) then
return Action.Status.RUNNING;
end
-- Subtract a single bullet per shot.
userData.ammo = userData.ammo - 1;
return Action.Status.TERMINATED;
end
[ 182 ]
Chapter 6
SoldierLogic.lua:
local function ShootAction(userData)
return Action.new(
"shoot",
SoldierActions_ShootInitialize,
SoldierActions_ShootUpdate,
SoldierActions_ShootCleanUp,
userData);
end
Decision Making
end
function SoldierActions_RandomMoveUpdate(userData)
return Action.Status.TERMINATED;
end
SoldierLogic.lua:
local function RandomMoveAction(userData)
return Action.new(
"randomMove",
SoldierActions_RandomMoveInitialize,
SoldierActions_RandomMoveUpdate,
SoldierActions_RandomMoveCleanUp,
userData);
end
When applying the move action onto our agents, the indirect soldier controller will
manage all animation playback and steer our agent along their path.
[ 184 ]
Chapter 6
Setting a time limit for the move action will still allow our agents to move to their
final target position, but gives other actions a chance to execute in case the situation
has changed. Movement paths can be long, and it is undesirable to not handle
situations such as death until the move action has terminated:
SoldierActions.lua:
function SoldierActions_MoveToUpdate(deltaTimeInMillis, userData)
-- Terminate the action after the allotted 0.5 seconds. The
-- decision structure will simply repath if the Agent needs
-- to move again.
local sandboxTimeInMillis =
Sandbox.GetTimeInMillis(userData.agent:GetSandbox());
if (sandboxTimeInMillis >= userData.moveEndTime) then
userData.moveEndTime = nil;
return Action.Status.TERMINATED;
end
path = userData.agent:GetPath();
if (#path ~= 0) then
offset = Vector.new(0, 0.05, 0);
[ 185 ]
Decision Making
DebugUtilities_DrawPath(
path, false, offset, DebugUtilities.Orange);
Core.DrawCircle(
path[#path] + offset, 1.5, DebugUtilities.Orange);
end
-- Terminate movement is the Agent is close enough to the
-- target.
if (Vector.Distance(userData.agent:GetPosition(),
userData.agent:GetTarget()) < 1.5) then
Agent.RemovePath(userData.agent);
return Action.Status.TERMINATED;
end
return Action.Status.RUNNING;
end
SoldierLogic.lua:
local function MoveAction(userData)
return Action.new(
"move",
SoldierActions_MoveToInitialize,
SoldierActions_MoveToUpdate,
SoldierActions_MoveToCleanUp,
userData);
end
Chapter 6
local path = Sandbox.FindPath(
sandbox,
"default",
userData.agent:GetPosition(),
endPoint);
-- Find a valid position at least 16 units away from the
-- current enemy.
-- Note: Since pathfinding is not affected by the enemy,
-- it is entirely possible to generate paths that move the
-- Agent into the enemy, instead of away from the enemy.
while #path == 0 do
endPoint = Sandbox.RandomPoint(sandbox, "default");
while Vector.DistanceSquared(endPoint,
userData.enemy:GetPosition()) < 16.0 do
endPoint = Sandbox.RandomPoint(
sandbox, "default");
end
path = Sandbox.FindPath(
sandbox,
"default",
userData.agent:GetPosition(),
endPoint);
end
Soldier_SetPath(userData.agent, path, false);
userData.agent:SetTarget(endPoint);
userData.movePosition = endPoint;
else
-- Randomly move anywhere if the Agent has no current
-- enemy.
SoldierActions_RandomMoveInitialize(userData);
end
userData.controller:QueueCommand(
userData.agent,
SoldierController.Commands.MOVE);
return Action.Status.RUNNING;
end
[ 187 ]
Decision Making
Once a valid target position and path is found, the flee action acts very similar to
a move action with one exception, which is health. As our flee action continues to
move our agent until it reaches the target position, we must check whether the
agent is still alive; otherwise, we have to terminate the action early:
SoldierActions.lua:
function SoldierActions_FleeUpdate(deltaTimeInMillis, userData)
-- Terminate the Action if the agent is dead.
if (Agent.GetHealth(userData.agent) <= 0) then
return Action.Status.TERMINATED;
end
path = userData.agent:GetPath();
DebugUtilities_DrawPath(
path, false, Vector.new(), DebugUtilities.Blue);
Core.DrawCircle(
path[#path], 1.5, DebugUtilities.Blue);
if (Vector.Distance(
userData.agent:GetPosition(),
userData.agent:GetTarget()) < 1.5) then
Agent.RemovePath(userData.agent);
return Action.Status.TERMINATED;
end
return Action.Status.RUNNING;
end
When our agents start fleeing from their pursuers, they will now draw a blue path
indicating the position they are fleeing to.
[ 188 ]
Chapter 6
Instantiating the flee action requires creating a new action and specifying the
initialize, update, and cleanup functions.
SoldierLogic.lua:
local function FleeAction(userData)
return Action.new(
"flee",
SoldierActions_FleeInitialize,
SoldierActions_FleeUpdate,
SoldierActions_FleeCleanUp,
userData);
end
[ 189 ]
Decision Making
[ 190 ]
Chapter 6
As the enemy agent will typically be moving, we need to update our agent's path
during the update loop so that the agent can track down the enemy's new position.
If our agent gets within 3.0 meters of the enemy, the pursuit ends and another
action can be run. As pursuit is a long running action, we also check for the health
condition of our agent that can terminate pursuits early:
SoldierActions.lua:
function SoldierActions_PursueUpdate(deltaTimeInMillis, userData)
-- Terminate the Action if the agent dies.
if (Agent.GetHealth(userData.agent) <= 0) then
return Action.Status.TERMINATED;
end
-- Constantly repath to the enemy's new position.
local sandbox = userData.agent:GetSandbox();
local endPoint = userData.enemy:GetPosition();
local path = Sandbox.FindPath(
sandbox,
"default",
userData.agent:GetPosition(),
endPoint);
if (#path ~= 0) then
Soldier_SetPath(userData.agent, path, false);
userData.agent:SetTarget(endPoint);
userData.movePosition = endPoint;
offset = Vector.new(0, 0.1, 0);
path = userData.agent:GetPath();
DebugUtilities_DrawPath(
path, false, offset, DebugUtilities.Red);
Core.DrawCircle(
path[#path] + offset, 3, DebugUtilities.Red);
end
-- Terminate the pursuit Action when the Agent is within
-- shooting distance to the enemy.
if (Vector.Distance(userData.agent:GetPosition(),
userData.agent:GetTarget()) < 3) then
Agent.RemovePath(userData.agent);
[ 191 ]
Decision Making
return Action.Status.TERMINATED;
end
return Action.Status.RUNNING;
end
When our soldiers are pursuing their enemy, we'll now see a red path that is
constantly updated to move toward the enemy position.
Instantiating a pursue action is identical to the previous actions we've created and
requires passing our pursue initialize, update, and cleanup functions to each new
action instance.
SoldierLogic.lua:
local function PursueAction(userData)
return Action.new(
"pursue",
SoldierActions_PursueInitialize,
SoldierActions_PursueUpdate,
SoldierActions_PursueCleanUp,
userData);
end
[ 192 ]
Chapter 6
Evaluators
Evaluators are the principal method of handling conditional checks in our decision
structures. While actions perform the eventual behaviors that our agents exhibit, it's
the responsibly of evaluators to determine which action is allowed to run at what time.
Creating an evaluator object simply wraps a function call that returns true or false
when the userData table is passed into the function:
Evaluator.lua:
function Evaluator.Evaluate(self)
return self.function_(self.userData_);
end
function Evaluator.new(name, evalFunction, userData)
local evaluator = {};
-- data members
evaluator.function_ = evalFunction;
evaluator.name_ = name or "";
evaluator.type_ = Evaluator.Type;
evaluator.userData_ = userData;
-- object functions
evaluator.evaluate_ = Evaluate;
return evaluator;
end
Creating evaluators
Creating evaluators relies on simple functions that perform isolated operations on
the agent's userData table. Typically, most evaluators will only perform calculations
based on userData instead of modifying the data itself, although there is no
limitation on doing this. As the same evaluator might appear within a decision
structure, care must be taken to create consistent decision choices.
Evaluators that modify userData can easily become a source of bugs;
try to avoid this if at all possible. Actions should modify the agent's state,
when evaluators begin to modify the agent's state as a decision structure
is processed; it becomes very difficult to tell how and why any eventual
action was chosen, as the order of operations will affect the outcome.
[ 193 ]
Decision Making
Constant evaluators
First, we'll need to create the two most basic evaluators; one that always returns true,
and another that always returns false. These evaluators come in handy as a means of
enabling or disabling actions during development:
SoldierEvaluators.lua:
function SoldierEvaluators_True(userData)
return true;
end
function SoldierEvaluators_False(userData)
return false;
end
[ 194 ]
Chapter 6
Since our agents will need to know when they have an enemy as well as when there
are no valid enemies, we'll create a normal HasEnemy function evaluator and the
inverse HasNoEnemy function.
SoldierEvaluators.lua:
function SoldierEvaluators_HasEnemy(userData)
local sandbox = userData.agent:GetSandbox();
local position = Agent.GetPosition(userData.agent);
local agents = Sandbox.GetAgents(userData.agent:GetSandbox());
local closestEnemy;
local distanceToEnemy;
for index=1, #agents do
local agent = agents[index];
if (Agent.GetId(agent) ~= Agent.GetId(userData.agent) and
Agent.GetHealth(agent) > 0) then
-- Find the closest enemy.
local distanceToAgent = Vector.DistanceSquared(
position, Agent.GetPosition(agent));
if (closestEnemy == nil or
distanceToAgent < distanceToEnemy) then
local path = Sandbox.FindPath(
sandbox,
"default",
position,
agent:GetPosition());
-- If the agent can path to the enemy, use this
-- enemy as the best possible enemy.
if (#path ~= 0) then
closestEnemy = agent;
distanceToEnemy = distanceToAgent;
[ 195 ]
Decision Making
end
end
end
end
userData.enemy = closestEnemy;
return userData.enemy ~= nil;
end
function SoldierEvaluators_HasNoEnemy(userData)
return not SoldierEvaluators_HasEnemy(userData);
end
Is alive evaluator
IsAlive simply informs you whether the agent has health left, and the IsNotAlive
SoldierEvaluators.lua:
function SoldierEvaluators_IsAlive(userData)
return Agent.GetHealth(userData.agent) > 0;
end
function SoldierEvaluators_IsNotAlive(userData)
return not SoldierEvaluators_IsAlive(userData);
end
[ 196 ]
Chapter 6
Decision structures
With actions and evaluators at our disposal, we'll begin to create different types
of logic structures that use both of these primitive operators to build our agent's
behaviors. While each decision structure uses different approaches and techniques,
we'll create similar behaving agents based on the actions and evaluators we have.
[ 197 ]
Decision Making
Decision trees
Decision trees will be the first structure we'll implement and are, by far, the easiest
way to understand how a decision was made. A decision tree is composed of
branches and leaves. Each branch in the tree will wrap an evaluator, while each
leaf will be composed of an action. Through a sequence of branch conditions, our
decision tree will always result in a final action that our agent will perform.
To create a decision tree structure, we'll implement an update loop for our tree,
which evaluates the root branch within the tree and then proceeds to process the
resulting action. Once the action has been initialized, updated, and eventually,
terminated, the decision tree will re-evaluate the tree from the root branch to
determine the next action to be executed:
DecisionTree.lua:
require "Action"
DecisionTree = {};
function DecisionTree.SetBranch(self, branch)
self.branch_ = branch;
end
function DecisionTree.Update(self, deltaTimeInMillis)
-- Skip execution if the tree hasn't been setup yet.
if (self.branch_ == nil) then
return;
end
-- Search the tree for an Action to run if not currently
-- executing an Action.
if (self.currentAction_ == nil) then
self.currentAction_ = self.branch_:Evaluate();
self.currentAction_:Initialize();
end
local status = self.currentAction_:Update(deltaTimeInMillis);
-- Clean up the Action once it has terminated.
if (status == Action.Status.TERMINATED) then
self.currentAction_:CleanUp();
self.currentAction_ = nil;
[ 198 ]
Chapter 6
end
end
function DecisionTree.new()
local decisionTree = {};
-- The DecisionTree's data members.
decisionTree.branch_ = nil;
decisionTree.currentAction_ = nil;
-- The DecisionTree's accessor functions.
decisionTree.SetBranch = DecisionTree.SetBranch;
decisionTree.Update = DecisionTree.Update;
return decisionTree;
end
Branches
Branches in a decision tree consist of a conditional evaluator that determines which
child is executed. It is the responsibility of the evaluator to return a value that ranges
from 1 to the maximum number of children in the branch. Even though we'll only be
creating binary decision trees, the structure itself can branch out to any number of
children, as shown in the following diagram:
Our decision branch class will have basic assessors such as adding additional
children as well as setting the evaluator function used during branch calculation.
DecisionBranch.lua:
DecisionBranch = {}
DecisionBranch.Type = "DecisionBranch";
function DecisionBranch.new()
local branch = {};
-- The DecisionBranch's data members.
branch.children_ = {};
[ 199 ]
Decision Making
branch.evaluator_ = nil;
branch.type_ = DecisionBranch.Type;
-- The DecisionBranch's accessor functions.
branch.AddChild = DecisionBranch.AddChild;
branch.Evaluate = DecisionBranch.Evaluate;
branch.SetEvaluator = DecisionBranch.SetEvaluator;
return branch;
end
function DecisionBranch.AddChild(self, child, index)
-- Add the child at the specified index, or as the last child.
index = index or (#self.children_ + 1);
table.insert(self.children_, index, child);
end
function DecisionBranch.SetEvaluator(self, evaluator)
self.evaluator_ = evaluator;
end
Decision leaves
As the leaves of the decision tree are merely actions, we can completely encase
each leaf action into the branches themselves without the need for any additional
structures. The use of the type_ variable allows us to determine whether a child
of the branch is another branch or an action to be executed.
Branch evaluation
To evaluate a branch, we execute the evaluator and use the return value to further
process the tree. Once a choice is made, we either return an action node if the
selected child is a leaf, otherwise we recursively evaluate another branch until an
action is found.
Every branch within a decision tree must eventually end with an
action node; trees without actions as leafs are malformed and will
not evaluate properly.
To implement evaluation, we'll use the type_ field to determine if a child should be
considered as a branch or as an action to return.
DecisionBranch.lua:
function DecisionBranch.Evaluate(self)
-- Execute the branch's evaluator function, this much return a
-- numeric value which indicates what child should execute.
[ 200 ]
Chapter 6
local eval = self.evaluator_();
local choice = self.children_[eval];
if (choice.type_ == DecisionBranch.Type) then
-- Recursively evaluate children that are decisions
-- branches.
return choice:Evaluate();
else
-- Return the leaf action.
return choice;
end
end
Creating branches
The tree we'll be creating combines each of the actions and evaluators we
implemented previously and gives our agents the ability to pursue, flee,
move, shoot, idle, reload, and die.
First we'll create each branch instance that our decision tree will contain before
adding any evaluators or actions.
[ 201 ]
Decision Making
SoldierLogic.lua:
function SoldierLogic_DecisionTree(userData)
local tree = DecisionTree.new();
local
local
local
local
local
local
local
local
isAliveBranch = DecisionBranch.new();
criticalBranch = DecisionBranch.new();
moveFleeBranch = DecisionBranch.new();
enemyBranch = DecisionBranch.new();
ammoBranch = DecisionBranch.new();
shootBranch = DecisionBranch.new();
moveRandomBranch = DecisionBranch.new();
randomBranch = DecisionBranch.new();
tree:SetBranch(isAliveBranch);
return tree;
end
Once we've created each branch in our decision tree, we'll begin to hook up the
parent-child relationships between branches as well as add leaf node actions.
As our decision tree follows a binary tree design, each branch will typically have
one action and another branch. Branches at the tips of the tree will end with two
different actions:
SoldierLogic.lua:
function SoldierLogic_DecisionTree(userData)
...
isAliveBranch:AddChild(criticalBranch);
[ 202 ]
Chapter 6
isAliveBranch:AddChild(DieAction(userData));
isAliveBranch:SetEvaluator(
function()
if SoldierEvaluators_IsNotAlive(userData) then
return 2;
end
return 1;
end);
criticalBranch:AddChild(moveFleeBranch);
criticalBranch:AddChild(enemyBranch);
criticalBranch:SetEvaluator(
function()
if SoldierEvaluators_HasCriticalHealth(
userData) then
return 1;
end
return 2;
end);
moveFleeBranch:AddChild(MoveAction(userData));
moveFleeBranch:AddChild(FleeAction(userData));
moveFleeBranch:SetEvaluator(
function()
if SoldierEvaluators_HasMovePosition(userData) then
return 1;
end
return 2;
end);
tree:SetBranch(isAliveBranch);
return tree;
end
So far, we've added death, move, and flee actions; now, we'll add the remaining
reload, shoot, pursue, move, random move, and idle actions:
SoldierLogic.lua:
function SoldierLogic_DecisionTree(userData)
...
enemyBranch:AddChild(ammoBranch);
enemyBranch:AddChild(moveRandomBranch);
[ 203 ]
Decision Making
enemyBranch:SetEvaluator(
function()
if SoldierEvaluators_HasAmmo(userData) then
return 2;
end
return 1;
end);
ammoBranch:AddChild(shootBranch);
ammoBranch:AddChild(ReloadAction(userData));
ammoBranch:SetEvaluator(
function()
if SoldierEvaluators_HasAmmo(userData) then
return 1;
end
return 2;
end);
shootBranch:AddChild(ShootAction(userData));
shootBranch:AddChild(PursueAction(userData));
shootBranch:SetEvaluator(
function()
if SoldierEvaluators_CanShootAgent(userData) then
return 1;
end
return 2;
end);
moveRandomBranch:AddChild(MoveAction(userData));
moveRandomBranch:AddChild(randomBranch);
moveRandomBranch:SetEvaluator(
function()
if SoldierEvaluators_HasMovePosition(userData) then
return 1;
end
return 2;
end);
randomBranch:AddChild(RandomMoveAction(userData));
randomBranch:AddChild(IdleAction(userData));
randomBranch:SetEvaluator(
function()
if SoldierEvaluators_Random(userData) then
return 1;
end
return 2;
[ 204 ]
Chapter 6
end);
tree:SetBranch(isAliveBranch);
return tree;
end
soldier;
soldierController;
soldierDecisionTree;
soldierUserData;
function Agent_Initialize(agent)
Soldier_InitializeAgent(agent);
soldier = Soldier_CreateSoldier(agent);
weapon = Soldier_CreateWeapon(agent);
soldierController = SoldierController.new(
agent, soldier, weapon);
Soldier_AttachWeapon(soldier, weapon);
weapon = nil;
soldierUserData = {};
soldierUserData.agent = agent;
soldierUserData.controller = soldierController;
soldierUserData.maxHealth = soldierUserData.health;
soldierUserData.alive = true;
soldierUserData.ammo = 10;
[ 205 ]
Decision Making
soldierUserData.maxAmmo = 10;
soldierDecisionTree = SoldierLogic_DecisionTree(
soldierUserData);
end
function Agent_Update(agent, deltaTimeInMillis)
if (soldierUserData.alive) then
soldierDecisionTree:Update(deltaTimeInMillis);
end
soldierController:Update(agent, deltaTimeInMillis);
end
[ 206 ]
Chapter 6
States
States within an FSM are responsible for associating an action with the state. We
create a state by passing an action and naming the state for debug convenience:
FiniteState.lua:
require "Action";
require "FiniteState";
require "FiniteStateTransition";
FiniteState = {};
function FiniteState.new(name, action)
local state = {};
-- The FiniteState's data members.
state.name_ = name;
state.action_ = action;
return state;
end
Transitions
Transitions encapsulate the state to be transitioned to as well as the evaluator that
determines whether the transition should be taken. The responsibility for evaluating
transitions is left to the finite state machine itself:
FiniteStateTransition.lua:
FiniteStateTransition = {};
function FiniteStateTransition.new(toStateName, evaluator)
local transition = {};
-- The FiniteStateTransition's data members.
transition.evaluator_ = evaluator;
transition.toStateName_ = toStateName;
return transition;
end
[ 207 ]
Decision Making
Helper functions
We can implement additional helper functions in order to allow interactions
with the FSM in a safe manner:
FiniteStateMachine.lua:
function FiniteStateMachine.ContainsState(self, stateName)
return self.states_[stateName] ~= nil;
end
function FiniteStateMachine.ContainsTransition(
self, fromStateName, toStateName)
return self.transitions_[fromStateName] ~= nil and
self.transitions_[fromStateName][toStateName] ~= nil;
end
function FiniteStateMachine.GetCurrentStateName(self)
if (self.currentState_) then
return self.currentState_.name_;
end
end
function FiniteStateMachine.GetCurrentStateStatus(self)
if (self.currentState_) then
return self.currentState_.action_.status_;
end
[ 208 ]
Chapter 6
end
function FiniteStateMachine.SetState(self, stateName)
if (self:ContainsState(stateName)) then
if (self.currentState_) then
self.currentState_.action_:CleanUp();
end
self.currentState_ = self.states_[stateName];
self.currentState_.action_:Initialize();
end
end
Creating our AddState and AddTransition functions will modify the internal tables
held by the FSM in order to add additional states and map one state to another
using transitions
FiniteStateMachine.lua:
function FiniteStateMachine.AddState(self, name, action)
self.states_[name] = FiniteState.new(name, action);
end
function FiniteStateMachine.AddTransition(
self, fromStateName, toStateName, evaluator)
-- Ensure both states exist within the FSM.
if (self:ContainsState(fromStateName) and
[ 209 ]
Decision Making
self:ContainsState(toStateName)) then
if (self.transitions_[fromStateName] == nil) then
self.transitions_[fromStateName] = {};
end
-- Add the new transition to the "from" state.
table.insert(
self.transitions_[fromStateName],
FiniteStateTransition.new(toStateName, evaluator));
end
end
First we'll create an EvaluateTransitions function to determine the next state our
FSM will move to once a state has finished. Afterwards we can create the FSM Update
function that manages a running action and determines when state transitions occur.
FiniteStateMachine.lua:
local function EvaluateTransitions(self, transitions)
for index = 1, #transitions do
-- Find the first transition that evaluates to true,
-- return the state the transition points to.
if (transitions[index].evaluator_(self.userData_)) then
return transitions[index].toStateName_;
end
end
end
function FiniteStateMachine.Update(self, deltaTimeInMillis)
if (self.currentState_) then
[ 210 ]
Chapter 6
local status = self:GetCurrentStateStatus();
if (status == Action.Status.RUNNING) then
self.currentState_.action_:Update(deltaTimeInMillis);
elseif (status == Action.Status.TERMINATED) then
-- Evaluate all transitions to find the next state
-- to move the FSM to.
local toStateName = EvaluateTransitions(
self,
self.transitions_[self.currentState_.name_]);
if (self.states_[toStateName] ~= nil) then
self.currentState_.action_:CleanUp();
self.currentState_ = self.states_[toStateName];
self.currentState_.action_:Initialize();
end
end
end
end
Decision Making
[ 212 ]
Chapter 6
[ 213 ]
Decision Making
As our move action is time-based, instead of terminating when our agent reaches
its target position, we need to continue looping within the state until a better option
becomes available:
SoldierLogic.lua:
function SoldierLogic_FiniteStateMachine(userData)
...
-- move action
fsm:AddTransition(
"move", "die", SoldierEvaluators_IsNotAlive);
fsm:AddTransition(
"move", "flee", SoldierEvaluators_HasCriticalHealth);
fsm:AddTransition(
"move", "reload", SoldierEvaluators_HasNoAmmo);
fsm:AddTransition(
"move", "shoot", SoldierEvaluators_CanShootAgent);
fsm:AddTransition(
"move", "pursue", SoldierEvaluators_HasEnemy);
fsm:AddTransition(
"move", "move", SoldierEvaluators_HasMovePosition);
fsm:AddTransition(
"move", "randomMove", SoldierEvaluators_Random);
[ 214 ]
Chapter 6
fsm:AddTransition("move", "idle", SoldierEvaluators_True);
fsm:SetState("idle");
return fsm;
end
[ 215 ]
Decision Making
As our state machine only has eight possible states, states such as idle and shoot are
nearly fully connected to any possible action our agent can perform. Having fully
connected states produces more reactive agents overall:
SoldierLogic.lua:
function SoldierLogic_FiniteStateMachine(userData)
...
-- shoot action
fsm:AddTransition(
"shoot", "die", SoldierEvaluators_IsNotAlive);
fsm:AddTransition(
"shoot", "flee", SoldierEvaluators_HasCriticalHealth);
fsm:AddTransition(
"shoot", "reload", SoldierEvaluators_HasNoAmmo);
fsm:AddTransition(
"shoot", "shoot", SoldierEvaluators_CanShootAgent);
fsm:AddTransition(
"shoot", "pursue", SoldierEvaluators_HasEnemy);
fsm:AddTransition(
"shoot", "randomMove", SoldierEvaluators_Random);
fsm:AddTransition("shoot", "idle", SoldierEvaluators_True);
fsm:SetState("idle");
return fsm;
end
[ 216 ]
Chapter 6
The only way for our agent to exit from fleeing its enemy is through death:
SoldierLogic.lua:
function SoldierLogic_FiniteStateMachine(userData)
...
-- flee action
fsm:AddTransition(
"flee", "die", SoldierEvaluators_IsNotAlive);
fsm:AddTransition("flee", "move", SoldierEvaluators_True);
fsm:SetState("idle");
return fsm;
end
[ 217 ]
Decision Making
In this case, the only thing our agent cares about while pursuing an enemy is to flee
in order to prevent death, shoot the enemy when it comes within range, or idle if the
enemy is no longer valid. As there are limited transitions from pursuit, our agent will
sometimes need to jump through another state to find a valid action, such as reload.
Even though cases like these exist, very little noticeable latency is introduced, and
the finite state machine is less complex because of the reduced number of transitions:
SoldierLogic.lua:
function SoldierLogic_FiniteStateMachine(userData)
...
-- pursue action
fsm:AddTransition(
"pursue", "die", SoldierEvaluators_IsNotAlive);
fsm:AddTransition(
"pursue", "shoot", SoldierEvaluators_CanShootAgent);
fsm:AddTransition(
"pursue", "move", SoldierEvaluators_HasMovePosition);
fsm:AddTransition("pursue", "idle", SoldierEvaluators_True);
fsm:SetState("idle");
return fsm;
end
[ 218 ]
Chapter 6
This reduced number of transitions will also incur latency in behavior selection,
but it further reduces the complexity of our agent:
SoldierLogic.lua:
function SoldierLogic_FiniteStateMachine(userData)
...
-- reload action
fsm:AddTransition(
"reload", "die", SoldierEvaluators_IsNotAlive);
fsm:AddTransition(
"reload", "shoot", SoldierEvaluators_CanShootAgent);
fsm:AddTransition(
"reload", "pursue", SoldierEvaluators_HasEnemy);
fsm:AddTransition(
"reload", "randomMove", SoldierEvaluators_Random);
fsm:AddTransition("reload", "idle", SoldierEvaluators_True);
fsm:SetState("idle");
return fsm;
end
[ 219 ]
Decision Making
soldier;
soldierController;
soldierFSM;
soldierUserData;
function Agent_Initialize(agent)
...
soldierFSM = SoldierLogic_FiniteStateMachine(
soldierUserData);
end
function Agent_Update(agent, deltaTimeInMillis)
if (soldierUserData.alive) then
soldierFSM:Update(deltaTimeInMillis);
end
soldierController:Update(agent, deltaTimeInMillis);
end
[ 220 ]
Chapter 6
Behavior trees
With decision trees focusing on the ifelse style of action selection and state
machines focusing on the statefulness of actions, behavior trees fill a nice middle
ground with reaction-based decision making.
Decision Making
node.action_ = nil;
node.children_ = {};
node.evaluator_ = nil;
node.name_ = name or "";
node.parent_ = nil;
node.type_ = type or BehaviorTreeNode.Type.ACTION;
end
Helper functions
Adding some object-oriented helper functions to handle child management as well
as a backward link to the nodes parent will allow the behavior tree to evaluate the
nodes more efficiently:
BehaviorTreeNode.lua:
function BehaviorTreeNode.AddChild(self, child, index)
index = index or (#self.children_ + 1);
table.insert(self.children_, index, child);
child.parent_ = self;
end
function BehaviorTreeNode.ChildIndex(self, child)
for index=1, #self.children_ do
if (self.children_[index] == child) then
return index;
end
end
return -1;
end
function BehaviorTreeNode.GetChild(self, childIndex)
return self.children_[childIndex];
end
function BehaviorTreeNode.GetNumberOfChildren(self)
return #self.children_;
end
function BehaviorTreeNode.GetParent(self)
return self.parent_;
[ 222 ]
Chapter 6
end
function BehaviorTreeNode.SetAction(self, action)
self.action_ = action;
end
function BehaviorTreeNode.SetEvaluator(self, evaluator)
self.evaulator_ = evaluator;
end
function BehaviorTreeNode.SetType(self, type)
self.type_ = type;
end
[ 223 ]
Decision Making
Actions
The first node type is a basic action. We can create a wrapper function that will
instantiate an action node and set the internal action accordingly. Actions are only
designed to execute behaviors on an agent and shouldn't be assigned any children.
They should be considered leaves in a behavior tree:
SoldierLogic.lua:
local function CreateAction(name, action)
local node = BehaviorTreeNode.new(
name, BehaviorTreeNode.Type.ACTION);
node:SetAction(action);
return node;
end
Conditions
Conditions are similar to actions and are also leaves in a behavior tree. Condition
nodes will execute the evaluator assigned to them and return the result to the caller
to determine how they should be processed.
SoldierLogic.lua:
local function CreateCondition(name, evaluator)
local condition = BehaviorTreeNode.new(
name, BehaviorTreeNode.Type.CONDITION);
condition:SetEvaluator(evaluator);
return condition;
end
Selectors
Selectors are the first type of nodes that can have children within the behavior tree.
A selector can have any number of children, but will only execute the first child that is
available for execution. Essentially, selectors act as if, ifelse, and else structures
within behavior trees. A selector will return true if at least one child node is able to run;
otherwise, the selector returns false:
SoldierLogic.lua:
local function CreateSelector()
return BehaviorTreeNode.new(
"selector", BehaviorTreeNode.Type.SELECTOR);
end
[ 224 ]
Chapter 6
Sequences
Lastly, we have sequences, which act as sequential blocks of execution that will
execute each of their children in an order until a condition, selector, or child sequence
fails to execute. Sequences will return true if all their children run successfully; if any
one of their children returns false, the sequence immediately exits and returns false
in turn:
SoldierLogic.lua:
local function CreateSequence()
return BehaviorTreeNode.new(
"sequence", BehaviorTreeNode.Type.SEQUENCE);
end
[ 225 ]
Decision Making
Selector evaluation
As selectors only return false if all child nodes have executed without returning true,
we can iterate over all children and return the first positive result we get back. We
need to return two values, the evaluation result as well as an action node if one is
found. To do this, we'll return a table containing both values.
As our behavior tree can be of any arbitrary depth, we will recursively evaluate both
selectors and sequences till we have a return result:
BehaviorTree.lua:
_EvaluateSelector = function(self, node, deltaTimeInMillis)
-- Try and evaluate all children. Returns the first child
-- that can execute. If no child can successfully execute the
-- selector fails.
for index = 1, #node.children_ do
local child = node:GetChild(index);
if (child.type_ == BehaviorTreeNode.Type.ACTION) then
-- Execute all Actions, since Actions cannot fail.
return { node = child, result = true};
elseif (child.type_ ==
BehaviorTreeNode.Type.CONDITION) then
-- Conditions are only valid within sequences, if one
-- is encountered in a selector the tree is malformed.
assert(false);
return { result = false };
elseif (child.type_ ==
BehaviorTreeNode.Type.SELECTOR) then
-- Recursively evaluate child selectors.
local result = _EvaluateSelector(
[ 226 ]
Chapter 6
self, child, deltaTimeInMillis);
if (result.result) then
return result;
end
elseif (child.type_ ==
BehaviorTreeNode.Type.SEQUENCE) then
-- Evaluate a sequence, if it returns successfully
-- then return the result.
-- The result of a sequence may not contain a node to
-- execute.
local result = _EvaluateSequence(
self, child, deltaTimeInMillis);
if (result.result) then
return result;
end
end
end
return { result = false };
end
Sequence evaluation
A sequence is nearly the opposite of a selector where the first failure will result in the
sequence returning a failure. As sequences can execute multiple actions sequentially,
we can take in an index number that represents the current child from which we
should start our evaluation. This allows the behavior tree to continue the evaluation
from where it left off:
BehaviorTree.lua:
_EvaluateSequence = function(self, node, deltaTimeInMillis, index)
-- Try and evaluate all children. Returns a false result if a
-- child is unable to execute, such as a condition failing or
-- child sequence/selector being unable to find a valid Action
-- to run.
index = index or 1;
for count=index, #node.children_ do
local child = node:GetChild(count);
if (child.type_ == BehaviorTreeNode.Type.ACTION) then
-- Execute all Actions, since Actions cannot fail.
[ 227 ]
Decision Making
return { node = child, result = true};
elseif (child.type_ ==
BehaviorTreeNode.Type.CONDITION) then
local result = child.evaluator_(self.userData_);
-- Break out of execution if a condition fails.
if (not child.evaluator_(self.userData_)) then
return { result = false };
end
elseif (child.type_ ==
BehaviorTreeNode.Type.SELECTOR) then
local result = _EvaluateSelector(
self, child, deltaTimeInMillis);
-- Unable to find an Action to run, return failure.
if (not result.result) then
return { result = false };
elseif (result.result and result.node ~= nil) then
-- Found an Action to execute, pass the result
-- back to the caller.
return result;
end
-- A selector must return an Action to be considered
-- successful, if no Action was found, then the
-- selector failed.
elseif (child.type_ ==
BehaviorTreeNode.Type.SEQUENCE) then
local result = _EvaluateSequence(
self, child, deltaTimeInMillis);
-- Sequence reported failure, propagate failure to the
-- caller.
if (not result.result) then
return { result = false };
elseif (result.result and result.node ~= nil) then
-- Found an Action to execute, pass the result
-- back to the caller.
return result;
end
-- There is a third possible case, the sequence
-- completed successfully and has no additional
[ 228 ]
Chapter 6
-- children to execute. In that case let the sequence
-- continue executing additional children.
end
-- Move to the next child to execute.
count = count + 1;
end
-- Returns success without an Action to run if all children
-- executed successfully.
return { result = true };
end
Node evaluation
Basic node evaluation is only used to begin the recursive tree evaluation of the root
node and handles all four possible root node type evaluations. If the root node is a
condition, an assert is called to indicate a malformed tree:
BehaviorTree.lua:
local function _EvaluateNode(self, node, deltaTimeInMillis)
if (node.type_ == BehaviorTreeNode.Type.ACTION) then
-- No further evaluation is necessary if an Action is
-- found.
return node;
elseif (node.type_ == BehaviorTreeNode.Type.CONDITION) then
-- Conditions should be evaluated immediately, if the
-- behavior tree is trying to evaluate this node, there is
-- something structurally wrong in the behavior tree.
assert(false); -- invalid structure
elseif (node.type_ == BehaviorTreeNode.Type.SELECTOR) then
-- Treat the node like a selector and find the first valid
-- child action.
local result = _EvaluateSelector(
self, node, deltaTimeInMillis);
if (result.result) then
return result.node;
end
elseif (node.type_ == BehaviorTreeNode.Type.SEQUENCE) then
-- Treat the node like a sequence and find the first valid
-- child action.
local result = _EvaluateSequence(
self, node, deltaTimeInMillis);
[ 229 ]
Decision Making
if (result.result) then
return result.node;
end
end
end
end
[ 230 ]
Chapter 6
end
-- Move one parent up in the tree.
childNode = parentNode;
parentNode = childNode:GetParent();
end
end
[ 231 ]
Decision Making
[ 232 ]
Chapter 6
We can start creating a behavior tree using a similarly wrapped function that
instantiates a behavior tree and creates the first selector node for the tree:
SoldierLogic.lua:
function SoldierLogic_BehaviorTree(userData)
local tree = BehaviorTree.new(userData);
local node;
local child;
node = CreateSelector();
tree:SetNode(node);
return tree;
end
[ 233 ]
Decision Making
Chapter 6
node = node:GetParent();
child = CreateSequence();
node:AddChild(child);
node = child;
child = CreateCondition(
"has critical health",
SoldierEvaluators_HasCriticalHealth);
node:AddChild(child);
node = child;
node = node:GetParent();
child = CreateAction("flee", FleeAction(userData));
node:AddChild(child);
node = child;
return tree;
end
Combat behaviors
Combat behaviors encompass reloading, shooting, and pursuit; all have a common
SoldierEvaluators_HasEnemy condition that must be true for any of these actions
to execute. The strength of a behavior tree allows you to group common behaviors
under common conditionals to reduce costly evaluations.
As our agent must choose a combat behavior if they have an enemy, the fallback
action, pursuit requires no additional conditions before executing. If the behavior isn't
able to process reloading or shooting, the pursuit action will be chosen automatically:
SoldierLogic.lua:
function SoldierLogic_BehaviorTree(userData)
...
-- reload/shoot/pursue actions
node = node:GetParent();
node = node:GetParent();
child = CreateSequence();
[ 235 ]
Decision Making
node:AddChild(child);
node = child;
child = CreateCondition(
"has enemy", SoldierEvaluators_HasEnemy);
node:AddChild(child);
node = child;
node = node:GetParent();
child = CreateSelector();
node:AddChild(child);
node = child;
[ 236 ]
Chapter 6
[ 237 ]
Decision Making
child = CreateAction("pursue", PursueAction(userData));
node:AddChild(child);
node = child;
return tree;
end
[ 238 ]
Chapter 6
[ 239 ]
Decision Making
soldier;
soldierController;
soldierBT;
soldierUserData;
function Agent_Initialize(agent)
...
soldierBT = SoldierLogic_BehaviorTree(
[ 240 ]
Chapter 6
soldierUserData);
end
function Agent_Update(agent, deltaTimeInMillis)
if (soldierUserData.alive) then
soldierBT:Update(deltaTimeInMillis);
end
soldierController:Update(agent, deltaTimeInMillis);
end
Summary
With some rudimentary actions and decision-making logic controlling our agents,
we can now begin to enhance how our agents see the world, as well as how they
store information about the world.
In the next chapter, we'll create a data structure that can store knowledge as well as
create senses for our agents to actually see and hear.
[ 241 ]
Alternatively, you can buy the book from Amazon, BN.com, Computer Manuals and
most internet book retailers.
www.PacktPub.com