Welcome, Guest! Login | Register

Checkpoint system for spawnpoints [Print this Article]
Posted by: jim_the_coder
Date posted: May 12 2004
User Rating: N/A
Number of views: 4472
Number of comments: 5
Description: A checkpoint system designed for cooperative play but possible in other game modes.
The purpose of this tutorial is to help you implement a checkpoint system into your mod. This means you start at checkpoint 0, and as you pass each checkpoint (who's number is set in Hammer/Worldcraft) it records your progress. When you die, you then respawn at the highest number checkpoint you passed. You can have multiple checkpoints with the same number; these will be picked from at random. I use this in my mod for large cooperative maps where you don't want to have to restart from the beginning of the map to catch up with the team every time you die. As my mod has four teams, there's also a master setting which allows you to specify which team can use the checkpoint. If no master is set, anyone can use it.

NOTE: This tutorial is based on code which incorporates the team system from the well-known tutorial by DarkKnight. If you have not completed this tutorial then the code may not function correctly.

To begin with, we need to actually implement our info_player_coop entity. Go into subs.cpp, and underneath CBaseDMStart::IsTriggered (or if you've changed stuff, just after all the CBaseDMStart stuff), add this code:

 CODE (C++) 

// ********************
// jim - checkpoint entity
// ********************
class CBaseCoopStart : public CPointEntity
{
public:
    void    Spawn( void );
    void    PassCheckpoint( void );
    void    EXPORT FindThink( void );
    CBaseEntity *FindEntity( void );
    void    KeyValue( KeyValueData *pkvd );
    void    Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value );

private:
    float   m_flRadius;     // range to search
};

#define SF_COOP_TRIGGERONLY     0// trigger only option in hammer

LINK_ENTITY_TO_CLASS(info_player_coop,CBaseCoopStart);

//
// Spawn the entity and decided whether or not to look for players in the radius
//
void CBaseCoopStart::Spawn( void )
{
//  ALERT( at_console, "Coop spawn point spawned, master %s, checkpoint number %i\n", STRING(m_sCheckpointMaster), m_iCheckpointNumber );
       
    if ( !(pev->spawnflags & SF_COOP_TRIGGERONLY) )
        SetThink( FindThink );
   
    pev->nextthink = gpGlobals->time;
}

//
// Read off values set in hammer
//
void CBaseCoopStart::KeyValue( KeyValueData *pkvd )
{
    if (FStrEq(pkvd->szKeyName, "number"))
    {
        m_iCheckpointNumber = atoi( pkvd->szValue );
        pkvd->fHandled = TRUE;
    }
    else if (FStrEq(pkvd->szKeyName, "radius"))
    {
        m_flRadius = atof( pkvd->szValue );
        pkvd->fHandled = TRUE;
    }
    else if (FStrEq(pkvd->szKeyName, "master"))
    {
        m_sCheckpointMaster = ALLOC_STRING(pkvd->szValue);
        pkvd->fHandled = TRUE;
    }
    else
        CPointEntity::KeyValue( pkvd );
}

//
// What to do when the entity is triggered by another ent
//
void CBaseCoopStart::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value )
{
    CBasePlayer *pPlayer = NULL;
   
    // cast the entity to a player...
    if ( pActivator )
        pPlayer = (CBasePlayer*)CBaseEntity::Instance( pActivator->pev );

    // check it was a player and that they're not dead (don't let dead bodies fall into triggers!)
    if ( pPlayer && pPlayer->IsAlive() )
    {
         // if we're trying to activate a checkpoint that's greater than the highest one we've reached and it's not the first one...
        if ( pPlayer->m_iCurrentCheckpointNumber < m_iCheckpointNumber && m_iCheckpointNumber > 0)
        {
            char text[128];
            sprintf( text, "%s activated checkpoint %i\n", STRING(pPlayer->pev->netname), m_iCheckpointNumber );
            UTIL_SayTextAll( text, pPlayer );// notify everyone who activated which checkpoint
 
            PassCheckpoint();// update everyone with the current checkpoint
        }
    }
}

