Autonomous Movement of Objects and Characters in a Game





5.00/5 (1 vote)
How to control game elements based on predefined conditions
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.
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.)
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:
"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:
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
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.
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):
/*
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:
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.
#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:
audience:
Tia tactical;
When creating the character, we read its "associated AI file":
// 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
:
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;
}
}
}
// 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
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