Sunday, May 1, 2011

D&D BattleMap Using the UDK - Special Effects

Our battlemap is working fairly well at this point.  But unless our heroes happen upon one of our prescripted triggers, it's pretty static.  Let's add some special effects that we can toss out during combat for fun.

We're going to expand on what we learned about creating torches (extending an existing game object) and extend our own objects.

BattleMapEmitterEffect.uc

Let's start with a basic fireball.

Our torch was based on the EmitterSpawnable object, and extended to include movement and interactivity.  This is a similar object extended from EmitterSpawnable, but we're only adding two features: a self-destruct timer (we only want it alive for a few seconds) and a camera shake.  However, we've also made many of the values parameters so that they can be extended:

class BattleMapEmitterEffect extends EmitterSpawnable;
var float EffectLifespan;
var float EffectStarted;
var float ShakeScale;
var float OscillationDur;
var float PitchAmp;
var float PitchFreq;
var float YawAmp;
var float YawFreq;
var float RollAmp;
var float RollFreq;

simulated function PostBeginPlay()
{
 EffectStarted = WorldInfo.TimeSeconds;
 DoCameraEffects();
}

function Tick( float DeltaTime )
{
 if ((WorldInfo.TimeSeconds - EffectStarted) > EffectLifespan)
  self.Destroy();
}

function DoCameraEffects()
{
 local CameraShake Shake;
 local BattleMapPlayerController PC;

 foreach WorldInfo.LocalPlayerControllers(class'BattleMapPlayerController', PC)
 {
  if (PC.PlayerCamera != None)
  {
   Shake = new class'CameraShake';
   Shake.OscillationDuration = OscillationDur;
   Shake.RotOscillation.Pitch.Amplitude = PitchAmp;
   Shake.RotOscillation.Pitch.Frequency = PitchFreq;
   Shake.RotOscillation.Yaw.Amplitude = YawAmp;
   Shake.RotOscillation.Yaw.Frequency = YawFreq;
   Shake.RotOscillation.Roll.Amplitude = RollAmp;
   Shake.RotOscillation.Roll.Frequency = RollFreq;
   PC.ClientPlayCameraShake(Shake, ShakeScale, false);
  }
 }
}

DefaultProperties
{
 EffectLifespan=3.0
 ShakeScale=1.0
 OscillationDur=1.0
 PitchAmp=20.0
 PitchFreq=40.0
 YawAmp=20.0
 YawFreq=30.0
 RollAmp=20.0
 RollFreq=50.0

 DrawScale=3.000000

 Begin Object Name=ParticleSystemComponent0
        Template=ParticleSystem'Envy_Effects.Particles.P_VH_Gib_Explosion'
        ReplacementPrimitive=None
        LightingChannels=(bInitialized=True,Dynamic=True)
  LODLevel=1
 End Object
 ParticleSystemComponent=ParticleSystemComponent0

   RemoteRole=ROLE_SimulatedProxy
   bAlwaysRelevant=true
   bReplicateMovement=true
   bNetTemporary=true
   bNoDelete=false
}

When we spawn this with a keystroke, it will throw down a screen shaking fireball wherever we're pointing.

With this as a base, creating other effects is simple.  We extend our own object and swap out the particle system template.  Below are three more effects: a force ball, a lightning bolt, and for the ranger in our group who fires Tomahawk cruise missiles instead of arrows, an earth-shattering nuclear explosion.

BattleMapEmitterEffectForce.uc
class BattleMapEmitterEffectForce extends BattleMapEmitterEffect;
DefaultProperties
{
 DrawScale=1.000000

 Begin Object Name=ParticleSystemComponent0
  Template=ParticleSystem'P_WP_ShockRifle_Explo'
 End Object
}
BattleMapEmitterEffectLightning.uc

class BattleMapEmitterEffectLightning extends BattleMapEmitterEffect;
DefaultProperties
{
 DrawScale=2.000000
 Rotation=(Pitch=16384,Yaw=0,Roll=0)

 Begin Object Name=ParticleSystemComponent0
  Template=ParticleSystem'PS_Scorpion_Gun_Impact'
 End Object
}
BattleMapEmitterEffectNuke.uc

class BattleMapEmitterEffectNuke extends BattleMapEmitterEffect;
DefaultProperties
{
 PitchAmp=150.0
 PitchFreq=40.0
 YawAmp=75.0
 YawFreq=30.0
 RollAmp=150.0
 RollFreq=50.0

 Begin Object Name=ParticleSystemComponent0
  Template=ParticleSystem'P_VH_Death_SpecialCase_1_Base_Near'
 End Object
}

BattleMapPlayerInput.uc

We already have a function for spawning objects (so far, just the torch) so we just need to add our new effects to BMSpawn():

exec function BMSpawn(string ObjectToSpawn)
{
 if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)
 {
  switch(ObjectToSpawn)
  {
   case "Torch":
    Spawn(class'BattleMapTorch',Outer,,MouseOrigin,,,true );
    break;
   case "Fireball":
    Spawn(class'BattleMapEmitterEffect',Outer,,MouseOrigin );
    break;
   case "Force":
    Spawn(class'BattleMapEmitterEffectForce',Outer,,MouseOrigin );
    break;
   case "Lightning":
    Spawn(class'BattleMapEmitterEffectLightning',Outer,,MouseOrigin );
    break;
   case "Nuke":
    Spawn(class'BattleMapEmitterEffectNuke',Outer,,MouseOrigin );
    break;
  }
 }
}
DefaultInput.ini

Finally, bind a key to the spawn function just like we did the torch:

-Bindings=(Name="F1",Command="GBA_ShowScores")
-Bindings=(Name="F2",Command="GBA_ShowMap")
-Bindings=(Name="F3",Command="GBA_ToggleMinimap")
.Bindings=(Name="F1",Command="BMSpawn Fireball")
.Bindings=(Name="F2",Command="BMSpawn Force")
.Bindings=(Name="F3",Command="BMSpawn Lightning")
.Bindings=(Name="F4",Command="BMSpawn Nuke")

BattleMapBlood.uc

We also need a way to mark creatures that have been "bloodied" during battle.  The UDK has the perfect system for this: Decals.  We'll create our own Decal actor to "spray" blood on the map.  But first, we need some blood.

For simplicity's sake, I made a copy of the Bio_Splat_Decal_001 DecalMaterial (I liked the "wet" effect) and saved it to my BattleMapAssets package.  Then I inserted a "Multiply" at the end to make it red instead of green:


Our extended DecalActorMovable object below has two extra features: it ensures the decal is always sprayed straight "down", and it adds a collision component so that we can detect it with the mouse:

class BattleMapBlood extends DecalActorMovable;
simulated function PostBeginPlay()
{
 local Rotator newRot;

 newRot.Pitch = -16384;
 newRot.Yaw = Rand(65536);
 newRot.Roll = -65536;
 SetRotation(newRot);
}

DefaultProperties
{
 Begin Object Name=NewDecalComponent
  DecalMaterial=DecalMaterial'BattleMapAssets.Materials.Blood_Decal'
 End Object

 Begin Object Class=CylinderComponent Name=CollisionCylinder
  CollisionRadius=+0064.000000
  CollisionHeight=+0064.000000
  BlockNonZeroExtent=true
  BlockZeroExtent=true
  BlockActors=true
  CollideActors=true
 End Object
 CollisionComponent=CollisionCylinder
 Components.Add(CollisionCylinder)

   RemoteRole=ROLE_SimulatedProxy
   bAlwaysRelevant=true
   bReplicateMovement=true
   bNetTemporary=false
   bCanBeDamaged = false
   bCollideActors = true
   bBlockActors = true
   bCollideWorld = false
   bStatic=false
   bNoDelete=false
   bMovable=true
}

Since we're only using a single decal, the PostBeginPlay() function also randomly rotates the blood so that the same splat doesn't look mirrored all over the battle field.  You could also randomly swap out the Material if you had several blood textures.

Don't forget to add the reference to our BattleMapPlayerInput.BMSpawn() function:

   case "Blood":
    Spawn(class'BattleMapBlood',Outer,,MouseOrigin );
    break;
