Bot Programming - Getting Started Last edited 9 months ago2023-05-31 11:21:14 UTC

Half-Life Programming

  • Bot Programming - Getting Started
This chapter covers the creation of a very simple bot that can spawn, stand and be attacked.

Updating the source code to support bots

The first thing we need to do is update the source code to support bots. The original Half-Life SDK erases the FL_FAKECLIENT flag which we need to detect which players are bots. This flag is used to distinguish real players from bots.
If you are using an up-to-date version of Half-Life Updated then these changes have already been made for you.
Change this:
https://github.com/ValveSoftware/halflife/blob/c7240b965743a53a29491dd49320c88eecf6257b/dlls/player.cpp#L2859-L2860

To this:
pev->flags &= FL_PROXY | FL_FAKECLIENT; // keep proxy and fakeclient flags set by engine
pev->flags |= FL_CLIENT;
If you are making an Opposing Force mod you will need to update a few other places:

In CBasePlayer::Menu_Char_Input there is code to reset players that are going from observer mode to joining a time. This line is part of that:
pev->flags = FL_CLIENT;
Change this to:
pev->flags &= FL_FAKECLIENT;
pev->flags |= FL_CLIENT;
Much like the code above this keeps the fake client flag and sets the client flag as originally intended. The proxy flag isn't needed here because it only applies to HLTV spectators, who can't participate in gameplay.

In CDisplacerBall::BallTouch there is code to teleport players hit by Displacer balls. This line is part of that:
pPlayer->pev->flags = FL_CLIENT;
Change this to:
//Clear any flags set on player (onground, using grapple, etc).
pPlayer->pev->flags &= FL_FAKECLIENT;
pPlayer->pev->flags |= FL_CLIENT;
You should check for any other occurrences of the pev->flags variable (search for ->flags and .flags as well to be sure) being changed to catch cases where the flag is removed.

Updating IsNetClient

We also need to update this method to check the flag:
https://github.com/ValveSoftware/halflife/blob/c7240b965743a53a29491dd49320c88eecf6257b/dlls/player.h#L222
BOOL IsNetClient() override { return (pev->flags & FL_FAKECLIENT) == 0; }
This is required mainly to make certain entities ignore bots.
If you are using an up-to-date version of Half-Life Updated then you will need to use bool instead of BOOL.

Keeping track of connected players

We're going to need to keep track of whether a player is currently connected to the server so we can skip bots that have been removed from the server.

Before this line:
https://github.com/ValveSoftware/halflife/blob/c7240b965743a53a29491dd49320c88eecf6257b/dlls/player.h#L326

Add this:
/**
*    @brief True if the player is currently connected to the server.
*    Should only be false in multiplayer games, for players that have disconnected.
*/
bool m_bIsConnected = true;
Before this line:
https://github.com/ValveSoftware/halflife/blob/c7240b965743a53a29491dd49320c88eecf6257b/dlls/client.cpp#L136

Add this:
auto pPlayer = reinterpret_cast<CBasePlayer*>(GET_PRIVATE(pEntity));

if (pPlayer)
{
    pPlayer->m_bIsConnected = false;
}
This will let us check m_bIsConnected to see if the player is connected.
If you are using an up-to-date version of Half-Life Updated then some of this code already exists. Make sure you don't add duplicates.

Allocating a player slot and creating the bot entity

Now that the source code fully supports bots we can create one.

Let's add a server command that will add a new bot. To do this, open the file game.cpp.

After this line:
https://github.com/ValveSoftware/halflife/blob/c7240b965743a53a29491dd49320c88eecf6257b/dlls/game.cpp#L18

Add this:
#include "client.h"
We're going to need some of the functions declared in that header.

Now scroll to this line:
https://github.com/ValveSoftware/halflife/blob/c7240b965743a53a29491dd49320c88eecf6257b/dlls/game.cpp#L463

