• Stars
    star
    556
  • Rank 80,098 (Top 2 %)
  • Language
    C
  • Created almost 12 years ago
  • Updated 9 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

General Purpose Goal Oriented Action Planning

General Purpose GOAP

Introduction

GOAP, or Goal Oriented Action Planning is a powerful tool to create game AI. For all the details I will refer to Jeff Orkin's collection of articles. But in short: GOAP will let computer controlled characters (NPCs) make action plans that can achieve desired goals. It will do so in a highly maintainable, easily extendible, highly modular fashion. Naive implementation of AI code will invariably blow up for any non trivial problem. GOAP on the other hand, is robust and is unlikely to buckle under large complexity. This software implements GOAP in the C programming language. It does so in a generic fashion, which makes it suitable for many projects.

Basics of GOAP

Creating a plan for AI controlled entities comes down to the following steps:

  1. Describe the repertoire of actions available.
  2. Describe the current state of the world.
  3. Describe the desired state of the world (goal).

To describe the actions, we specify:

  • The preconditions for the action.
  • The postconditions (effects) of the action.
  • The cost of the action.

To describe world state, we define a set of world state atoms. Each atom has a tag, and a boolean value.

The planner will then be able to formulate a plan of actions that takes the world to the desired state, provided such a path exists. The plan formulated is guaranteed to be the lowest cost plan.

Example Scenario

Let us consider a planner for an AI soldier. Our soldier can perform the following actions: scout, approach, aim, shoot, load, detonatebomb, flee.

  • scout requires: armedwithgun. Effect: enemyvisible.
  • approach requires: enemyvisible. Effect: nearenemy.
  • aim requires: enemyvisible and weaponloaded. Effect: enemylinedup.
  • shoot requires: enemylinedup. Effect: !enemyalive.
  • load requires: armedwithgun. Effect: weaponloaded.
  • detonatebomb requires: armedwithbomb and nearenemy. Effect: !alive and !enemyalive.
  • flee requires: enemyvisible. Effect: !nearenemy.

Next, we will tell the planner that currently: Enemy is not visible, we are armed with gun, our weapon is not loaded, enemy is not lined up, enemy is alive, we are armed with bomb, we are not near enemy, we are alive.

Then we tell our planner what our desired world looks like. We only care about one thing: our enemy is not alive.

With this, the planner can formulate a plan of actions to make this happen. For the moment, let's assume all our actions have the default cost 1 associated with it. This is what the planner will return to us:

                       ARMEDWITHGUN,enemyvisible,nearenemy,weaponloaded,enemylinedup,ENEMYALIVE,ARMEDWITHBOMB,ALIVE,
0: scout               ARMEDWITHGUN,ENEMYVISIBLE,nearenemy,weaponloaded,enemylinedup,ENEMYALIVE,ARMEDWITHBOMB,ALIVE,
1: approach            ARMEDWITHGUN,ENEMYVISIBLE,NEARENEMY,weaponloaded,enemylinedup,ENEMYALIVE,ARMEDWITHBOMB,ALIVE,
2: detonatebomb        ARMEDWITHGUN,ENEMYVISIBLE,NEARENEMY,weaponloaded,enemylinedup,enemyalive,ARMEDWITHBOMB,alive,

Note: this notation uses lowercase if the condition is false, and uppercase if the condition is true. The first line shows the current world state, and the last line the desired world state, with all intermediate states in between. The plan to execute is: (scout, approach, detonatebomb).

If we follow this plan, we have the unfortunate side effect that not only our enemy dies, but we die as well. This is easily solved by making detonatingbomb a higher cost action. But a more elegant approach would be to change our desired world state, and tell the planner that not only do we want our enemy dead, we want to ourselves alive. The planner will now create:

                       ARMEDWITHGUN,enemyvisible,nearenemy,weaponloaded,enemylinedup,ENEMYALIVE,ARMEDWITHBOMB,ALIVE,
