Wayback Machine
DEC AUG JAN
Previous capture 8 Next capture
2004 2007 2009
7 captures
23 May 04 - 20 Jun 10
sparklines
Close Help
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: 2001
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 11: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 10: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, 08:53:07

Posted By: Zipster on May 29 2004 at 04: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, 04:20:05

Posted By: GreenElephant on Dec 10 2004 at 01: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 05: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.
Donate
Any money that is donated will go towards the server costs that I incur for running our server. Don't feel you HAVE to donate, but the link is there if you want to help us out a little. Thanks :)

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 04 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
3 State Zoom For Any Weapon
Half-Life 2 | Coding | Server Side Tutorials
By: Ennuified | Oct 18 2007
 
Storing weapons on ladder
Half-Life 2 | Coding | Snippets
By: cct | Sep 07 2007
 
CTF Gameplay Part 1
Half-Life | Coding | Shared Tutorials
By: DarkNight | Aug 28 2007
 
CTF Gameplay Part 1
Half-Life | Coding | Shared Tutorials
By: deedok | Aug 20 2007
 
Debugging your half-life 2 mod
Half-Life 2 | Coding | Articles
By: blackshark | Aug 17 2007
 
How to add Ammocrates for missing ammo type and/or create new ammo types
Half-Life 2 | Coding | Server Side Tutorials
By: EoD | Aug 15 2007
 
GameUI
Half-Life 2 | Coding | Client Side Tutorials
By: G.I. Jimbo | May 19 2007
 
Reading and Writing Entire Structures
General | General Coding
By: monokrome | Jan 27 2007
 
VGUI Scope
Half-Life | Coding | Client Side Tutorials
By: Pongles | Jan 19 2007
 
Where do we go from here
General | News
By: SilentSounD | Jan 18 2007
 

Site Info
296 Approved Articless
3 Pending Articles
3940 Registered Members
0 People Online (13 guests)
About - Credits - Contact Us

Wavelength version: 3.0.0.9
Valid XHTML 1.0! Valid CSS!