六、多人游戏的服务端创建过程代码解析

496人浏览 / 0人评论

    最早的操作系统是黑洞洞的窗口叫命令行,后来出现了美观的图形界面,其实图形界面出现后命令行并没有消失,只是图形界面将用户的鼠标操作转成命令替用户发送到了命令行,干活的还是命令行。这样即使小白用户也能操作计算机了,大大提高了计算机的普及率。在起源游戏引擎里也有命令行和图形界面,在游戏里开启控制台后,控制台即是命令行。大名鼎鼎的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数据结构然后和传入的端口绑定。之后客户端发送的数据包不再有这个标识,数据类型也会多种多样,服务器端用另一种方式处理数据包。

以上就是服务端创建游戏的整个过程。

全部评论