Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / artificial-intelligence

Autonomous Movement of Objects and Characters in a Game

5.00/5 (1 vote)
1 Mar 2024CPOL3 min read 6K  
How to control game elements based on predefined conditions
This article outlines a simple method for implementing artificial intelligence in a game, involving defining actions and their parameters in a file, reading and interpreting these actions in the program, and executing corresponding routines to control character movements, animations, and other game elements based on predefined conditions and links.

Introduction

An "artificial intelligence" for a game can be done quite simply. We are talking here about making a character follow a path, moving decorative elements, displaying game help, etc.

You will find on https://philthebjob.wixsite.com/moteur3d-eco the source codes for the entire game, with a lot of other code, deferred shader display, particles, billboards, obj-mtl loader, etc.

Image 1

How It Works

There are obviously several solutions. Here is mine: we put in a file the lines of code which will be read as for interpreted code.

Example: You want your character to go from position 0,0,0 to 5,0,0, at a speed of 0.05 per frame. You enter its identifier code (not the name of the character, a unique code), the action-verb "mvt", the parameters "0.05 / 5 0 0" and the link when the action is resolved.

In the program, the action will be read, the corresponding routine called, and the character's movement carried out, by a perso.setPosition().

For the character in the background, called Mossy, here is the text file:

(See file modJeu/common_jeu.h in the program, on the site.)

C++
const std::string ia_perso2d[][2] = {
    //
    {"mossy", "000|end|null|000|000"},
    {"mossy", "001|tps|3|002|000"},
    {"mossy", "002|ani|ani walks 999|003|000"},
    {"mossy", "003|mvt|0.05 / -4 8 1|004|000"},
    {"mossy", "004|ani|ani jumps 1|005|000"},
    {"mossy", "005|bez|0.025 / -4.03 8 1 > -3.05 8 1.98 + -0.46 8 1 > -1.2 8 1.78 r 0 0 0|010|000"},
    {"mossy", "010|ani|ani walks 999|011|000"},
    {"mossy", "011|mvt|0.05 / 22.47 8 1|012|000"},
    {"mossy", "012|del|null|000|000"}
};

The character arrives from the left, moves to the right, jumps the "ravine", continues to the right and disappears.

The lines of code consist of a key (the code/key of the character, the object, etc.) and a line of actions. If we take the two lines:

C++
"mossy", "001|tps|3|002|000"},
"mossy", "002|ani|ani walks 999|003|000"

We see that the action is a delay (tps) of 3 seconds, before connecting (yes) to line (002). The AI line therefore consists of a line index, a verb determining the action, a set of parameters (which you determine), and two connections, yes and no (it happens that we are dealing with a double condition: condition0 & condition1 => yes or no).

The second line launches an animation, animation runs, in a loop, and immediately goes to the third line.

For the elevator:

C++
const std::string ia_meca[][2] = {
    //
    {"asc_00", "000|end|null|000|000"},
    {"asc_00", "001|tps|2|002|000"},
    {"asc_00", "002|mvt|0.05 / % % -10|003|000"}, // goes from vertical z -10 to -5
    {"asc_00", "003|mvt|0.05 / % % -5|002|000"},  // ! loop on the previous line!

Always this 2 second delay, before the elevator begins its movement, speed 0.05, vertically from -10 to -5, and loops back to line 002. Your elevator no longer stops!

For Help

C++
const std::string ia_tuto[][2] = {
    //
    {"level_0", "000|end|null|000|000"},
    {"level_0", "001|msg|help.png 0 0 640 64 / 200 64 / 5000|002|000"},

    {"level_0", "020|obj|yellow container quantity ***|021|000"},
    {"level_0", "021|msg|aide.png 192 64 192 64 / 1600 370 / 5000|000|000"},

Here is a piece of the help.png file. This message cuts the title (0 0 640 64), displays it in position 200.64 (top screen - 64), for 5 seconds, before moving to line 002.

Image 2

The other two lines wait until the yellow hat (code qte_jaune) has been reached by the player (the hat code changes to "***" following a collision test) before moving on to line 021. In my programs, the objects are managed by the container code. If the container code is null, the object is visible, on the ground. If the container code is "mossy", then the object belongs to mossy and is no longer visible (it is in its bag). "***" is a destroyed object.

All of this is loaded into a dedicated class and read constantly. So you can create the actions you want.

The Class

The class is a simple container of values. We will store for each line (index), the action, the parameters, and the links. The variables are free, it's up to you to use them as you wish. For example, the bool variable is essentially used to initialize the action (pass to true), then to block initialization during subsequent program loops. We use the OVec3_f (a floating xyz vector, like glm::vec3) to keep a position; uint64_t is used to keep a time value; etc.

First, load the lines (make a loop to send the lines of the ia_perso2d structure, check that the key is that of the character, add the lines):

C++
/*
000|action-verbe|param|lien oui|lien non
*/
void Tia::add(std::string ligne)
{
    std::vector<std::string> strList;
    int idx;
    //ECO_error_set("Tia.add ligne > %s", ligne.c_str());

    strList.clear();
    // cuts the line taking into account the separators '|' (code below)
    ECO_strCut(strList, ligne, IA_SEP)   ;

    if (strList.size() != 5) // idx action param lien1 lien2
    {
        ECO_error_set("erreur code ordre : %s %s >Tia.add", strList[0].c_str(), strList[1].c_str());
        return;
    }

    idx = list_ia.size();

    // searches for the match of the verb/action (see gl_strAIA below)
    for (size_t i = 0; i < ECO_sizeOf(gl_strAIA); i++)
    {
        if (gl_strAIA[i] == strList[1])
        {
            //ECO_error_set("Tia.add > %s", strList[1].c_str());

            list_ia.push_back(s_ia());
            list_ia[idx].index = ECO_strToInt(strList[0]);	// str index devient int index
            list_ia[idx].action = i;
            list_ia[idx].param = strList[2];
            list_ia[idx].lien1 = ECO_strToInt(strList[3]);
            list_ia[idx].lien2 = ECO_strToInt(strList[4]);
            return;
        }
    }
    //ECO_error_set("erreur action : %s >Tia.add", strList[1].c_str());
}

for the line to find, we will proceed as follows:

C++
void Tia::setLigne(int value)
{
    for (size_t i = 0; i < list_ia.size(); i++)
    {
        if (list_ia[i].index == value)
        {
            m_index = i;
            break;
        }
    }
}

Here is the header, the functions are just getSomething and setSomething which give a value or retrieve the value of the variable.

C++
#include <string>
#include <vector>
#include <fstream>

#define IA_NULL     "null"
#define IA_REM      '#'
#define IA_SEP      '|'
#define IA_SEP_INTERNE ' '
#define gl_ordNull  "000|null|param|000|000"

// Verbs/actions (to be defined according to your requirement)
const std::string gl_strAIA[] = {
    "null",
    // delay / mouvement-deplacement / lance animation / bezier / son
    "tps", "mvt", "ani", "bez", "snd",
    // particule / visiblite (couche alpha) / scale / delete
    "prt", "vis", "sca", "del",
    "fin"
};

// action ia
enum {
    AIA_NULL,
    AIA_TPS, AIA_MVT, AIA_ANI, AIA_BEZ, AIA_SND,
    AIA_PRT, AIA_VIS, AIA_SCA, AIA_DEL,
    AIA_FIN
};

struct s_ia
{
    s_ia()
    {
        index = 0; action = 0; param = ""; lien1 = 0, lien2 = 0;
    }

    int         index;
    int         action;
    std::string param;
    int         lien1;
    int         lien2;
};

class Tia
{
public:
    Tia();
    ~Tia();

    void        	add(std::string ligne);

    void        	clear();
    void        	create(std::string folder, std::string file, int branchement = 1);

    int         	getAction();
    int         	getIndex();
    int         	getLien1();
    int         	getLien2();
    std::string 	getParam(int ligne = 0);
    //
    int         	getValue_bool();
    int         	getValueI();
    float       	getValueF();
    double      	getValueD();
    OVec3_f&  getValue_vec3();
    uint64_t    	getValueT64();

    void        	razValues();

    void        setLigne(int value);
    void        setValue_bool(int value);
    void        setValueI(int value);
    void        setValueF(float value);
    void        setValueD(double value);
    void        setValue_vec3(OVec3_f& value);
    void        setValue_vec3(float x, float y, float z);
    void        setValueT64(uint64_t value);

private:

    std::string 	m_fichier;
    std::vector<s_ia> list_ia;
    int         	m_index;

    // free storage values ​​/ reset to zero after use
    int         	m_value_bool;   // initiated action marker
    int         	m_valueI;
    float       	m_valueF;
    double      	m_valueD;
    OVec3_f      m_value_vec3;
    uint64_t    	m_value_tps;

public:

    //ONurb       nurb;
    OBezier     bezier;
};

The Decryption

In the character header, we declare a tactical class, based on the Tia class:

C++
audience:
    Tia tactical;

When creating the character, we read its "associated AI file":

C++
 // basic neutral position
 anim_start("neutral");

tactic.clear();
// we read the text file seen above
for (size_t i = 0; i < ECO_sizeOf(ia_perso2d); i++)
{
// m_code is the character code, here "mossy"
if (m_code == ia_perso2d[i][0]) tactic.add(ia_perso2d[i][1]);
}
tactics.setLine(m_ia_line); // the save indicates which line we are at!

When executing the program, for example, before display (draw or render), we call ia_tactic:

C++
void TmodGame::ia_tactic()
{
    // for all 2d characters (here the character in the background)
    for (size_t i = 0; i < perso2d.size(); i++)
    {
        // the ia file is, for characters, called tactics
        // we therefore ask what is the expected action (according to the current line)
        switch(custom2d[i].tactic.getAction())
        {
        //case AIA_NULL:
        // break;
        box AIA_TPS:
            ia_perso2d_tps(i); // here we call the delay routine
            break;
        box AIA_MVT:
            ia_perso2d_mvt(i);
            break;
        box AIA_ANI:
            ia_perso2d_ani(i); // here we call the routine to launch an animation
            break;
        box AIA_BEZ:
            ia_perso2d_bezier(i);
            break;
        box AIA_DEL:
            ia_perso2d_del(i);
            break;
        }
    }
}
C++
// 001|times|3|002|000
void TmodGame::ia_perso2d_tps(size_t ref)
{
    std::string str;
    uint64_t tps;

    // initialize the action
    if (personal2d[ref].tactic.getValue_bool() == 0)
    {
        personal2d[ref].tactic.setValue_bool(1); // blocked

        str = personal2d[ref].tactic.getParam(); // we know that this action 
                                                 // requires a value in seconds
        tps = ECO_strToInt(str);
        perso2d[ref].tactic.setValueT64(ECO_getTicks() + (tps*1000)); // we indicate the 
                                                                      // value to reach
    }
    // when the value is reached, we move to the next line
    // (ECO_getTicks() must be replaced by a frame counter 
    // if the game supports acceleration)
    if (ECO_getTicks() > perso2d[ref].tactic.getValueT64())
    {
        personal2d[ref].tactic.setLine(personal2d[ref].tactic.getLink1());
        personal2d[ref].tactic.razValues(); // especially clear all values!
    }
}

// the animation depends on your animation system, but the principle is the same

//002|ani|ani walks 1|003|000
void TmodGame::ia_perso2d_ani(size_t ref)
{
    std::vector<std::string> strList;
    std::string str;

    // split the parameter string, i.e.: strlist[0] = 
    // ani / strList[1] = march / strList[2] = "1" (the function is given below)
    ECO_strCut(strList, perso2d[ref].tactic.getParam(), IA_SEP_INTERNE);

    // ani for an animation (for me we also find cycle which is a series of 
    // several animations)
    if (strList[0] == gl_code_ani[ANI_ANI]) // it's "ani"
    {
        perso2d[ref].anim_start( strList[1], ECO_strToInt(strList[2]) ); // ref animation
                                                                         // and loop
    }
    //else if (strList[0] == gl_code_ani[ANI_CYC])
    //{
    // personal2d[ref].anim_start(strList[1], 9); // run the animation 9 times
    //}

    // the animation is started, moves to the next line
    personal2d[ref].tactic.setLine(personal2d[ref].tactic.getLink1());
    personal2d[ref].tactic.razValues();
}

// here is a simple movement in a straight line

// 002|mvt|0.05 / -10.5 0 -10|003|000
void TmodGame::ia_perso2d_mvt(size_t ref)
{
    std::vector<std::string> strList;
    std::string str;

    if (personal2d[ref].tactic.getValue_bool() == 0)
    {
        OVec3_f pos = perso2d[ref].getPos();

        personal2d[ref].tactic.setValue_bool(1);

        str = personal2d[ref].tactic.getParam();
        ECO_strCut(strList, str, IA_SEP_INTERNE);

        if (strList[2] != "%") pos.x = ECO_strToFloat(strList[2]);
        if (strList[3] != "%") pos.y = ECO_strToFloat(strList[3]);
        if (strList[4] != "%") pos.z = ECO_strToFloat(strList[4]);

        personal2d[ref].tactic.setValue_vec3(pos);
        perso2d[ref].tactic.setValueF( ECO_strToFloat(strList[0]) );
    }
    else
    {
        OVec3_f pos0, pos1, direction;
        //int ok = 0;
        float lives;

        lives = personal2d[ref].tactic.getValueF();
        pos0 = personal2d[ref].getPos();
        pos1 = personal2d[ref].tactic.getValue_vec3();
        direction = pos1 - pos0;
        pos0 += sens.normalized() * vit;

        if (ECO_distanceVF(pos0,pos1) <= vit)
        {
            personal2d[ref].setPos(pos1); // correctly replace the character2d
            personal2d[ref].tactic.setLine(personal2d[ref].tactic.getLink1());
            personal2d[ref].tactic.razValues();
        }
        else
        {
            personal2d[ref].setPos(pos0);
        }
    }
}

Additional Procedures

C++
void ECO_strCut(std::vector<std::string>& array, std::string str, char separator)
{
    //#include <sstream>
    std::istringstream iss( str );
    std::string word;

    while (std::getline(iss, word, separator))
    {
        // if there are three spaces/separators to follow, 
        // we find a separator and two empty strings
        if (word != "")
            array.push_back(word);
    }
}

#define ECO_sizeOf(a) (sizeof(a)/sizeof(a[0]))

History

  • 29th February, 2024: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)