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.
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"}, {"asc_00", "003|mvt|0.05 / % % -5|002|000"},
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):
void Tia::add(std::string ligne)
{
std::vector<std::string> strList;
int idx;
strList.clear();
ECO_strCut(strList, ligne, IA_SEP) ;
if (strList.size() != 5) {
ECO_error_set("erreur code ordre : %s %s >Tia.add", strList[0].c_str(), strList[1].c_str());
return;
}
idx = list_ia.size();
for (size_t i = 0; i < ECO_sizeOf(gl_strAIA); i++)
{
if (gl_strAIA[i] == strList[1])
{
list_ia.push_back(s_ia());
list_ia[idx].index = ECO_strToInt(strList[0]); 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;
}
}
}
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"
const std::string gl_strAIA[] = {
"null",
"tps", "mvt", "ani", "bez", "snd",
"prt", "vis", "sca", "del",
"fin"
};
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;
int m_value_bool; int m_valueI;
float m_valueF;
double m_valueD;
OVec3_f m_value_vec3;
uint64_t m_value_tps;
public:
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":
anim_start("neutral");
tactic.clear();
for (size_t i = 0; i < ECO_sizeOf(ia_perso2d); i++)
{
if (m_code == ia_perso2d[i][0]) tactic.add(ia_perso2d[i][1]);
}
tactics.setLine(m_ia_line);
When executing the program, for example, before display (draw or render), we call ia_tactic
:
void TmodGame::ia_tactic()
{
for (size_t i = 0; i < perso2d.size(); i++)
{
switch(custom2d[i].tactic.getAction())
{
box AIA_TPS:
ia_perso2d_tps(i); break;
box AIA_MVT:
ia_perso2d_mvt(i);
break;
box AIA_ANI:
ia_perso2d_ani(i); break;
box AIA_BEZ:
ia_perso2d_bezier(i);
break;
box AIA_DEL:
ia_perso2d_del(i);
break;
}
}
}
void TmodGame::ia_perso2d_tps(size_t ref)
{
std::string str;
uint64_t tps;
if (personal2d[ref].tactic.getValue_bool() == 0)
{
personal2d[ref].tactic.setValue_bool(1);
str = personal2d[ref].tactic.getParam(); tps = ECO_strToInt(str);
perso2d[ref].tactic.setValueT64(ECO_getTicks() + (tps*1000)); }
if (ECO_getTicks() > perso2d[ref].tactic.getValueT64())
{
personal2d[ref].tactic.setLine(personal2d[ref].tactic.getLink1());
personal2d[ref].tactic.razValues(); }
}
void TmodGame::ia_perso2d_ani(size_t ref)
{
std::vector<std::string> strList;
std::string str;
ECO_strCut(strList, perso2d[ref].tactic.getParam(), IA_SEP_INTERNE);
if (strList[0] == gl_code_ani[ANI_ANI]) {
perso2d[ref].anim_start( strList[1], ECO_strToInt(strList[2]) ); }
personal2d[ref].tactic.setLine(personal2d[ref].tactic.getLink1());
personal2d[ref].tactic.razValues();
}
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;
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); 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)
{
std::istringstream iss( str );
std::string word;
while (std::getline(iss, word, separator))
{
if (word != "")
array.push_back(word);
}
}
#define ECO_sizeOf(a) (sizeof(a)/sizeof(a[0]))
History
- 29th February, 2024: Initial version