Worst-case scenario: the UEd Goblin wipes the map and burns down your house.

Legacy:Replication Examples/Giant Spider Execution

From Unreal Wiki, The Unreal Engine Documentation Site
Jump to: navigation, 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[edit]

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[edit]

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.

  1. //=============================================================================
  2. // JBGiantSpiderMine
  3. // Copyright (c) 2004 by Wormbo <spamtheworm@koehler-homepage.de>
  4. //
  5. // A standalone version of the parasite mine.
  6. //=============================================================================
  7.  
  8.  
  9. class JBGiantSpiderMine extends Actor
  10.   placeable;
  11.  
  12.  
  13. //=============================================================================
  14. // Imports
  15. //=============================================================================
  16.  
  17. #exec obj load file=..\Textures\XGameShaders.utx
  18. #exec obj load file=..\Sounds\WeaponSounds.uax
  19.  
  20.  
  21. //=============================================================================
  22. // Properties
  23. //=============================================================================
  24.  
  25. var(Events) edfindable array<JBInfoJail> AssociatedJails; // players in these jails will be killed by the explosion
  26. var(Events) name PreExplosionEvent;    // the event used to switch the camera view
  27. var() float PreSpawnDelay;             // a delay between getting triggered and setting bHidden=false
  28. var() float PreExplosionDelay;         // a delay between triggering PreExplosionEvent and Event
  29. var() float ExplosionDelay;            // the delay from getting triggered to exploding
  30. var() Material SpawnOverlayMaterial;   // the overlay material to display after spawning
  31. var() float SpawnOverlayTime;          // the time, the overlay is displayed
  32. var() float MomentumTransfer;          // amount of momentum applied when damagin players (so gibs fly around :P)
  33. var() class<DamageType> MyDamageType;  // the damage type to use for killing players
  34. var(Sounds) array<Sound> BulletSounds; // sounds played back when shots hit the (invulnerable) spider
  35.  
  36.  
  37. //=============================================================================
  38. // Variables
  39. //=============================================================================
  40.  
  41. var name IdleAnims[4];        // animations are randomly played before exploding (animations handle the sounds)
  42. var float ExplosionCountdown; // counts down from ExplosionDelay to 0
  43. var bool bPreExplosion;       // tells, whether PreExplosionEvent was already triggered
  44.  
  45.  
  46. //== EncroachingOn ============================================================
  47. /**
  48. Telefrag players blocking the spawn point.
  49. */
  50. //=============================================================================
  51.  
  52. event bool EncroachingOn(Actor Other)
  53. {
  54.   if ( Pawn(Other) != None )
  55.     Pawn(Other).GibbedBy(Self);
  56.  
  57.   return Super.EncroachingOn(Other);
  58. }
  59.  
  60.  
  61. //== state Sleeping ===========================================================
  62. /**
  63. Wait hidden and non-colliding until triggered.
  64. */
  65. //=============================================================================
  66.  
  67. simulated state Sleeping
  68. {
  69.   function Trigger(Actor Other, Pawn EventInstigator)
  70.   {
  71.     local JBInfoJail thisJail;
  72.     local int i;
  73.     local PlayerReplicationInfo PRI;
  74.     local JBTagPlayer TagPlayer;
  75.     local Pawn thisPawn;
  76.  
  77.     if ( AssociatedJails.Length == 0 ) { // not associated with any jails, try to find matching jails
  78.       foreach AllActors(class'JBInfoJail', thisJail) {
  79.         if ( thisJail.ContainsActor(Self) ) {
  80.           AssociatedJails[0] = thisJail;
  81.           break;
  82.         }
  83.       }
  84.       if ( AssociatedJails.Length == 0 ) {
  85.         // no associated jails found, associate with all jails
  86.         log("!!!!" @ Self @ "not associated with any jails!", 'Warning');
  87.         foreach AllActors(class'JBInfoJail', thisJail) {
  88.           AssociatedJails[0] = thisJail;
  89.         }
  90.       }
  91.     }
  92.  
  93.     // check if we actually have someone in this jail
  94.     foreach DynamicActors(class'PlayerReplicationInfo', PRI) {
  95.       TagPlayer = class'JBTagPlayer'.static.FindFor(PRI);
  96.       if ( TagPlayer != None && TagPlayer.IsInJail() && TagPlayer.GetPawn() != None ) {
  97.         thisJail = TagPlayer.GetJail();
  98.         thisPawn = TagPlayer.GetPawn();
  99.         for (i = 0; i < AssociatedJails.Length; ++i) {
  100.           if ( thisJail == AssociatedJails[i] ) {
  101.             // prisoner found, now spawn
  102.             NetUpdateTime = Level.TimeSeconds - 1; // force replication right now
  103.             bClientTrigger = !bClientTrigger;
  104.             GotoState('Spawning');
  105.             return;
  106.           }
  107.         }
  108.       }
  109.     }
  110.   }
  111.  
  112.   simulated event ClientTrigger()
  113.   {
  114.     GotoState('Spawning');
  115.   }
  116.  
  117. Begin:
  118.   bHidden = True;
  119.   SetCollision(False, False, False);
  120. }
  121.  
  122.  
  123. //== TakeDamage ===============================================================
  124. /**
  125. Play sound effects for bullet hits.
  126. */
  127. //=============================================================================
  128.  
  129. event TakeDamage(int Damage, Pawn EventInstigator, vector HitLocation, vector Momentum, class<DamageType> DamageType)
  130. {
  131.   if ( !bHidden && DamageType != None && DamageType.Default.bBulletHit && BulletSounds.Length > 0 )
  132.     PlaySound(BulletSounds[Rand(BulletSounds.Length)], SLOT_None, 2.0, False, 100);
  133. }
  134.  
  135.  
  136. //== state Spawning ===========================================================
  137. /**
  138. Play a spawn effect.
  139. */
  140. //=============================================================================
  141.  
  142. simulated state Spawning
  143. {
  144. Begin:
  145.   if ( PrespawnDelay > 0 )
  146.     Sleep(PrespawnDelay); // wait until external spawn effect is over
  147.   bHidden = False;
  148.   SetCollision(True, True);
  149.   SetLocation(Location);  // "telefrag" players at this location
  150.   if ( SpawnOverlayTime > 0 && SpawnOverlayMaterial != None )
  151.     SetOverlayMaterial(SpawnOverlayMaterial, SpawnOverlayTime, True);
  152.   PlayAnim('Startup', 1.0);
  153.   FinishAnim();
  154.   GotoState('Waiting');
  155. }
  156.  
  157.  
  158. //== state Waiting ============================================================
  159. /**
  160. Spider idles a bit before detonating.
  161. */
  162. //=============================================================================
  163.  
  164. simulated state Waiting
  165. {
  166.   simulated function Timer()
  167.   {
  168.     local JBInfoJail thisJail;
  169.     local int i;
  170.     local PlayerReplicationInfo PRI;
  171.     local JBTagPlayer TagPlayer;
  172.     local Pawn thisPawn;
  173.  
  174.     ExplosionCountdown -= 0.1;
  175.     if ( !bPreExplosion && ExplosionCountdown <= PreExplosionDelay ) {
  176.       // trigger the pre-explosion event (camera switch)
  177.       bPreExplosion = True;
  178.       TriggerEvent(PreExplosionEvent, Self, None);
  179.     }
  180.     if ( ExplosionCountdown <= 0 ) {
  181.       SetTimer(0.0, False);
  182.       TriggerEvent(Event, Self, None);
  183.  
  184.       if ( Role == ROLE_Authority ) {
  185.         foreach DynamicActors(class'PlayerReplicationInfo', PRI) {
  186.           TagPlayer = class'JBTagPlayer'.static.FindFor(PRI);
  187.           if ( TagPlayer != None && TagPlayer.IsInJail() && TagPlayer.GetPawn() != None ) {
  188.             thisJail = TagPlayer.GetJail();
  189.             thisPawn = TagPlayer.GetPawn();
  190.             for (i = 0; i < AssociatedJails.Length; ++i) {
  191.               if ( thisJail == AssociatedJails[i] ) {
  192.                 thisPawn.TakeDamage(1000, None, thisPawn.Location, MomentumTransfer * Normal(thisPawn.Location - Location) * 1000 / VSize(thisPawn.Location - Location), MyDamageType);
  193.                 if ( thisPawn.Health > 0 )
  194.                   thisPawn.Died(None, MyDamageType, thisPawn.Location);
  195.                 break;
  196.               }
  197.             }
  198.           }
  199.         }
  200.       }
  201.       GotoState('Sleeping');
  202.     }
  203.  
  204.   }
  205.  
  206. Begin:
  207.   ExplosionCountdown = ExplosionDelay;
  208.   bPreExplosion = False;
  209.   SetTimer(0.1, True);
  210.   while (True) {
  211.     PlayAnim('Idle', 1.0, 0.3);
  212.     FinishAnim();
  213.     PlayAnim(IdleAnims[Rand(ArrayCount(IdleAnims))], 1.0, 0.3);
  214.     FinishAnim();
  215.   }
  216. }
  217.  
  218.  
  219. //=============================================================================
  220. // Default properties
  221. //=============================================================================
  222.  
  223. defaultproperties
  224. {
  225.   DrawType=DT_Mesh              // The mesh used for this actor is a special version of the
  226.   Mesh=CollidingSpiderMineMesh  // Onslaught parasite mine mesh, that has sound notifications
  227.   bUseCylinderCollision=False   // and collision boxes matching the spider's size and shape.
  228.   bEdShouldSnap=True
  229.   bProjTarget=True              // shots should hit the spider
  230.   CollisionHeight=60.0          // These dimensions help placing
  231.   CollisionRadius=150.0         // the spider in Unrealed.
  232.   IdleAnims(0)=Clean
  233.   IdleAnims(1)=Look
  234.   IdleAnims(2)=Bob
  235.   IdleAnims(3)=FootTap
  236.   DrawScale=1.5
  237.   bUseDynamicLights=True
  238.   bDramaticLighting=True
  239.   RemoteRole=ROLE_SimulatedProxy    // The spider should be replicated to clients.
  240.   InitialState=Sleeping             // the startup state
  241.   SpawnOverlayMaterial=VehicleSpawnShaderRed
  242.   SpawnOverlayTime=2.0
  243.   PreSpawnDelay=2.0
  244.   PreExplosionDelay=1.0
  245.   ExplosionDelay=5.0
  246.   MomentumTransfer=100000.0
  247.   MyDamageType=DamTypeONSMine
  248.   SurfaceType=EST_Metal         // for players walking on the spider and shots hitting it
  249.   BulletSounds(0)=Sound'WeaponSounds.BBulletReflect1'
  250.   BulletSounds(1)=Sound'WeaponSounds.BBulletReflect2'
  251.   BulletSounds(2)=Sound'WeaponSounds.BBulletReflect3'
  252.   BulletSounds(3)=Sound'WeaponSounds.BBulletReflect4'
  253.   BulletSounds(4)=Sound'WeaponSounds.BBulletImpact1'
  254.   BulletSounds(5)=Sound'WeaponSounds.BBulletImpact2'
  255.   BulletSounds(6)=Sound'WeaponSounds.BBulletImpact3'
  256.   BulletSounds(7)=Sound'WeaponSounds.BBulletImpact4'
  257.   BulletSounds(8)=Sound'WeaponSounds.BBulletImpact5'
  258.   BulletSounds(9)=Sound'WeaponSounds.BBulletImpact6'
  259.   BulletSounds(10)=Sound'WeaponSounds.BBulletImpact7'
  260.   BulletSounds(11)=Sound'WeaponSounds.BBulletImpact8'
  261.   BulletSounds(12)=Sound'WeaponSounds.BBulletImpact9'
  262.   BulletSounds(13)=Sound'WeaponSounds.BBulletImpact11'
  263.   BulletSounds(14)=Sound'WeaponSounds.BBulletImpact12'
  264.   BulletSounds(15)=Sound'WeaponSounds.BBulletImpact13'
  265.   BulletSounds(16)=Sound'WeaponSounds.BBulletImpact14'
  266. }

How Does It Work?[edit]

Before We Start[edit]

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[edit]

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[edit]

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[edit]

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[edit]

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[edit]