Legacy:Replication Examples/Giant Spider Execution

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

This example describes the replication (or better: simulation) magic behind the alternate execution sequence on JB-Aswan-v2. The spider actor waits for a serverside trigger event and then starts parallel execution of server- and clientside state code. Only the change of a single replicated value starts the entire clientside simulation machinery, which otherwise doesn't require any replication because it mainly relies on default properties.

The Actors Involved

I don't want to bore you to death with how Jailbreak's jails and execution sequences are set up, but basically the game triggers an event serversidely that should eventually result in the prisoners in a certain jail getting killed.

On JB-Aswan-v2, the execution is actually a quite complex system of ScriptedTriggers to randomly select between the default spider invasion execution or the giant spider execution. The giant spider execution uses a custom actor that, once triggered, dispatches events for a camera view switch (a JDN:JBCamera) and the explosion Emitter. The giant spider mine also uses a spawn effect, but that is simply triggered at the same time as the giant spider itself.
The spawn effect emitter uses a setup similar to the Onslaught vehicle spawn effect and is reset on triggering. The explosion emitter spawns a few explosion effect sprites with spawn sounds, a few yellow/orange-colored sprites to fill the jail and four groups of black sprites coming towards the camera through the jail bars.

Interested in how exactly this looks? I've prepared a short video sequence for you:

The Giant Spider's Code

The giant spider actor is a custom actor. The following code is basically identical with the code I compiled for JB-Aswan-v2, but has a few comments added for clarification. An explaination of how the code works follows below.

<uscript line> //============================================================================= // JBGiantSpiderMine // Copyright (c) 2004 by Wormbo <[email protected]> // // A standalone version of the parasite mine. //=============================================================================


class JBGiantSpiderMine extends Actor

 placeable;


//============================================================================= // Imports //=============================================================================

  1. exec obj load file=..\Textures\XGameShaders.utx
  2. exec obj load file=..\Sounds\WeaponSounds.uax


//============================================================================= // Properties //=============================================================================

var(Events) edfindable array<JBInfoJail> AssociatedJails; // players in these jails will be killed by the explosion var(Events) name PreExplosionEvent; // the event used to switch the camera view var() float PreSpawnDelay; // a delay between getting triggered and setting bHidden=false var() float PreExplosionDelay; // a delay between triggering PreExplosionEvent and Event var() float ExplosionDelay; // the delay from getting triggered to exploding var() Material SpawnOverlayMaterial; // the overlay material to display after spawning var() float SpawnOverlayTime; // the time, the overlay is displayed var() float MomentumTransfer; // amount of momentum applied when damagin players (so gibs fly around :P) var() class<DamageType> MyDamageType; // the damage type to use for killing players var(Sounds) array<Sound> BulletSounds; // sounds played back when shots hit the (invulnerable) spider


//============================================================================= // Variables //=============================================================================

var name IdleAnims[4]; // animations are randomly played before exploding (animations handle the sounds) var float ExplosionCountdown; // counts down from ExplosionDelay to 0 var bool bPreExplosion; // tells, whether PreExplosionEvent was already triggered


//== EncroachingOn ============================================================ /** Telefrag players blocking the spawn point.

  • /

//=============================================================================

event bool EncroachingOn(Actor Other) {

 if ( Pawn(Other) != None )
   Pawn(Other).GibbedBy(Self);
 
 return Super.EncroachingOn(Other);

}


//== state Sleeping =========================================================== /** Wait hidden and non-colliding until triggered.

  • /

//=============================================================================

simulated state Sleeping {

 function Trigger(Actor Other, Pawn EventInstigator)
 {
   local JBInfoJail thisJail;
   local int i;
   local PlayerReplicationInfo PRI;
   local JBTagPlayer TagPlayer;
   local Pawn thisPawn;
   
   if ( AssociatedJails.Length == 0 ) { // not associated with any jails, try to find matching jails
     foreach AllActors(class'JBInfoJail', thisJail) {
       if ( thisJail.ContainsActor(Self) ) {
         AssociatedJails[0] = thisJail;
         break;
       }
     }
     if ( AssociatedJails.Length == 0 ) {
       // no associated jails found, associate with all jails
       log("!!!!" @ Self @ "not associated with any jails!", 'Warning');
       foreach AllActors(class'JBInfoJail', thisJail) {
         AssociatedJails[0] = thisJail;
       }
     }
   }
   
   // check if we actually have someone in this jail
   foreach DynamicActors(class'PlayerReplicationInfo', PRI) {
     TagPlayer = class'JBTagPlayer'.static.FindFor(PRI);
     if ( TagPlayer != None && TagPlayer.IsInJail() && TagPlayer.GetPawn() != None ) {
       thisJail = TagPlayer.GetJail();
       thisPawn = TagPlayer.GetPawn();
       for (i = 0; i < AssociatedJails.Length; ++i) {
         if ( thisJail == AssociatedJails[i] ) {
           // prisoner found, now spawn
           NetUpdateTime = Level.TimeSeconds - 1; // force replication right now
           bClientTrigger = !bClientTrigger;
           GotoState('Spawning');
           return;
         }
       }
     }
   }
 }
 
 simulated event ClientTrigger()
 {
   GotoState('Spawning');
 }
 

Begin:

 bHidden = True;
 SetCollision(False, False, False);

}


