I'm a doctor, not a mechanic
What happens at map startup
The information in this article are based on a snippet of native UT2004 code posted by Steve Polge on Epic Game's ut2003mods mailing list. The code can be found as UnGame.cpp in the Engine\Src directory of the UT2004 UnrealScript source code download.
This article only describes the map startup process. It applies to offline games, network servers and clients, including UTV clients (but not the UTV proxy server) and demo playback. Note that this article does not apply to loading saved game states in engine builds that support this. Those "simply continue" from where the game was saved. In UT2004 the PostLoadSavedGame() event is called for all existing actors and all ragdolls are destroyed because their state cannot be stored in save game files.
The seamless travel feature of Unreal Engine 3 seems to behave similarly to a regular map startup, except that the new map is loaded in the background. However, additional GameInfo events, such as GetSeamlessTravelActorList(), PostSeamlessTravel() and HandleSeamlessTravelPlayer() are called at some point during the map change process. (This needs further research.)
Summary of events[edit]
Before any of the following events happen, the engine loads the map and any packages it depends on.
Next it spawns the GameInfo, unless loading the map for a saved game or in a client environment. The engine always tries to load the GameInfo class from the ?game=
URL option. If that fails, a default game type for the map is determined, either using the LevelInfo's DefaultGameType property (UE1 and UE2) or the map prefix (UE3). If that fails as well, a default game type as specified in the main INI file is used. The final fallback option is the GameInfo class itself.
Additionally, if running a listen or dedicated server, any actors mentioned in the ServerActors list of INI section [Engine.GameEngine] are created. Non-standard property values for these actors may be specified in the form propertyname=value
on the ServerActors line, separated form the actor class and from each other by spaces.
Note that at this point there are no player actors in the level yet. Offline and on a listen server, the local player joins right after map startup completed. In network client environments (including UTV and demo playback), the local player actor is received through replication soon after map startup has completed.
During the events described here, the LevelInfo or WorldInfo's bStartup property is set to True
.
Client preparations[edit]
One of the first things the engine does in client environments is deleting all actors that have neither bStatic nor bNoDelete set to True
. (UT2004 also treats xEmitters as undeletable in this context.)
Any remaining actors have their Role and RemoteRole swapped as if they were received through replication. The only exception are actors with bClientAuthoritative set to True
, which only really applies to ClientMovers.
General preparations[edit]
All actors start out touching nothing and using the level's DefaultPhysicsVolume as their current PhysicsVolume. Native code also initializes state support and Karma physics here.
Only at this point UnrealScript support is enabled for the actors in this level by setting the LevelInfo's bBegunPlay to True
. Any actors spawned before this point did not get the UnrealScript events during what happens when an Actor is spawned and any actors destroyed did not receive the events during what happens when an Actor is destroyed.
GameInfo.InitGame()[edit]
Offline and on network servers the GameInfo's InitGame() event is called now. UT2004 also calls SetGrammar() afterwards, which initializes speech recognition grammars.
InitGame() performs general and gametype-specific initialization. It parses the URL options and among other things also spawns the AccessControl, Mutators and similar actors. Those actors are initialized according to what happens when an Actor is spawned. Starting with UE2 these actors exclude themselves from receiving the UnrealScript events mentioned below by setting their bScriptInitialized property to True
, usually in their SetInitialState() event.
PreBeginPlay()[edit]
Next the PreBeginPlay() function is called for all actors with bScriptInitialized=False
.
The default implementation of PreBeginPlay() allows mutators to modify the actor while it is being initialized by calling the GameInfo's IsRelevant() in UE1, the base mutator's CheckRelevance() in UE2 or the GameInfo's CheckRelevance() in UE3. These call Mutator.AlwaysKeep() and Mutator.IsRelevant(), which in turn calls Mutator.CheckReplacement().
The above logic doesn't apply if PreBeginPlay() is overridden without calling Super, the actor is flagged as bGameRelevant, the actor is bStatic (only UE3) or spawning happens on a network client.
BeginPlay()[edit]
Then the BeginPlay() function is called for all actors with bScriptInitialized=False
.
This event does nothing by default and no longer exists in Unreal Engine 3 as the same effect could be achieved by overriding PreBeginPlay() and calling Super.PreBeginPlay() first. It can be used to implement logic that should happen after mutators had a chance to modify the actor.
Zones/Volumes[edit]
At this point all actors' containing zones and PhysicsVolumes are initialized. Also, if the actor touches any Volumes, the actor and the Volume add each other to their list of touching actors.
Unlike for spawning actors, none of the actions in this startup stage cause UnrealScript events to be called.
PostBeginPlay()[edit]
Now all actors with bScriptInitialized=False
have their UnrealScript and C++ PostBeginPlay() functions called. These do nothing by default.
PostNetBeginPlay()[edit]
Right after PostBeginPlay(), PostNetBeginPlay() is called on all actors with bScriptInitialized=False
. This event does no longer exist in Unreal Engine 3.
Note that unlike for spawning actors, PostNetBeginPlay() is called before SetInitialSate().
SetInitialState()[edit]
The SetInitialState() function is the last to be called on all actors (with bScriptInitialized=False
) during initialization. As the name suggests, this function sets the actor's initial state. Using GotoState() it either switches to the state specified in the InitialState property or to the state marked with the auto modifier. As a result, the corresponding state's BeginState() event will be called.
Note that the AddToPackageMap() function in UT2004, which can dynamically add server packages at runtime, only works before this stage of map startup.
Finding bases[edit]
In this step, any actors with an AttachTag are attached to the first actor with a matching Tag or Name. If a matching actor is found, it receives the Attach() event.
For actors without an AttachTag, that bShouldBaseOnStartup, have world collision enabled and have their Physics set to None or Rotating, a good base actor is determined. If a base is found, its Attach() event and this actor's BaseChange() event are called.
Final initialization step[edit]
The last iteration over all actors updates any Projectors and disables Karma simulation for high-detail actors in low-detail mode and for high-detail and client-only actors in dedicated server mode.
The actual UnGame.cpp snippet[edit]
The following is a copy of the code snippet this article is based on. This particular piece of code is executed after a new map has been loaded and before the local player enters the game or any replication kicks in. Keep in mind that much of the initialization also happens at the UnrealScript level, so for a complete picture you should have the UT2004 sources ready and browse to the relevant UnrealScript functions.
Hints for reading the code[edit]
A few things that will help understanding the code even if you're not into C++:
- The
->
operator does the same as the dot does in UnrealScript - it accessed variables or functions of an object. GLevel
is a Level object. In UnrealScript this object is referenced by the XLevel property of any Actor.GLevel->GetLevelInfo()
returns the actual LevelInfo object. TheInfo
property also points to the LevelInfo.- Variables in UnrealScript objects have the same name in C++. For example
Info->bBegunPlay
refers to a variable you'd access asLevel.bBegunPlay
in UnrealScript code. - The LevelInfo's
bBegunPlay
acts as general gate to UnrealScript execution. During map load it is 0 (False), so no UnrealScript code will be executed until it is set to 1 (True) in this code snippet. - UnrealScript functions declared with the event keyword are called from C++ via
eventNameOfUScriptFunction(parameters)
, for exampleeventPreBeginPlay
calls the PreBeginPlay() function. - Dynamic arrays have a slightly different syntax in native code. They are declared as
TArray<type>
, their length is returned viaarray.Num()
and individual elements are accessed via round parentheses instead of square brackets.array.Empty()
does whataray.Length = 0;
would do in UnrealScript. GLevel->IsServer()
basically returns the same as(Level.NetMode != NM_Client)
in UnrealScript. SimilarlyGIsClient
basically returns the same asLevel.NetMode != NM_DedicatedServer
in UnrealScript.- The
GLevel->Actors
array is the same list that the AllActors iterator traverses in UnrealScript. - The native class name of Actor subclasses is prefixed with an A, the native name of non-Actor classes with a U and the native name of structs with an F, for example APhysicsVolume, UKarmaParams or FVector.
- Name literals do not exist. Instead, the values of all natively-used names are hard-coded as
NAME_name
, for example NAME_None for 'None'. - Class literals do exist, but look very different from UnrealScript. They are expressed as
nativeClassName::StaticClass()
. bScriptInitialized
is an Actor property that is set to True in the SetinitialState() event.if (!someObjectReference)
checks if a variable actually points to an object, similar to the UnrealScript codeif (someObjectReference != None)
.- Values for bool variables are 0 and 1, not False and True.
GetClass()->GetDefaultActor()
corresponds to UnrealScript's.default.
syntax for accessing the default variable values of a class.
General notes about this code snippet[edit]
- When running as a (listen and dedicated) server, actors mentioned in the [Engine.GameEngine] ServerActors list in the main INI file (e.g. UnrealTournament.ini, UT2004.ini or UTEngine.ini) are created before this code is executed. UnrealScript execution is not yet enabled at that point, so these actors are initialized like any other actor already in the map.
- Players do not exist while this code is executed. Any (local or remote) players join "much" later.
Notes for Unreal Engine 1[edit]
- The
bScriptInitialized
variable does not exist in Unreal Engine 1, so unlike in later engine generations actors spawned during initialization may receive *BeginPlay() calls twice. - Unreal Engine 1 does not have volumes or Karma physics, so these parts of the code here do not apply.
Notes for Unreal Engine 3[edit]
- The Actor's Level variable and the LevelInfo class are both called WorldInfo in Unreal Engine 3.
- The BeginPlay() and PostNetBeginPlay() functions have been removed, with PostBeginPlay() now taking PostNetBeginPlay()'s role.
- Unreal Engine 3 does not use zones, so the related parts of the code here do not apply.
- Initialization of levels streamed in at runtime is not covered by this article.
- Kismet initialization is not covered in this article. (More research is required on this.)
The code[edit]
/*============================================================================= UnGame.cpp: Unreal game engine. Copyright 1997-2003 Epic Games, Inc. All Rights Reserved. =============================================================================*/ GLevel->iFirstDynamicActor = 0; if( !Info->bBegunPlay ) { appResetTimer(); // sjs // fix up level problems FixUpLevel(); // Update draw distance. if (GIsClient) { GLevel->GetLevelInfo()->InitDistanceFogLOD(); GLevel->GetLevelInfo()->UpdateDistanceFogLOD( Client->DrawDistanceLOD ); } // Lock the level. debugf( NAME_Log, TEXT("Bringing %s up for play (%i) appSeconds: %f..."), GLevel->GetFullName(), appRound(GetMaxTickRate()), appSeconds() ); // sjs GLevel->FinishedPrecaching = 0; GLevel->TimeSeconds = 0; GLevel->GetLevelInfo()->TimeSeconds = 0; GLevel->GetLevelInfo()->GetDefaultPhysicsVolume()->bNoDelete = true; // Kill off actors that aren't interesting to the client. if( !GLevel->IsServer() ) { for( INT i=0; i<GLevel->Actors.Num(); i++ ) { AActor* Actor = GLevel->Actors(i); if( Actor ) { if( Actor->bStatic || Actor->bNoDelete || Actor->IsA(AxEmitter::StaticClass()) ) { if ( !Actor->bClientAuthoritative ) Exchange( Actor->Role, Actor->RemoteRole ); } else GLevel->DestroyActor( Actor ); } } } // Init touching actors & clear LastRenderTime for( INT i=0; i<GLevel->Actors.Num(); i++ ) if( GLevel->Actors(i) ) { GLevel->Actors(i)->LastRenderTime = 0.f; GLevel->Actors(i)->Touching.Empty(); GLevel->Actors(i)->PhysicsVolume = GLevel->GetLevelInfo()->GetDefaultPhysicsVolume(); } // Init scripting. for( INT i=0; i<GLevel->Actors.Num(); i++ ) if( GLevel->Actors(i) ) GLevel->Actors(i)->InitExecution(); // Enable actor script calls. Info->bBegunPlay = 1; Info->bStartup = 1; Info->TimeDilation = ((ALevelInfo *)(Info->GetClass()->GetDefaultActor()))->TimeDilation; #ifdef WITH_KARMA if(!GIsEditor && !GLevel->GetLevelInfo()->bKNoInit) { KInitLevelKarma(GLevel); for( INT i=0; i<GLevel->Actors.Num(); i++ ) if( GLevel->Actors(i) ) KInitActorKarma( GLevel->Actors(i) ); } #endif // Init the game. if( Info->Game ) { Info->Game->eventInitGame( Options, Error ); Info->Game->eventSetGrammar(); } // Send PreBeginPlay. for( INT i=0; i<GLevel->Actors.Num(); i++ ) if( GLevel->Actors(i) && !GLevel->Actors(i)->bScriptInitialized ) GLevel->Actors(i)->eventPreBeginPlay(); // Set BeginPlay. for( INT i=0; i<GLevel->Actors.Num(); i++ ) if( GLevel->Actors(i) && !GLevel->Actors(i)->bScriptInitialized ) GLevel->Actors(i)->eventBeginPlay(); // Set zones && gather volumes. TArray<AVolume*> LevelVolumes; for( INT i=0; i<GLevel->Actors.Num(); i++ ) { if( GLevel->Actors(i) && !GLevel->Actors(i)->bScriptInitialized ) GLevel->Actors(i)->SetZone( 1, 1 ); AVolume* Volume = Cast<AVolume>(GLevel->Actors(i)); if( Volume ) LevelVolumes.AddItem(Volume); } // Set appropriate volumes for each actor. for( INT i=0; i<GLevel->Actors.Num(); i++ ) if( GLevel->Actors(i) && !GLevel->Actors(i)->bScriptInitialized ) GLevel->Actors(i)->SetVolumes( LevelVolumes ); // Post begin play. for( INT i=0; i<GLevel->Actors.Num(); i++ ) if( GLevel->Actors(i) && !GLevel->Actors(i)->bScriptInitialized ) { GLevel->Actors(i)->eventPostBeginPlay(); if(GLevel->Actors(i)) GLevel->Actors(i)->PostBeginPlay(); } // Post net begin play. for( INT i=0; i<GLevel->Actors.Num(); i++ ) if( GLevel->Actors(i) && !GLevel->Actors(i)->bScriptInitialized ) GLevel->Actors(i)->eventPostNetBeginPlay(); // Begin scripting. for( INT i=0; i<GLevel->Actors.Num(); i++ ) if( GLevel->Actors(i) && !GLevel->Actors(i)->bScriptInitialized ) GLevel->Actors(i)->eventSetInitialState(); // Find bases for( INT i=0; i<GLevel->Actors.Num(); i++ ) { if( GLevel->Actors(i) ) { if ( GLevel->Actors(i)->AttachTag != NAME_None ) { //find actor to attach self onto for( INT j=0; j<GLevel->Actors.Num(); j++ ) { if( GLevel->Actors(j) && ((GLevel->Actors(j)->Tag == GLevel->Actors(i)->AttachTag) || (GLevel->Actors(j)->GetFName() == GLevel->Actors(i)->AttachTag)) ) { GLevel->Actors(i)->SetBase(GLevel->Actors(j), FVector(0,0,1), 0); break; } } } else if( GLevel->Actors(i)->bCollideWorld && GLevel->Actors(i)->bShouldBaseAtStartup && ((GLevel->Actors(i)->Physics == PHYS_None) || (GLevel->Actors(i)->Physics == PHYS_Rotating)) ) { GLevel->Actors(i)->FindBase(); } } } for( INT i=0; i<GLevel->Actors.Num(); i++ ) { if(GLevel->Actors(i)) { if( GLevel->Actors(i)->IsA(AProjector::StaticClass())) // sjs - why is this needed?!! { GLevel->Actors(i)->PostEditChange(); } #ifdef WITH_KARMA AActor* actor = GLevel->Actors(i); if(actor->Physics != PHYS_Karma || !actor->KParams || !actor->KParams->IsA(UKarmaParams::StaticClass())) continue; UKarmaParams* kparams = Cast<UKarmaParams>(actor->KParams); // If running below HighDetailPhysics, turn off karma dynamics for actors with bHighDetailOnly set true. if( GLevel->GetLevelInfo()->PhysicsDetailLevel < PDL_High && kparams->bHighDetailOnly ) KTermActorDynamics(actor); // If dedicated server, turn off karma for actors with bHighDetailOnly or bClientsOnly if( GLevel->GetLevelInfo()->NetMode == NM_DedicatedServer && (kparams->bHighDetailOnly || kparams->bClientOnly) ) KTermActorDynamics(actor); #endif } } Info->bStartup = 0; }