I love the smell of UnrealEd crashing in the morning. – tarquin

What happens when an Actor is spawned

From Unreal Wiki, The Unreal Engine Documentation Site
Jump to: navigation, search

The information in this article are based on a snipped of native UT2004 code originally posted on Epic Game's ut2003mods mailing list. The code can be found as UnLevAct.cpp in the Engine\Src directory of the UT2004 UnrealScript source code download. These events happen not only when the Spawn() function is used, but also when a network client (this includes demo playback) receives a replicated actor.

Summary of events[edit]

For those who don't want to dig through the code, here's a summary of events the Spawn() function calls before returning the newly spawned actor. It is possible to call Destroy() on the spawned actor at any point during initialization. If that happens, initialization stops and the Spawn() function returns None.

Pre-spawn checks[edit]

Before actually creating the new actor, the native code ensures certain preconditions are met:

  • The level must not be in the process of being garbage-collected.
  • The class to spawn must not be None, cannot be abstract and must be an Actor subclass.
  • The actor class to spawn must be neither bStatic nor bNoDelete.
  • If it's a high-detail actor, it is not spawned on low detail settings, if frame rate is low or on a dedicated server.
  • For actors that can collide with world geometry, the spawn location is verified and potentially adjusted so the spawned actor will not be partially embedded in world geometry. If that fails, spawning is aborted.

Spawning[edit]

If the preconditions are met, the actor is created with its properties initialized to the specified template actor (UE3) or the class default properties if no template was specified.

Before the first UnrealScript event is called, some of the actor's properties are initialized as follows:

Tag
The actor's class name.
Region
The Zone is set to the LevelInfo, iLeaf to -1 and ZoneNumber to 0.
Level
The LevelInfo.
bTicked, XLevel
These are set, but aren't really interesting for the UnrealScript level.
Role, RemoteRole
If the actor was received through replication, the values of Role and RemoteRole are exchanged. In other words, RemoteRole becomes ROLE_Authority and Role becomes ROLE_DumpProxy, ROLE_SimulatedProxy or ROLE_AutonomousProxy. ROLE_None isn't possible here because the actor wouldn't have been replicated in the first place. Note that the server automatically downgrades RemoteRole from ROLE_AutonomousProxy to ROLE_SimulatedProxy during replication if it isn't owned by the target client. Also note that bClientAuthoritative does not have any effect here.
Location, Rotation
The values specified in Spawn() or received as part of the initial replication packet. If the default value for bNetInitialRotation is False for the actor class, Rotation is initialized with (0,0,0) for a replicated actor.
Collision hash
If the actor collides with other actors, it is added to the collision hash. (makes it findable for Trace(), CollidingActors(), etc.)
PhysicsVolume
The LevelInfo's PhysicsVolume, which always is the level's DefaultPhysicsVolume.

Owner.GainedChild()[edit]

If an owner was specified in the Spawn() call, that actor's GainedChild() function is called with the newly spawned actor as parameter. Since the Owner variable is not part of the replicated spawning data (only class, location and usually also rotation are), this will never happen clientsidely for replicated actors.

Further initialization[edit]

Between Owner.GainedChild() and the spawned actor's first own event, a number of additional initialization steps happen. These do not call any UnrealScript events, but should be kept in mind when using GainedChild() to do something with the actor while it is being spawned.

  • The Instigator is set to the Instigator of the actor Spawn() was called on or None for replicated actors, since it is not part of the initial replication packet.
  • Karma physics are initialized for the spawned actor. Before this happens, the actor's Karma-related functions and properties cannot be used.
  • The actor's state support is initialized. Prior to this, calls to state-changing functions, such as GotoState(), do not have any effect.

Spawned()[edit]

In Unreal Engine 1, the Spawned() event is called at this point. It is only called on actors spawned at runtime and can be used to perform initialization that should not be done for mapper-placed actors. Note that it also isn't called for the GameInfo or any actors spawned through the ServerActors list.

PreBeginPlay()[edit]

The default implementation of the PreBeginPlay() function allows mutators to modify the actor while it is being spawned 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]

This event does nothing by default. It can be used to implement logic that should happen after mutators had a chance to modify the actor. This event no longer exists in Unreal Engine 3 as the same effect could be achieved by overriding PreBeginPlay() and calling Super.PreBeginPlay() first.

Zone/PhysicsVolume[edit]

At this point, the actor's Region.Zone and PhysicsVolume are initialized.

