Sunday, May 29, 2011

D&D BattleMap Using the UDK - Something Old, Something New, Something Borrowed...

No new groundbreaking features this post.  We’re going to spend this time tinkering with what we have a bit.

First order of business is fixing BMInteractObject(), which selects (picks up and drags) placeable objects.  It appears to work well, except when clicking over both a map object and a HUD object.  In that case, it never lets go of the HUD object.  This is caused by a small bug.

Actually, the only reason it works at all is due to that bug.  The code is divided into two sections: a drop and a pickup.  In the drop code at the top, the switch statement checks for “CrossHairMovie” to see if the HUD has an object selected.  That is incorrect, as the code checks the name of the OBJECT not the name of the CLASS.  That causes the drop section to always fail for the HUD, which is actually a good thing.  The code then falls to the pickup section, which calls the HUD’s InteractObject() again if a map object was not selected.

If we fix the reference in the drop section, another problem appears: the cursors never lets go of any HUD object.  The HUD’s InteractObject() is self-contained with its own drop and pickup code, including a special clause to ensure it doesn’t pick up the same thing it dropped.  If we try to drop and pick up in two separate calls, it always picks up what it dropped.

The easy answer was to move the call to the HUD’s InteractObject() to the top and always do it first. That’s less than ideal however, since HUD objects overlay map objects.  We had to move a HUD object out of the way in order to click on a map object.  So, the HUD’s InteractObject() call needs to be at the end.  But what if we clicked on a map object?  We passed a parameter to InteractObject() to only drop what’s selected and not select anything new.

Then it worked!  And created a new problem.  If we moved a map object under a HUD object, or a HUD object directly over a map object, and clicked to let go it would hot-swap between the two.  We couldn’t let go unless there was nothing else to select.

The final solution is to simply do one or the other.  Swapping was an interesting bit of code, but simply not feasible during gameplay.  Here’s the final BMInteractObject():

BattleMapPlayerInput.uc
exec function BMInteractObject()
{
 local BattleMapTorch To;
 local Trigger Tr;
 local int i, j, k;

 if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)
 {
  // stop doing whatever the currently selected object is doing
  if (ObjectSelected != none)
  {
   switch(ParseObjectName(ObjectSelected.Name))
   {
    case "Torch":
     To = BattleMapTorch(ObjectSelected);
     To.StopTorch(MouseOrigin);
     break;
    case "GfxHud":
     BattleMapHUD(myHUD).CrossHairMovie.CallInteractObject(false);
     break;
   }
   ObjectSelected = none;
  }
  else
  {
   //start doing something with a newly selected object
   switch(ParseObjectName(ObjectUnderMouse))
   {
    case "Torch":
     foreach DynamicActors(class'BattleMapTorch', To)
      if (To.Name == ObjectUnderMouse)
      {
       ObjectSelected = To;
       To.MoveTorch();
      }
     break;
    case "Trigger":
     // activate each Kismet event attached to this trigger
     foreach DynamicActors(class'Trigger', Tr)
      if (Tr.Name == ObjectUnderMouse)
       for (i=0; i<Tr.GeneratedEvents.Length; i++)
        for (j=0; j<Tr.GeneratedEvents[i].OutputLinks.Length; j++)
         for (k=0; k<Tr.GeneratedEvents[i].OutputLinks[j].Links.Length; k++)
          Tr.GeneratedEvents[i].OutputLinks[j].Links[k].LinkedOp.ForceActivateInput(Tr.GeneratedEvents[i].OutputLinks[j].Links[k].InputLinkIdx);
     break;
   }
//if no map object found, check for a HUD object
   if (ObjectSelected == none)
    if(BattleMapHUD(myHUD).CrossHairMovie.CallInteractObject(true) > 0)
     ObjectSelected = BattleMapHUD(myHUD).CrossHairMovie;
  }
 }
}
BattleMapGfxHud.uc

Add a parameter to CallInteractObject:
function int CallInteractObject(bool Param1)
ActionScript

In InteractObject(), update the definition to include the new parameter:
function InteractObject(Param1:Boolean)
And preface the loop that checks for objects under the mouse with the passed parameter:
 //find new object to interact with
 if (Param1)
  for (var MousedObject in _root)
Finally, update the call in DeleteObject() to include the parameter:
 InteractObject(true);

