Entity Programming - Handling Player Input Last edited 2 years ago2022-10-30 12:52:21 UTC

Half-Life Programming

Introduction

Interaction is an essential part of Half-Life. Sometimes though, you might feel limited by the very few mechanisms in Half-Life (shooting at things, using things, walking into triggers), and you might want something more unique, whether it's special controls for certain types of terminals, cranes, or even fully functional vehicles. Generally speaking, you want the player to have more direct control over some entity.

In the SDK, there are a few ways to check if the player has pressed/is pressing a certain button, but in the context of entity programming, we will only focus on the simplest (and most limited) way. Namely, pev->button.

How it works

Every frame, in CBasePlayer::PreThink, this happens:
int buttonsChanged = (m_afButtonLast ^ pev->button);    // These buttons have changed this frame

// Debounced button codes for pressed/released
m_afButtonPressed =  buttonsChanged & pev->button;        // The changed ones still down are "pressed"
m_afButtonReleased = buttonsChanged & (~pev->button);    // The ones not down are "released"
You may think of: There are 16 inputs you can test against pev->button, which can be found in in_buttons.h:
#define IN_ATTACK    (1 << 0)
#define IN_JUMP      (1 << 1)
#define IN_DUCK      (1 << 2)
#define IN_FORWARD   (1 << 3)
#define IN_BACK      (1 << 4)
#define IN_USE       (1 << 5)
#define IN_CANCEL    (1 << 6)
#define IN_LEFT      (1 << 7)
#define IN_RIGHT     (1 << 8)
#define IN_MOVELEFT  (1 << 9)
#define IN_MOVERIGHT (1 << 10)
#define IN_ATTACK2   (1 << 11)
#define IN_RUN       (1 << 12)
#define IN_RELOAD    (1 << 13)
#define IN_ALT1      (1 << 14)
#define IN_SCORE     (1 << 15)   // Used by client.dll for when scoreboard is held down
You can't add new inputs!
It is not possible to directly add more, unless you "sacrifice" some of the existing ones. 16 bits is all you're going to get, and it's impossible to change this limitation, even in delta.lst, because in usercmd_t, the buttons variable is a 16-bit integer, despite being a 32-bit integer in entvars_t.
So, with all that said, if you want to check if the player is holding a certain key in a given moment, you can do it like so:
// If the player is holding down the reload key
if ( pPlayer->pev->button & IN_RELOAD )
{
    // Do stuff here
}

Example

Let's write a very simplistic vehicle that moves on a 2D plane via WASD.

Class

To start off, here is the CFuncSlidicle (slidicle as in, "sliding vehicle") class with a Spawn method:
#include "extdll.h"
#include "util.h"
#include "cbase.h"
#include "player.h"

class CFuncSlidicle : public CBaseEntity
{
public:
    void Spawn() override;

    int ObjectCaps() override { return CBaseEntity::ObjectCaps() | FCAP_IMPULSE_USE; }

    void Use( CBaseEntity* pActivator, CBaseEntity* pCaller, USE_TYPE useType, float value ) override;
    void Think() override;

private:
    CBasePlayer* driver = nullptr;
};

LINK_ENTITY_TO_CLASS( func_slidicle, CFuncSlidicle );

// This is assumed to be a brush entity, so we set it up like this in Spawn
void CFuncSlidicle::Spawn()
{
    pev->movetype = MOVETYPE_PUSHSTEP;
    pev->solid = SOLID_BSP;
    SET_MODEL( edict(), STRING( pev->model ) );
}

Think method

The way it works will be pretty simple. When it's used by the player, it'll "turn on" and be able to receive player inputs, which will affect its velocity.
We have a driver variable which will point to a player that is currently controlling the vehicle, and we'll sample the inputs there.
void CFuncSlidicle::Think()
{
    // If we got no driver, then we got nothing
    if ( !driver )
    {
        return;
    }

    // Movement logic here

    pev->nextthink = gpGlobals->time + 0.01f;
}
Let's expand on this comment about movement logic. First, we'll sample the current button states:
const int& buttons = driver->pev->button;
Then, we'll have a vector to store some type of "wish velocity", i.e. where the player wants to go.
// vec3_origin is the same as Vector( 0, 0, 0 )
Vector addVelocity = vec3_origin;
It is (0,0,0) by default, so it can be added upon like so:
if ( buttons & IN_FORWARD )
    addVelocity.x += 16.0f; // alternative: define a constant for this speed instead of typing 16 every time
