Git Product home page Git Product logo

gpgoap's Introduction

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 Deleted, mirror here. 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.

gpgoap's People

Contributors

stolk avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

gpgoap's Issues

Does the A* algo start from the desired goal, or from the current world state?

When I was reading about GOAP few weeks (or was it months?) ago, I could find that the F.E.A.R. original implementation started from the desired goal instead of the current world model.

The argument, which made sense to me (but I'm rather new to the topic of game bots or automatons, and don't have bagage on the area), is that it could invalid the plan sooner if some actions are impossible.
This being true or false (or me misunderstanding it) won't make it least interesting or useful to document it though, (helps trying to understand bugs in our codebases :)), and if possible the reasons for the choice would be nice (simpler to implement being perfectly valid).

Node with lower cost in closed set is removed without being re-added to open set

astar.c lines 188-192.

if ( idx_c >= 0 && cost < closed[ idx_c ].g )
{
    // remove neighbor from CLOSED
    if ( numClosed ) closed[ idx_c ] = closed[ numClosed-1 ];
    numClosed--;
}

Should add idx_c = -1;

if ( idx_c >= 0 && cost < closed[ idx_c ].g )
{
    // remove neighbor from CLOSED
    if ( numClosed ) closed[ idx_c ] = closed[ numClosed-1 ];
    numClosed--;
    **idx_c = -1;**
}

MAXATOMS and MAXACTIONS are misleading

actionplanner_t should be able to support up to MAXATOMS of atoms and MAXACTIONS of actions but it can only actually support MAXATOMS-1 and MAXACTIONS-1 of each before returning false.

This comes from how the lookup functions are written:
static int idx_for_atomname( actionplanner_t* ap, const char* atomname )
{
int idx;
for ( idx=0; idx < ap->numatoms; ++idx )
if ( !strcmp( ap->atm_names[ idx ], atomname ) ) return idx;

if ( idx < MAXATOMS-1 )

static int idx_for_actionname( actionplanner_t* ap, const char* actionname )
{
int idx;
for ( idx=0; idx < ap->numactions; ++idx )
if ( !strcmp( ap->act_names[ idx ], actionname ) ) return idx;

if ( idx < MAXACTIONS-1 )

goap_actionplanner_clear doesn't necessarily clear all actions

typedef struct
{
    const char* atm_names[ MAXATOMS ];  //!< Names associated with all world state atoms.
    int numatoms;               //!< Number of world state atoms.

    const char* act_names[ MAXACTIONS ];    //!< Names of all actions in repertoire.
    worldstate_t act_pre[ MAXACTIONS ]; //!< Preconditions for all actions.
    worldstate_t act_pst[ MAXACTIONS ]; //!< Postconditions for all actions (action effects).
    int act_costs[ MAXACTIONS ];        //!< Cost for all actions.
    int numactions;             //!< The number of actions in out repertoire.

} actionplanner_t;

void goap_actionplanner_clear( actionplanner_t* ap )
{
    ap->numatoms = 0;
    ap->numactions = 0;
    for ( int i=0; i<MAXATOMS; ++i ) 
    {
        ap->atm_names[ i ] = 0;
        ap->act_names[ i ] = 0;
        ap->act_costs[ i ] = 0;
        goap_worldstate_clear( ap->act_pre+i );
        goap_worldstate_clear( ap->act_pst+i );
    }
}

const char* act_names[ MAXACTIONS ]; yet the loop only clears for for ( int i=0; i<MAXATOMS; ++i )

Although in your example MAXACTIONS==MAXATOMS, maybe this would not always be the case. I hope this is useful. Thank you for your time.

astar.c functions are not thread safe

Due to the use of global data the goap library can only be called by a single thread.

static astarnode_t opened[ MAXOPEN ]; //!< The set of nodes we should consider.
static astarnode_t closed[ MAXCLOS ]; //!< The set of nodes we already visited.

static int numOpened = 0; //!< The nr of nodes in our opened set.
static int numClosed = 0; //!< The nr of nodes in our closed set.

If these were added to a struct that was passed in to the various functions, it would provide thread safety.

Heuristic function overestimates cost, leading to suboptimal plans.

A* with a closed set can select suboptimal plans if the heuristic function overestimates the remaining cost of an intermediate state. The current heuristic of counting the conflicting states can be an overestimate in some cases. Consider the following setup:

Six state values: a, b, c, d, e, f
Initial state: all six values are False
Goal state: all six values are True
Four actions:

  • "setA", no preconditions, sets a:=True, cost of 2
  • "setAB", no preconditions, sets a:=True and b:=True, cost of 1
  • "optimal", precondition a==True and b==False, sets all six state values to True, cost of 1
  • "trap", precondition a==True and b==False, sets all six state values to True, cost of 5

The optimal plan is "setA" followed by "optimal", with a total cost of 3. However, with the current heuristic, the code actually selects a plan of "setAB" followed by "trap", with a total cost of 6. This is because the heuristic overestimates the remaining cost after the action "setA" as having a likely remaining cost of 5, when in fact the remaining cost is only 1.

This can be fixed by changing the heuristic function as follows:

After all actions and costs are known, find the action with the minimum (cost / number of output states) ratio. Save this optimal ratio, and pass it as an additional argument to the heuristic function. The heuristic function should then return the number of conflicting states multiplied by the cost/output ratio.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.