Now let's add a command. Put this code before that line:
g_engfuncs.pfnAddServerCommand("sv_addbot", []()
    {
        if (CMD_ARGC() != 2)
        {
            g_engfuncs.pfnServerPrint("Usage: sv_addbot <bot_name>");
        }

        const char* name = CMD_ARGV(1);

        //The engine will validate the name and change it to "unnamed" if it's not valid.
        //Any duplicates will be disambiguated by prepending a (%d) where %d is a number.
        const auto fakeClient = g_engfuncs.pfnCreateFakeClient(name);

        if (!fakeClient)
        {
            return;
        }

        //Use the netname variable here in case the engine changed it for some reason (usually duplicate names).
        //Use localhost as the IP address.
        char reject[128];
        if (0 == ClientConnect(fakeClient, STRING(fakeClient->v.netname), "127.0.0.1", reject))
        {
            //Bot was refused connection, kick it from the server to free the slot.
            SERVER_COMMAND(UTIL_VarArgs("kick %s\n", STRING(fakeClient->v.netname)));
            return;
        }

        //Finish connecting, create player.
        ClientPutInServer(fakeClient);

        //Do remaining logic at least one frame later to avoid race conditions.
    });
Compile your mod, run it and start a multiplayer server. You can now run the console command sv_addbot foo to add a bot with the name foo.

You will notice that the bot is not appearing at any spawn point. Depending on the map you may see the bot appear in a seemingly random location. This location is the world origin. The bot appears there because we aren't updating the bot entity which means the game doesn't set the bot's visible position to where it actually is.

The next section explains how to do this.

Updating the bot every frame

Open the file client.cpp and scroll to this function:
https://github.com/ValveSoftware/halflife/blob/c7240b965743a53a29491dd49320c88eecf6257b/dlls/client.cpp#L789-L799

We're going to update the bots here.

Add this before the function:
static float g_LastBotUpdateTime = 0;
Add this code to the end of the function:
// Handle level changes and other problematic time changes.
float frametime = gpGlobals->time - g_LastBotUpdateTime;

if (frametime > 0.25f || frametime < 0)
{
    frametime = 0;
}

const byte msec = byte(frametime * 1000);

g_LastBotUpdateTime = gpGlobals->time;

for (int i = 1; i <= gpGlobals->maxClients; ++i)
{
    auto player = static_cast<CBasePlayer*>(UTIL_PlayerByIndex(i));

    if (!player)
    {
        continue;
    }

    if (!player->m_bIsConnected)
    {
        continue;
    }

    if ((player->pev->flags & FL_FAKECLIENT) == 0)
    {
        continue;
    }

    //If bot is newly created finish setup here.

    //Run bot think here.

    //Now update the bot.
    g_engfuncs.pfnRunPlayerMove(player->edict(), player->pev->angles, 0, 0, 0, player->pev->button, player->pev->impulse, msec);
}
This for loop enumerates all player slots and filters out slots that aren't in use and that aren't bots.

We're only making a very simple bot, so all we need to do here is update the bot by running the movement functions. g_engfuncs.pfnRunPlayerMove tells the engine to run the player movement code using the given angles, forward, side and up movement speeds, button bit mask and impulse value, and the frametime.

Since we're not simulating bot AI logic we're just going to pass the current angles, zero movement speed on all axes, the current button bit mask and impulse value, and the frametime between engine frames.

Compile your mod, run it and start a multiplayer server. Run the console command sv_addbot foo to add a bot with the name foo.

You should now find a bot spawned at one of the map's spawn points. You can add more bots to find them a bit more easily.

These bots can be shot and killed, and will respawn (unless you've changed the respawn rules).

Removing a bot from the server

To remove a bot, simply kick it as you would a player: kick "bot_name".

Conclusion

You can now spawn bots in the game and shoot them like you can any other player.

Depending on the game you may need additional logic to spawn the bot. Opposing Force CTF for example requires bots to join a team and select a character in order to spawn. If you need to do this make sure to handle it in StartFrame (preferably in a separate function called from there). If you try to make the bot join a team immediately the server will send messages to the client before the client has received information about the bot. This will cause glitches like the bot not appearing on the scoreboard.

See this bug report for more information about that: https://github.com/ValveSoftware/halflife/issues/3257

For help with implementing AI logic i recommend studying the RCBot source code: https://github.com/APGRoboCop/rcbotold

Comments

You must log in to post a comment. You can login or register a new account.