Entity Programming - Player Interaction Last edited 2 weeks ago2021-11-18 19:57:03 UTC

Half-Life Programming

On this page, you will learn how to write entities that are usable by the player, and how player usage fundamentally works.

There are 4 kinds of player usage in Half-Life:
We won't cover on/off and directional use, as the former is unused and the latter is highly specific to trains.

Introduction

Let's look at this process from the very start. How do we go from pressing the "E" key, to a map entity's Use method?
When the player presses the use key, a certain pev->button flag is set (IN_USE). This is then checked for every frame in CBasePlayer::PlayerUse.
// Was use pressed or released?
if ( !((pev->button | m_afButtonPressed | m_afButtonReleased) & IN_USE) )
    return;
If the player is/was holding it, then a certain algorithm picks an entity based on proximity and how well it is within the player's line of sight. Note that, due to the way it works, it won't always pick the entity that is under the player's crosshair.

If an entity has been found, the player checks the entity's object capabilities to see if it's usable, and if so, finally uses it:
// This code is a very simplified version of the original
// player.cpp, line 1482
if ( pObject )
{
    int caps = pObject->ObjectCaps();

    if ( (isUsing && (caps & FCAP_CONTINUOUS_USE )) ||
         (pressedUse && (caps & (FCAP_IMPULSE_USE|FCAP_ONOFF_USE)) )
    {
        pObject->Use( this, this, USE_SET, 1 );
    }
    else if ( stoppedUsing && (caps & FCAP_ONOFF_USE) )
    {
        pObject->Use( this, this, USE_SET, 0 );
    }
}
With all of the above said, it is pretty easy to figure out how to make an entity usable by the player. Just make sure it returns one of the following in ObjectCaps:

Simple example

To demonstrate simple player usage, here is the code for a brush entity that will print something to the console only if it gets used by the player:
class CMyEntity : public CBaseEntity
{
public:
   void Spawn() override;
   void Use( CBaseEntity* pActivator, CBaseEntity* pCaller, USE_TYPE useType, float value ) override;

   // This entity will be impulse-used by the player
   int ObjectCaps() override { return FCAP_IMPULSE_USE; }
};

LINK_ENTITY_TO_CLASS( my_entity, CMyEntity );

// Brush entities are typically initialised like so
void CMyEntity::Spawn()
{
   pev->solid = SOLID_BSP;
   pev->movetype = MOVETYPE_NONE;
   UTIL_SetOrigin( pev->origin );
}

// Here we detect if the activator is the player and do stuff if so
void CMyEntity::Use( CBaseEntity* pActivator, CBaseEntity* pCaller, USE_TYPE useType, float value )
{
   if ( pActivator->IsPlayer() )
      ALERT( at_console, "Hello, player! I see you are using me\n" );
}
Note how ObjectCaps returns FCAP_IMPULSE_USE and CMyEntity::Use checks if the activator is, indeed, the player.
Without such checks, non-player entities can still trigger an entity with such object caps. In order to filter out non-player entities, write this into your Use method:
if ( !pActivator->IsPlayer() )
   return;
This will only prevent non-player activator entities (e.g. trigger_auto), meaning if the player presses a button or walks into a trigger which targets this entity, it will still be used. If you wish to have strict player-only interaction, you may instead check if pCaller is a player:
if ( !pCaller->IsPlayer() )
   return;

Impulse use in the SDK

Perhaps the most obvious example of FCAP_IMPULSE_USE is the button entity.
void CBaseButton::ButtonUse ( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value )
{
    if (m_toggle_state == TS_GOING_UP || m_toggle_state == TS_GOING_DOWN )
        return;

    m_hActivator = pActivator;
    if ( m_toggle_state == TS_AT_TOP)
    {
        if (!m_fStayPushed && FBitSet(pev->spawnflags, SF_BUTTON_TOGGLE))
        {
            EMIT_SOUND(ENT(pev), CHAN_VOICE, (char*)STRING(pev->noise), 1, ATTN_NORM);

            ButtonReturn();
        }
    }
    else
        ButtonActivate( );
}
Every time the button is pressed, it toggles its state.

Continuous use in the SDK

An example of FCAP_CONTINUOUS_USE would be health chargers and HEV chargers.
Here is a simplified version of CWallHealth::Use:
// If it's not a player, ignore
if ( !pActivator->IsPlayer() )
    return;

// If there is no juice left, turn it off
if ( m_iJuice <= 0 )
{
    pev->frame = 1; // Changes the texture to the depleted variant
    return;
}

// Time to recharge yet?
if ( m_flNextCharge >= gpGlobals->time )
    return;

// Charge the player
if ( pActivator->TakeHealth( 1, DMG_GENERIC ) )
{
    m_iJuice--;
}

// Govern the rate of charge
m_flNextCharge = gpGlobals->time + 0.1f;
It is also an example of a simple internal timer, so when the entity is used, it doesn't "charge" the player every frame, but rather, every 0.1 seconds.

Conclusion

As you can see, making entities usable by the player is very trivial, and you can write very interactive entities that way.

Comments

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