//
// Search for entities think code
//
void CBaseCoopStart :: FindThink( void )
{
    CBaseEntity *pEnt = FindEntity();// call our entity finding function

    CBasePlayer *pPlayer = NULL;
   
    if ( pEnt )
        pPlayer = (CBasePlayer*)CBaseEntity::Instance( pEnt->pev );// cast our entity to a player...

    // check it was a player and that they're not dead (don't let dead bodies fall into checkpoints!)
    if ( pPlayer && pPlayer->IsAlive() )
    {
        // if we're trying to pass a checkpoint that's greater than the highest one we've reached and it's not the first one...
        if ( pPlayer->m_iCurrentCheckpointNumber < m_iCheckpointNumber && m_iCheckpointNumber > 0)
        {
            char text[128];
            sprintf( text, "%s reached checkpoint %i\n", STRING(pPlayer->pev->netname), m_iCheckpointNumber );
            UTIL_SayTextAll( text, pPlayer );// notify everyone who activated which checkpoint
 
            PassCheckpoint();// update everyone with the current checkpoint
        }
    }

    pev->nextthink = gpGlobals->time + 0.5;// don't search again for 1/2 a second
}

//
// The actual entity finding function called by FindThink
//
CBaseEntity *CBaseCoopStart :: FindEntity( void )
{
    CBaseEntity *pEntity = NULL;

    // run through all the entities in a sphere (size set by radius in hammer)
    while ((pEntity = UTIL_FindEntityInSphere( pEntity, pev->origin, m_flRadius )) != NULL)
    {
        // and return then if they're clients
        if ( FBitSet( pEntity->pev->flags, FL_CLIENT ) )
        {
            return pEntity;
        }
    }
   
    return NULL;// if we don't find any, return NULL
}

//
// Update all the players on the server with the current checkpoint
//
void CBaseCoopStart::PassCheckpoint()
{
    for ( int i = 1; i <= gpGlobals->maxClients; i++ )
    {
        CBaseEntity *pEntity = UTIL_PlayerByIndex( i );// get the player entities by their indices

        CBasePlayer *pPlayer = (CBasePlayer*)pEntity;// cast them to CBasePlayers

        if ( pPlayer )// check the player's valid and then set the checkpoint number
            pPlayer->m_iCurrentCheckpointNumber = m_iCheckpointNumber;
    }
}

// ***********************
// jim - end checkpoint entity
// ***********************


Now for an explanation of this code; there's quite a bit going on. Firstly, the check point can be either reached or activated. If you set a radius value and don't check "Trigger only" in Hammer, then the checkpoint will be reached when a player enters that radius. This is simple and effective.
Activating the checkpoint can be done in two ways. Having named the checkpoint, you can then target it with a button to activate it when pressed. This way you can skip out several checkpoints if you want to map it like that. The other more useful use of activation is if you don't want to use the radius search (which isn't that great as if you set it large it will go through floors to the next level etc making activation a bit of a hit and miss affair with people being able to reach it from corridors which aren't supposed to allow access to the checkpoint yet. In this case, just make a brush-based trigger_once and target the checkpoint. Now you can cover several corridors, or just put a small trigger somewhere instead of a sphere. If you want the checkpoint to only be activated and not reached, you can check the "Trigger only" in Hammer. If you still want to allow it to be reached, leave this unchecked.
Note: setting the radius value to 0 merely makes the checkpoint effectively a point entity. It doesn't disable the radius find function.

Anyway, next we need to declare our variables somewhere. Open up cbase.h, and in the CBaseEntity class add this right at the bottom after int m_fireState;

 CODE (C++) 

    int         m_iCheckpointNumber;// jim
    string_t    m_sCheckpointMaster;// jim


These store data about the checkpoints themselves. We also need to store which is the highest number checkpoint reached by the player. Open up player.h and put this at the bottom of the CBasePlayer class under float m_flNextChatTime;:

 CODE (C++) 

    int     m_iCurrentCheckpointNumber;


That's everything done for the actual spawnpoints. The other piece of code we need to change is the spawn point select function. Open player.cpp and find the EntSelectSpawnPoint function (just above CBasePlayer::Spawn()). It should read like this:

 CODE (C++) 

// choose a info_player_deathmatch point
    if (g_pGameRules->IsCoOp())
    {
        pSpot = UTIL_FindEntityByClassname( g_pLastSpawn, "info_player_coop");
        if ( !FNullEnt(pSpot) )
            goto ReturnSpot;
        pSpot = UTIL_FindEntityByClassname( g_pLastSpawn, "info_player_start");
        if ( !FNullEnt(pSpot) )
            goto ReturnSpot;
    }


