Half-Life и Adrenaline Gamer форум

Всё об игре в Халф-Лайф и АГ
Текущее время: 06 апр 2020, 14:01

Часовой пояс: UTC + 5 часов [ Летнее время ]




Начать новую тему Ответить на тему  [ 1 сообщение ] 
Автор Сообщение
 Заголовок сообщения: [TUT] Spectator mode
СообщениеДобавлено: 22 мар 2012, 15:20 
Не в сети
Site Admin
Зарегистрирован:
01 июн 2010, 01:27
Последнее посещение:
05 апр 2020, 06:13
Сообщения: 6749
SPECTATOR TUTORIAL

Единственный в своем роде туториал. Сохраним, чтоб не пропал.
URL: http://hlpp.thewavelength.net/tuts/spectator.htm

This tutorial is designed to make it easy for you to add a spectator mode to your HL Mod. This tutorial will not work for you if your mod is still using HL SDK 1.0. You MUST be using HL SDK 2.0. It doesn't matter whether you've got the Pro or Standard SDK 2.0.

To make sure the spectator movement is smooth regardless of the spectator's latency, the spectator movement code is all contained within the player movement code, which is shared between the client and the game DLLs. This allows the client DLL to predict the movement of the spectator smoothly, even when it gets few updates from the server. The shared movement code is contained within pm_shared.cpp in the pm_shared directory of the SDK. You'll also need to do some editing within both the client and server code, mostly make sure the client and server are in synch.

Spectator mode has three different modes. The first is Roaming, where the spectator can just fly around the level freely. The second is Locked Chasecam, where the camera is locked onto a player, riding behind the player's head, and always looking wherever the player is looking. The third is FreeLooking ChaseCam, where the camera is locked onto a player, but the spectator can rotate himself around the player in any way s/he likes. You can easily add new modes if you like, as long as you put the movement behaviour for them into the shared movement code. SDK 2.0 adds some new variables into the entity structure designed just for mods to use. We'll use these to store the spectator mode, and the spectator's target (the player the spectator is locked onto, if any).

One Note: due to the existence of code for Spectators that already exists in the Game DLL, I've used the word "Observer" instead of "Spectator"everywhere to differentiate it. The Spectator code that already exists in the Game DLL is for clients connecting as spectators, which HL does not currently support. The code we're adding in this tutorial is for players already within the game becoming spectators.

STEP 1: THE GAME DLL

The first thing you need to do is add a new file to your game DLL's project. This file will contain the Game DLL's logic for the spectator mode. Then paste the following chunk of code into it. It has a few comments in places where I expected you'd like to change some logic.

Код:
//=============================================================================
// observer.cpp
//
#include   "extdll.h"
#include   "util.h"
#include   "cbase.h"
#include   "player.h"
#include   "weapons.h"

extern int gmsgCurWeapon;
extern int gmsgSetFOV;
extern int gmsgTeamInfo;
extern int gmsgSpectator;

// Player has become a spectator. Set it up.
// This was moved from player.cpp.
void CBasePlayer::StartObserver( Vector vecPosition, Vector vecViewAngle )
{
   // clear any clientside entities attached to this player
   MESSAGE_BEGIN( MSG_PAS, SVC_TEMPENTITY, pev->origin );
      WRITE_BYTE( TE_KILLPLAYERATTACHMENTS );
      WRITE_BYTE( (BYTE)entindex() );
   MESSAGE_END();

   // Holster weapon immediately, to allow it to cleanup
   if (m_pActiveItem)
      m_pActiveItem->Holster( );

   if ( m_pTank != NULL )
   {
      m_pTank->Use( this, this, USE_OFF, 0 );
      m_pTank = NULL;
   }

   // clear out the suit message cache so we don't keep chattering
   SetSuitUpdate(NULL, FALSE, 0);

   // Tell Ammo Hud that the player is dead
   MESSAGE_BEGIN( MSG_ONE, gmsgCurWeapon, NULL, pev );
      WRITE_BYTE(0);
      WRITE_BYTE(0XFF);
      WRITE_BYTE(0xFF);
   MESSAGE_END();

   // reset FOV
   m_iFOV = m_iClientFOV = 0;
   pev->fov = m_iFOV;
   MESSAGE_BEGIN( MSG_ONE, gmsgSetFOV, NULL, pev );
      WRITE_BYTE(0);
   MESSAGE_END();

   // Setup flags
   m_iHideHUD = (HIDEHUD_HEALTH | HIDEHUD_WEAPONS);
   m_afPhysicsFlags |= PFLAG_OBSERVER;
   pev->effects = EF_NODRAW;
   pev->view_ofs = g_vecZero;
   pev->angles = pev->v_angle = vecViewAngle;
   pev->fixangle = TRUE;
   pev->solid = SOLID_NOT;
   pev->takedamage = DAMAGE_NO;
   pev->movetype = MOVETYPE_NONE;
   ClearBits( m_afPhysicsFlags, PFLAG_DUCKING );
   ClearBits( pev->flags, FL_DUCKING );
   pev->deadflag = DEAD_RESPAWNABLE;
   pev->health = 1;
   
   // Clear out the status bar
   m_fInitHUD = TRUE;

   // Update Team Status
   pev->team = 0;
   MESSAGE_BEGIN( MSG_ALL, gmsgTeamInfo );
      WRITE_BYTE( ENTINDEX(edict()) );
      WRITE_STRING( "" );
   MESSAGE_END();

   // Remove all the player's stuff
   RemoveAllItems( FALSE );

   // Move them to the new position
   UTIL_SetOrigin( pev, vecPosition );

   // Find a player to watch
   m_flNextObserverInput = 0;
   Observer_SetMode(OBS_CHASE_LOCKED);

   // Tell all clients this player is now a spectator
   MESSAGE_BEGIN( MSG_ALL, gmsgSpectator ); 
      WRITE_BYTE( ENTINDEX( edict() ) );
      WRITE_BYTE( 1 );
   MESSAGE_END();
}

