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:
| |
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; };
#define SF_COOP_TRIGGERONLY 0
LINK_ENTITY_TO_CLASS(info_player_coop,CBaseCoopStart);
void CBaseCoopStart::Spawn( void ) {
if ( !(pev->spawnflags & SF_COOP_TRIGGERONLY) ) SetThink( FindThink ); pev->nextthink = gpGlobals->time; }
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 ); }
void CBaseCoopStart::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) { CBasePlayer *pPlayer = NULL; if ( pActivator ) pPlayer = (CBasePlayer*)CBaseEntity::Instance( pActivator->pev );
if ( pPlayer && pPlayer->IsAlive() ) { 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 ); PassCheckpoint(); } } }
void CBaseCoopStart :: FindThink( void ) { CBaseEntity *pEnt = FindEntity();
CBasePlayer *pPlayer = NULL; if ( pEnt ) pPlayer = (CBasePlayer*)CBaseEntity::Instance( pEnt->pev );
if ( pPlayer && pPlayer->IsAlive() ) { 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 ); PassCheckpoint(); } }
pev->nextthink = gpGlobals->time + 0.5; }
CBaseEntity *CBaseCoopStart :: FindEntity( void ) { CBaseEntity *pEntity = NULL;
while ((pEntity = UTIL_FindEntityInSphere( pEntity, pev->origin, m_flRadius )) != NULL) { if ( FBitSet( pEntity->pev->flags, FL_CLIENT ) ) { return pEntity; } } return NULL; }
void CBaseCoopStart::PassCheckpoint() { for ( int i = 1; i <= gpGlobals->maxClients; i++ ) { CBaseEntity *pEntity = UTIL_PlayerByIndex( i );
CBasePlayer *pPlayer = (CBasePlayer*)pEntity;
if ( pPlayer ) pPlayer->m_iCurrentCheckpointNumber = m_iCheckpointNumber; } }
|
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;
| | int m_iCheckpointNumber; string_t m_sCheckpointMaster;
|
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;:
| | 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:
| |
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:
| | 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 ) { if( !FNullEnt(pSpot) && pPlayer->m_iCurrentCheckpointNumber == pSpot->m_iCheckpointNumber ) { if ( FStringNull( pSpot->m_sCheckpointMaster) ) { spawnPoints.push_back(pSpot); } else { if ( (pPlayer->pev->flags & PFLAG_OBSERVER) || !strcmp( pPlayer->m_szTeamName, "") || !strcmp( pPlayer->m_szTeamName, STRING(pSpot->m_sCheckpointMaster)) ) { spawnPoints.push_back(pSpot); } } } }
CBaseEntity* pMySpot = spawnPoints[ RANDOM_LONG( 0, spawnPoints.size() -1 ) ];
if ( !FNullEnt(pMySpot) ) { g_pLastSpawn = pMySpot; return pMySpot->edict(); }
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:
| | @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). |
|
User Comments
Showing comments 1-5
Good article, jim, lots of code, lots of text, an interesting problem.
And of course I always love to see people using STL. :) |
|
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:
| | #include >windows.h< #include >vector<
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
|
|
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
|
|
where is this well known tut by DarkKnight?? can someone give me a link please? |
|
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.
|
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 :)
|
296 Approved Articless
3 Pending Articles
3940 Registered Members
0 People Online (13 guests)
|
|