RUST: Sentry Tutorial Last edited 1 year ago2022-09-29 07:53:56 UTC

Introduction

This is a simple "Make you own sentry" tutorial, at the moment I haven't bothered about animations (it's just a CBaseEntity at the moment, not a CBaseMonster) so It looks quite simplistic, but you'll get the idea on how to set their animations once you get your own model for your own sentry.

There isn't MUCH explanation here, so just remember the Sentry is pretty basic, so you will be okay!

This tutorial is for a team multiplayer game, where the sentry will have its own Team name and will shoot players whose team doesn't match it's own (Hopefully!). I haven't tested all of this because I made my own sentry with a different name and fought different enemies so I had to change that, and don't know if everything will work okay.

The final sentry will look and work something like the sentries in TFC that the engineers build, we are going to have a phase when a player builds the sentry and can give it ammo and rockets.

Another addition to this is another tutorial that I done, which will allow you only to build a sentry when you pick up "sentry parts", when you have the sentry parts a Hud icon will show aswell. The sentry-parts tutorial is a follow-up from this one.

Setting up UTIL functions

I've made my own UTIL functions to help with this, these are useful functions that will be used later. To make them, you will need to go into "UTIL.H" file and add these lines first for the function prototypes...
Vector UTIL_RelationToWorld(edict_t *player, Vector relation);
Vector UTIL_LengthFromVector(Vector relation, float length);
Vector UTIL_FixAngles( Vector vec );
float UTIL_FixFloatAngle(float vec);
Now open "UTIL.CPP" and add these functions...
Vector UTIL_FixAngles( Vector vec )
{
    vec.x = UTIL_FixFloatAngle(vec.x);
    vec.y = UTIL_FixFloatAngle(vec.y);
    vec.z = UTIL_FixFloatAngle(vec.z);

    return vec;
}

float UTIL_FixFloatAngle(float vec)
{
    if (vec > 180.0) vec -= 360.0;
    if (vec < -180.0) vec += 360.0;

    return vec;
}

Vector UTIL_RelationToWorld(edict_t *player, Vector relation)
{
    if( !player )
        return Vector(0,0,0);

    Vector v_src,v_dest,v_forward,v_viewpoint,v_origin;

    v_origin = player->v.origin;
    v_src = v_origin + player->v.view_ofs;

    v_viewpoint = player->v.v_angle + player->v.punchangle;
    UTIL_MakeVectors(v_viewpoint);
    v_forward = v_viewpoint + (relation * 8192.0);
    v_dest = v_src + v_forward;

    return v_dest;
}

Vector UTIL_LengthFromVector(Vector relation, float length)
{
    return ( (relation / relation.Length()) * length );
}

Setting up the sentry code

For the sentry, make a new file called "MySentry.cpp" or whatever you want and add it to your project.

Note: All of this code is in order from start of the file to the end of the file.

Okay for the start of the sentry code we need some included files to use, these are the ones we need below:
#include "extdll.h"
#include "util.h"
#include "cbase.h"
#include "weapons.h"
#include "player.h"
#include "gamerules.h"
#include "explode.h" // for effects
#include "teamplay_gamerules.h" // for knowing teams

Constants

Next, we want to set some constants that might come in useful...
#define SENTRY_MAX_PITCH 10.0
This is the maximum angle the sentry will look up and down, you can change this totally if you want, because I've just been using pev->angle to make it easier but when you change the pev->angle.x to a high value, the sentry looks weird!

Now we need some more constants, such as fire rates and maximum amount of shells/rockets this Sentry can have...
#define SHOOT_ROCKET_TIME 2.0
#define SHOOT_SHELL_TIME 0.1

#define SENTRY_MAX_SHELLS  400
#define SENTRY_MAX_ROCKETS 50
Okay that's nearly all of the constants, we just need one more. I've added "build" functions to my sentry so this means it will take SENTRY_BUILD_TIME seconds for it to finish being built and start working. This is defined like this below:
#define SENTRY_BUILD_TIME  4.0 // 4 Seconds to build sentry