// Leave observer mode
void CBasePlayer::StopObserver( void )
{
   // Turn off spectator
   if ( pev->iuser1 || pev->iuser2 )
   {
      // Tell all clients this player is not a spectator anymore
      MESSAGE_BEGIN( MSG_ALL, gmsgSpectator ); 
         WRITE_BYTE( ENTINDEX( edict() ) );
         WRITE_BYTE( 0 );
      MESSAGE_END();

      pev->iuser1 = pev->iuser2 = 0;
      m_hObserverTarget = NULL;
   }
}

// Find the next client in the game for this player to spectate
void CBasePlayer::Observer_FindNextPlayer( bool bReverse )
{
   // MOD AUTHORS: Modify the logic of this function if you want to restrict the observer to watching
   //            only a subset of the players. e.g. Make it check the target's team.

   int      iStart;
   if ( m_hObserverTarget )
      iStart = ENTINDEX( m_hObserverTarget->edict() );
   else
      iStart = ENTINDEX( edict() );
   int       iCurrent = iStart;
   m_hObserverTarget = NULL;
   int iDir = bReverse ? -1 : 1;

   do
   {
      iCurrent += iDir;

      // Loop through the clients
      if (iCurrent > gpGlobals->maxClients)
         iCurrent = 1;
      if (iCurrent < 1)
         iCurrent = gpGlobals->maxClients;

      CBaseEntity *pEnt = UTIL_PlayerByIndex( iCurrent );
      if ( !pEnt )
         continue;
      if ( pEnt == this )
         continue;
      // Don't spec observers or invisible players
      if ( ((CBasePlayer*)pEnt)->IsObserver() || (pEnt->pev->effects & EF_NODRAW) )
         continue;

      // MOD AUTHORS: Add checks on target here.

      m_hObserverTarget = pEnt;
      break;

   } while ( iCurrent != iStart );

   // Did we find a target?
   if ( m_hObserverTarget )
   {
      // Store the target in pev so the physics DLL can get to it
      pev->iuser2 = ENTINDEX( m_hObserverTarget->edict() );
      // Move to the target
      UTIL_SetOrigin( pev, m_hObserverTarget->pev->origin );
      
      ALERT( at_console, "Now Tracking %s\n", STRING( m_hObserverTarget->pev->netname ) );
   }
   else
   {
      ALERT( at_console, "No observer targets.\n" );
   }
}

// Handle buttons in observer mode
void CBasePlayer::Observer_HandleButtons()
{
   // Slow down mouse clicks
   if ( m_flNextObserverInput > gpGlobals->time )
      return;

   // Jump changes from modes: Chase to Roaming
   if ( m_afButtonPressed & IN_JUMP )
   {
      if ( pev->iuser1 == OBS_ROAMING )
         Observer_SetMode( OBS_CHASE_LOCKED );
      else if ( pev->iuser1 == OBS_CHASE_LOCKED )
         Observer_SetMode( OBS_CHASE_FREE );
      else
         Observer_SetMode( OBS_ROAMING );

      m_flNextObserverInput = gpGlobals->time + 0.2;
   }

   // Attack moves to the next player
   if ( m_afButtonPressed & IN_ATTACK && pev->iuser1 != OBS_ROAMING )
   {
      Observer_FindNextPlayer( false );

      m_flNextObserverInput = gpGlobals->time + 0.2;
   }

   // Attack2 moves to the prev player
   if ( m_afButtonPressed & IN_ATTACK2 && pev->iuser1 != OBS_ROAMING )
   {
      Observer_FindNextPlayer( true );

      m_flNextObserverInput = gpGlobals->time + 0.2;
   }
}