First the actor's actual zone is determined, causing the ZoneChange() event of the actor and the ActorEntered() event of the new zone to be called. If you want to be picky, there's also an ActorLeaving() call on the LevelInfo before that, but it usually doesn't have any effect and the LevelInfo cannot be replaced.

Next, the PhysicsVolume is determined. For PhysicsVolumes, this is the PhysicsVolume itself and no events are called. For all other actor types if the actor is contained in any PhysicsVolume, they get the PhysicsVolumeChange() event. For non-Pawn actors the volume's ActorEnteredVolume() event is called, for Pawns the volume's PawnEnteredVolume() is called instead. Pawns also receive the HeadVolumeChange() event.

Collision check[edit]

Next, the engine check if the newly spawned actor overlaps any other actor in a way that would cause "telefragging" on players. This only happens on colliding actors that can block other actors. During this check, the events EncroachingOn(), RanInto(), EncroachedBy(), Touch() and UnTouch() may get called on the spawned actor and any actors it overlaps.

If it is found that the spawned actor is EncroachingOn() another actor, spawning fails and the spawned actor is destroyed.

PostBeginPlay()[edit]

This event does nothing by default. It can be used to implement logic that should happen after engine-side initialization of the actor is complete.

Note that in Unreal Engine 3 this event took the place of PostNetBeginPlay().

C++ PostBeginPlay()[edit]

Here certain special actors can perform their final initialization steps. For example UT2004 Volumes spawn DecoVolumeObjects here according to their DecoList.

SetInitialState()[edit]

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.

Starting with Unreal Engine 2, this function also sets the bScriptInitialized flag, so the map startup initialization is not applied to actors spawned during startup. If you override SetInitialState() without calling Super, you should manually execute bScriptInitialized = True; in your overridden version of this function.

Finding a base[edit]

If the actor bShouldBaseOnStartup, has world collision enabled and its Physics set to None or Rotating, a good base actor is determined. If a base is found, the base actor's Attach() event and this actor's BaseChange() event are called.

PostNetBeginPlay()[edit]

For actors spawned locally, this event is called right after SetInitialState(). For actors received through replication, this event is instead called by the replication code after applying the initial bunch of replicated variables. This still happens right after spawning, but may be preceded by a call to PostNetReceive() in Unreal Engine 2 or one or more calls to ReplicatedEvent() in Unreal Engine 3.

Note that the name of this event is PostBeginPlay() in Unreal Engine 3.

SpawnNotify.SpawnNotification()[edit]

In UT actors of the special class SpawnNotify receive the SpawnNotification() event for locally spawned if the spawned actor's class matches the SpawnNotify's ActorClass or is a subclass thereof. Unlike the Mutator functions called during PreBeginPlay(), the SpawnNotification() event can actually replace the actor eventually returned by the Spawn() function.

Tag[edit]

If the actor is created with the Spawn() function and the SpawnTag parameter was specified as something different than '' or 'None', the spawned actor's Tag is set to that value here.

The actual UnLevAct.cpp snippet[edit]

The following is a copy of the code snippet this article is based on. This particular piece of code is called called from within the UnrealScript Spawn() function, to create the GameInfo and any ServerActors at map startup and also for any received replicated Actors. Even UnrealEd uses it to place new Actors in the map. Note that this code calls various UnrealScript functions, so for a complete picture you should have the UT2004 sources ready and browse to the relevant 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 accesses variables or functions of an object.
  • GetLevelInfo() returns the current LevelInfo object, similar to the UnrealScript variable Level.
  • Variables in UnrealScript objects have the same name in C++. For example Actor->PhysicsVolume refers to UnrealScript variable of the same name.
  • The LevelInfo's bBegunPlay acts as general gate to UnrealScript execution. During map load and in UnrealEd it is 0 (False), so no UnrealScript code will be executed. At some point in map startup it is set to 1 (True), and from that point on until the map is unloaded UnrealScript functions can run. Any actors spawned before that (e.g. the GameInfo or any ServerActors) do not get any of the UnrealScript calls mentioned here and are instead initialized as part of map startup.
  • UnrealScript functions declared with the event keyword are called from C++ via eventNameOfUScriptFunction(parameters), for example eventPreBeginPlay calls the PreBeginPlay() function.
  • 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().
  • Values for bool variables are 0 and 1, not False and True.
  • Class->GetDefaultActor() corresponds to UnrealScript's .default. syntax for accessing the default variable values of a class.
  • DestroyActor() is the function that is called by the UnrealScript Destroy() function. (see What happens when an Actor is destroyed)

