Adding level transitions to multiplayer:fr
From Valve Developer Community
Warning: Les transitions de niveaux semblent ne plus marcher avec la nouvelle mise à jour du SDK du 06 Août 2006.
Description de l'erreur: Lorque deux joueurs ou plus sont sur le serveur au moment de la transition, le serveur déconnecte avec un message d'erreur dans la console.
Normalement, lorsque vous changez de niveau, tout est réinitialisé par défaut : les points de vie, les armes, les scores… Pourtant, ce n’est pas le cas en mode simple joueur puisque vous conservez tous les éléments précédents d’un niveau à l’autre. Pourquoi ne pas proposer la même chose en multijoueur lorsque vous avez une série de niveaux. C’est de ceci que cet article traite.
| Table of contents |
Théorie
En joueur seul, on passe au niveau suivant lorsque l’on touche un trigger_changelevel En multijoueur on se sert de la même chose pour changer de niveau. Toutefois, en mode simple joueur, une entité supplémentaire est utilisée : info_landmark.
Pour conserver tous les éléments précédents, le moteur du jeu regarde quelles sont les objets qui sont à proximité de l’info_landmark. Le moteur sauvegarde ensuite chacun de ces objets, puis les restore dans le niveau suivant. Notez toutefois que le processus de save/restore fonctionne parfaitement en simple joueur, mais génère des bugs en multijoueur.
Ce qui se passe en fait est très simple : le code suppose qu’un seul et unique joueur est présent dans le jeu. En multijoueur cela pose donc problème. De plus, même si cela fonctionnait quand même, il resterait des soucis de jouabilité. En effet, si des joueurs restaient en arrière au moment de la transition, ils ne seraient donc pas sauvegardés. Vous comprenez donc bien qu’en dehors des problèmes techniques, il y a des problèmes de jouabilité
Cependant il est possible de modifier le code pour permettre de palier à certains de ces inconvénients. On peut par exemple téléporter les joueurs qui sont trop loin dans la zone du info_landmark. Toutefois il faut disposer de suffisamment d’espace dans la zone du landmark pour pas que les joueurs ne se bloquent. Il existent plusieurs méthodes pour s’affranchir de ce problème. A vous de vous en occuper.
Le code
Puisqu'on va modifier le code du jeu, pourquoi ne pas rajouter quelques fonctionnalités intéressantes?
multiplay_gamerules.cpp
Après les includes au début du fichier, ajoutez le code suivant:
ConVar mp_transition_players_percent( "mp_transition_players_percent", "50", FCVAR_NOTIFY|FCVAR_REPLICATED, "Combien de joueurs, en pourcents, sont requis pour déclencher le changement de niveau?”); #ifndef CLIENT_DLL ConVar sv_transitions( "sv_transitions", "1", FCVAR_NOTIFY|FCVAR_GAMEDLL, "Activer les transitions" ); #endif
Ces deux nouvelles Variables Consoles serviront à ajouter des fonctionnalités intéressantes à l'ensemble. La première permet d'exiger que tant pourcent ( % ) de joueurs aient touché le trigger_changelevel pour que le niveau puisse changer. L'autre permet d'activer ou non les changements complexes de niveaux.
triggers.cpp
Ajoutez le fichier multiplay_gamerules.h aux includes, puis repérez la fonction CChangeLevel::ChangeLevelNow.
Remplacez tout son contenu par ce qui suit:
extern ConVar mp_transition_players_percent;
extern ConVar sv_transitions;
void CChangeLevel::ChangeLevelNow( CBaseEntity *pActivator )
{
CBaseEntity *pLandmark;
levellist_t levels[16];
CBasePlayer *pPlayer = (pActivator && pActivator->IsPlayer()) ? ToBasePlayer( pActivator ) : NULL;
if ( !pPlayer )
return;
pPlayer->m_bTransition = true;
if ( mp_transition_players_percent.GetInt() > 0 )
{
int totalPlayers = 0;
int transitionPlayers = 0;
for( int i = 1; i <= gpGlobals->maxClients; i++)
{
CBasePlayer* pPlayer = UTIL_PlayerByIndex( i );
if ( pPlayer && pPlayer->IsAlive() )
{
totalPlayers++;
if ( pPlayer->m_bTransition )
transitionPlayers++;
}
}
if ( ( (int) (transitionPlayers / totalPlayers * 100) ) < mp_transition_players_percent.GetInt() )
{
Msg("Transitions: Not enough players to trigger level change\n");
return;
}
}
// Cet objet sera supprimé plus tard lors de l'appel à engine->ChangeLevel. On copy donc les paramètres dans une mémoire "sure"
Q_strncpy(st_szNextMap, m_szMapName, sizeof(st_szNextMap));
if ( !sv_transitions.GetBool() )
engine->ChangeLevel( st_szNextMap, NULL );
// Some people are firing these multiple times in a frame, disable
if ( m_bTouched )
return;
m_bTouched = true;
int transitionState = InTransitionVolume(pPlayer, m_szLandmarkName);
if ( transitionState == TRANSITION_VOLUME_SCREENED_OUT )
{
DevMsg( 2, "Player isn't in the transition volume %s, aborting\n", m_szLandmarkName );
return;
}
// On cherche l'entite info_landmark
pLandmark = FindLandmark( m_szLandmarkName );
if ( !pLandmark )
return;
//TRANSITIONS DE NIVEAUX
// no transition volumes, check PVS of landmark
if ( transitionState == TRANSITION_VOLUME_NOT_FOUND )
{
byte pvs[MAX_MAP_CLUSTERS/8];
int clusterIndex = engine->GetClusterForOrigin( pLandmark->GetAbsOrigin() );
engine->GetPVSForCluster( clusterIndex, sizeof(pvs), pvs );
// DM: on passe en revue tous les joueurs dans le niveau, et on repère quels sont ceux qui ne sont pas dans la zone du landmark.
for( int i = 1; i <= gpGlobals->maxClients; i++ )
{
CBasePlayer *pl = UTIL_PlayerByIndex( i );
if ( pPlayer && pl && pl->entindex() != pPlayer->entindex() && pl->IsAlive() )
{
Vector vectorSurroundMins, vectorSurroundMaxs;
pl->CollisionProp()->WorldSpaceSurroundingBounds( &vectorSurroundMins, &vectorSurroundMaxs );
bool playerInPVS = engine->CheckBoxInPVS( vectorSurroundMins, vectorSurroundMaxs, pvs, sizeof( pvs ) );
// DM: Si le joueur n'est pas dans la zone du landmark, on le teleporte tout simplement.
if ( !playerInPVS )
{
pl->JumptoPosition( pPlayer->GetAbsOrigin(), pPlayer->GetAbsAngles() );
pl->m_bTransitionTeleported = true;
}
}
}
if ( pPlayer )
{
Vector vecSurroundMins, vecSurroundMaxs;
pPlayer->CollisionProp()->WorldSpaceSurroundingBounds( &vecSurroundMins, &vecSurroundMaxs );
bool playerInPVS = engine->CheckBoxInPVS( vecSurroundMins, vecSurroundMaxs, pvs, sizeof( pvs ) );
if ( !playerInPVS )
{
Warning( "Player isn't in the landmark's (%s) PVS, aborting\n", m_szLandmarkName );
return;
}
}
}
g_iDebuggingTransition = 0;
st_szNextSpot[0] = 0; // Init landmark to NULL
Q_strncpy(st_szNextSpot, m_szLandmarkName,sizeof(st_szNextSpot));
m_hActivator = pActivator;
m_OnChangeLevel.FireOutput(pActivator, this);
NotifyEntitiesOutOfTransition();
//// Msg( "Level touches %d levels\n", ChangeList( levels, 16 ) );
if ( g_debug_transitions.GetInt() )
{
Msg( "CHANGE LEVEL: %s %s\n", st_szNextMap, st_szNextSpot );
}
// If we're debugging, don't actually change level
if ( g_debug_transitions.GetInt() == 0 )
{
engine->ChangeLevel( st_szNextMap, st_szNextSpot );
}
else
{
// Build a change list so we can see what would be transitioning
CSaveRestoreData *pSaveData = SaveInit( 0 );
if ( pSaveData )
{
g_pGameSaveRestoreBlockSet->PreSave( pSaveData );
pSaveData->levelInfo.connectionCount = BuildChangeList( pSaveData->levelInfo.levelList, MAX_LEVEL_CONNECTIONS );
g_pGameSaveRestoreBlockSet->PostSave();
}
SetTouch( NULL );
}
}
Ce code est assez simple au final. Il regarde dans un premier temps combien de joueurs ont touché le trigger_changelevel, et combien ne l'ont pas touché. Si suffisament de joueurs, en pourcents ( % ), ont touché le trigger, alors on exécute le reste de la fonction et on change donc de niveau. RAPPEL: pour la valeur en pourcents, voyez la variable console mp_transition_players_percent.
De plus, si la variable sv_transitions est mise à zéro, on se contentera d'un changement de niveau basique à la Counter-Strike, c'est à dire sans sauvegarde des éléments.
Enfin, la fonction détermine les joueurs qui ne sont pas dans la zone de landmark, et si tel est le cas, elle téléporte ces joueurs à l'endroit où se trouve la personne qui à touché le trigger_changelevel. Chacun des joueurs seront ainsi bloqués car ils se trouveront tous à la même position. Mais pas de panique, le souci sera réglé dans le niveau suivant dans la fonction Spawn() du joueur.
player.h
Il vous faut d'abord déclarer ces deux variables dans la classe CBasePlayer.
Sous l'onglet public:>/code> ajoutez ceci:
bool m_bTransition; bool m_bTransitionTeleported;
player.cpp
Dans la fonction <code>CBasePlayer::CBasePlayer ajoutez ceci pour initialiser nos variables:
m_bTransition = false; m_bTransitionTeleported = false;
Also, one of the more important code changes: Look for BEGIN_DATADESC( CBasePlayer ).
As you can see at the comment, every variable defined in the datadesc is saved and restored by the engine. The 2 new booleans need to be saved or else the function Spawn of the player is called, which results in resetting the player, even though the engine saved the weapons/health and other information of the player.
Un autre changement capital réside dans la table des descriptions des données.
Chose à savoir: toute variable déclarée dans la datadesc sera utilisée dans le processus save/restore.
Par conséquent nos deux booléens seront sauvegardés lors du processus de changement de niveau.
Dans la fonction Spawn() on pourra donc récupérer cette valeur et savoir si l'on a affaire à un joueur qui vient d'une changement de niveau ou d'un joueur qui vient d'arriver dans le serveur.
Bon assez causé. Recherchez d'abord la déclaration de BEGIN_DATADESC( CBasePlayer ). Ajoutez ce code à l'intérieur du bloc:
Add:
DEFINE_FIELD( m_bTransition, FIELD_BOOLEAN ), DEFINE_FIELD( m_bTransitionTeleported, FIELD_BOOLEAN ),
Commentez cette ligne également.
DEFINE_FIELD( m_fLastPlayerTalkTime, FIELD_FLOAT ),
Ceci corrige un bug avec la fenêtre de chat dans le jeu.
En fait le moteur du jeu empêche les joueurs de flooder en utilisant la variable fLastPlayerTalkTime.
Toutefois, lors du changement de niveau, le temps global du jeu gpGlobals->curtime est remis à zéro.
Ainsi en conservant la valeur de fLastPlayerTalkTime, on pourrait attendre un bon bout de temps avant de pouvoir parler à nouveau. Du coup en commentant cette ligne, on fait en sorte que fLastPlayerTalkTime soit réinitialisé. Les joueurs porront donc continuer à papotter dans le jeu sans problème.
hl2mp_player.cpp
Trouvez la fonction CHL2MP_Player::Spawn et ajoutez ceci tout au sommet de la fonction:
if ( m_bTransition )
{
if ( m_bTransitionTeleported )
g_pGameRules->GetPlayerSpawnSpot( this );
m_bTransition = false;
m_bTransitionTeleported = false;;
return;
}
Ce code simple vérifie en fait si le joueur provient d'un changement de niveau ou pas. Si c'est le cas, on saborde la fonction Spawn() pour que le processus de save/restore fonctionne comme il faut.
Ce code regarde également s'il s'agit d'un joueur qui a dû être téléporté ou pas. Si c'est le cas, on ne conserve pas sa position, on le fait téléporter à un point de spawn tel qu'un info_player_deathmatch. Le problème des joueurs qui se bloquaient est donc résolu!
NOTE: Si vous utilisez une classe de joueur personnalisée, par exemple CMonMOD_Player, il vous faudra alors mettre ce bout de code au tout début de la fonction CMonMOD_Player::Spawn(). Celà va de soi, mais il est préférable de le préciser tout de même.
Jouabilité et conclusion
Notez que le fait d'ajouter des transitions de niveaux peut apporter des problèmes de jouabilité. Grâce à notre petite variable mp_transition_players_percent, il est possible d'exiger un nombre minimal de joueurs dans la zone de transition pour passer au niveau suivant.
Toutefois d'autres problèmes peuvent survenir. Un joueur peut par exemple être coincé derrière le trigger_changelevel. On peut aussi être dans le cas de joueurs qui ne sont pas conscients qu'ils ont atteint la fin du niveau. Il faudrait donc songer à mettre une alerte du style "En attente de joueurs. x joueurs restants."
Bref, cet article vous a donné les bases pour des transitions de niveaux fluides et fonctionnelles, à vous de concevoir le reste.
This page is also available in: anglais (English)
