五、CEngine类代码解析

460人浏览 / 0人评论

CEngine的Load方法内容如下:

bool CEngine::Load( bool bDedicated, const char *rootdir )
{
	bool success = false;

	// Activate engine
	// NOTE: We must bypass the 'next state' block here for initialization to work properly.
	m_nDLLState = m_nNextDLLState = InEditMode() ? DLL_PAUSED : DLL_ACTIVE;

	if ( Sys_InitGame( 
		g_AppSystemFactory,
		rootdir, 
		game->GetMainWindowAddress(), 
		bDedicated ) )
	{
		success = true;

		UpdateMaterialSystemConfig();
	}
	
	return success;
}

在Sys_InitGame方法里做了许多初始化动作,

int Sys_InitGame( CreateInterfaceFn appSystemFactory, const char* pBaseDir, void *pwnd, int bIsDedicated )
{
#ifdef BENCHMARK
	if ( bIsDedicated )
	{
		Error( "Dedicated server isn't supported by this benchmark!" );
	}
#endif

	extern void InitMathlib( void );
	InitMathlib();
	
	FileSystem_SetWhitelistSpewFlags();

	// Activate console spew
	// Must happen before developer.InstallChangeCallback because that callback may reset it 
	SpewActivate( "console", 1 );

	// Install debug spew output....
	developer.InstallChangeCallback( DeveloperChangeCallback );

	SpewOutputFunc( Sys_SpewFunc );
	...
	TRACEINIT( Sys_InitMemory(), Sys_ShutdownMemory() );

	TRACEINIT( Host_Init( s_bIsDedicated ), Host_Shutdown() );

	if ( !host_initialized )
	{
		return 0;
	}

	TRACEINIT( Sys_InitAuthentication(), Sys_ShutdownAuthentication() );

	MapReslistGenerator_BuildMapList();

	BuildMinidumpComment( NULL, false );
	return 1;
}

注意这个Host_Init方法,它里面初始化了许多我们要学习的模块

	TRACEINIT( Memory_Init(), Memory_Shutdown() );

	TRACEINIT( Con_Init(), Con_Shutdown() );

	TRACEINIT( Cbuf_Init(), Cbuf_Shutdown() );

	TRACEINIT( Cmd_Init(), Cmd_Shutdown() );	

	TRACEINIT( g_pCVar->Init(), g_pCVar->Shutdown() ); // So we can list cvars with "cvarlst"

#ifndef SWDS
	TRACEINIT( V_Init(), V_Shutdown() );
#endif

	TRACEINIT( COM_Init(), COM_Shutdown() );


#ifndef SWDS
	TRACEINIT( saverestore->Init(), saverestore->Shutdown() );
#endif

	TRACEINIT( Filter_Init(), Filter_Shutdown() );

#ifndef SWDS
	TRACEINIT( Key_Init(), Key_Shutdown() );
#endif
	...
	if ( !bDedicated )
	{
		TRACEINIT( CL_Init(), CL_Shutdown() );

		// NOTE: This depends on the mod search path being set up
		TRACEINIT( InitMaterialSystem(), ShutdownMaterialSystem() );

		TRACEINIT( modelloader->Init(), modelloader->Shutdown() );

		TRACEINIT( StaticPropMgr()->Init(), StaticPropMgr()->Shutdown() );

		TRACEINIT( InitStudioRender(), ShutdownStudioRender() );

		//startup vgui
		TRACEINIT( EngineVGui()->Init(), EngineVGui()->Shutdown() );

		TRACEINIT( TextMessageInit(), TextMessageShutdown() );

		TRACEINIT( ClientDLL_Init(), ClientDLL_Shutdown() );

		TRACEINIT( SCR_Init(), SCR_Shutdown() );

		TRACEINIT( R_Init(), R_Shutdown() ); 

		TRACEINIT( Decal_Init(), Decal_Shutdown() );

		// hookup interfaces
		EngineVGui()->Connect();
	}