// Attempt to change the observer mode
void CBasePlayer::Observer_SetMode( int iMode )
{
   // Just abort if we're changing to the mode we're already in
   if ( iMode == pev->iuser1 )
      return;

   // Changing to Roaming?
   if ( iMode == OBS_ROAMING )
   {
      // MOD AUTHORS: If you don't want to allow roaming observers at all in your mod, just abort here.
      pev->iuser1 = OBS_ROAMING;
      pev->iuser2 = 0;

      ClientPrint( pev, HUD_PRINTCENTER, "#Spec_Mode3" );
      pev->maxspeed = 320;
      return;
   }

   // Changing to Chase Lock?
   if ( iMode == OBS_CHASE_LOCKED )
   {
      // If changing from Roaming, or starting observing, make sure there is a target
      if ( m_hObserverTarget == NULL )
         Observer_FindNextPlayer( false );

      if (m_hObserverTarget)
      {
         pev->iuser1 = OBS_CHASE_LOCKED;
         pev->iuser2 = ENTINDEX( m_hObserverTarget->edict() );
         ClientPrint( pev, HUD_PRINTCENTER, "#Spec_Mode1" );
         pev->maxspeed = 0;
      }
      else
      {
         ClientPrint( pev, HUD_PRINTCENTER, "#Spec_NoTarget"  );
         Observer_SetMode(OBS_ROAMING);
      }

      return;
   }

   // Changing to Chase Freelook?
   if ( iMode == OBS_CHASE_FREE )
   {
      // If changing from Roaming, or starting observing, make sure there is a target
      if ( m_hObserverTarget == NULL )
         Observer_FindNextPlayer( false );

      if (m_hObserverTarget)
      {
         pev->iuser1 = OBS_CHASE_FREE;
         pev->iuser2 = ENTINDEX( m_hObserverTarget->edict() );
         ClientPrint( pev, HUD_PRINTCENTER, "#Spec_Mode2" );
         pev->maxspeed = 0;
      }
      else
      {
         ClientPrint( pev, HUD_PRINTCENTER, "#Spec_NoTarget"  );
         Observer_SetMode(OBS_ROAMING);
      }
      
      return;
   }
}

One of these functions, void CBasePlayer::StartObserver( Vector vecPosition, Vector vecViewAngle ), has replaced the old one that used to be in player.cpp. So open up player.cpp, search for the StartObserver function, and delete the entire thing. While you're in player.cpp, search for this line:

Код:
   CheckTimeBasedDamage();
And append this code after it:

Код:
   // Observer Button Handling
   if ( IsObserver() )
   {
      Observer_HandleButtons();
      pev->impulse = 0;
      return;
   }
Now edit the player.h file. Search for "StartObserver" and you should find this line:

Код:
   void StartObserver( Vector vecPosition, Vector vecViewAngle );
Add these lines after it:

Код:
   void    StopObserver( void );
   void   Observer_FindNextPlayer( bool bReverse );
   void   Observer_HandleButtons();
   void   Observer_SetMode( int iMode );
   EHANDLE   m_hObserverTarget;
   float   m_flNextObserverInput;
   int     IsObserver() { return pev->iuser1; };
Then go to the end of the file and insert these lines before the #endif // PLAYER_H line:

Код:
   // Observer Movement modes (stored in pev->iuser1, so the physics code can get at them)
   #define OBS_CHASE_LOCKED      1
   #define OBS_CHASE_FREE         2
   #define OBS_ROAMING         3      
A couple of notes:

We store the current mode of the spectator in pev->iuser1. It will always be one of the three defines we added to the end of player.h.
We store the entindex of the player the spectator is locked onto in pev->iuser2. This is 0 if the spectator's in Roaming mode.
We store an entity handle to the player the spectator is locked onto as well. This persists even when the spectator goes into Roaming mode, which allows us to return to the same player when the spectator returns to a Locked mode.

Next, open the client.cpp file. Search for this line:

Код:
   #include "netadr.h"
And append these lines after it:

Код:
   #include "game.h"
   edict_t *EntSelectSpawnPoint( CBaseEntity *pPlayer );
Now Find the "ClientCommand" function, and look for this code:

Код:
   else if ( FStrEq(pcmd, "say_team" ) )
   {
      Host_Say( pEntity, 1 );
   }
Then append this code after it:

Код:
   else if ( FStrEq(pcmd, "spectate" ) )
   {
      // Prevent this is the cvar is set
      if ( allow_spectators.value )
      {
         CBasePlayer *pPlayer = GetClassPtr((CBasePlayer *)pev);
         edict_t *pentSpawnSpot = EntSelectSpawnPoint( pPlayer );
         pPlayer->StartObserver( VARS(pentSpawnSpot)->origin, VARS(pentSpawnSpot)->angles);
      }
   }
This puts the player into spectator mode when s/he types the "spectate" command. It would be preferable that your mod doesn't make the players type the command. TFC, for instance, uses VGUI to present the player with a menu, which has a Spectate option on it, and sends a "spectate" command to the server when the button is clicked on. You'll notice that players can only become spectators if the allow_spectators cvar is set. We need to register this cvar, so open up the game.cpp file. Find this line:

Код:
   cvar_t   timeleft   = {"mp_timeleft","0" , FCVAR_SERVER | FCVAR_UNLOGGED };     // "      "
And append this code after it:

Код:
   cvar_t  allow_spectators = { "allow_spectators", "1.0", FCVAR_SERVER };      // 0 prevents players from being spectators
Then find this line below:

Код:
   CVAR_REGISTER (&timeleft);
And append this code after it:

Код:
   CVAR_REGISTER( &allow_spectators );
This will allow server operators to set allow_spectators to 0 and prevent anyone from spectating, should they desire it. If you have client side menus that allow players to click on buttons to become spectators, it's probably a good idea to send down the state of allow_spectators to the client, so the client can remove the button if the server says no spectators. However, this is not covered in this tutorial. Now open up the game.h file and search for this line:

Код:
   extern cvar_t   defaultteam;
And append this line after it:

Код:
   extern cvar_t   allow_spectators;
Now, we need to tell the Game DLL to send the iuser1/2 variables down the client. The SDK 2.0 networking is extremely efficient at not sending things it doesn't need to, and so we need to let it know we want these variables sent. Open up the client.cpp file again and search for this function:

Код:
   void UpdateClientData ( const struct edict_s *ent, int sendweapons, struct clientdata_s *cd )
Find this line:

Код:
   cd->pushmsec      = ent->v.pushmsec;
And append these lines after it:

Код:
   // Spectator
   cd->iuser1         = ent->v.iuser1;
   cd->iuser2         = ent->v.iuser2;
While we're here, we also need to override the PVS of the spectator so they receive the same entities as the player they're tracking. Find this function:

Код:
   void SetupVisibility( edict_t *pViewEntity, edict_t *pClient, unsigned char **pvs, unsigned char **pas )
In it, find this code:

Код:
   if ( pViewEntity )
   {
      pView = pViewEntity;
   }
And append this code after it:

Код:
   // Tracking Spectators use the visibility of their target
   CBasePlayer *pPlayer = (CBasePlayer *)CBaseEntity::Instance( pClient );
   if ( (pPlayer->pev->iuser2 != 0) && (pPlayer->m_hObserverTarget != NULL) )
   {
      pView = pPlayer->m_hObserverTarget->edict();
   }
Just a couple more things. First, to let every client know the spectator state of other players in the game so they can show them as Spectators in the scoreboard. Open up the multiplay_gamerules.cpp file, and find this line:

Код:
   extern int gmsgMOTD;
And append the following line after it:

Код:
   extern int gmsgSpectator;
Then search below for this function:

Код:
   void CHalfLifeMultiplay :: InitHUD( CBasePlayer *pl )
In it, find the following code:

Код:
   MESSAGE_BEGIN( MSG_ONE, gmsgScoreInfo, NULL, pl->edict() );
      WRITE_BYTE( i );   // client number
      WRITE_SHORT( plr->pev->frags );
      WRITE_SHORT( plr->m_iDeaths );
   MESSAGE_END();      
And append this after it:

Код:
   // Send their spectator state
   MESSAGE_BEGIN( MSG_ONE, gmsgSpectator, NULL, pl->edict() ); 
      WRITE_BYTE( i );
      WRITE_BYTE( (plr->pev->iuser1 != 0) );
   MESSAGE_END();
While we're in this file, we need to add some code for spectators to handle disconnecting players. If a spectator is locked onto a player who disconnect, we want to move the spectator's lock onto another player. Search for this function:

Код:
   void CHalfLifeMultiplay :: ClientDisconnected( edict_t *pClient )
Find this line:

Код:
   pPlayer->RemoveAllItems( TRUE );// destroy all of the players weapons and items
And append this code after it:

Код:
   // Tell all clients this player isn't a spectator anymore
   MESSAGE_BEGIN( MSG_ALL, gmsgSpectator ); 
      WRITE_BYTE( ENTINDEX(pClient) );
      WRITE_BYTE( 0 );
   MESSAGE_END();

   CBasePlayer *client = NULL;
   while ( ((client = (CBasePlayer*)UTIL_FindEntityByClassname( client, "player" )) != NULL) && (!FNullEnt(client->edict())) )
   {
      if ( !client->pev )
         continue;
      if ( client == pPlayer )
         continue;

      // If a spectator was chasing this player, move him/her onto the next player
      if ( client->m_hObserverTarget == pPlayer )
      {
         int iMode = client->pev->iuser1;
         client->pev->iuser1 = 0;
         client->m_hObserverTarget = NULL;
         client->Observer_SetMode( iMode );
      }
   }
This tells all the clients in the game that the disconnecting player slot is no longer a spectator, and then searches through all the players and finds any spectators who are locked onto the disconnecting player. It then resets their spectator flags and tells the spectator code to set them back into the same mode. This will either move the spectator onto the next player correctly, or push the spectator into Roaming mode if there aren't any more players to lock onto.

Now we need to register the gmsgSpectator user message, so open up player.cpp again and search for this line:

Код:
   int gmsgGeigerRange = 0;
Append this line after it:

Код:
   int gmsgSpectator = 0;
Then search for this line:

Код:
   gmsgAmmoX = REG_USER_MSG("AmmoX", 2);
And append this line after it:

Код:
   gmsgSpectator = REG_USER_MSG( "Spectator", 2 );
You may have noticed that the code we pasted into observer.cpp at the start of the tutorial used the gmsgSpectator user message to send down the state of the spectator in StartObserver().

Finally, you'll need to figure out how players are going to leave spectator mode and get into the game. In TFC, players leave spectator mode by selecting a class, so the StopObserver() function is called in the changeclass function. Obviously, if your mod isn't class based you'll find the right place for this code in your own mod. StopObserver() simply tells all clients that this player is no longer a spectator, and clears out the iuser1/2 variables so the player movement code won't think s/he's a spectator anymore.

STEP 2: THE DELTA.LST FILE

Now that the Game DLL's finished, all you need to do is edit the delta.lst file in your game directory. If you don't have one, copy the one from the valve directory in. Then open it up in a text editor and find this line:

Код:
   DEFINE_DELTA( waterlevel, DT_INTEGER, 2, 1.0 )
And append these lines after it:

Код:
   DEFINE_DELTA( iuser1, DT_INTEGER, 2, 1.0 ),            // Non-zero if I'm a spectator (1,2,3 different spec modes)
   DEFINE_DELTA( iuser2, DT_INTEGER, 5, 1.0 )            // Holds the ent num of the target I'm spectating

STEP 3: THE SHARED PLAYER MOVEMENT CODE

In your SDK there should be a directory called pm_shared. This directory contains the player movement code. It's shared between the client.dll and the game dll so that each of them can predict the player movement. Whenever you make changes to these files, you'll need to rebuild both the client DLL and the game DLL, or player movement will be incorrectly predicted. Now, open up the pm_shared.c file. Find this line:

Код:
   static int pm_shared_initialized = 0;
And append this code after it:

Код:
   #ifdef CLIENT_DLL
      // Spectator Mode
      float   vecNewViewAngles[3];
      float   vecNewViewOrigin[3];
      int      iHasNewViewAngles;
      int      iHasNewViewOrigin;
      int      iIsSpectator;
   #endif
And that's it. You should be able to run your mod, type spectate at the console, and fly around in spectator mode. You'll want to edit the client.dll to change what happens when players hit their ENTER key in spectate mode. Right now it'll try and bring up the TFC class menu and let players change class, which isn't what you'll want for your mod. The easiest thing to do is probably to make it immediately send a "stopspectate" command to the server and have your game dll take it from there.

If you want to change the way the spectator modes work, the observer.cpp file contains most of the logic. If you want to remove Roaming mode, or prevent players from spectating enemy team members, that's the place to do it.


Вернуться к началу
 Профиль 
  
Показать сообщения за:  Поле сортировки  
Начать новую тему Ответить на тему  [ 1 сообщение ] 

Часовой пояс: UTC + 5 часов [ Летнее время ]


Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 2


Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете добавлять вложения

Найти:
Перейти:  
Создано на основе phpBB® Forum Software © phpBB Group
Русская поддержка phpBB