0: scout               ARMEDWITHGUN,ENEMYVISIBLE,nearenemy,weaponloaded,enemylinedup,ENEMYALIVE,ARMEDWITHBOMB,ALIVE,
1: load                ARMEDWITHGUN,ENEMYVISIBLE,nearenemy,WEAPONLOADED,enemylinedup,ENEMYALIVE,ARMEDWITHBOMB,ALIVE,
2: aim                 ARMEDWITHGUN,ENEMYVISIBLE,nearenemy,WEAPONLOADED,ENEMYLINEDUP,ENEMYALIVE,ARMEDWITHBOMB,ALIVE,
3: shoot               ARMEDWITHGUN,ENEMYVISIBLE,nearenemy,WEAPONLOADED,ENEMYLINEDUP,enemyalive,ARMEDWITHBOMB,ALIVE,

Example Code

The entire scenario described above is implemented succinctly in these few lines of code:

#include "goap.h"
#include "astar.h"

actionplanner_t ap;
goap_actionplanner_clear( &ap ); // initializes action planner

// describe repertoire of actions
goap_set_pre( &ap, "scout", "armedwithgun", true );
goap_set_pst( &ap, "scout", "enemyvisible", true );

goap_set_pre( &ap, "approach", "enemyvisible", true );
goap_set_pst( &ap, "approach", "nearenemy", true );

goap_set_pre( &ap, "aim", "enemyvisible", true );
goap_set_pre( &ap, "aim", "weaponloaded", true );
goap_set_pst( &ap, "aim", "enemylinedup", true );

goap_set_pre( &ap, "shoot", "enemylinedup", true );
goap_set_pst( &ap, "shoot", "enemyalive", false );

goap_set_pre( &ap, "load", "armedwithgun", true );
goap_set_pst( &ap, "load", "weaponloaded", true );

goap_set_pre( &ap, "detonatebomb", "armedwithbomb", true );
goap_set_pre( &ap, "detonatebomb", "nearenemy", true );
goap_set_pst( &ap, "detonatebomb", "alive", false );
goap_set_pst( &ap, "detonatebomb", "enemyalive", false );

goap_set_pre( &ap, "flee", "enemyvisible", true );
goap_set_pst( &ap, "flee", "nearenemy", false );

// describe current world state.
worldstate_t fr; 
goap_worldstate_clear( &fr );
goap_worldstate_set( &ap, &fr, "enemyvisible", false );
goap_worldstate_set( &ap, &fr, "armedwithgun", true );
goap_worldstate_set( &ap, &fr, "weaponloaded", false );
goap_worldstate_set( &ap, &fr, "enemylinedup", false );
goap_worldstate_set( &ap, &fr, "enemyalive", true );
goap_worldstate_set( &ap, &fr, "armedwithbomb", true );
goap_worldstate_set( &ap, &fr, "nearenemy", false );
goap_worldstate_set( &ap, &fr, "alive", true );

// describe desired world state.
worldstate_t goal;
goap_worldstate_clear( &goal );
goap_worldstate_set( &ap, &goal, "enemyalive", false );
//goap_worldstate_set( &ap, &goal, "alive", true ); // add this to avoid suicide actions in the plan.

worldstate_t states[16];
const char* plan[16]; // The planner will return the action plan in this array.
int plansz=16; // Size of our return buffers.
const int plancost = astar_plan( &ap, fr, goal, plan, states, &plansz );

And that is it. The plan's size will be returned in plansz which is not necessarily the same as the plan cost. You can access the plan array elements for the actual steps (actions). For each action in the plan, the resulting world state is also available in the states array.

Implementation Notes

To build, use a C99 compliant C compiler. You can invoke with: $ gcc -std=c99 astar.c goap.c main.c

The strength of GPGOAP is that the API is very generic: both world state and actions are solely described with C strings and booleans. No game specific enums end up in the API. With this power comes responsibility: do not make typos in the action names or atom names. They will end up representing different atoms if you do.

