| Home Page | Recent Changes

Replication Examples/Giant Spider Execution

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 logo 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.

00001  //=============================================================================
00002  // JBGiantSpiderMine
00003  // Copyright (c) 2004 by Wormbo <spamtheworm@koehler-homepage.de>
00004  //
00005  // A standalone version of the parasite mine.
00006  //=============================================================================
00007  
00008  
00009  class JBGiantSpiderMine extends Actor
00010    placeable;
00011  
00012  
00013  //=============================================================================
00014  // Imports
00015  //=============================================================================
00016  
00017  #exec obj load file=..\Textures\XGameShaders.utx
00018  #exec obj load file=..\Sounds\WeaponSounds.uax
00019  
00020  
00021  //=============================================================================
00022  // Properties
00023  //=============================================================================
00024  
00025  var(Events) edfindable array<JBInfoJail> AssociatedJails; // players in these jails will be killed by the explosion
00026  var(Events) name PreExplosionEvent;    // the event used to switch the camera view
00027  var() float PreSpawnDelay;             // a delay between getting triggered and setting bHidden=false
00028  var() float PreExplosionDelay;         // a delay between triggering PreExplosionEvent and Event
00029  var() float ExplosionDelay;            // the delay from getting triggered to exploding
00030  var() Material SpawnOverlayMaterial;   // the overlay material to display after spawning
00031  var() float SpawnOverlayTime;          // the time, the overlay is displayed
00032  var() float MomentumTransfer;          // amount of momentum applied when damagin players (so gibs fly around :P)
00033  var() class<DamageType> MyDamageType;  // the damage type to use for killing players
00034  var(Sounds) array<Sound> BulletSounds; // sounds played back when shots hit the (invulnerable) spider
00035  
00036  
00037  //=============================================================================
00038  // Variables
00039  //=============================================================================
00040  
00041  var name IdleAnims[4];        // animations are randomly played before exploding (animations handle the sounds)
00042  var float ExplosionCountdown; // counts down from ExplosionDelay to 0
00043  var bool bPreExplosion;       // tells, whether PreExplosionEvent was already triggered
00044  
00045  
00046  //== EncroachingOn ============================================================
00047  /**
00048  Telefrag players blocking the spawn point.
00049  */
00050  //=============================================================================
00051  
00052  event bool EncroachingOn(Actor Other)
00053  {
00054    if ( Pawn(Other) != None )
00055      Pawn(Other).GibbedBy(Self);
00056    
00057    return Super.EncroachingOn(Other);
00058  }
00059  
00060  
00061  //== state Sleeping ===========================================================
00062  /**
00063  Wait hidden and non-colliding until triggered.
00064  */
00065  //=============================================================================
00066  
00067  simulated state Sleeping
00068  {
00069    function Trigger(Actor Other, Pawn EventInstigator)
00070    {
00071      local JBInfoJail thisJail;
00072      local int i;
00073      local PlayerReplicationInfo PRI;
00074      local JBTagPlayer TagPlayer;
00075      local Pawn thisPawn;
00076      
00077      if ( AssociatedJails.Length == 0 ) { // not associated with any jails, try to find matching jails
00078        foreach AllActors(class'JBInfoJail', thisJail) {
00079          if ( thisJail.ContainsActor(Self) ) {
00080            AssociatedJails[0] = thisJail;
00081            break;
00082          }
00083        }
00084        if ( AssociatedJails.Length == 0 ) {
00085          // no associated jails found, associate with all jails
00086          log("!!!!" @ Self @ "not associated with any jails!", 'Warning');
00087          foreach AllActors(class'JBInfoJail', thisJail) {
00088            AssociatedJails[0] = thisJail;
00089          }
00090        }
00091      }
00092      
00093      // check if we actually have someone in this jail
00094      foreach DynamicActors(class'PlayerReplicationInfo', PRI) {
00095        TagPlayer = class'JBTagPlayer'.static.FindFor(PRI);
00096        if ( TagPlayer != None && TagPlayer.IsInJail() && TagPlayer.GetPawn() != None ) {
00097          thisJail = TagPlayer.GetJail();
00098          thisPawn = TagPlayer.GetPawn();
00099          for (i = 0; i < AssociatedJails.Length; ++i) {
00100            if ( thisJail == AssociatedJails[i] ) {
00101              // prisoner found, now spawn
00102              NetUpdateTime = Level.TimeSeconds - 1; // force replication right now
00103              bClientTrigger = !bClientTrigger;
00104              GotoState('Spawning');
00105              return;
00106            }
00107          }
00108        }
00109      }
00110    }
00111    
00112    simulated event ClientTrigger()
00113    {
00114      GotoState('Spawning');
00115    }
00116    
00117  Begin:
00118    bHidden = True;
00119    SetCollision(False, False, False);
00120  }
00121  
00122  
00123  //== TakeDamage ===============================================================
00124  /**
00125  Play sound effects for bullet hits.
00126  */
00127  //=============================================================================
00128  
00129  event TakeDamage(int Damage, Pawn EventInstigator, vector HitLocation, vector Momentum, class<DamageType> DamageType)
00130  {
00131    if ( !bHidden && DamageType != None && DamageType.Default.bBulletHit && BulletSounds.Length > 0 )
00132      PlaySound(BulletSounds[Rand(BulletSounds.Length)], SLOT_None, 2.0, False, 100);
00133  }
00134  
00135  
00136  //== state Spawning ===========================================================
00137  /**
00138  Play a spawn effect.
00139  */
00140  //=============================================================================
00141  
00142  simulated state Spawning
00143  {
00144  Begin:
00145    if ( PrespawnDelay > 0 )
00146      Sleep(PrespawnDelay); // wait until external spawn effect is over
00147    bHidden = False;
00148    SetCollision(True, True);
00149    SetLocation(Location);  // "telefrag" players at this location
00150    if ( SpawnOverlayTime > 0 && SpawnOverlayMaterial != None )
00151      SetOverlayMaterial(SpawnOverlayMaterial, SpawnOverlayTime, True);
00152    PlayAnim('Startup', 1.0);
00153    FinishAnim();
00154    GotoState('Waiting');
00155  }
00156  
00157  
00158  //== state Waiting ============================================================
00159  /**
00160  Spider idles a bit before detonating.
00161  */
00162  //=============================================================================
00163  
00164  simulated state Waiting
00165  {
00166    simulated function Timer()
00167    {
00168      local JBInfoJail thisJail;
00169      local int i;
00170      local PlayerReplicationInfo PRI;
00171      local JBTagPlayer TagPlayer;
00172      local Pawn thisPawn;
00173      
00174      ExplosionCountdown -= 0.1;
00175      if ( !bPreExplosion && ExplosionCountdown <= PreExplosionDelay ) {
00176        // trigger the pre-explosion event (camera switch)
00177        bPreExplosion = True;
00178        TriggerEvent(PreExplosionEvent, Self, None);
00179      }
00180      if ( ExplosionCountdown <= 0 ) {
00181        SetTimer(0.0, False);
00182        TriggerEvent(Event, Self, None);
00183        
00184        if ( Role == ROLE_Authority ) {
00185          foreach DynamicActors(class'PlayerReplicationInfo', PRI) {
00186            TagPlayer = class'JBTagPlayer'.static.FindFor(PRI);
00187            if ( TagPlayer != None && TagPlayer.IsInJail() && TagPlayer.GetPawn() != None ) {
00188              thisJail = TagPlayer.GetJail();
00189              thisPawn = TagPlayer.GetPawn();
00190              for (i = 0; i < AssociatedJails.Length; ++i) {
00191                if ( thisJail == AssociatedJails[i] ) {
00192                  thisPawn.TakeDamage(1000, None, thisPawn.Location, MomentumTransfer * Normal(thisPawn.Location - Location) * 1000 / VSize(thisPawn.Location - Location), MyDamageType);
00193                  if ( thisPawn.Health > 0 )
00194                    thisPawn.Died(None, MyDamageType, thisPawn.Location);
00195                  break;
00196                }
00197              }
00198            }
00199          }
00200        }
00201        GotoState('Sleeping');
00202      }
00203  
00204    }
00205    
00206  Begin:
00207    ExplosionCountdown = ExplosionDelay;
00208    bPreExplosion = False;
00209    SetTimer(0.1, True);
00210    while (True) {
00211      PlayAnim('Idle', 1.0, 0.3);
00212      FinishAnim();
00213      PlayAnim(IdleAnims[Rand(ArrayCount(IdleAnims))], 1.0, 0.3);
00214      FinishAnim();
00215    }
00216  }
00217  
00218  
00219  //=============================================================================
00220  // Default properties
00221  //=============================================================================
00222  
00223  defaultproperties
00224  {
00225    DrawType=DT_Mesh              // The mesh used for this actor is a special version of the
00226    Mesh=CollidingSpiderMineMesh  // Onslaught parasite mine mesh, that has sound notifications
00227    bUseCylinderCollision=False   // and collision boxes matching the spider's size and shape.
00228    bEdShouldSnap=True
00229    bProjTarget=True              // shots should hit the spider
00230    CollisionHeight=60.0          // These dimensions help placing
00231    CollisionRadius=150.0         // the spider in Unrealed.
00232    IdleAnims(0)=Clean
00233    IdleAnims(1)=Look
00234    IdleAnims(2)=Bob
00235    IdleAnims(3)=FootTap
00236    DrawScale=1.5
00237    bUseDynamicLights=True
00238    bDramaticLighting=True
00239    RemoteRole=ROLE_SimulatedProxy    // The spider should be replicated to clients.
00240    InitialState=Sleeping             // the startup state
00241    SpawnOverlayMaterial=VehicleSpawnShaderRed
00242    SpawnOverlayTime=2.0
00243    PreSpawnDelay=2.0
00244    PreExplosionDelay=1.0
00245    ExplosionDelay=5.0
00246    MomentumTransfer=100000.0
00247    MyDamageType=DamTypeONSMine
00248    SurfaceType=EST_Metal         // for players walking on the spider and shots hitting it
00249    BulletSounds(0)=Sound'WeaponSounds.BBulletReflect1'
00250    BulletSounds(1)=Sound'WeaponSounds.BBulletReflect2'
00251    BulletSounds(2)=Sound'WeaponSounds.BBulletReflect3'
00252    BulletSounds(3)=Sound'WeaponSounds.BBulletReflect4'
00253    BulletSounds(4)=Sound'WeaponSounds.BBulletImpact1'
00254    BulletSounds(5)=Sound'WeaponSounds.BBulletImpact2'
00255    BulletSounds(6)=Sound'WeaponSounds.BBulletImpact3'
00256    BulletSounds(7)=Sound'WeaponSounds.BBulletImpact4'
00257    BulletSounds(8)=Sound'WeaponSounds.BBulletImpact5'
00258    BulletSounds(9)=Sound'WeaponSounds.BBulletImpact6'
00259    BulletSounds(10)=Sound'WeaponSounds.BBulletImpact7'
00260    BulletSounds(11)=Sound'WeaponSounds.BBulletImpact8'
00261    BulletSounds(12)=Sound'WeaponSounds.BBulletImpact9'
00262    BulletSounds(13)=Sound'WeaponSounds.BBulletImpact11'
00263    BulletSounds(14)=Sound'WeaponSounds.BBulletImpact12'
00264    BulletSounds(15)=Sound'WeaponSounds.BBulletImpact13'
00265    BulletSounds(16)=Sound'WeaponSounds.BBulletImpact14'
00266  }

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


Category Tutorial

The Unreal Engine Documentation Site

Wiki Community

Topic Categories

Recent Changes

Offline Wiki

Unreal Engine

Console Commands

Terminology

FAQs

Help Desk

Mapping Topics

Mapping Lessons

UnrealEd Interface

UnrealScript Topics

UnrealScript Lessons

Making Mods

Class Tree

Modeling Topics

Chongqing Page

Log In