Posted by: jim_the_coder
Date posted: May 12 2004 User Rating: N/A | Number of views: 2299 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.
|
296 Approved Articless
5 Pending Articles
3940 Registered Members
0 People Online (61 guests)
|
|