Sentry Class

Right now that's all the constants we need, let's get into the grips of the sentry class. As you'd probably know, we need our own Precache() and Spawn() function. I've also added my own Think functions for different events such as Build, Aim/Shoot, and when Killed, remember that your own Think/Touch/Use functions (etc.) should be set if you want to use them (By using SetThink(FUNCTION) for example).
class CMySentry : public CBaseEntity
{
public:
    void Spawn        ( void );
    void Precache    ( void );

    int AddShells  ( int l_iNewShells );  // Returns the amount of shells USED...
    int AddRockets ( int l_iNewRockets ); // Returns the amount of rockets USED...

    void EXPORT SentryThink    ( void );
    void EXPORT SentryBuildThink( void );

    void EXPORT MySentryDeath ( void );

    void Dismantle ( void );

    virtual int TakeDamage(entvars_t *pevInflictor, entvars_t *pevAttacker, float flDamage, int bitsDamageType);

private:

    void Aim        ( void );
    void Shoot        ( void );
    void FireShell    ( void );
    void FireRocket    ( void );

    void RemoveFromPlayer ( void );

    CBaseEntity *GetBestEnemy ( void );

    int m_iRockets;
    int m_iShells;

    float m_fLastShootRocket;
    float m_fLastShootShell;

    float m_fLastAimTime;

    float m_fShellPercentFull;
    float m_fRocketPercentFull;

    float m_fBuildTime; // Time the sentry was BUILT...

    float m_fLastCheckVisibleEnemy; // Last checked to see if enemy was visible

    char m_szTeamName[MAX_TEAMNAME_LENGTH];

    CBaseEntity *m_pEnemy; // Current enemy

    Vector m_vBarrel; // Barrel position (gun point) NOT USED!
};
Don't forget this bit below!
LINK_ENTITY_TO_CLASS( MySentry, CMySentry );
This makes the class (CMySentry) link to the classname of "MySentry" you can change this to whatever you want.

Sentry Class Methods