CEngine的Unload方法内容如下:

void CEngine::Unload( void )
{
	Sys_ShutdownGame();

	m_nDLLState			= DLL_INACTIVE;
	m_nNextDLLState		= DLL_INACTIVE;
}

这个Sys_ShutdownGame方法里进行清理资源的操作

void Sys_ShutdownGame( void )
{
	TRACESHUTDOWN( Sys_ShutdownAuthentication() );

	TRACESHUTDOWN( Host_Shutdown() );

	TRACESHUTDOWN( Sys_ShutdownMemory() );

	// TRACESHUTDOWN( Sys_ShutdownArgv() );

	TRACESHUTDOWN( Sys_Shutdown() );

	// Remove debug spew output....
	developer.InstallChangeCallback( 0 );
	SpewOutputFunc( 0 );
}

这个Host_Shutdown就是Host_Init的反向操作,,也是需要学习的

	if ( !sv.IsDedicated() )
	{
		TRACESHUTDOWN( Decal_Shutdown() );

		TRACESHUTDOWN( R_Shutdown() );

		TRACESHUTDOWN( SCR_Shutdown() );

		TRACESHUTDOWN( S_Shutdown() );

		TRACESHUTDOWN( ClientDLL_Shutdown() );

		TRACESHUTDOWN( TextMessageShutdown() );

		TRACESHUTDOWN( EngineVGui()->Shutdown() );

		TRACESHUTDOWN( StaticPropMgr()->Shutdown() );

		// Model loader must shutdown before StudioRender
		// because it calls into StudioRender
		TRACESHUTDOWN( modelloader->Shutdown() );

		TRACESHUTDOWN( ShutdownStudioRender() );

		TRACESHUTDOWN( ShutdownMaterialSystem() );

		TRACESHUTDOWN( CL_Shutdown() );
	}

CEngine的Frame是处理游戏每一帧的方法

