Creating NPCs

From Pragma
Jump to: navigation, search

Chapter 1: Basics - Tasks and schedules

AI behavior can become very complex very quickly. To prevent code from becoming too confusing, convoluted and unintelligible, it's important to break up the behavior into as small as possible, independent structured code-pieces, that can be worked with without having to keep all of the specifics of the entire AI behavior in mind all the time.

These atomic pieces are called "tasks". A task can be anything from playing an animation, moving to a specific position to simply checking for a condition, as long as the task can't be split any further.
For example, if you want to implement a simple melee attack, you can split it into the following tasks.
Task a: Play the attack animation. Return true if the animation has played successfully, otherwise false.
Task b: Check if the enemy is in attack range. Return true if the enemy is in range, otherwise false.
Task c: Move towards the target during this tick. Return true if the movement was successful, otherwise false.
Note that we don't care about the order of the tasks at this point. These three tasks have nothing to do with each other and can be implemented completely independent from each other. This has several advantages:

  • If you know there's a bug with your NPC not moving for the melee attack, you don't have to wade through the entire code of your NPC, you only have to check out the code for Task c.
  • Tasks can be re-used more easily for different NPCs or different behavior. If you want to add a ranged attack to the same NPC, you can re-use all three tasks with slightly different parameters. If you want a NPC to follow the player, you can re-use task c.
  • The code doesn't get bloated because each task can be easily extracted into its own file.

Tasks can't actually be executed directly. To create some specific behavior for your NPC, you'll usually want to tie several different tasks together in some order. To do this, you have to create a Schedule.
A schedule is essentially just a collection of tasks, which tell your NPC what to do in what order, with some additional rules and parameters set in place. The schedule for the melee attack for example could look something like this:
1) Execute Task b. If the task returned true, go to 2), otherwise end schedule.
2) Execute Task c. If the task returned true, go to 3), otherwise end schedule.
3) Execute Task a. End schedule.
The NPC can then be told to run this schedule, which would cause the steps to be executed in sequential order.
The schedule can also assign parameters to each of its task to allow minor variations in their execution. Task a could have a parameter to allow changing the animation, for example, which could then be set by the schedule. This way you could have a schedule for a melee attack, and another schedule for a range attack, with both using the same task a, just with different parameters.

Using schedules you can easily switch between different behavior, attacks, etc.

This should give you a very basic understanding of how the system works, but it's not enough to actually work with the system. We'll delve a little deeper in chapter two.

Chapter 2:

The examples in chapter 1 were very simplified. Before we go any further, let's categorize tasks into different types:
Conditional Tasks
Conditional tasks are probably the most simple tasks. They simply check for one or more condition and return a result (usually success or failure).

  • Check if we have an enemy, and the enemy is in melee range, and not hidden behind a wall.
  • Check if we have at least half health left.

You can think of conditional tasks as simple if-conditions.
Blocking Tasks
While a conditional tasks simply checks a condition and returns immediately, a blocking task can stay active for some time. As long as a task is active, it will be updated each tick until it completes, and all other tasks will have to wait.

  • Play an animation.
  • Simply do nothing and stand idle for a few seconds.

Immediate Tasks
Immediate tasks are the counterpart of Blocking Tasks. Immediate tasks return (as the name implies) immediately and don't block execution of subsequent tasks.

  • Play a sound.
  • Generate a random number.
  • Conditional Tasks are also Immediate Tasks

Decorator Tasks
These are special tasks that can change how child-tasks should be executed, or how their return value should be interpreted. We'll get into what that means in a later chapter.

Technically there is no difference between these types, but it can be helpful to make the distinction.

Now let's take a look at some very simple code examples. First, let's create a schedule which causes a NPC to move to a position and then play an animation:

local pl = ents.get_local_player()
if(pl == nil) then return end
-- TODO: Actually closest target?
ents.IteratorFilterSphere -> Add sort parameter? (closest / furthest?)
local npc = ents.iterator({ents.IteratorFilterComponent(ents.COMPONENT_AI),ents.IteratorFilterSphere(origin,radius)})()
if(npc == nil) then return end

local sched = ai.create_schedule()

local taskMove = ai.create_task(ai.TASK_MOVE_TO_TARGET)
taskMove:SetParameterVector(ai.TaskMoveToTarget.PARAMETER_TARGET,Vector()) util.units_to_meters(2.0) [TODO]

local taskPlayAnimation = ai.create_task(ai.TASK_PLAY_ACTIVITY)


This will cause the NPC located closest to the player to move forward by 2 meters, and then play a melee attack animation (if the NPC has one). Here's a step-by-step explanation of each segment:

[TODO] Note than in this case the parameter can be a vector or an entity, depends on task implementation.

-- TODO --