Now, let’s take what we’ve done with Zones and create map tokens for our monsters and players.  Even though we place miniatures on the table during gameplay, it is still helpful to represent creatures within the battlemap for several reasons:
  • The DM doesn’t have to move around the table to reposition minis.  He can move a token with a click then ask a player (or demand, depending on the DM) to move the miniature.
  • The DM can easily see the position of creatures without moving around for a better view.  I can’t tell you how many times I’ve declared an invalid opportunity attack by an NPC, sparking a bewildered debate among the players.  Or missed an opportunity attack on players, all due to my skewed view from the end of the table.
  • The minis are lit by whatever texture they happen to be standing on.  A white token would “spotlight” the minis, despite standing on black rock, yellow stone or green goo.
Flash
Create a new token symbol within Flash.  I simply made a 256x256 white circle with a black border.  Don’t forget to check “Export for ActionScript”:
Next, add a block of Dynamic Text with a “P1” placeholder.  Give it a variable name of “TokenText”:
You will also need to Embed the font (button next to Style).  I chose only Uppercase and Numerals.
ActionScript
Add new global variables to the top:
var DefaultZoneScale:Number = 70;
var DefaultTokenScale:Number = 50;
var Tokens:Number = 0;
Add a “Token” case to the Spawn() function:
  case "Token":
   SpawnToken();
   break;
Add the new SpawnToken() function below.  It’s a separate function like SpawnZone() just to be consistent, though it didn’t need to be.  SpawnZone() had additional parameters because the SwapObject() function replaced a zone by spawning a new zone with the same properties as the old one.  When we swap a token, we’ll simply be changing the text.

For now, we just assign a counter to the token text.  When we implement a pop-up UI within the HUD for manipulating objects, we'll pass in typed text.  For now, just note the P# or M# next to the creature in your combat tracker.

We’re also ensuring that the cursor is always on top.  Since a token will never need to remember what layer it was on (like swapping a zone) we just grab the highest available depth.  That, of course, places it over the cursor so we need to bump that as well.
function SpawnToken()
{
 Tokens += 1;
 var t1 = _root.attachMovie("TokenSymbol", "Tokn" + Tokens, _root.getNextHighestDepth());
 t1._x = _root._xmouse;
 t1._y = _root._ymouse;
 t1._xscale = DefaultTokenScale;
 t1._yscale = DefaultTokenScale;
 t1.TokenText = "M" + Tokens;

 CursorInst.swapDepths(_root.getNextHighestDepth());
}
Now, update the InteractObject() function so it will grab tokens as well.  We want it to work exactly the same as selecting a Zone, so we simply need to update the case statement:
     case "Zone":
     case "Tokn":
      //find zones/tokens, but not the one we had clicked on
The same is true for the ModifyObject() function.  Modifying a token will manipulate its scale just like a zone.  However, we want to implement a new feature while we’re at it.  If we size a token to a 1x1 square on our battlemap, it is highly likely that additional tokens will need to be that size as well.  We already used the DefaultTokenScale global variable when spawning a new token, so we need to set that value here when modifying an object:
   case "Zone":
    _root[ObjectSelected]._xscale *= (1 + (0.1 * Param1));
    _root[ObjectSelected]._yscale = _root[ObjectSelected]._xscale;
    DefaultZoneScale = _root[ObjectSelected]._xscale;
    break;
   case "Tokn":
    _root[ObjectSelected]._xscale *= (1 + (0.1 * Param1));
    _root[ObjectSelected]._yscale = _root[ObjectSelected]._xscale;
    DefaultTokenScale = _root[ObjectSelected]._xscale;
    break;
The SwapObject() function will simply update the text on the token, alternating between “P#” (PC) and “M#” (monster):
   case "Tokn":
    switch(_root[ObjectSelected].TokenText.substr(0, 1))
    {
     case "P":
      _root[ObjectSelected].TokenText = "M" + _root[ObjectSelected].TokenText.substr(1);
      break;
     case "M":
      _root[ObjectSelected].TokenText = "P" + _root[ObjectSelected].TokenText.substr(1);
      break;
    }
    break;
Finally, let’s implement the same default scaling for zones.  We've already done most of the work along the way.  In SpawnZone(), change the ZoneScale variable to:
 var ZoneScale = (Param5 != undefined) ? Param5 : DefaultZoneScale;
BattleMapPlayerInput.uc

Add a new case to BMSpawn():
   case "Token":
    BattleMapHUD(myHUD).CrossHairMovie.CallSpawn("Token");
DefaultInput.ini

Add a new key binding:
.Bindings=(Name="N",Command="BMSpawn Token")

Let’s enhance our blood decal a bit.  We’ll borrow the same movement functionality as our torch and apply it to the blood decal.

BattleMapBlood.uc

Insert the following block into the BattleMapBlood class between the declaration and PostBeginPlay().  This code is nearly identical to the Torch code (we changed the names and removed the VerticalOffset that the torch light required):
var repnotify Vector BloodLoc,BloodStopLoc;
replication
{
 if (bNetDirty)
  BloodLoc,BloodStopLoc;
}

// called on clients when BloodStopLoc gets replicated
simulated event ReplicatedEvent( name VarName )
{
 if (VarName == nameof(BloodLoc) )
  SetLocation( BloodLoc );
 else if (VarName == nameof(BloodStopLoc) )
  StopBlood( BloodStopLoc );
 else
  super.ReplicatedEvent( VarName );
}

// this function should be run on both clients and server
simulated function MoveBlood( )
{
 bCollideWorld = false;
 SetCollision(false, false);
 GotoState( 'BloodMoving' );
}

simulated function StopBlood( Vector NewBloodStopLoc )
{
 bCollideWorld = true;
 SetCollision(true, true);
 GotoState('BloodStopped');

 BloodStopLoc = NewBloodStopLoc;
 SetLocation(NewBloodStopLoc);
}

simulated state BloodMoving
{
 simulated event Tick( float DeltaTime )
 {
  // move the blood...
  BloodLoc = BattleMapPlayerController(Owner).MouseOrigin;
  SetLocation(BloodLoc);
 }
}

simulated state BloodStopped
{
 simulated event Tick( float DeltaTime )
 {
  // stop the blood...
 }
}
BattleMapPlayerInput.uc

In BMInteractObject(), add a new variable:
 local BattleMapBlood Bl;
Add a blood case to the “drop” switch statement:
    case "Blood":
     Bl = BattleMapBlood(ObjectSelected);
     Bl.StopBlood(MouseOrigin);
     break;
And a blood case to the “select” switch statement:
    case "Blood":
     foreach DynamicActors(class'BattleMapBlood', Bl)
      if (Bl.Name == ObjectUnderMouse)
      {
       ObjectSelected = Bl;
       Bl.MoveBlood();
      }
     break;
Next, we’ll borrow the same sizing functionality as our zones and tokens.  Add the following new variable to the top:
var float DefaultBloodSize;
In BMModifyObject(), add a new local variable:
 local BattleMapBlood B;
And a blood case to the switch statement.  We’ll manipulate the decal’s size:
    case "BattleMapBlood":
     B = BattleMapBlood(ObjectSelected);
     B.Decal.Width = B.Decal.Width * (1 + (0.1 * Modifier));
     B.Decal.Height = B.Decal.Height * (1 + (0.1 * Modifier));
     DefaultBloodSize = B.Decal.Width;
In BMSpawn (), add a new local variable:
 local BattleMapBlood Bl;
Next, update the “Blood” case in the switch statement to resize after spawning.  The call to SetLocation() is simply a dirty way to force a replication:
   case "Blood":
    Bl = Spawn(class'BattleMapBlood',Outer,,MouseOrigin );
    Bl.Decal.Width = DefaultBloodSize;
    Bl.Decal.Height = DefaultBloodSize;
    Bl.SetLocation(MouseOrigin);
    break;
Finally, set the DefaultBloodSize in DefaultProperties:
 DefaultBloodSize = 200f
Now our HUD interactivity is fixed, we have new creature tokens and we can manipulate our blood markers:



BattleMap mode source files

Sunday, May 15, 2011

D&D BattleMap Using the UDK – Placeable Zones

Normally I wait until the end for the big “reveal”, but this is too cool not to show off:


Our first implementation of zones involved creating a plane object with a scripted texture.  Color and text were passed to the object which would draw the text on a canvas and apply it to a Material Instance.  And of course, it had the same positioning code as the torch.

However now that we’re making use of UDK’s ScaleForm UI, I thought we could pull off some pretty spectacular effects with Flash.  We’re not including text (yet), but the tradeoff is worth it.  Like the grid, it takes a LOT less code to implement the zones in Flash.

We’ll continue to use our existing BMSpawn, BMInteract, BMModify and BMDelete functions to start things off, but all the work will done in similar functions in ActionScript.  Essentially, we’ll blindly “toss up” the commands to ActionScript and let it handle them with its own Spawn, Interact, Modify, etc.

First, let’s update our UnrealScript functions.

BattleMapGfxHud.uc

