六、多人游戏的服务端创建过程代码解析
最早的操作系统是黑洞洞的窗口叫命令行,后来出现了美观的图形界面,其实图形界面出现后命令行并没有消失,只是图形界面将用户的鼠标操作转成命令替用户发送到了命令行,干活的还是命令行。这样即使小白用户也能操作计算机了,大大提高了计算机的普及率。在起源游戏引擎里也有命令行和图形界面,在游戏里开启控制台后,控制台即是命令行。大名鼎鼎的vgui2即是图形界面。如下是创建游戏的界面:

在创建游戏的界面点击开始后其实是向控制台发送了一条命令:map xxx,xxx是游戏地图名字。例如直接在控制台发送 map dust2 命令也能创建游戏服务器。游戏控制台支持的命令是用ConCommand这个类提前注册的,ConCommand类接收一个字符串,一个函数,注释字符串,命令标识和一个命令自动补全函数。如下图是其构造函数:
void ConCommandBase::CreateBase( const char *pName, const char *pHelpString /*= 0*/, int flags /*= 0*/ )
{
m_bRegistered = false;
// Name should be static data
Assert( pName );
m_pszName = pName;
m_pszHelpString = pHelpString ? pHelpString : "";
m_nFlags = flags;
#ifdef ALLOW_DEVELOPMENT_CVARS
m_nFlags &= ~FCVAR_DEVELOPMENTONLY;
#endif
if ( !( m_nFlags & FCVAR_UNREGISTERED ) )
{
m_pNext = s_pConCommandBases;
s_pConCommandBases = this;
}
else
{
// It's unregistered
m_pNext = NULL;
}
ConCommand内部将所有注册的命令串成一条链,这样当在控制台输入命令后,就可以在链上寻找匹配的命令,如果找到了就用注册的函数来处理命令,如果找不到输入的命令就会提示无法识别。map命令即是由如下的第二行的ConCommand注册的,在输入map xxx命令后会调用Host_Map_f函数解析命令。
static ConCommand maps("maps", Host_Maps_f, "Displays list of maps." );
static ConCommand map("map", Host_Map_f, "Start playing on specified map.", FCVAR_DONTRECORD, Host_Map_f_CompletionFunc );
static ConCommand map_background("map_background", Host_Map_Background_f, "Runs a map as the background to the main menu.", FCVAR_DONTRECORD, Host_Map_f_CompletionFunc );
static ConCommand map_commentary("map_commentary", Host_Map_Commentary_f, "Start playing, with commentary, on a specified map.", FCVAR_DONTRECORD, Host_Map_Commentary_f_CompletionFunc );
static ConCommand changelevel("changelevel", Host_Changelevel_f, "Change server to the specified map", FCVAR_DONTRECORD, Host_Changelevel_f_CompletionFunc );
static ConCommand changelevel2("changelevel2", Host_Changelevel2_f, "Transition to the specified map in single player", FCVAR_DONTRECORD, Host_Changelevel2_f_CompletionFunc );
void Host_Map_f( const CCommand &args )
{
Host_Map_Helper( args, false, false, false );
}
void Host_Map_Helper( const CCommand &args, bool bEditmode, bool bBackground, bool bCommentary );
Host_Map_f方法调用了一个公共的方法Host_Map_Helper,参数bBackground的作用是标识是否是游戏界面背景地图,半条命2游戏打开后,菜单背景是一张地图,其实是创建了一个特殊的游戏模式,当作菜单背景。
void Host_Map_Helper( const CCommand &args, bool bEditmode, bool bBackground, bool bCommentary )
{
if ( cmd_source != src_command )
return;
if (args.ArgC() < 2)
{
Warning("No map specified\n");
return;
}
const char *pszReason = NULL;
if ( ( g_iServerGameDLLVersion >= 10 ) && !serverGameDLL->IsManualMapChangeOkay( &pszReason ) )
{
if ( pszReason && pszReason[0] )
{
Warning( "%s\n", pszReason );
}
return;
}
char szMapName[ MAX_QPATH ] = { 0 };
V_strncpy( szMapName, args[ 1 ], sizeof( szMapName ) );
// Call find map, proceed for any value besides NotFound
IVEngineServer::eFindMapResult eResult = g_pVEngineServer->FindMap( szMapName, sizeof( szMapName ) );
if ( eResult == IVEngineServer::eFindMap_NotFound )
{
Warning( "map load failed: %s not found or invalid\n", args[ 1 ] );
return;
}
在这儿先判断创建游戏命令是否完整,map后面如果不带地图名字是无法执行的。然后调用服务端引擎方法判断地图是否存在。判断没有问题继续执行
Host_Disconnect( false ); // stop old game
HostState_NewGame( szMapName, false, bBackground );
if (args.ArgC() == 10)
{
if (Q_stricmp(args[2], "setpos") == 0
&& Q_stricmp(args[6], "setang") == 0)
{
Vector newpos;
newpos.x = atof( args[3] );
newpos.y = atof( args[4] );
newpos.z = atof( args[5] );
QAngle newangle;
newangle.x = atof( args[7] );
newangle.y = atof( args[8] );
newangle.z = atof( args[9] );
HostState_SetSpawnPoint(newpos, newangle);
}
}
}
停止可能正在运行的游戏,然后开始新游戏
void HostState_NewGame( char const *pMapName, bool remember_location, bool background )
{
Q_strncpy( g_HostState.m_levelName, pMapName, sizeof( g_HostState.m_levelName ) );
g_HostState.m_landmarkName[0] = 0;
g_HostState.m_bRememberLocation = remember_location;
g_HostState.m_bWaitingForConnection = true;
g_HostState.m_bBackgroundLevel = background;
if ( remember_location )
{
g_HostState.RememberLocation();
}
g_HostState.SetNextState( HS_NEW_GAME );
}
设置好新游戏的参数值,比如地图名称,然后状态机切换到HS_NEW_GAME
void CHostState::State_NewGame()
{
CETWScope timer( "CHostState::State_NewGame" );
if ( Host_ValidGame() )
{
// Demand load game .dll if running with -nogamedll flag, etc.
if ( !serverGameClients )
{
SV_InitGameDLL();
}
if ( !serverGameClients )
{
Warning( "Can't start game, no valid server.dll loaded\n" );
}
else
{
if ( Host_NewGame( m_levelName, false, m_bBackgroundLevel ) )
{
// succesfully started the new game
SetState( HS_RUN, true );
return;
}
}
}
经过几轮状态切换,最终运行到State_NewGame方法,然后调用Host_NewGame方法
bool Host_NewGame( char *mapName, bool loadGame, bool bBackgroundLevel, const char *pszOldMap, const char *pszLandmark, bool bOldSave )
{
VPROF( "Host_NewGame" );
COM_TimestampedLog( "Host_NewGame" );
char previousMapName[MAX_PATH] = { 0 };
Q_strncpy( previousMapName, host_map.GetString(), sizeof( previousMapName ) );
#ifndef SWDS
SCR_BeginLoadingPlaque();
#endif
// The qualified name of the map, excluding path/extension
char szMapName[MAX_PATH] = { 0 };
// The file to load the map from.
char szMapFile[MAX_PATH] = { 0 };
Q_strncpy( szMapName, mapName, sizeof( szMapName ) );
Host_DefaultMapFileName( szMapName, szMapFile, sizeof( szMapFile ) );
// Steam may not have been started yet, ensure it is available to the game DLL before we ask it to prepare level
// resources
SV_InitGameServerSteam();
// Ask serverDLL to prepare this load
if ( g_iServerGameDLLVersion >= 10 )
{
serverGameDLL->PrepareLevelResources( szMapName, sizeof( szMapName ), szMapFile, sizeof( szMapFile ) );
}
if ( !modelloader->Map_IsValid( szMapFile ) )
{
#ifndef SWDS
SCR_EndLoadingPlaque();
#endif
return false;
}
DevMsg( "---- Host_NewGame ----\n" );
host_map.SetValue( szMapName );
CheckForFlushMemory( previousMapName, szMapName );
if (MapReslistGenerator().IsEnabled())
{
// uncache all the materials, so their files get referenced again for the reslists
// undone for now, since we're just trying to get a global reslist, not per-map accurate
// materials->UncacheAllMaterials();
MapReslistGenerator().OnLevelLoadStart(szMapName);
// cache 'em back in!
// materials->CacheUsedMaterials();
}
DownloadListGenerator().OnLevelLoadStart(szMapName);
if ( !loadGame )
{
VPROF( "Host_NewGame_HostState_RunGameInit" );
HostState_RunGameInit();
}
// init network mode
VPROF_SCOPE_BEGIN( "Host_NewGame_SpawnServer" );
NET_SetMutiplayer( sv.IsMultiplayer() );
NET_ListenSocket( sv.m_Socket, true ); // activated server TCP socket
// let's not have any servers with no name
if ( host_name.GetString()[0] == 0 )
{
host_name.SetValue( serverGameDLL->GetGameDescription() );
}
if ( !sv.SpawnServer ( szMapName, szMapFile, NULL ) )
{
return false;
}
sv.m_bIsLevelMainMenuBackground = bBackgroundLevel;
VPROF_SCOPE_END();
// make sure the time is set
g_ServerGlobalVariables.curtime = sv.GetTime();
COM_TimestampedLog( "serverGameDLL->LevelInit" );
#ifndef SWDS
EngineVGui()->UpdateProgressBar(PROGRESS_LEVELINIT);
audiosourcecache->LevelInit( szMapName );
#endif
g_pServerPluginHandler->LevelInit( szMapName, CM_EntityString(), pszOldMap, pszLandmark, loadGame && !bOldSave, bBackgroundLevel );
if ( loadGame && !bOldSave )
{
sv.SetPaused( true ); // pause until all clients connect
sv.m_bLoadgame = true;
g_ServerGlobalVariables.curtime = sv.GetTime();
}
if( !SV_ActivateServer() )
{
return false;
}
// Connect the local client when a "map" command is issued.
if ( !sv.IsDedicated() )
{
COM_TimestampedLog( "Stuff 'connect localhost' to console" );
char str[512];
Q_snprintf( str, sizeof( str ), "connect localhost:%d listenserver", sv.GetUDPPort() );
Cbuf_AddText( str );
}
else
{
// Dedicated server triggers map load here.
GetTestScriptMgr()->CheckPoint( "FinishedMapLoad" );
}
#ifndef SWDS
if ( !loadGame || bOldSave )
{
// clear the most recent remember save, so the level will just restart if the player dies
saverestore->ForgetRecentSave();
}
saverestore->SetMostRecentElapsedMinutes( 0 );
saverestore->SetMostRecentElapsedSeconds( 0 );
#endif
if (MapReslistGenerator().IsEnabled())
{
MapReslistGenerator().OnLevelLoadEnd();
}
DownloadListGenerator().OnLevelLoadEnd();
return true;
}
Host_DefaultMapFileName方法是补全地图名称,HostState_RunGameInit是给游戏逻辑代码一个初始化游戏的机会,NET_SetMutiplayer是设置是否多人游戏,许多逻辑因为是否多人游戏而不同,NET_ListenSocket是开始监听端口,从而可以接受客户端连接,sv.SpawnServer ( szMapName, szMapFile, NULL )是创建游戏服务器多个组件的封装,稍后解析,g_pServerPluginHandler->LevelInit是在服务器创建好后初始化游戏关卡,调用mod编写的初始化游戏代码,SV_ActivateServer是激活服务器。
我们创建服务器后为什么本地会自动连上服务器呢,是
if ( !sv.IsDedicated() )
{
COM_TimestampedLog( "Stuff 'connect localhost' to console" );
char str[512];
Q_snprintf( str, sizeof( str ), "connect localhost:%d listenserver", sv.GetUDPPort() );
Cbuf_AddText( str );
}
这几行代码的作用。如果开启本地游戏服务器,代码里自动帮用户执行了连接服务器的命令。在游戏服务器创建好后,游戏状态机又会切换成运行状态,等待客户端连接。下面看下SpawnServer的代码
deathmatch.SetValue( IsMultiplayer() ? 1 : 0 );
if ( coop.GetInt() )
{
deathmatch.SetValue( 0 );
}
current_skill = (int)(skill.GetFloat() + 0.5);
current_skill = max( current_skill, 0 );
current_skill = min( current_skill, 3 );
skill.SetValue( (float)current_skill );
COM_TimestampedLog( "StaticPropMgr()->LevelShutdown()" );
#if !defined( SWDS )
g_pShadowMgr->LevelShutdown();
#endif // SWDS
StaticPropMgr()->LevelShutdown();
// if we have an hltv relay proxy running, stop it now
if ( hltv && !hltv->IsMasterProxy() )
{
hltv->Shutdown();
}
// NOTE: Replay system does not deal with relay proxies.
COM_TimestampedLog( "Host_FreeToLowMark" );
Host_FreeStateAndWorld( true );
Host_FreeToLowMark( true );
开始是许多的shutdown清理过程,是为了防止上一个游戏的数据还没有清理,继续往后看
// Preload any necessary data from the xzps:
g_pFileSystem->SetupPreloadData();
g_pMDLCache->InitPreloadData( false );
// Allocate server memory
max_edicts = MAX_EDICTS;
g_ServerGlobalVariables.maxEntities = max_edicts;
g_ServerGlobalVariables.maxClients = GetMaxClients();
#ifndef SWDS
g_ClientGlobalVariables.network_protocol = PROTOCOL_VERSION;
#endif
// Assume no entities beyond world and client slots
num_edicts = GetMaxClients()+1;
COM_TimestampedLog( "SV_AllocateEdicts" );
SV_AllocateEdicts();
serverGameEnts->SetDebugEdictBase( edicts );
allowsignonwrites = true;
serverclasses = 0; // number of unique server classes
serverclassbits = 0; // log2 of serverclasses
该部分为游戏实体创建数据结构,
// allocate player data, and assign the values into the edicts
for ( i=0 ; i< GetClientCount() ; i++ )
{
CGameClient * pClient = Client(i);
// edict for a player is slot + 1, world = 0
pClient->edict = edicts + i + 1;
// Setup up the edict
InitializeEntityDLLFields( pClient->edict );
}
COM_TimestampedLog( "Set up players(done)" );
m_State = ss_loading;
// Set initial time values.
m_flTickInterval = host_state.interval_per_tick;
m_nTickCount = (int)( 1.0 / host_state.interval_per_tick ) + 1; // Start at appropriate 1
g_ServerGlobalVariables.tickcount = m_nTickCount;
g_ServerGlobalVariables.curtime = GetTime();
设置好最大玩家数后初始化玩家数据结构,初始化游戏状态
// Load the world model.
g_pFileSystem->AddSearchPath( szMapFile, "GAME", PATH_ADD_TO_HEAD );
g_pFileSystem->BeginMapAccess();
if ( !CommandLine()->FindParm( "-allowstalezip" ) )
{
if ( g_pFileSystem->FileExists( "stale.txt", "GAME" ) )
{
Warning( "This map is not final!! Needs to be rebuilt without -keepstalezip and without -onlyents\n" );
}
}
COM_TimestampedLog( "modelloader->GetModelForName(%s) -- Start", szMapFile );
host_state.SetWorldModel( modelloader->GetModelForName( szMapFile, IModelLoader::FMODELLOADER_SERVER ) );
if ( !host_state.worldmodel )
{
ConMsg( "Couldn't spawn server %s\n", szMapFile );
m_State = ss_dead;
g_pFileSystem->EndMapAccess();
return false;
}
COM_TimestampedLog( "modelloader->GetModelForName(%s) -- Finished", szMapFile );
然后加载游戏地图
// Create network string tables ( including precache tables )
SV_CreateNetworkStringTables();
// Leave empty slots for models/sounds/generic (not for decals though)
PrecacheModel( "", 0 );
PrecacheGeneric( "", 0 );
PrecacheSound( "", 0 );
COM_TimestampedLog( "Precache world model (%s)", szMapFile );
#ifndef SWDS
EngineVGui()->UpdateProgressBar(PROGRESS_PRECACHEWORLD);
#endif
// Add in world
PrecacheModel( szMapFile, RES_FATALIFMISSING | RES_PRELOAD, host_state.worldmodel );
COM_TimestampedLog( "Precache brush models" );
// Add world submodels to the model cache
for ( i = 1 ; i < host_state.worldbrush->numsubmodels ; i++ )
{
// Add in world brush models
char localmodel[5]; // inline model names "*1", "*2" etc
Q_snprintf( localmodel, sizeof( localmodel ), "*%i", i );
PrecacheModel( localmodel, RES_FATALIFMISSING | RES_PRELOAD, modelloader->GetModelForName( localmodel, IModelLoader::FMODELLOADER_SERVER ) );
}
之后创建服务器端字符串注册表,然后开始预先缓存模型、声音等,预缓存这些资源的作用是给每个资源编一个id,然后给客户端发送id和字符串的对应关系,这样之后服务器向客户端发送消息就只需要发送id即可,不需要发送长串的字符串,节省网络带宽。
SV_ClearWorld();
//
// load the rest of the entities
//
COM_TimestampedLog( "InitializeEntityDLLFields" );
InitializeEntityDLLFields( edicts );
// Clear the free bit on the world edict (entindex: 0).
ED_ClearFreeFlag( &edicts[0] );
if (coop.GetFloat())
{
g_ServerGlobalVariables.coop = (coop.GetInt() != 0);
}
else
{
g_ServerGlobalVariables.deathmatch = (deathmatch.GetInt() != 0);
}
g_ServerGlobalVariables.mapname = MAKE_STRING( m_szMapname );
g_ServerGlobalVariables.startspot = MAKE_STRING( m_szStartspot );
GetTestScriptMgr()->CheckPoint( "map_load" );
// set game event
IGameEvent *event = g_GameEventManager.CreateEvent( "server_spawn" );
然后做了一些初始化和清理数据结构,发送服务器创建的事件。
void CBaseServer::RunFrame( void )
{
VPROF_BUDGET( "CBaseServer::RunFrame", VPROF_BUDGETGROUP_OTHER_NETWORKING );
tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "CBaseServer::RunFrame" );
NET_ProcessSocket( m_Socket, this );
服务器创建完成后会在RunFrame方法里调用NET_ProcessSocket处理网络连接请求
while ( ( packet = NET_GetPacket ( sock, scratch->data ) ) != NULL )
{
if ( Filter_ShouldDiscard ( packet->from ) ) // filtering is done by network layer
{
Filter_SendBan( packet->from ); // tell them we aren't listening...
continue;
}
// check for connectionless packet (0xffffffff) first
if ( LittleLong( *(unsigned int *)packet->data ) == CONNECTIONLESS_HEADER )
{
packet->message.ReadLong(); // read the -1
if ( net_showudp.GetInt() )
{
Msg("UDP <- %s: sz=%i OOB '%c' wire=%i\n", packet->from.ToString(), packet->size, packet->data[4], packet->wiresize );
}
handler->ProcessConnectionlessPacket( packet );
continue;
}
// check for packets from connected clients
CNetChan * netchan = NET_FindNetChannel( sock, packet->from );
if ( netchan )
{
netchan->ProcessPacket( packet, true );
}
在循环里拿到传入的数据包,然后根据数据包开头的标识是否是CONNECTIONLESS_HEADER来分别处理,Connectionless翻译过来是无连接的,意思是还没有处理完成的的传入的客户端连接请求。服务端收到客户端连接请求后会先进行校验,然后符合一定的条件允许连接,校验期间只会有几种固定的数据包类型,校验通过后就会在服务端创建一个新的client数据结构然后和传入的端口绑定。之后客户端发送的数据包不再有这个标识,数据类型也会多种多样,服务器端用另一种方式处理数据包。
以上就是服务端创建游戏的整个过程。
全部评论