Now here comes the BuildThink() function, this is continously called while the Sentry is being built. All this function will do is wait until the SENTRY_BUILD_TIME is up, and make the sentry visible (as it was invisible whilst building because we set that in ::Spawn() )
void CMySentry :: SentryBuildThink( void )
{
    pev->nextthink = gpGlobals->time + 0.1;

    if( (m_fBuildTime + SENTRY_BUILD_TIME) <= gpGlobals->time )
    {
        EMIT_SOUND(ENT(pev), CHAN_BODY, "turret/tu_spinup.wav", 1.0, ATTN_NORM); // Spin up sound (when built)

        pev->effects &= ~EF_NODRAW; // Make visible again

        SetThink(SentryThink); // Change think function to normal sentry think function
    }
}
This is the main think function below. What this does is basically checks if the current enemy is still visible, if yes, aim and shoot at it, if not remove the enemy.
void CMySentry :: SentryThink ( void )
{
    pev->nextthink = gpGlobals->time + 0.1;

// BEGIN -- UPDATE BARREL POSITION (NOT USED!!)

    UTIL_MakeVectors(pev->angles);

    Vector v_End = UTIL_RelationToWorld(ENT(pev),gpGlobals->v_forward);
    m_vBarrel = UTIL_LengthFromVector((v_End - pev->origin),4);
    m_vBarrel.z += 32;

// END -- UPDATE BARREL POSITION (NOT USED!!)

    if( m_pEnemy && (m_fLastCheckVisibleEnemy <= gpGlobals->time) )
    {
        TraceResult tr;

        UTIL_TraceLine(pev->origin,m_pEnemy->pev->origin + m_pEnemy->pev->view_ofs,ignore_monsters,ENT(pev),&tr);

        if( tr.flFraction < 1.0 )
            m_pEnemy = NULL;

        m_fLastCheckVisibleEnemy = gpGlobals->time + 0.5; // Re-check visible enemy again in 0.5 seconds
    }

    if( (m_fLastAimTime + 0.2) <= gpGlobals->time ) // re-Aim every  0.2 seconds
    {
        m_fLastAimTime = gpGlobals->time;
        Aim(); // Aim at Nearest Enemy
    }

    if ( m_pEnemy ) // if got an enemy
    {
        if( m_pEnemy->IsAlive() )
            Shoot(); // Shoot Enemy (Should be facing enemy from Aim() )
        else
            m_pEnemy = NULL;
    }
}
Next should be the spawn function, this is where we initialise stuff and call ONLY call Precache() here. Also the sentry is set to invisible (using EF_NODRAW bitset) and the Think function is set to the BuildThink function so it will wait SENTRY_BUILD_TIME before functioning and returining visible to the player. (See the BuildThink function explanation)
void CMySentry :: Spawn ( void )
{
    Precache( );

    pev->movetype = MOVETYPE_FLY;
    pev->health = 100;
    pev->max_health = pev->health;
    pev->armorvalue = 100;

    m_iRockets = SENTRY_MAX_ROCKETS / 2; //
    m_iShells = SENTRY_MAX_SHELLS / 2;

    pev->gravity = 1;

    SET_MODEL(ENT(pev), "models/MySentry.mdl"); // the name of the model YOU want to use.

    UTIL_SetOrigin( pev, pev->origin );

    UTIL_SetSize(pev, Vector(-24, -24, -20), Vector(24, 24, 20));

    pev->nextthink        = gpGlobals->time + 0.1;
    pev->movetype        = MOVETYPE_FLY;
    pev->sequence        = 0;
    pev->frame            = 0;
    pev->solid            = SOLID_SLIDEBOX;
    pev->takedamage        = DAMAGE_AIM;

    SetBits (pev->flags, FL_MONSTER);

    m_fBuildTime = gpGlobals->time + 0.1;

    pev->effects |= EF_NODRAW;

    if( pev->owner )
      {
        if( pev->owner->v.flags & FL_CLIENT )
            strcpy(m_szTeamName,GetClassPtr((CBasePlayer*)pev)->m_szTeamName); // Initialise the team ID of the sentry
      }
    else
        strcpy(m_szTeamName,""); // Initialise the team ID of the sentry

    EMIT_SOUND(ENT(pev), CHAN_BODY, "sentry/building.wav", 1.0, ATTN_NORM);

    SetThink(SentryBuildThink);
}
The m_szTeamName string is the TeamName of the player who built it. Once the player builds the sentry, set the TeamName to the pev->owner's
void CMySentry :: Precache ( void )
{
    PRECACHE_MODEL("models/MySentry.mdl"); // The name of the model YOU want to use (NOTE: this MUST be the same as the model you set the sentry to)

    PRECACHE_SOUND("turret/tu_fire1.wav");

    PRECACHE_SOUND("turret/tu_die.wav");
    PRECACHE_SOUND("turret/tu_die2.wav");
    PRECACHE_SOUND("turret/tu_die3.wav");

    PRECACHE_SOUND("turret/tu_spinup.wav");

    PRECACHE_SOUND("sentry/building.wav"); // Building sound
}
We need the aim function to aim at its enemy so the rockets etc wikll face in the correct direction.
void CMySentry :: Aim ( void )
{
    if( m_pEnemy )
    {
        Vector v_Comp = (m_pEnemy->pev->origin + pev->view_ofs) - (pev->origin + pev->view_ofs);
        Vector v_IdealAngles = UTIL_VecToAngles(v_Comp);

        pev->angles = UTIL_FixAngles(v_IdealAngles);

        pev->angles.x = -pev->angles.x; // Must invert pitch

        pev->angles.z = 0;

        if (pev->angles.x > SENTRY_MAX_PITCH)
            pev->angles.x = SENTRY_MAX_PITCH;

        if( pev->angles.x < -SENTRY_MAX_PITCH)
            pev->angles.x = -SENTRY_MAX_PITCH;
    }
    else // Focus on one enemy at a time
    {
        m_pEnemy = GetBestEnemy();
    }
}
Simple function below is to get the best enemy. If The TeamNames don't match with a found enemy, it will set it as its enemy. This function is called by ::Aim()
CBaseEntity *CMySentry :: GetBestEnemy (void)
{
    CBaseEntity *l_pFoundEnemy = NULL;
    CBaseEntity *l_pBestEnemy = NULL;
    CBasePlayer *l_pFoundPlayer = NULL;

    TraceResult tr;

    float min_distance = 9999.0;
    float distance = 0.0;

    while ( (l_pFoundEnemy = UTIL_FindEntityByClassname( l_pFoundEnemy, "player")) != NULL )
    {
        if( !l_pFoundEnemy->IsAlive() )
            continue;

        if( l_pFoundEnemy->pev->flags & FL_CLIENT )
        {
            l_pFoundPlayer = GetClassPtr((CBasePlayer *)l_pFoundEnemy->pev);

            if( strcmp(l_pFoundPlayer->m_szTeamName,m_szTeamName) ) // If this is an enemy
            {
                UTIL_TraceLine((pev->origin + pev->view_ofs),l_pFoundEnemy->pev->origin,ignore_monsters,dont_ignore_glass,pev->pContainingEntity,&tr);

                if( tr.flFraction >= 1.0 )
                {
                    distance = (l_pFoundPlayer->pev->origin - pev->origin).Length();

                    if( distance < min_distance )
                    {
                        min_distance = distance;

                        l_pBestEnemy = l_pFoundEnemy;
                    }
                }
            }
        }
    }

    return l_pBestEnemy;
}
Now, The shoot function. This will check if it's time to fire another shell/rocket, and check if there is enough ammo, and then shoot a shell or rocket using FireRocket() and/or FireShell().
void CMySentry :: Shoot ( void )
{
    if( m_iRockets > 0 )
    {
        if( (m_fLastShootRocket + SHOOT_ROCKET_TIME) <= gpGlobals->time )
        {
            m_fLastShootRocket = gpGlobals->time;

            FireRocket();
        }
    }

    if( m_iShells > 0 )
    {
        if( (m_fLastShootShell + SHOOT_SHELL_TIME) <= gpGlobals->time )
        {
            m_fLastShootShell = gpGlobals->time;

            FireShell();
        }
    }
}
The fire rocket and fire shell functions (below) should be pretty similar except a rocket or shell is fired in the facing direction.
void CMySentry :: FireRocket ( void )
{
    UTIL_MakeVectors(pev->angles);

    CBaseEntity *m_Rocket = CBaseEntity::Create( "rpg_rocket", pev->origin, pev->angles, edict() );

    UTIL_SetOrigin(m_Rocket->pev,(pev->origin + m_vBarrel));

    UTIL_MakeVectors(m_Rocket->pev->angles);
    m_Rocket->pev->angles = pev->angles;

    Vector v_Forward = UTIL_RelationToWorld(pev->pContainingEntity,gpGlobals->v_forward);

    m_Rocket->pev->velocity = ( (v_Forward - pev->origin) / (( v_Forward - pev->origin ).Length()) ) * 200.0;

    m_iRockets --;
}