Replace that code with this:

 CODE (C++) 

    // choose an info_player_coop point
    if (g_pGameRules->IsCoop())
    {
        ALERT(at_console, "EntSelectSpawnPoint: looking for info_player_coop\n");
   
        std::vector<CBaseEntity*> spawnPoints;

        while( (pSpot = UTIL_FindEntityByClassname( pSpot, "info_player_coop" )) != NULL )
        {
            // jim - if it's valid and the spawnpoint's number matches our current number
            if( !FNullEnt(pSpot) && pPlayer->m_iCurrentCheckpointNumber == pSpot->m_iCheckpointNumber )
            {
                // jim - if theres no master set, we can use this one...
                if ( FStringNull( pSpot->m_sCheckpointMaster) )
                {
                    //ALERT(at_console, "info_player_coop: No master set\n");
                    spawnPoints.push_back(pSpot);// add this to the list
                }
               
                // jim - but if there is a master set...
                else
                {  
                    // jim - and it matches my team (or i'm not on a
                    // team or i'm an observer) we're allowed use this one as well
                    if ( (pPlayer->pev->flags & PFLAG_OBSERVER) || !strcmp( pPlayer->m_szTeamName, "") || !strcmp( pPlayer->m_szTeamName, STRING(pSpot->m_sCheckpointMaster)) )
                    {
                        spawnPoints.push_back(pSpot);// add this to the list
                    }
                }
            }
        }

        // using rand() (with the maximum set to the size of the vector)
        // choose a spawnpoint
        CBaseEntity* pMySpot = spawnPoints[ RANDOM_LONG( 0, spawnPoints.size() -1 ) ];

        // check it's valid
        if ( !FNullEnt(pMySpot) )
        {
            g_pLastSpawn = pMySpot;// tell the code it was the last spawnpoint used
            return pMySpot->edict();// return this spot
        }

        // JIM - our mappers are clever and know how to use this system,
        // but it's easy to screw it up -
        // for example if you forget to make a checkpoint for a certain team,
        // and then don't have any points with blank masters so they're unable
        // to spawn and it might crash the whole server (will have to test)
        // Also there's a risk that a certain number, say 4, might not have
        // a master for each team and again have no points with blank
        // masters. You might want to code in something which allows
        // players to spawn as many checkpoints further back as
        // neccessary if theres no valid points for this number.
        // Or you can just do what I did, which was to write a little loop
        // that goes through each point and checks that there's either
        // one with no master or a master for every team :-)
        // Anyway...

        // if we couldn't find any valid points, return a windows error
        // message and terminate
        char error[128];
        sprintf(error, "No valid info_player_coop in map\n" );
        MessageBox( NULL, error, "Fatal Error", MB_OK|MB_ICONERROR);
        exit(1);
    }


Here's an explanation of what's going on: using STL we create a vector of spawnpoints, and initialize the random number generator. Then, we scan through all the info_player_coop entities in the map, looking for any which aren't null entities (for safety) and which match the player's current max reached checkpoint. If the checkpoint has no master set, anyone can use it so we add it to the list of valid spawnpoints. If there is a master set, we check it against our team name and add it to the list if they match. At this point we also have to check in case the player is an observer or has no team set. This is because when you're choosing a team from DarkKnight's VGUI, you've already been spawned as an observer. Thus if you had a map with no blank-master spawnpoints, it would crash out as there would be no valid points.
Having created a list of all the valid spawnpoints we then select one at random, check again that it's safe, and then return it to the gamerules function that called it. If no valid spawn point has been found, the game spits out a windows error message and terminates.

Here is the entry for the spawnpoint entity in the FGD:

 CODE  
@PointClass base(PlayerClass,Targetname,Sequence) studio("models/player.mdl") = info_player_coop : "Player cooperative start"
[
    target(target_destination) : "Target on spawn"
    number(integer) : "Checkpoint number" : 0
    radius(integer) : "Checkpoint radius" : 64
    master(string) :"Master" : ""
    spawnflags(Flags) =
    [
        1 : "Trigger only"  : 0
    ]
]


I recommend you put it in after info_player_deathmatch. The values are all self-explanatory.