General notes about this code snippet[edit]

  • Only the parameters Class, Location, Rotation and Owner can be specified via the UnrealScript Spawn() function.
  • The InName and Template parameters are always None for normal spawns. They are probably only used in special cases by UnrealEd, e.g. Template for actor duplication.
  • The bRemoteOwned parameter is only True when the Actor was received clientsidely via replication.
  • The Instigator parameter is set to the Instigator of the Actor the UnrealScript Spawn() function is called on.
  • The UnrealScript Spawn() function parameter SpawnTag is applied by the native Spawn() implementation after ULevel::SpawnActor() returns. That means the value of the newly spawned Actor's Tag property does not have any meaningful value yet. (It is temporarily set to the class name.) Any changes to the Tag during the initialization of the spawned Actor are overridden with the SpawnTag value.
  • The call to SetOwner() triggers the Owner's GainedChild() UnrealScript event before the new Actor is initialized. Among other things, this means you cannot use GotoState() on that Actor yet.
  • Similarly, SetZone(), CheckEncroachment() and FindBase() may cause UnrealScript events to be called.
  • The call to PostNetBeginPlay() only happens if the Actor wasn't received through replication. For replicated actors PostNetBeginPlay() will be called a bit later after the initial bunch of replicated variables have been received. If the Actor's bNetNotify property is True, PostNetReceive() might be called right before the PostNetBeginPlay() call.

Notes for Unreal Engine 1[edit]

  • Unreal Engine 1 does not have volumes or Karma physics, so these parts of the code here do not apply.
  • Before calling PreBeginPlay(), the UnrealScript event Spawned() is called.
  • UT also calls the SpawnNotification() event of all SpawnNotify actors that want notification for this type of actor, somewhere between PostNetBeginPlay() and setting the Tag property. Unlike any other function called during execution of the Spawn() function, a SpawnNotify can actually change the actor eventually returned by the Spawn() call.

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. PostNetReceive() has been replaced by the more specific ReplicatedEvent() function that will be called for every received repnotify variable.
  • Unreal Engine 3 does not use zones, so the related parts of the code here do not apply.
  • The Template and bNoCollisionFail parameters can be passed via the Spawn() function, but for replicated actors the template must be an actor the client knows about, ideally an Actor subobject, an Actor archetype or a bStatic or bNoDelete Actor in the current level.

The code[edit]

/*=============================================================================
	UnLevAct.cpp: Level actor functions
	Copyright 1997-2001 Epic Games, Inc. All Rights Reserved.
 
=============================================================================*/
 