void CMySentry :: FireShell ( void )
{
    TraceResult tr;

    UTIL_MakeVectors( pev->angles );
    Vector vecSrc = pev->origin + pev->view_ofs;
    Vector vecDir = gpGlobals->v_forward * 8192.0;

    UTIL_TraceLine(vecSrc, vecSrc + vecDir, dont_ignore_monsters, ENT(pev), &tr);

    //if( tr.pHit )
    //    ALERT(at_console,"%s\n",STRING(tr.pHit->v.classname));

    FireBullets(1,vecSrc,vecSrc + vecDir,VECTOR_CONE_3DEGREES,4096.0,BULLET_MONSTER_12MM,4,5,pev);

    EMIT_SOUND(ENT(pev),CHAN_BODY,"turret/tu_fire1.wav",1.0,ATTN_NORM); // Emit turret shoot sound

    m_iShells --; // reduce shells
}
Your own Take Damage function. You'll need this to add shells/rockets to it at a later time.
int CMySentry::TakeDamage(entvars_t *pevInflictor, entvars_t *pevAttacker, float flDamage, int bitsDamageType)
{
    if ( !pev->takedamage )
        return 0;

    if ( pev->owner && (pev->Attacker == &pev->owner->v))
      {
          // Do stuff in here to add shells/rockets from the player perhaps...

         if( CVAR_GET_FLOAT("mp_friendlyfire") < 1.0 )
           return;
      }

    pev->health -= flDamage; // Remove this damage

    if (pev->health <= 0)
    {
        pev->health = 0;
        pev->takedamage = DAMAGE_NO; // Don't take anymore damage
        pev->dmgtime = gpGlobals->time; // Set damage time

        ClearBits (pev->flags, FL_MONSTER);

        SetThink(MySentryDeath); // Change think funtion so I don't try to shoot etc.

        pev->nextthink = gpGlobals->time + 0.1; // Think again in 0.1 seconds

        return 0;
    }
    else
    {
        if (RANDOM_FLOAT( 0, 10 ) > 5)

        UTIL_Sparks( pev->origin );   // Some Sparks
    }

    return 1;
}
This death function is continously called while the sentry is "dying", give it a funky death! I took some code from the turret.cpp file and made the turret look as tho it malfunctions by setting the avelocity (angle velocity)/yaw angles to +/- randomly and the speed of the angle velocity will slow down after time. Also sparks and smoke will appear!
void CMySentry :: MySentryDeath ( void )
{

    pev->nextthink = gpGlobals->time + 0.1;

    if (pev->deadflag != DEAD_DEAD)
    {
        pev->deadflag = DEAD_DEAD;

        float flRndSound = RANDOM_FLOAT ( 0 , 1 );

        if ( flRndSound <= 0.33 )
            EMIT_SOUND(ENT(pev), CHAN_BODY, "turret/tu_die.wav", 1.0, ATTN_NORM);
        else if ( flRndSound <= 0.66 )
            EMIT_SOUND(ENT(pev), CHAN_BODY, "turret/tu_die2.wav", 1.0, ATTN_NORM);
        else
            EMIT_SOUND(ENT(pev), CHAN_BODY, "turret/tu_die3.wav", 1.0, ATTN_NORM);

        //EMIT_SOUND_DYN(ENT(pev), CHAN_STATIC, "turret/tu_active2.wav", 0, 0, SND_STOP, 100);


        pev->solid = SOLID_NOT;

    }

    float f_time = (pev->dmgtime + 5) - gpGlobals->time;

    if( f_time > 0.0 )
    {
        if( RANDOM_LONG( 0, 100 ) <= 50 )
            pev->avelocity.y = f_time * 100;
        else
            pev->avelocity.y = -f_time * 100;
    }
    else
        pev->avelocity.y = 0;

    Vector vecSrc, vecAng;

    vecSrc = pev->origin;

    if (pev->dmgtime + RANDOM_FLOAT( 0, 2 ) > gpGlobals->time)
    {
        // lots of smoke
        MESSAGE_BEGIN( MSG_BROADCAST, SVC_TEMPENTITY );
            WRITE_BYTE( TE_SMOKE );
            WRITE_COORD( vecSrc.x + RANDOM_FLOAT( -16, 16 ) );
            WRITE_COORD( vecSrc.y + RANDOM_FLOAT( -16, 16 ) );
            WRITE_COORD( vecSrc.z - 32 );
            WRITE_SHORT( g_sModelIndexSmoke );
            WRITE_BYTE( 15 ); // scale * 10
            WRITE_BYTE( 8 ); // framerate
        MESSAGE_END();
    }

    if (pev->dmgtime + RANDOM_FLOAT( 0, 8 ) > gpGlobals->time)
    {
        UTIL_Sparks( vecSrc );
    }

    if ( (pev->dmgtime + 5) < gpGlobals->time)
    {
        ExplosionCreate(pev->origin,pev->angles,NULL,50,0);

        RemoveFromPlayer();

        Killed(pev,GIB_NEVER);

        SetThink( NULL );
    }
}
I've made a dismantle function which just kills the sentry, and remove from player function which will set the m_Sentry in CBasePlayer to NULL.
void CMySentry :: Dismantle ( void )
{
    RemoveFromPlayer();

    Killed(pev,GIB_NEVER);
}