That's it! If you find any problems, please PM me and I'll sort them out. This tutorial could easily be expanded to have a visible model/sprite at each info_player_coop which changes color/disappears when it's reached, or else improve it in other ways. If you come up with anything you think's really good tell me and I'll add it to the tut.

Thanks to Zipster for introducing me to STL and vectors, and Persuter for his tutorial on them here which explains exactly what goes on, as well as correcting all this (along with Omega).

Rate This Article
This article has not yet been rated.

You have to register to rate this article.
User Comments Showing comments 1-5

Posted By: Persuter on May 12 2004 at 17:50:04
Good article, jim, lots of code, lots of text, an interesting problem. And of course I always love to see people using STL. :)

Posted By: jim_the_coder on May 20 2004 at 16:01:36
I've just realised something; you might need to add some includes for STL...currently my main PC is offline but as soon as it's back (hopefully tonight) I'll check what I put and add it to this comment. Sorry about that!

[EDIT]

Ok, along with all the includes at the top of player.cpp you need to add:
 CODE (C++) 

#include >windows.h<// jim - for error messages
#include >vector<// jim - for entselectspawnpoint

using namespace std;


I've had to reverse the > and the < there otherwise it doesn't show up due to the website.

I also noticed another small bug: in the code in subs.cpp, I defined SF_COOP_TRIGGERONLY as 0, when it's actually 1 in the FGD, so you need to change that to 1 for that flag to work. Sorry!

[/EDIT]Edited by jim_the_coder on May 23 2004, 14:53:07

Posted By: Zipster on May 29 2004 at 10:18:19
Whahey, I get some mention somewhere!

* happy dance * :)

One of these days I'll introduce you to boost and you'll be the coolest kid on the block!Edited by Zipster on May 29 2004, 10:20:05

Posted By: GreenElephant on Dec 10 2004 at 07:43:02
where is this well known tut by DarkKnight?? can someone give me a link please?

Posted By: coldfeet on Jan 24 2005 at 11:56:28
I too would like a link to this well known tutorial by DarkKnight. A link in the article would be nice as well.


You must register to post a comment. If you have already registered, you must login.

Latest Articles
3rd person View in Multiplayer
Half-Life 2 | Coding | Client Side Tutorials
How to enable it in HL2DM

By: cct | Nov 13 2006

Making a Camera
Half-Life 2 | Level Design
This camera is good for when you join a map, it gives you a view of the map before you join a team

By: slackiller | Mar 05 2006

Making a camera , Part 2
Half-Life 2 | Level Design
these cameras are working monitors that turn on when a button is pushed.

By: slackiller | Mar 04 2006

Storing weapons on ladder
Half-Life 2 | Coding | Snippets
like Raven Sheild or BF2

By: British_Bomber | Dec 24 2005

Implementation of a string lookup table
Half-Life 2 | Coding | Snippets
A string lookup table is a set of functions that is used to convert strings to pre-defined values

By: deathz0rz | Nov 13 2005


Latest Comments
knock knock
General | News
By: MIFUNE | Dec 31 2017
 
knock knock
General | News
By: omega | Dec 22 2016
 
knock knock
General | News
By: MIFUNE | Oct 10 2015
 
New HL HUD Message System
Half-Life | Coding | Shared Tutorials
By: chbrules | Dec 31 2011
 
knock knock
General | News
By: Whistler | Nov 05 2011
 
Particle Engine tutorial part 4
Half-Life | Coding | Client Side Tutorials
By: darkPhoenix | Feb 18 2010
 
Particle Engine tutorial part 2
Half-Life | Coding | Client Side Tutorials
By: darkPhoenix | Feb 11 2010
 
Particle Engine tutorial part 3
Half-Life | Coding | Client Side Tutorials
By: darkPhoenix | Feb 11 2010
 
Game Movement Series #2: Analog Jumping and Floating
Half-Life 2 | Coding | Shared Tutorials
By: mars3554 | Oct 26 2009
 
Particle Engine tutorial part 5
Half-Life | Coding | Client Side Tutorials
By: Deadpool | Aug 02 2009
 

Site Info
297 Approved Articless
8 Pending Articles
3940 Registered Members
0 People Online (6 guests)
About - Credits - Contact Us

Wavelength version: 3.0.0.9
Valid XHTML 1.0! Valid CSS!