if ( buttons & IN_BACK )
    addVelocity.x -= 16.0f;
if ( buttons & IN_MOVELEFT )
    addVelocity.y += 16.0f;
if ( buttons & IN_MOVERIGHT )
    addVelocity.y -= 16.0f;

// And of course, you gotta finally add it to the entity's velocity:
pev->velocity = pev->velocity + addVelocity;
The final function will be the following:
void CFuncSlidicle::Think()
{
    // If we got no driver, then we got nothing
    if ( !driver )
    {
        return;
    }

    constexpr float VehicleAcceleration = 16.0f;

    const int& buttons = driver->pev->button;
    Vector addVelocity = vec3_origin;

    if ( buttons & IN_FORWARD )
        addVelocity.x += VehicleAcceleration;

    if ( buttons & IN_BACK )
        addVelocity.x -= VehicleAcceleration;

    if ( buttons & IN_MOVELEFT )
        addVelocity.y += VehicleAcceleration;

    if ( buttons & IN_MOVERIGHT )
        addVelocity.y -= VehicleAcceleration;

    pev->velocity = pev->velocity + addVelocity;
    pev->nextthink = gpGlobals->time + 0.01f;
}

Use method

Now let's tackle actually getting in and out of the car. Since this guide's focus is on player input, this part won't be explained in more detail than necessary:
void CFuncSlidicle::Use( CBaseEntity* pActivator, CBaseEntity* pCaller, USE_TYPE useType, float value )
{
    if ( !pActivator->IsPlayer() )
    {
        return;
    }

    // A potential driver wants to enter this vehicle
    if ( !driver )
    {
        driver = static_cast<CBasePlayer*>(pActivator);
        // Hack: lock the player in place
        driver->pev->maxspeed = 0.01f;

        // Think immediately
        pev->nextthink = gpGlobals->time + 0.01f;
        return;
    }

    // The current driver wants to eject
    if ( driver && pActivator == driver )
    {
        driver->pev->maxspeed = 0.0f;
        driver = nullptr;
    }
}
A couple of things to note here:

Conclusion

The intended way to test this entity would be to place the player somewhere near a button, and that button would trigger the vehicle. Due to the way we designed it, it simply can work this way, without the player directly using the vehicle. You should then see the vehicle slide around as you press WASD.

There are some things you can modify, such as the movement logic. Instead of moving the vehicle along XY coordinates, you can calculate the current direction using UTIL_MakeVectors, rotate the vehicle with A and D instead of moving it left-right, and then add the forward vector to the velocity, or subtract if the player's going backwards.

Something like this:
if ( buttons & IN_MOVELEFT )
    pev->angles.y += 1.0f;
if ( buttons & IN_MOVERIGHT )
    pev->angles.y -= 1.0f;

UTIL_MakeVectors( pev->angles );

if ( buttons & IN_FORWARD )
    pev->velocity = pev->velocity + gpGlobals->v_forward * VehicleAcceleration;
if ( buttons & IN_BACK )
    pev->velocity = pev->velocity - gpGlobals->v_forward * VehicleAcceleration;
Finally, you might also want to set up a save-restore table, so that driver gets saved and loaded properly.

And that's essentially it. Enjoy your pseudo-func_vehicle. Here are some other ideas: You can find the source code here.

2 Comments

Commented 1 year ago2023-04-20 23:37:53 UTC Comment #105215
so... you say we cannot add new inputs. what about things such as Xash3D? it is technically an open-source goldsrc, meaning there aren't any programming limitations. it should be possible to make this "buttons" variable 32 bit or something, right?
Commented 1 year ago2023-05-16 17:49:10 UTC Comment #105266
so... you say we cannot add new inputs. what about things such as Xash3D?
Since you have access to the engine's internals with Xash, you could change the data type but you need to be careful to not break everything. Also, if the game/mod is multiplayer, bandwidth/data size need to be taken into account.
it is technically an open-source goldsrc
Xash is an open-source heavily modified version of Quake to be compatible with Half-Life, it is not an "open-source GoldSrc". Valve only released the game code to the public, not the rest (launcher/engine...)
meaning there aren't any programming limitations.
Friendly advice: just because you have access to the engine's internals does not means that "simply bumping limits" will work. There will always be limits regardless of the engine/programming language you are using.

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