为了管理时间,Unreal将游戏运行时间片分隔为"Ticks"。一个Tick是关卡中所有Actors更新的最小时间单位。一个tick一般是10ms-100ms(CPU性能越好,游戏逻辑越简单,tick的时间越短)
UE3使用的是游戏循环模型为:可变FPS决定游戏速度 详见:游戏主循环(Game Loop)
注1:能够tick的对象类型有 -- 从FTickableObject派生的Object、Actor、UActorComponent等
注2:当前Tick使用的DeltaTime为上一个Tick执行花费的逻辑时间
注3:DeltaTime=GDeltaTime*Info->TimeDilation(在UWorld::Tick中进行计算,见下面游戏循环示意代码)
可通过slomo 5命令将WorldInfo的TimeDilation变量设置成5,那么游戏将以5倍的速率运行逻辑
注4:TimeSeconds为游戏逻辑世界的运行时间,RealTimeSeconds为真实世界的运行时间,也在UWorld::Tick中进行计算
每次LoadMap加载地图关卡时,会在UWorld::BeginPlay中将TimeSeconds、RealTimeSeconds重置为0
注5:绝大部分函数是同步的,不会跨越tick来执行;UE3提供了"latent functions(延迟函数)",在State code中需要跨越多个tick才能完成
这种类似于协程的机制,非常适合开发者使用线性代码来完成需要异步的逻辑
注6:UE3中使用appSeconds()【通过调用QueryPerformanceFrequency/QueryPerformanceCounter实现】获取当前系统时间
CPU上也有一个计数器,以机器的clock为单位,可以通过rdtsc读取,而不用中断,精度为微妙级
常见的laten函数:
// Actor类 final latent function Sleep( float Seconds ); final latent function FinishAnim( AnimNodeSequence SeqNode ); // Controller类 final latent function MoveTo(vector NewDestination, optional Actor ViewFocus, optional float DestinationOffset, optional bool bShouldWalk = (Pawn != None) ? Pawn.bIsWalking : false); final latent function MoveToDirectNonPathPos(vector NewDestination, optional Actor ViewFocus, optional float DestinationOffset, optional bool bShouldWalk = (Pawn != None) ? Pawn.bIsWalking : false); final latent function MoveToward(Actor NewTarget, optional Actor ViewFocus, optional float DestinationOffset, optional bool bUseStrafing, optional bool bShouldWalk = (Pawn != None) ? Pawn.bIsWalking : false); final latent function FinishRotation(); final latent function WaitForLanding(optional float waitDuration); // UDKBot类 final latent function WaitToSeeEnemy(); final latent function LatentWhatToDoNext();
UE3游戏循环示意代码:
while( !GIsRequestingExit ) { EngineTick() { GEngineLoop.Tick(); { void FEngineLoop::Tick() { // 计算GDeltaTime,对循环时间进行控制 appUpdateTimeAndHandleMaxTickRate(); { static DOUBLE LastTime = appSeconds() - 0.0001; GLastTime = GCurrentTime; if( GIsBenchmarking || GUseFixedTimeStep ) // UDK.exe CAT2-City_20_Main.udk -benmark -dumpmovie { GDeltaTime = GFixedDeltaTime; LastTime = GCurrentTime; GCurrentTime += GDeltaTime; } else { GCurrentTime = appSeconds(); FLOAT DeltaTime = GCurrentTime - LastTime; const FLOAT MaxTickRate = GEngine->GetMaxTickRate( DeltaTime ); // 获取配置的限制帧数 FLOAT WaitTime = 0; // Convert from max FPS to wait time. if( MaxTickRate > 0 ) { WaitTime = Max( 1.f / MaxTickRate - DeltaTime, 0.f ); } if( WaitTime > 0 ) { // Give up timeslice for remainder of wait time. const DOUBLE WaitStartTime = GCurrentTime; while( GCurrentTime - WaitStartTime < WaitTime ) { GCurrentTime = appSeconds(); appSleep( 0 ); } } GDeltaTime = GCurrentTime - LastTime; LastTime = GCurrentTime; } } GEngine->Tick( GDeltaTime ); { Client->Tick( DeltaSeconds ); { void UWindowsClient::Tick( FLOAT DeltaTime ) { ProcessDeferredMessages(); //处理Windows延迟消息 // 更新所有的viewports for( … ) Viewports(ViewportIndex)->Tick(DeltaTime); //使用dxInput8读取玩家鼠标键盘输入缓冲区到UInput成员变量中, //通过Binds配置信息查找对应的响应函数并执行 ProcessInput( DeltaTime ); } } GWorld->Tick( LEVELTICK_All, DeltaSeconds ); { AWorldInfo* Info = GetWorldInfo(); InTick=1; // 是否在Tick过程中 // 网络复制(变量同步,远程函数调用) NetDriver->TickDispatch( DeltaSeconds ); // Update time. Info->RealTimeSeconds += DeltaSeconds; // Audio always plays at real-time regardless of time dilation, but only when NOT paused if( !IsPaused() ) { Info->AudioTimeSeconds += DeltaSeconds; } // apply time multipliers DeltaSeconds *= Info->TimeDilation; // Clamp time between 2000 fps and 2.5 fps. DeltaSeconds = Clamp(DeltaSeconds,0.0005f,0.40f); Info->DeltaSeconds = DeltaSeconds; if (!IsPaused()) { Info->TimeSeconds += DeltaSeconds; } // 执行Kismet逻辑 for( … ) CurrentLevel->GameSequences(SeqIdx)->UpdateOp( DeltaSeconds ); // Tick Actors(Actor.TickGroup = TG_PreAsyncWork)即:物理仿真前类型Actor TickGroup = TG_PreAsyncWork; // 这一步APlayerController::Tick会被执行,然后会调用Interactions(InteractionIndex)->Tick(DeltaSeconds) // 从而会调用UInput::Tick,当DeltaSeconds!=-1.0f时,该函数会处理按下ASDW持续移动逻辑;当DeltaSeconds==-1.0f时, // 该函数会首次将镜头参数绑定到AxisArray数组中,方便后续每帧清理镜头参数(将AxisArray 各元素置0即可) TickActors<FDeferredTickList::FGlobalActorIterator>(this,DeltaSeconds,TickType,GDeferredList); { World->NewlySpawned.Reset(); for (ITER It(DeferredList); It; ++It) { AActor* Actor = *It; Actor->Tick(DeltaSeconds*Actor->CustomTimeDilation,TickType); { UBOOL AActor::Tick( FLOAT DeltaSeconds, ELevelTick TickType ) { // 调用uc脚本中Tick方法 eventTick(DeltaSeconds); // 执行state code ProcessState( DeltaSeconds ); // 执行Timers UpdateTimers( DeltaSeconds ); } } // 更新Actor的组件(骨骼、粒子特效、光照等) TickActorComponents(Actor,DeltaSeconds,TickType,&DeferredList); } // If an actor was spawned during the async work, tick it in the post // async work, so that it doesn't try to interact with the async threads if (World->TickGroup == TG_DuringAsyncWork) { DeferNewlySpawned(World,DeferredList); } else { TickNewlySpawned(World,DeltaSeconds,TickType); } } TickAsyncWork(Info->bPlayersOnly == FALSE ? DeltaSeconds : 0.f); // 同步数据到物理仿真线程 // Tick Actors(Actor.TickGroup = TG_DuringAsyncWork)即:物理仿真过程中类型Actor TickGroup = TG_DuringAsyncWork; TickActors<FDeferredTickList::FActorDuringAsyncWorkIterator>(this,DeltaSeconds,TickType,GDeferredList); { ... ... } TickDeferredComponents<FDeferredTickList::FComponentDuringAsyncWorkIterator>(DeltaSeconds,GDeferredList); WaitForAsyncWork(); // 等待物理仿真线程结束 // Tick Actors(Actor.TickGroup = TG_PostAsyncWork)即:物理仿真后类型Actor TickGroup = TG_PostAsyncWork; DispatchRBCollisionNotifies(RBPhysScene); TickActors<FDeferredTickList::FActorPostAsyncWorkIterator>(this,DeltaSeconds,TickType,GDeferredList); { ... ... } TickDeferredComponents<FDeferredTickList::FComponentPostAsyncWorkIterator>(DeltaSeconds,GDeferredList); // Tick all objects inheriting from FTickableObjects. for( INT i=0; i<FTickableObject::TickableObjects.Num(); i++ ) { FTickableObject* TickableObject = FTickableObject::TickableObjects(i); if( TickableObject->IsTickable() ) { TickableObject->Tick(DeltaSeconds); } } // Update cameras last. This needs to be done before NetUpdates, and after all actors have been ticked. for( AController *C = this->GetFirstController(); C != NULL; C = C->NextController) { APlayerController* PC = C->GetAPlayerController(); // if it is a player, update the camra. if( PC && PC->PlayerCamera ) { PC->PlayerCamera->eventUpdateCamera(DeltaSeconds); } } InTick = 0; // Tick过程结束 // 执行垃圾回收(GC) if(…) PerformGarbageCollection(); // GC Mark Time else IncrementalPurgeGarbage( TRUE ); // GC Sweep Time } //按照UUTConsole、UGFxInteraction、UTGGameInteraction、UPlayerManagerInteraction顺序响应玩家输入 GameViewport->Tick(DeltaSeconds); // 渲染上屏 RedrawViewports(); // 播放声音 Client->GetAudioDevice()->Update( !GWorld->IsPaused() ); } // 处理Windows消息循环 appWinPumpMessages(); } } } }
UE3的UGameEngine::LoadMap示意代码:
UBOOL UGameEngine::LoadMap( const FURL& URL, UPendingLevel* Pending, FString& Error ) { // send a callback message GCallbackEvent->Send(CALLBACK_PreLoadMap); // 清理地图关卡 CleanupPackagesToFullyLoad(FULLYLOAD_Map, GWorld->PersistentLevel->GetOutermost()->GetName()); // cleanup the existing per-game pacakges // @todo: It should be possible to not unload/load packages if we are going from/to the same gametype. // would have to save the game pathname here and pass it in to SetGameInfo below CleanupPackagesToFullyLoad(FULLYLOAD_Game_PreLoadClass, TEXT("")); CleanupPackagesToFullyLoad(FULLYLOAD_Game_PostLoadClass, TEXT("")); CleanupPackagesToFullyLoad(FULLYLOAD_Mutator, TEXT("")); // 关闭网络连接,回收老的游戏世界GWorld if( GWorld ) { // close client connections { UNetDriver* NetDriver = GWorld->GetNetDriver(); if (NetDriver != NULL && NetDriver->ServerConnection == NULL) { for (INT i = NetDriver->ClientConnections.Num() - 1; i >= 0; i--) { if (NetDriver->ClientConnections(i)->Actor != NULL && NetDriver->ClientConnections(i)->Actor->Pawn != NULL) { GWorld->DestroyActor(NetDriver->ClientConnections(i)->Actor->Pawn, TRUE); } NetDriver->ClientConnections(i)->CleanUp(); } } } // Clean up game state. GWorld->SetNetDriver(NULL); GWorld->FlushLevelStreaming( NULL, TRUE ); GWorld->TermWorldRBPhys(); GWorld->CleanupWorld(); // send a message that all levels are going away (NULL means every sublevel is being removed // without a call to RemoveFromWorld for each) GCallbackEvent->Send(CALLBACK_LevelRemovedFromWorld, (UObject*)NULL); // Disassociate the players from their PlayerControllers. for(FLocalPlayerIterator It(this);It;++It) { if(It->Actor) { if(It->Actor->Pawn) { GWorld->DestroyActor(It->Actor->Pawn, TRUE); } GWorld->DestroyActor(It->Actor, TRUE); It->Actor = NULL; } } GWorld->RemoveFromRoot(); GWorld = NULL; } // Clean up the previous level out of memory. UObject::CollectGarbage( GARBAGE_COLLECTION_KEEPFLAGS, TRUE ); // 加载新的地图 UPackage* WorldPackage = LoadPackage(MapOuter, *URL.Map, LOAD_None); // 创建和初始化新的游戏世界GWorld GWorld = FindObjectChecked<UWorld>( WorldPackage, TEXT("TheWorld") ); GWorld->AddToRoot(); GWorld->Init(); // 将GPendingLevel的网络连接移交给新的GWorld if( Pending ) { check(Pending==GPendingLevel); // Hook network driver up to level. GWorld->SetNetDriver(Pending->NetDriver); if( GWorld->GetNetDriver() ) { GWorld->GetNetDriver()->Notify = GWorld; UPackage::NetObjectNotifies.AddItem(GWorld->GetNetDriver()); } // Setup level. GWorld->GetWorldInfo()->NetMode = NM_Client; } // 设置新的GWorld的GameInfo并初始化游戏 GWorld->SetGameInfo(URL); { AWorldInfo* Info = GetWorldInfo(); if( IsServer() && !Info->Game ) { Info->Game = (AGameInfo*)SpawnActor( GameClass ); } } // Initialize gameplay for the level. GWorld->BeginPlay(URL); { AWorldInfo* Info = GetWorldInfo(); // 重置TimeSeconds、RealTimeSeconds和AudioTimeSeconds为0 if (bResetTime) { GetWorldInfo()->TimeSeconds = 0.0f; GetWorldInfo()->RealTimeSeconds = 0.0f; GetWorldInfo()->AudioTimeSeconds = 0.0f; } // Init level gameplay info. if( !HasBegunPlay() ) { // Enable actor script calls. Info->bBegunPlay = 1; Info->bStartup = 1; // 调用GameInfo中的InitGame函数 if (Info->Game != NULL && !Info->Game->bScriptInitialized) { Info->Game->eventInitGame( Options, Error ); } } } // 创建LocalPlayer的本地Controller // 单机模式下使用该创建的Controller(GWorld->IsServer()为true) // 联网模式下Loading地图时使用该创建的Controller,但当玩家成功加入游戏后 // 客户端从服务器同步自动创建新的Controller(UActorChannel::ReceivedBunch函数中), // 然后调用UNetConnection::HandleClientPlayer销毁此处为LocalPlayer创建的Controller,并将LocalPlayer设置给新的Controller for(FLocalPlayerIterator It(this);It;++It) { It->SpawnPlayActor(URL.String(1),Error2); } // send a callback message GCallbackEvent->Send(CALLBACK_PostLoadMap); return TRUE; }
更多请参加: