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 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:
- [Giant Spider Execution] (DivX required)
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.