void CEngine::Frame( void )
{
	// yield the CPU for a little while when paused, minimized, or not the focus
	// FIXME:  Move this to main windows message pump?
	if ( IsPC() && !game->IsActiveApp() && !sv.IsDedicated() && engine_no_focus_sleep.GetInt() > 0 )
	{
		VPROF_BUDGET( "Sleep", VPROF_BUDGETGROUP_SLEEPING );
#if defined( RAD_TELEMETRY_ENABLED )
		if( !g_Telemetry.Level )
#endif
			g_pInputSystem->SleepUntilInput( engine_no_focus_sleep.GetInt() );
	}

	if ( m_flPreviousTime == 0 )
	{
		(void) FilterTime( 0.0f );
		m_flPreviousTime = Sys_FloatTime() - m_flMinFrameTime;
	}
	for (;;)
	{
		// Get current time
		m_flCurrentTime	= Sys_FloatTime();

		// Determine dt since we last ticked
		m_flFrameTime = m_flCurrentTime - m_flPreviousTime;

		// This should never happen...
		Assert( m_flFrameTime >= 0.0f );
		if ( m_flFrameTime < 0.0f )
		{
			// ... but if the clock ever went backwards due to a bug,
			// we'd have no idea how much time has elapsed, so just 
			// catch up to the next scheduled server tick.
			m_flFrameTime = host_nexttick;
		}

		if ( FilterTime( m_flFrameTime )  )
		{
			// Time to render our frame.
			break;
		}

这个for循环的作用是保证游戏每隔一定时间间隔处理一次帧,如果处理时间间隔还不到,线程就睡眠一会儿。如果不这样做cpu会跑满,浪费cpu资源。因为游戏并不需要cpu在每时每刻都处理,而是保证最小的时间间隔处理就可以。因为cpu速度太快了,在宏观上看起来仍然是连续的。代码继续往后看。

	switch( m_nDLLState )
	{
	case DLL_PAUSED:			// paused, in hammer
	case DLL_INACTIVE:			// no dll
		break;

	case DLL_ACTIVE:			// engine is focused
	case DLL_CLOSE:				// closing down dll
	case DLL_RESTART:			// engine is shutting down but will restart right away
		// Run the engine frame
		HostState_Frame( m_flFrameTime );
		break;
	}

注意这个HostState_Frame方法逐渐接近了游戏的核心,在这个方法里处理CHostState这个状态机的状态切换

	while ( true )
	{
		int oldState = m_currentState;

		// execute the current state (and transition to the next state if not in HS_RUN)
		switch( m_currentState )
		{
		case HS_NEW_GAME:
			g_pMDLCache->BeginMapLoad();
			State_NewGame();
			break;
		case HS_LOAD_GAME:
			g_pMDLCache->BeginMapLoad();
			State_LoadGame();
			break;
		case HS_CHANGE_LEVEL_MP:
			g_pMDLCache->BeginMapLoad();
			m_flShortFrameTime = 0.5f;
			State_ChangeLevelMP();
			break;
		case HS_CHANGE_LEVEL_SP:
			g_pMDLCache->BeginMapLoad();
			m_flShortFrameTime = 1.5f; // 1.5s of slower frames
			State_ChangeLevelSP();
			break;
		case HS_RUN:
			State_Run( time );
			break;
		case HS_GAME_SHUTDOWN:

这个状态机在新游戏、加载游戏、切换单人关卡、切换多人关卡、游戏运行、游戏结束、游戏重启几种状态间切换。其中的游戏正常运行状态State_Run占据了大部分游戏时间。接着调用到Host_RunFrame方法,在这里会根据参数决定多线程运行方式。接着调用_Host_RunFrame方法,这个方法根据多线程运行模式决定服务端功能和客户端功能的运行模式,如果是单线程模式,主线程会将服务端功能和客户端功能在同一线程先后运行。而如果是多线程模式则当前线程运行客户端功能,然后在新线程运行服务端功能。_Host_RunFrame方法详解如下

void _Host_RunFrame (float time)
{
	MDLCACHE_COARSE_LOCK_(g_pMDLCache);
	static double host_remainder = 0.0f;
	double prevremainder;
	bool shouldrender;

#if defined( RAD_TELEMETRY_ENABLED )
	if( g_Telemetry.DemoTickEnd == ( uint32 )-1 )
	{
		Cbuf_AddText( "quit\n" );
	}
#endif

	int numticks;

这个numticks是用来计算过去的时间间隔可以分成多少个tick,继续往后看

		Host_AccumulateTime ( time );
		_Host_SetGlobalTime();

		shouldrender = !sv.IsDedicated();

		// FIXME:  Could track remainder as fractional ticks instead of msec
		prevremainder = host_remainder;
		if ( prevremainder < 0 )
			prevremainder = 0;

	#if !defined(SWDS)
		if ( !demoplayer->IsPlaybackPaused() )
	#endif
		{
			host_remainder += host_frametime;
		}

		numticks = 0;	// how many ticks we will simulate this frame
		if ( host_remainder >= host_state.interval_per_tick )
		{
			numticks = (int)( floor( host_remainder / host_state.interval_per_tick ) );

			// round to nearest even ending tick in alternate ticks mode so the last
			// tick is always simulated prior to updating the network data
			// NOTE: alternate ticks only applies in SP!!!
			if ( Host_IsSinglePlayerGame() &&
				sv_alternateticks.GetBool() )
			{
				int startTick = g_ServerGlobalVariables.tickcount;
				int endTick = startTick + numticks;
				endTick = AlignValue( endTick, 2 );
				numticks = endTick - startTick;
			}

			host_remainder -= numticks * host_state.interval_per_tick;
		}

Host_AccumulateTime方法是将当前时间间隔累加到真实时间,将当前时间间隔设置为当前帧时间,按照一定逻辑缩放当前帧时间而不影响真实时间,host_remainder这个变量是上一帧处理tick剩余的时间,将会累加到当前帧时间内来计算tick,host_state.interval_per_tick是每一个tick占用的时长,numticks = (int)( floor( host_remainder / host_state.interval_per_tick ) )这行代码是用当前帧的时长除以每一个tick占用的时长,从而得到当前帧总共包含几个tick,host_remainder -= numticks * host_state.interval_per_tick这行代码是计算当前帧除以每一个tick占用时长后的余数,将在下个帧处理。Cbuf_Execute ();这个方法是执行缓存以前的控制台命令。得到总tick数后即可依次处理tick

			for ( int tick = 0; tick < numticks; tick++ )
			{ 
				// Emit an ETW event every simulation frame.
				ETWSimFrameMark( sv.IsDedicated() );

				double now = Plat_FloatTime();
				float jitter = now - host_idealtime;

				// Track jitter (delta between ideal time and actual tick execution time)
				host_jitterhistory[ host_jitterhistorypos ] = jitter;
				host_jitterhistorypos = ( host_jitterhistorypos + 1 ) % ARRAYSIZE(host_jitterhistory);

				// Very slowly decay "ideal" towards current wall clock unless delta is large
				if ( fabs( jitter ) > 1.0f )
				{
					host_idealtime = now;
				}
				else
				{
					host_idealtime = 0.99 * host_idealtime + 0.01 * now;
				}

				// process any asynchronous network traffic (TCP), set net_time
				NET_RunFrame( now );

				// Only send updates on final tick so we don't re-encode network data multiple times per frame unnecessarily
				bool bFinalTick = ( tick == (numticks - 1) );

NET_RunFrame是处理网络收发数据,bFinalTick标识是否是当前帧的最后一个tick,

				g_ServerGlobalVariables.tickcount = sv.m_nTickCount;
				// NOTE:  Do we want do this at start or end of this loop?
				++host_tickcount;
				++host_currentframetick;
#ifndef SWDS
				g_ClientGlobalVariables.tickcount = cl.GetClientTickCount();

				// Make sure state is correct
				CL_CheckClientState();
#endif
				//-------------------
				// input processing
				//-------------------
				_Host_RunFrame_Input( prevremainder, bFinalTick );
				prevremainder = 0;
				//-------------------
				//
				// server operations
				//
				//-------------------

				_Host_RunFrame_Server( bFinalTick );

				// Additional networking ops for SPLITPACKET stuff (99.9% of the time this will be an empty list of work)
				NET_SendQueuedPackets();

增加tick数,_Host_RunFrame_Input是处理输入设备信号,_Host_RunFrame_Server是运行服务端tick,_Host_RunFrame_Client是处理客户端tick,

		if ( shouldrender )
		{
#if LOG_FRAME_OUTPUT
			if ( !cl.IsPaused() || !sv.IsPaused() )
			{
				static float lastFrameTime = 0;
				float frametime = g_ClientGlobalVariables.curtime - lastFrameTime;
				Msg("RENDER AT: %6.4f: %.2fms [%.2fms implicit] frametime\n", 
					g_ClientGlobalVariables.curtime, g_ClientGlobalVariables.frametime*1000.0f, frametime * 1000.0f);
				lastFrameTime = g_ClientGlobalVariables.curtime;
			}
#endif
			//-------------------
			// rendering
			//-------------------
			_Host_RunFrame_Render();

			//-------------------
			// sound
			//-------------------
			_Host_RunFrame_Sound();

			if ( g_bVCRSingleStep )
			{
				VCR_EnterPausedState();
			}

_Host_RunFrame_Render是渲染客户端图形,_Host_RunFrame_Sound是处理客户端声音,ClientDLL_Update是更新客户端HUD,

至此,引擎主流程基本介绍完毕。一个有效的学习方法是把某一行代码注释,然后运行游戏,观察结果来确定代码作用,还有善用搜索,搜索某个代码的所有出现的地方,结合上下文猜测代码作用。如果只对游戏逻辑代码感兴趣,引擎部分的代码不需要细究,只要大概知道代码作用即可。

全部评论