For performance, all the tags for the world state atoms are converted to an entry in a bit field. This bit field is implemented as a 'long long int', which typically is 8 bytes or 64 bits. This means that you cannot use more than 64 atoms to describe the world and the actions to the planner. When using multiple NPCs in your game, I suggest using separate planners for them, so that if two NPCs have different action sets, you don't end up combining their atom name space and exceeding 64 tags.

The only atom type supported is boolean. This means that scalars cannot be used to describe the world. I advice splitting up a scalar value with multiple booleans. E.g. fuelempy, fuellow, fuelfull to encode a single scalar fuel value.

I strongly advice using lowercase names for the world state atoms. I have provided a debug function that will return a string representation of a world state that you can print out. In this representation, lowercase is used for false valued atoms, and uppercase is used for true valued atoms.

Bugs

No known bugs.

Files

  • goap.h goap.c implements the planner.
  • astar.h astar.c implements A* search over the world state space.
  • main.c sample scenario.

goap and astar are codependent unfortunately. Keeping them in separate files makes sense though, as they address two distinct parts of the system.

Thanks

  • Thank you Amit Patel for describing A* search so succinctly.
  • Thank you Jeff Orkin for the origins of GOAP.

License

Copyright 2012 Abraham T. Stolk

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

More Repositories

1

imcat

Show any image in a terminal window.
C
322
star
2

chiaharvestgraph

Graphs the activity of a chia harvester in a linux terminal.
C
205
star
3

diskgraph

Graphs the disk IO in a linux terminal.
C
197
star
4

sotrace

Traces the shared-object dependencies of a binary, and graphs them.
Python
161
star
5

energygraph

Graphs the energy use of a host inside a text terminal, using intel-rapl data from the /sys filesystem.
C
124
star
6

ThreadTracer

Lightweight inline profiler that measures wall-time, cpu-time and premptive context switches for threads.
C
72
star
7

ifgraph

Network Interface Grapher.
C
67
star
8

chiaplotgraph

Chia Plot Graph
C
55
star
9

forvor

Fortune's Voronoi Generator
C
31
star
10

sino

sino - a Simplex Noise implementation in C.
C
30
star
11

freqtop

Monitor for the CPU Frequency Scaling under Linux.
C
29
star
12

dutch-blunt

Low poly font rendering system. Uses triangles not textures, for always sharp edges.
C
17
star
13

icmp_watch

Send batch requests for ICMP and show results in a console window to monitor availability of hosts.
C
14
star
14

turboledz

Daemon for Turbo LEDz devices.
C
12
star
15

chiaheightgraph

Graphs the progress of block height in your Chia Full Node.
C
9
star
16

ip2ensign

Converts an IP number to a flag image in a texture atlas.
Python
8
star
17

GBase

Minimalistic Basis for Game Development.
C++
8
star
18

MinimalVulkanCompute

Minimal code to do vulkan compute.
C
6
star
19

allrgb

My entry for the allRGB challenge: image where each 24b colour is used for one pixel, and one pixel only.
C
6
star
20

hidscan

Scan for HID devices, and print out their usages.
C
5
star
21

little_kiss_fft

A port of KISS FFT that targets μcontrollers.
C
4
star
22

objsplitv

Splits Wavefront OBJ vertices that suffer from inappropriate sharing.
Python
4
star
23

hsvbench

Benchmarking HSV to RGB conversions.
C
3
star
24

no-noise-android

Filters out the noisiest spam from the device log of Android devices.
Python
2
star
25

colourfactory

Generates distinct colours.
Python
2
star
26

Blocher

WEBGL visualization of quantum gates operating on the Bloch Sphere.
JavaScript
2
star
27

airQplot

CO2 meter
C++
1
star
28

NBody

N-Body simulation, 2D.
C
1
star
29

osino

Open Simplex Noise for Enoki/CUDA
C
1
star
30

twinprimetest

Test the twin primes like Thomas Nicely did, but on AVX.
C
1
star
31

vmtest

Demonstrate that virtual memory is cheap.
C
1
star
32

ZeroToSnake

Zero To Snake in 3600 seconds.
Python
1
star
33

gbmtest

Test program to see which formats can be used by libgbm.
C
1
star