Add the following wrapper functions to BattleMapGfxHud.  Note that most of these do not expect a return value.  We’re just forwarding the command along.  The UnrealScript code doesn’t care how ActionScript handles it.  Except for InteractObject(), and all we need to know there is if we actually did click on something.
function CallSpawn(string Param1)
{
 ActionScriptVoid("Spawn");
}

function int CallInteractObject()
{
 local int ObjectFound;
 ObjectFound = ActionScriptInt("InteractObject");
 return ObjectFound;
}

function CallModifyObject( int Param1 )
{
 ActionScriptVoid("ModifyObject");
}

function CallDeleteObject()
{
 ActionScriptVoid("DeleteObject");
}

function CallSwapObject( int Param1 )
{
 ActionScriptVoid("SwapObject");
}
BattleMapPlayerInput.uc

We need to extend our existing functions for new “zone” objects.  In BMSpawn(), add this to our switch statement:
   case "Zone":
    BattleMapHUD(myHUD).CrossHairMovie.CallSpawn("Zone");
    break;

The BMInteractObject() function tracks what we selected.  In this case, we don’t necessarily know the specific item since ActionScript is handling it.  So, we’ll just track that our CrossHairMovie object is selected since we know something in there is.  (Yes, in hindsight, “CrossHairMovie” probably wasn’t the most appropriate name for that.  Note to self: fix that later…)  In the first switch statement which stops selecting, add:
    case "CrossHairMovie":
     BattleMapHUD(myHUD).CrossHairMovie.CallInteractObject();
     ObjectSelected = none;
     break;

After the next switch statement that determines what we selected, add this CallInteractObject() check.  If ActionScript tells us that it selected something, then we track that the CrossHairMovie has something:
  if(BattleMapHUD(myHUD).CrossHairMovie.CallInteractObject() > 0)
  {
   ObjectSelected = BattleMapHUD(myHUD).CrossHairMovie;
  }
In the BMModifyObject() switch statement, pass the modifier along:
    case "BattleMapGfxHu":
     BattleMapHUD(myHUD).CrossHairMovie.CallModifyObject(Modifier);
     break;

Yes, the case parameter is correct.  The switch statement checks the first 14 characters of the object type, not the name.

Replace the entire BMDeleteObject() function with the code below.  We need to add an “object deleted?” check throughout the function.  Since it would be easier to click inside a large zone than a torch, we want to track if we actually deleted an UnrealScript object first so that we don’t accidently delete multiple items in both UnrealScript and ActionScript.  If nothing was found, then we pass the command along:
exec function BMDeleteObject()
{
 local BattleMapTorch To;
 local BattleMapBlood Bl;
 local bool ObjectDeleted;

 if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)
 {
  ObjectDeleted = false;
  switch(ParseObjectName(ObjectUnderMouse))
  {
   case "Torch":
    foreach DynamicActors(class'BattleMapTorch', To)
     if (To.Name == ObjectUnderMouse)
     {
      To.Destroy();
      ObjectDeleted = true;
     }
    break;
   case "Blood":
    foreach DynamicActors(class'BattleMapBlood', Bl)
     if (Bl.Name == ObjectUnderMouse)
     {
      Bl.Destroy();
      ObjectDeleted = true;
     }
    break;
  }
  if (!ObjectDeleted)
   BattleMapHUD(myHUD).CrossHairMovie.CallDeleteObject();
 }
}
Finally, add a new BMSwapObject() function.  Our ModifyObject() function increases an item’s size, for example, but BMSwapObject() will exchange it for something else.  Like different blood decals, or a sunrod in place of a torch, or different types of zones:
exec function BMSwapObject(int Modifier)
{
 if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)
 {
  if (ObjectSelected != none)
  {
   switch(Left(ObjectSelected, 14))
   {
    case "BattleMapGfxHu":
     BattleMapHUD(myHUD).CrossHairMovie.CallSwapObject(Modifier);
     break;
   }
  }
 }
}

DefaultInput.ini

We need three more keybindings for spawning a zone and swapping an object:
.Bindings=(Name="Z",Command="BMSpawn Zone")
.Bindings=(Name="LeftBracket", Command="BMSwapObject -1")
.Bindings=(Name="RightBracket", Command="BMSwapObject 1")

Photoshop/Gimp

Now, something new.  The zones are three layered images rotating in different directions animated in Flash.  The runic circles themselves are brushes from Obsidian Dawn.  At the bottom of their page, they provide instructions for importing and using the brushes.  Essentially, we:
  • Created three layers in a transparent image
  • Chose a color
  • Painted a large, medium and small runic circle on its own layer
  • Align the circles
  • Crop and resize the image to a power of 2 (256x256, 512x512, etc.)
  • Save as PNG into the .\UDKGame\Flash\BMHud\BMHud folder along with the cursor