void CMySentry :: RemoveFromPlayer ( void )
{
    if( pev->owner )
    {
        if( pev->owner->v.flags & FL_CLIENT )
        {
            CBasePlayer *pPlayer = GetClassPtr((CBasePlayer*)&pev->owner->v);

            pPlayer->m_Sentry = NULL;
        }
    }
}
Now the adding shells and rockets...
//------------------------------
// ADD ROCKETS / SHELLS
//-----------------------------
// Adds rockets/shells to the sentry and returns the difference from the new rockets/shells for use when taking rockets/shells from a player into the sentry

int CMySentry :: AddShells  ( int l_iNewShells )
{
    int l_iOldShells = 0;

    if( m_iShells == SENTRY_MAX_SHELLS )
    {
        return l_iOldShells;
    }
    else if ( (m_iShells + l_iNewShells) >= SENTRY_MAX_SHELLS )
    {
        l_iOldShells = SENTRY_MAX_SHELLS - m_iShells;

        m_iShells = SENTRY_MAX_SHELLS;

        return l_iOldShells;
    }
    else
    {
        m_iShells += l_iNewShells;

        return l_iNewShells;
    }

    return 0;
}

int CMySentry :: AddRockets ( int l_iNewRockets )
{
    int l_iOldRockets = 0;

    if( m_iRockets == SENTRY_MAX_ROCKETS )
    {
        return l_iOldRockets;
    }
    else if ( (m_iRockets + l_iNewRockets) >= SENTRY_MAX_ROCKETS )
    {
        l_iOldRockets = SENTRY_MAX_ROCKETS - m_iRockets;

        m_iRockets = SENTRY_MAX_ROCKETS;

        return l_iOldRockets;
    }
    else
    {
        m_iRockets += l_iNewRockets;

        return l_iNewRockets;
    }

    return 0;
}
Now that's all the sentry code. To build a sentry, open up CLIENT.CPP and look in the ClientCommand Function. You'll need to add your own command to build a sentry.