//== TakeDamage =============================================================== /** Play sound effects for bullet hits.

  • /

//=============================================================================

event TakeDamage(int Damage, Pawn EventInstigator, vector HitLocation, vector Momentum, class<DamageType> DamageType) {

 if ( !bHidden && DamageType != None && DamageType.Default.bBulletHit && BulletSounds.Length > 0 )
   PlaySound(BulletSounds[Rand(BulletSounds.Length)], SLOT_None, 2.0, False, 100);

}


//== state Spawning =========================================================== /** Play a spawn effect.

  • /

//=============================================================================

simulated state Spawning { Begin:

 if ( PrespawnDelay > 0 )
   Sleep(PrespawnDelay); // wait until external spawn effect is over
 bHidden = False;
 SetCollision(True, True);
 SetLocation(Location);  // "telefrag" players at this location
 if ( SpawnOverlayTime > 0 && SpawnOverlayMaterial != None )
   SetOverlayMaterial(SpawnOverlayMaterial, SpawnOverlayTime, True);
 PlayAnim('Startup', 1.0);
 FinishAnim();
 GotoState('Waiting');

}


//== state Waiting ============================================================ /** Spider idles a bit before detonating.

  • /

//=============================================================================

simulated state Waiting {

 simulated function Timer()
 {
   local JBInfoJail thisJail;
   local int i;
   local PlayerReplicationInfo PRI;
   local JBTagPlayer TagPlayer;
   local Pawn thisPawn;
   
   ExplosionCountdown -= 0.1;
   if ( !bPreExplosion && ExplosionCountdown <= PreExplosionDelay ) {
     // trigger the pre-explosion event (camera switch)
     bPreExplosion = True;
     TriggerEvent(PreExplosionEvent, Self, None);
   }
   if ( ExplosionCountdown <= 0 ) {
     SetTimer(0.0, False);
     TriggerEvent(Event, Self, None);
     
     if ( Role == ROLE_Authority ) {
       foreach DynamicActors(class'PlayerReplicationInfo', PRI) {
         TagPlayer = class'JBTagPlayer'.static.FindFor(PRI);
         if ( TagPlayer != None && TagPlayer.IsInJail() && TagPlayer.GetPawn() != None ) {
           thisJail = TagPlayer.GetJail();
           thisPawn = TagPlayer.GetPawn();
           for (i = 0; i < AssociatedJails.Length; ++i) {
             if ( thisJail == AssociatedJails[i] ) {
               thisPawn.TakeDamage(1000, None, thisPawn.Location, MomentumTransfer * Normal(thisPawn.Location - Location) * 1000 / VSize(thisPawn.Location - Location), MyDamageType);
               if ( thisPawn.Health > 0 )
                 thisPawn.Died(None, MyDamageType, thisPawn.Location);
               break;
             }
           }
         }
       }
     }
     GotoState('Sleeping');
   }
 }
 

Begin:

 ExplosionCountdown = ExplosionDelay;
 bPreExplosion = False;
 SetTimer(0.1, True);
 while (True) {
   PlayAnim('Idle', 1.0, 0.3);
   FinishAnim();
   PlayAnim(IdleAnims[Rand(ArrayCount(IdleAnims))], 1.0, 0.3);
   FinishAnim();
 }

}


//============================================================================= // Default properties //=============================================================================