Flash

To get the circles animating, import the individual images into Flash.  Don’t forget to update the properties of each PNG file to “Lossless (PNG/GIF)” Compression, otherwise they won’t import into the UDK:

Next, create a new Symbol.  Just as in Photoshop/Gimp, create three layers and put a rune image in each later.  (If you lined them up correctly before cropping/saving earlier, then here you simply need to stack them perfectly on top of each other.  If not, you can still make fine adjustments here.)

Insert a frame way out on the timeline for however long you want the animation to run.  Ours goes 360 frames, which is 15 seconds at 24 fps.  We didn’t want to cause epileptic seizures with fast spinning circles.  Create a motion tween on each later, setting the number of rotations and the direction:


Once you’re done creating your runic circle, edit the symbol’s properties in the library:
  • Enable “Export for ActionScript”
  • Enable “Export in frame 1”
  • Enter an identifier.  Ours are CircleXXX.  (CircleBlue, CircleGreen, CircleRed, etc.)
ActionScript

Once you’re done creating all your circles, we just need to add object manipulation functions to ActionScript.  First, a simple Spawn function:
function Spawn(Param1:String)
{
 switch(Param1)
 {
  case "Zone":
   SpawnZone();
   break;
 }

}

Next, a function to create a zone.  This function will perform a double duty.  It will create a brand new zone, as well as replace an existing one.  It does that by creating a zone at the same level as an existing one.  (Flash allows one movieclip per level.)  So, the function accepts input parameters to predefine a zone’s color and shape, or sets defaults if none are provided.

To keep things simple, we’re also performing some slight-of-hand with the movieclip’s properties.  We need to track two things: which symbol this zone is using and what level it’s on.  Rather than creating a custom class to extend movieclip for two properties, we’re going to appropriate existing properties.  We’ll attach the level to the zone’s name (since both the level and the name need to be unique anyway, it seemed a good fit), and store the type of symbol in the taborder property.

“Wait, what?”  Yeah, that’s kinda tricky.  Flash doesn’t provide a property to determine what linkage type a symbol instance was created from.  So, we’ll store our linkage names in an array then save the individual index in the taborder property – also an integer value.  Which as it turns out, makes it easy to swap instances since we simply need to rotate the index number:
var Zones:Array = Array("CircleBlue", "CircleGreen", "CirclePurple", "CircleRed", "CircleYellow");
// Param1:Name, Param2:Index, Param3:x, Param4:y, Param5:scale
function SpawnZone(Param1:String, Param2:Number, Param3:Number, Param4:Number, Param5:Number)
{
 var ZoneName = (Param1 != undefined) ? Param1 : GetNextZone();
 var ZoneIndex = (Param2 != undefined) ? Param2 : Math.floor(Math.random() * Zones.length);
 var ZoneX = (Param3 != undefined) ? Param3 : _root._xmouse;
 var ZoneY = (Param4 != undefined) ? Param4 : _root._ymouse;
 var ZoneScale = (Param5 != undefined) ? Param5 : 70;
 var ZoneLevel = int(ZoneName.substr(4));

 var z1 = _root.attachMovie(Zones[ZoneIndex], ZoneName, ZoneLevel);
 z1._x = ZoneX;
 z1._y = ZoneY;
 z1.tabIndex = ZoneIndex;
 z1._xscale = ZoneScale;
 z1._yscale = ZoneScale;
}

