Mostly Harmless
Legacy: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.
Contents
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:
- 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.
-
//=============================================================================
-
// JBGiantSpiderMine
-
// Copyright (c) 2004 by Wormbo <spamtheworm@koehler-homepage.de>
-
//
-
// A standalone version of the parasite mine.
-
//=============================================================================
-
-
-
class JBGiantSpiderMine extends Actor
-
placeable;
-
-
-
//=============================================================================
-
// Imports
-
//=============================================================================
-
-
#exec obj load file=..\Textures\XGameShaders.utx
-
#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'
-
}
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.