And bind a key to it in DefaultInput.ini:

-Bindings=(Name="B",Command="GBA_ToggleSpeaking")
.Bindings=(Name="B",Command="BMSpawn Blood")
BattleMapPlayerInput.uc

To wrap it up, we need a way to clean up our mess and delete torches and blood that we've spawned.  We already know how to determine what object we're pointing at, so we just call that object's Destroy() function.  Add the following function to BattleMapPlayerInput:

exec function BMDeleteObject()
{
 local BattleMapTorch To;
 local BattleMapBlood Bl;

 if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)
 {
  switch(ParseObjectName(ObjectUnderMouse))
  {
   case "Torch":
    foreach DynamicActors(class'BattleMapTorch', To)
     if (To.Name == ObjectUnderMouse)
     {
      To.Destroy();
     }
    break;
   case "Blood":
    foreach DynamicActors(class'BattleMapBlood', Bl)
     if (Bl.Name == ObjectUnderMouse)
     {
      Bl.Destroy();
     }
    break;
  }
 }
}
Add bind a key to BMDeleteObject() in DefaultInput.ini:
.Bindings=(Name="Delete",Command="BMDeleteObject")
Now, you can litter the battlefield with torches, blood and explosions while snuffing out your player's precious light sources.


BattleMap mode source files

10 comments:

  1. Hey guys, completely amazing post, thanks so much.
    I managed to get everything working except for the blood decal, and i cant understand why it isnt calling it, the rest of the effects and delete function work just fine, i save the blood decal with the right name in the right place, but still it wont appear when i press B, any ideas what could i had missed? (i changed the Input file too)

    ReplyDelete
  2. just made a weird discovery, if i spawn a torch i can spawn blood, but directly under the torch and nowhere else

    ReplyDelete
  3. I'm getting the same problem, everything is fine except for spawning blood which only works if i have a torch. The blood then spawns under the torch.

    I love the nuke effect though, very cool.

    ReplyDelete
  4. i also noticed i can spawn blood near static meshes too, if the mouse tracker reads anything other than "world Info" is possible to spawn blood, but it doesnt always work.

    PS: the nuke effect will work lovely when the players roll a critic :D

    ReplyDelete
  5. Yes, I can spawn blood if I am hovering over meshes, tiggers or other objects (torch, blood).

    Oh and I fixed the offset mouse coordinates problem I was having with spawned objects and triggers. Objects would spawn down from the pointer location and triggers were shifted up and to the left. I had to disable the 'Constrain Aspect Ratio' setting for the Camera Actor to fix it. Now objects spawn directly under my pointer and triggers are located exactly where they are supposed to be. Thanks for the very cool effects.

    One question - could we have a keyboard toggle for the HUD information (top left corner), I can then use UDK's in built hi-res screen capture console commands to capture some nice images for VTT (FGII) play.

    ReplyDelete
  6. OK. I think I have found the bug with the Blood splatter, well its fixed it for me anyway.

    Change the following line in the DefaultProperties of the BattleMapBlood class, in BattleMapBlood.uc.

    bCollideWorld = true

    to

    bCollideWord = false

    ReplyDelete
  7. Sweet, thanks! I'll check back when I have it running.

    ReplyDelete
  8. Absolutely right, DrZeuss. Good catch! bCollideWorld should indeed be false for BattleMapBlood. That's what I get for testing on a terrain layer instead of a BSP.

    ReplyDelete
  9. Oh right, thanks a lot mate, its fixed for me too, now i can go all Tarantino about it :D

    ReplyDelete
  10. I had a test run of the BattleMap yesterday with a few friends, it went down a storm, particularly the special effects I was able to throw in during the combat encounters. :) Thank you once again, may your dice rolls always be 20.

    One request, would it be possible to lock the scale and alignment of the grid once you have it initially setup? I only ask as zooming in/out of the maps (using C and Spacebar) currently requires the DM to rescale and realign the grid, this can be a little cumbersome especially when using large maps. Once again, many thanks and looking forward to the next post.

    ReplyDelete