defaultproperties {

 DrawType=DT_Mesh              // The mesh used for this actor is a special version of the
 Mesh=CollidingSpiderMineMesh  // Onslaught parasite mine mesh, that has sound notifications
 bUseCylinderCollision=False   // and collision boxes matching the spider's size and shape.
 bEdShouldSnap=True
 bProjTarget=True              // shots should hit the spider
 CollisionHeight=60.0          // These dimensions help placing
 CollisionRadius=150.0         // the spider in Unrealed.
 IdleAnims(0)=Clean
 IdleAnims(1)=Look
 IdleAnims(2)=Bob
 IdleAnims(3)=FootTap
 DrawScale=1.5
 bUseDynamicLights=True
 bDramaticLighting=True
 RemoteRole=ROLE_SimulatedProxy    // The spider should be replicated to clients.
 InitialState=Sleeping             // the startup state
 SpawnOverlayMaterial=VehicleSpawnShaderRed
 SpawnOverlayTime=2.0
 PreSpawnDelay=2.0
 PreExplosionDelay=1.0
 ExplosionDelay=5.0
 MomentumTransfer=100000.0
 MyDamageType=DamTypeONSMine
 SurfaceType=EST_Metal         // for players walking on the spider and shots hitting it
 BulletSounds(0)=Sound'WeaponSounds.BBulletReflect1'
 BulletSounds(1)=Sound'WeaponSounds.BBulletReflect2'
 BulletSounds(2)=Sound'WeaponSounds.BBulletReflect3'
 BulletSounds(3)=Sound'WeaponSounds.BBulletReflect4'
 BulletSounds(4)=Sound'WeaponSounds.BBulletImpact1'
 BulletSounds(5)=Sound'WeaponSounds.BBulletImpact2'
 BulletSounds(6)=Sound'WeaponSounds.BBulletImpact3'
 BulletSounds(7)=Sound'WeaponSounds.BBulletImpact4'
 BulletSounds(8)=Sound'WeaponSounds.BBulletImpact5'
 BulletSounds(9)=Sound'WeaponSounds.BBulletImpact6'
 BulletSounds(10)=Sound'WeaponSounds.BBulletImpact7'
 BulletSounds(11)=Sound'WeaponSounds.BBulletImpact8'
 BulletSounds(12)=Sound'WeaponSounds.BBulletImpact9'
 BulletSounds(13)=Sound'WeaponSounds.BBulletImpact11'
 BulletSounds(14)=Sound'WeaponSounds.BBulletImpact12'
 BulletSounds(15)=Sound'WeaponSounds.BBulletImpact13'
 BulletSounds(16)=Sound'WeaponSounds.BBulletImpact14'

} </uscript>

How Does It Work?

Before We Start

JBGiantSpiderMine is a placeable, replicated actor. That means, the actor is placed in the map and exists as separate versions on the server and on all clients before any replication happens. These clientside versions will never do anything and could as well be destroyed in PreBeginPlay() when (Level.NetMode == NM_Client) and (Role == ROLE_Authority).

The giant spider is initially invisible and will never receive the trigger events in the clients, so we might as well leave it alone. You should still keep this in mind when creating replicated actors for mappers.

The JBGiantSpiderMine starts in its InitialState 'Sleeping' both on the server and on clients.

Press The Start Button

The giant spider is triggered serversidely by an event matching its Tag value. This will cause the Trigger() function in state Sleeping to be executed. This is a non-simulated function, because it never needs to be executed clientsidely.

The Trigger() function checks, whether there are actually players in the desired jail. If it finds players, three things happen:

  • The value of bClientTrigger is toggled. This change will be replicated to all clients and cause some native replication magic to do its work. (see below)
  • The value of NetUpdateTime is set to a time index in the past. This will force all changed replicated variables to be replicated as soon as possible.
  • The JBGiantSpiderMine switches to state 'Spawning' serversidely.

Changing the value of bClientTrigger will cause the ClientTrigger() function to be called clientsidely once the change reaches the client. Since the JBGiantSpiderMine is also in state 'Sleeping' on the client, it will call the corresponding ClientTrigger() function, which switches to state 'Spawning'.

From this point on, the server and clients process their visual and sound effects independantly from each other.

Making The Spider Appear

The 'Spawning' state waits until the spawn effect emitter is done (the required amount time for this must be set manually by the mapper) and makes the spider visible and enables its collision. The call to SetLocation() makes sure, that all players touching the spider are immediately "telefragged". The spider plays its startup animation and goes to state 'Waiting'.

Waiting For The Big Bang

Like the 'Sleeping' and 'Spawning' states, the 'Waiting' state is entered independently on server and clients. Only the fixed time intervals used on server and clients ensure that they enter this state at about the same time!

Once state 'Waiting' starts, two things are done independantly form each other:

  • The state code randomly plays animations and waits for them to finish.
  • The Timer() function is called every 0.1 game seconds and decreases the ExplosionCounter. If it drops below PreExplosionDelay, the PreExplosionEvent is trigger on the server and clients independantly. If the ExplosionCounter reaches 0, the Event is triggered also on the server and the clients independantly and the server (Role == ROLE_Authority) kills the players in the associated jails. After that, server and client go back to state 'Sleeping' independantly.

Conclusion

Sometimes (like in this case) the big challenge in replication is not the replication itself, but not using it. This example relies more on simulation than on replication. The only part where the simulation is syncronized is the native magic behind the bClientTrigger variable, which calls the ClientTrigger() function once its changed value reaches the client. It should be mentioned, that bClientTrigger is only useful when you know, that it will not change more than once within a short time span. With a higher frequence of changes you should use a replicated byte variable and check its value in PostNetReceive() on the clients.

Related Topics