//
// Create a new actor. Returns the new actor, or NULL if failure.
//
AActor* ULevel::SpawnActor
(
	UClass*			Class,
	FName			InName,
	FVector			Location,
	FRotator		Rotation,
	AActor*			Template,
	UBOOL			bNoCollisionFail,
	UBOOL			bRemoteOwned,
	AActor*			Owner,
	APawn*			Instigator,
	UBOOL			bNoFail
)
{
	guard(ULevel::SpawnActor);
 
    if( GetFlags() & RF_Unreachable )
        return NULL;
 
	// Make sure this class is spawnable.
	if( !Class )
	{
		debugf( NAME_Warning, TEXT("SpawnActor failed because no class was specified") );
		return NULL;
	}
	if( Class->ClassFlags & CLASS_Abstract )
	{
		debugf( NAME_Warning, TEXT("SpawnActor failed because class %s is abstract"), Class->GetName() );
		return NULL;
	}
	else if( !Class->IsChildOf(AActor::StaticClass()) )
	{
		debugf( NAME_Warning, TEXT("SpawnActor failed because %s is not an actor class"), Class->GetName() );
		return NULL;
	}
	else if( !GIsEditor && (Class->GetDefaultActor()->bStatic || Class->GetDefaultActor()->bNoDelete) )
	{
		debugf( NAME_Warning, TEXT("SpawnActor failed because class %s has bStatic or bNoDelete"), Class->GetName() );
		if ( !bNoFail )
			return NULL;		
	}
 
	// don't spawn bHighDetail actors if not wanted
	if( !GIsEditor && Class->GetDefaultActor()->bHighDetail && !bNoFail )
	{
	    if( GetLevelInfo()->DetailMode == DM_Low || GetLevelInfo()->bDropDetail || (GetLevelInfo()->NetMode == NM_DedicatedServer) )
		{
			//debugf(TEXT("%s not spawned"),Class->GetName());
			return NULL;
		}
    }
#if 1
    // sjs - level's outer is not transient so we must do this
    // doing this is a huge benefit for long running names, as the name table grows > 40 megs after long multiplayer games.
    if( !GTransientNaming && InName==NAME_None)
        InName = NAME_Transient;
#endif
 
	// Use class's default actor as a template.
	if( !Template )
		Template = Class->GetDefaultActor();
	check(Template!=NULL);
 
	// Make sure actor will fit at desired location, and adjust location if necessary.
	if( (Template->bCollideWorld || (Template->bCollideWhenPlacing && (GetLevelInfo()->NetMode != NM_Client))) && !bNoCollisionFail )
		if( !FindSpot( Template->GetCylinderExtent(), Location ) )
			return NULL;
 
	// Add at end of list.
	INT iActor = Actors.Add();
    AActor* Actor = Actors(iActor) = (AActor*)StaticConstructObject( Class, GetOuter(), InName, 0, Template );
 
	Actor->SetFlags( RF_Transactional );
 
	// Set base actor properties.
	Actor->Tag		= Class->GetFName();
	Actor->Region	= FPointRegion( GetLevelInfo() );
	Actor->Level	= GetLevelInfo();
	Actor->bTicked  = !Ticked;
	Actor->XLevel	= this;
 
	// Set network role.
	check(Actor->Role==ROLE_Authority);
	if( bRemoteOwned )
		Exchange( Actor->Role, Actor->RemoteRole );
 
	// Remove the actor's brush, if it has one, because moving brushes are not duplicatable.
	if( Actor->Brush )
		Actor->Brush = NULL;
 
	// Set the actor's location and rotation.
	Actor->Location = Location;
	Actor->Rotation = Rotation;
	if( Actor->bCollideActors && Hash  )
		Hash->AddActor( Actor );
 
	// init actor's physics volume
	Actor->PhysicsVolume = 	GetLevelInfo()->PhysicsVolume; 
 
	// Set owner.
	Actor->SetOwner( Owner );
 
	// Set instigator
	Actor->Instigator = Instigator;
 
#ifdef WITH_KARMA
    // Initilise Karma physics for this actor (if there are any)
    KInitActorKarma(Actor);
#endif
 
	// Send messages.
	Actor->InitExecution();
	Actor->Spawned();
	Actor->eventPreBeginPlay();
	if( Actor->bDeleteMe && !bNoFail )
		return NULL;
	Actor->eventBeginPlay();
	if( Actor->bDeleteMe && !bNoFail )
		return NULL;
 
	// Set the actor's zone.
	Actor->SetZone( iActor==0, 1 );
 
	// Update the list of leaves this actor is in.
	Actor->ClearRenderData();
 
	// Check for encroachment.
	if( !bNoCollisionFail )
	{
		if( Actor->bCollideActors && Hash )
			Hash->RemoveActor( Actor );
 
		if( CheckEncroachment( Actor, Actor->Location, Actor->Rotation, 1 ) )
		{
			DestroyActor( Actor );
			return NULL;
		}
		if( Actor->bCollideActors && Hash )
			Hash->AddActor( Actor );
	}
	//if ( Actor->bCollideActors && !Actor->bBlockActors && !Actor->bUseCylinderCollision && (Actor->DrawType == DT_StaticMesh) )
	//	debugf(TEXT("%s shouldn't be using static mesh collision"),Actor->GetName());
 
	// Send PostBeginPlay.
	Actor->eventPostBeginPlay();
	if( Actor->bDeleteMe && !bNoFail )
		return NULL;
 
	Actor->PostBeginPlay();
 
	// Init scripting.
	Actor->eventSetInitialState();
 
	// Find Base
	if( !GIsEditor && !Actor->Base && Actor->bCollideWorld && Actor->bShouldBaseAtStartup 
		 && ((Actor->Physics == PHYS_None) || (Actor->Physics == PHYS_Rotating)) )
		Actor->FindBase();
 
	// Success: Return the actor.
	if( InTick )
		NewlySpawned = new(GEngineMem)FActorLink(Actor,NewlySpawned);
 
	// replicated actors will have postnetbeginplay() called in net code, after initial properties are received
	if ( !bRemoteOwned )
		Actor->eventPostNetBeginPlay();
 
	return Actor;
	unguardf(( TEXT("(%s)"), Class->GetName() ));
}