Stick this code in between some client commands, You will also need to add a "CBaseEntity *m_Sentry" in Player.h in CBasePlayer, and set it to NULL when you spawn.
else if ( FStrEq(pcmd, "build_sentry" ) )
{
    if( m_pPlayer->m_Sentry )
    {
        ClientPrint( &pEntity->v, HUD_PRINTCENTER, "Sentry Already Built!\n");
    }
    else
    {
        Vector origin = UTIL_ForwardPosition(pEntity,64.0);

        origin.z = m_pPlayer->pev->absmin.z + 24;

        CBaseEntity *Sentry = CBaseEntity::Create( "avp_sentry", origin, pev->angles, pEntity );


        if( !FNullEnt(Sentry->edict()) )
        {
            m_pPlayer->m_Sentry = Sentry;
            Sentry->pev->owner = pEntity;

            ClientPrint( pev, HUD_PRINTCENTER, "Building Sentry...\n");

        }
        else
            ALERT(at_console,"Error allocating new edict for sentry!\n");
    }
}

Known Problems:

For some reason when the Sentry is built by the player, you can walk through the sentry and the shells don't seems to hit anything (The trace line doesn't seem to work) But it works okay when spawned by the map for some reason... although the rockets work okay.

Follow-ups

This article was originally published on Gamedesign.net RUST.
The original URL of the article was http://www.gamedesign.net/node/538.
The archived page is available here.
TWHL only publishes archived articles from defunct websites, or with permission. For more information on TWHL's archiving efforts, please visit the TWHL Archiving Project page.

Comments

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