function GetNextZone()
{
 var LastZone:Number = 1;
 for (var RootObject in _root)
  if (RootObject.substr(0, 4) == "Zone")
   LastZone = (int(RootObject.substr(4)) > LastZone) ? int(RootObject.substr(4)) : LastZone;

 return "Zone" + (LastZone + 1);
}
Implement the InteractObject() function to handle clicking an item.  This works exactly like its UnrealScript counterpart.  On each mouse click it first stops interacting with whatever may be currently selected, then tries to find a new item to select:
var ObjectSelected:String;
function InteractObject()
{
 var PrevObjectSelected:String;

 //stop Interacting with existing object
 if (ObjectSelected.length > 0)
 {
  DragObject("");
  PrevObjectSelected = ObjectSelected;
  ObjectSelected = "";
 }

 //find new object to interact with
 for (var MousedObject in _root)
 {
  if (_root[MousedObject].hitTest(_root._xmouse, _root._ymouse, false))
  {
   switch (_root[MousedObject]._name.substr(0, 4))
   {
    case "Zone":
     //find zones, but not the one we had clicked on
     if (_root[MousedObject]._name != PrevObjectSelected)
     {
      ObjectSelected = _root[MousedObject]._name;
     }
     break;
   }
  }
  if (ObjectSelected.length > 0)
  {
   DragObject(MousedObject);
   return 1;
  }
 }
 return 0;
}
When moving torches in UnrealScript, we set the object to either a moving or stationary state.  The moving state updated itself every tick to the mouse’s position.  Here, we can let Flash do all the work.  Just like our custom cursor, we tell Flash to drag around our symbol.  Flash will only drag one item at a time, so we hide our cursor symbol and reattach it when we’re not dragging something else:
function DragObject(ObjectDragging:String)
{
 if (ObjectDragging.length > 0)
 {
  CursorInst._visible = false;
  startDrag(ObjectDragging);
 } else {
  CursorInst._visible = true;
  startDrag("CursorInst", true);
 }
}

ModifyObject(), again, works just like the UnrealScript version.  It determines what’s selected, then takes an appropriate action.  For a zone, we modify the scale:
function ModifyObject(Param1:Number)
{
 if (ObjectSelected.length > 0)
 {
  switch(ObjectSelected.substr(0, 4))
  {
   case "Zone":
    _root[ObjectSelected]._xscale *= (1 + (0.1 * Param1));
    _root[ObjectSelected]._yscale = _root[ObjectSelected]._xscale;
    break;
  }
 }
}
DeleteObject() first calls InteractObject() to see if there’s something under the mouse to delete.  If so, delete it, then start dragging the cursor again:
function DeleteObject()
{
 InteractObject();
 if (ObjectSelected.length > 0)
 {
  _root[ObjectSelected].removeMovieClip();
  DragObject();
 }
}
Finally, a new function SwapObject().  This will swap an existing zone for another.  It simply records the properties of the selected zone then passes those to SpawnZone().  Since it’s inserted at the same level, Flash removes the old one for us.  The modifier we pass in determines which direction we loop through the array of linkage names:
function SwapObject(Param1:Number)
{
 if (ObjectSelected.length > 0)
 {
  switch(ObjectSelected.substr(0, 4))
  {
   case "Zone":
    var ZoneID:Number = _root[ObjectSelected].tabIndex;
    ZoneID += Param1;
    ZoneID = (ZoneID >= Zones.length) ? 0 : ZoneID;
    ZoneID = (ZoneID < 0) ? Zones.length - 1 : ZoneID;

    SpawnZone(_root[ObjectSelected]._name,
        ZoneID,
        _root[ObjectSelected]._x,
        _root[ObjectSelected]._y,
        _root[ObjectSelected]._xscale);
    startDrag(ObjectSelected);
    break;
  }
 }
}

Now, fire up the UDK Editor and reimport your BMHud SwfMovie.  Make sure the package also imported all your circle images.  I’m not sure when it was fixed, but the FrontEnd is now smart enough to detect the updated package so you shouldn’t have to manually copy it into the CookedPC folder.

Last but not least, by request, a way to toggle the debug text.

DefaultInput.ini

Bind a key for toggling debug mode:
-Bindings=(Name="Q",Command="GBA_ToggleTranslocator")
.Bindings=(Name="Q",Command="BMDebug")
BattleMapPlayerInput.uc

Add a function for toggling debug mode:
exec function BMDebug()
{
 DebugMode = !DebugMode;
}
BattleMapPlayerController.uc

At the top add a declaration for a DebugMode variable:
var bool DebugMode;

BattleMapHUD.uc

In the PostRender() event, wrap the debug string and DrawText() in a condition:
 if (bmPlayerController.DebugMode)
 {
  StringMessage = "NetMode=" @ WorldInfo.NetMode @ "\nMousePosition=" @ bmPlayerController.MousePosition.X @ "," @ bmPlayerController.MousePosition.Y @ "\nMouseOrigin=" @ mouseOrigin.X @ "," @ mouseOrigin.Y @ "," @ mouseOrigin.Z @ "\nObjectUnderMouse=" @ traceHit.Name @ "\nObjectClass=" @ traceHit.Class;
  //`Log(StringMessage);

  Canvas.DrawColor = GreenColor;
  Canvas.SetPos( 10, 10 );
  Canvas.DrawText( StringMessage, false, , , TextRenderInfo );
 }
Thanks for the suggestion DrZuess.

BattleMap mode source files

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