Sunday, June 26, 2011

D&D BattleMap Using the UDK – Transitions, Menus and Config Files

The UI for our placeable object commands isn’t quite ready yet (it seems everyone’s suddenly a critic), but this post we’ll introduce similar concepts while implementing another desperately needed feature: transitioning between maps.  I’m getting a little tired of having to close down the UDK and reconfiguring the Unreal FrontEnd just to change maps:



We’ll provide the option to choose a map with a flyout menu built in Flash.  But to achieve our objective of keeping the maps independent of the BattleMap mod, we can’t hard code the map list into the menu.  Therefore, we’ll store the list in a config file and pass it to the HUD on startup.

Flash

Once again, our friends at MonsterLayer.com produced some gorgeous assets for us in an astonishingly short amount of time.  I’ll do my best to break down how they built our menu, but you can certainly create any style of menu that suits you.  All that’s important is that the asset names match the code.

Below is a snapshot of an individual menu button.  This will be duplicated multiple times: one for each map.

The “Scripts” layer simply contains:
stop();
The second frame of the background later changes the color.  That will become the “hover” when moused over.  (Yes, Flash does include a Button type.  However, we had multiple issues trying to get it to work correctly so we went with the programmer’s approach of just coding it ourselves.)

Make sure the Text object is named “ButtonText” and save the symbol as a MovieClip named “MapButton”.

Create another button the exact same way:

Name this MovieClip “CloseButton”.

Next, create a menu that contains your button objects:

Name the map button instances “MapButton0”, “MapButton1”, etc.  Name the close button instance “CloseButton”.  Save this MovieClip as “MapList”.

Create another MovieClip called “MapListMenu”.  Add an instance of MapList named “MapListInst”.  Create a motion tween of it moving onto the stage:

The scripts on the 1st and last frames both simply contain:
stop();
Now, add an instance of MapListMenu to your root movie named “MapListMenuInst”.  Position it so that it starts outside the scene, but ends inside at the end of its animation.  (Edit In Place comes in real handy here.)

We need one more object.  Transitioning between maps can take a few seconds to initiate.  We should provide a visual clue that the transition has begun and the UDK isn’t hung.  Create an object named “Overlay”.  I drew a giant semi-transparent background with the ever-polite instructions “Please Wait…”:

Add an instance of your Overlay object to your root movie as “OverlayInst”.

ActionScript

Now, we need to wire it all up.  Add the following to your root ActionScript.  It initializes an array to store the map list, hides the Overlay and sets up mouse events for the buttons.  The click event for the map buttons will show the Overlay on top, toggle off the menu and pass the chosen map back to UnrealScript:
var Maps:Array;
// Hide overlay
OverlayInst._visible = false;

// Init map buttons
i=0;
while(MapListMenuInst.MapListInst["MapButton"+i])
{
 MapListMenuInst.MapListInst["MapButton"+i]._visible = false;
 MapListMenuInst.MapListInst["MapButton"+i].onRollOver = function(){
  this.gotoAndStop(2);
 }
 MapListMenuInst.MapListInst["MapButton"+i].onRollOut = function(){
  this.gotoAndStop(1);
 }
 MapListMenuInst.MapListInst["MapButton"+i].onRelease = function(){
  OverlayInst.swapDepths(_root.getNextHighestDepth());
  OverlayInst._visible = true;
  ShowMapList();
  ExternalInterface.call( "OpenMap", Maps[this._name.substr(9)]["File"] );
 }
 i++;
}

// Init close button
MapListMenuInst.MapListInst.CloseButton.onRollOver = function(){
 this.gotoAndStop(2);
}
MapListMenuInst.MapListInst.CloseButton.onRollOut = function(){
 this.gotoAndStop(1);
}
MapListMenuInst.MapListInst.CloseButton.onRelease = function(){
 ExternalInterface.call( "CloseMap" );
}
Next, add the following InitMapList() function.  This will be called by UnrealScript to pass in the map list and setup the map buttons:
function InitMapList(Param1:Array)
{
 Maps = Param1;

 //Show only buttons with maps
 for (i=0; i<Maps.length; i++)
 {
  MapListMenuInst.MapListInst["MapButton"+i].ButtonText.text = Maps[i]["Name"];
  MapListMenuInst.MapListInst["MapButton"+i]._visible = true;
 }
}
Finally, add the ShowMapList() function to toggle the menu on/off:
function ShowMapList()
{
 if (MapListMenuInst._currentframe == 1)
 {
  MapListMenuInst.swapDepths(_root.getNextHighestDepth());
  CursorInst.swapDepths(_root.getNextHighestDepth());
  MapListMenuInst.gotoAndPlay(2);
 }
 else
  MapListMenuInst.gotoAndStop(1);
}
Republish the .swf file then open up the UDK, find the BattleMapHud package, and reimport BMHud.

BattleMapConfig.uc

Now, let’s setup a config file to store the map names.  The UDK builds in configuration file functionality into the base Object class.   That means that every class can implement its own config file.  Mougli’s portfolio includes a very good tutorial on UDK configuration files.  To sum up, any variable declared globally in a class becomes an entry in its config file.

Here’s the code for a simple object containing an array of MapItem structs:
class BattleMapConfig extends Object config(BattleMap);
struct MapItem
{
 var config string Name;
 var config string File;
};

var config array <MapItem> Maps;
The “config(BattleMap)” directive tells the UDK that this object will read and write to a BattleMap.ini file.

DefaultBattleMap.ini

In your /UDKGame/Config directory, create a new .ini file called “DefaultBattleMap.ini” and enter the titles and file names of your maps.  For example, mine looks like:
[BattleMap.BattleMapConfig]
Maps=(Name="Demon Queen's Enclave U1, L1",File="BM_DemonQueensEnclave.udk")
Maps=(Name="Demon Queen's Enclave L13-16",File="BM_DemonQueensEnclave1.udk")
Maps=(Name="Demon Queen's Enclave L2-6,11,V1",File="BM_DemonQueensEnclave2.udk")
Maps=(Name="Demon Queen's Enclave V2,V4-V10",File="BM_DemonQueensEnclave3.udk")
Maps=(Name="Demon Queen's Enclave V12",File="BM_DemonQueensEnclave4.udk")
The format should look very familiar, it’s similar to DefaultInput.ini where we include new key bindings.  Notice that the section heading is the name of our class.

DefaultInput.ini

Speaking of DefaultInput.ini, while we’re here go ahead and add a key binding to toggle our menu:
-Bindings=(Name="Escape",Command="GBA_ShowMenu")
.Bindings=(Name="Escape",Command="BMShowMapList")
(In case you’re curious, the period in front of .Bindings means that duplicate entries are allowed.)

BattleMapPlayerController.uc

Here, we simply need to instantiate our new BattleMapConfig class, which will cause it to automatically initialize its variables from the config file:
var BattleMapConfig BMConfig;
simulated function PostBeginPlay()
{
 super.PostBeginPlay();
 BMConfig = new class'BattleMapConfig';
}
BattleMapHUD.uc

Inside PostBeginPlay(), add a call to a CallInitMapList() function right after the CrosshairMovie.Initialize() statement and pass in the newly loaded Map array:
  CrosshairMovie.CallInitMapList(BattleMapPlayerController(PlayerOwner).BMConfig.Maps);
BattleMapPlayerInput.uc

Create a new command to tell the HUD to toggle the menu and another to tell the UDK to load a new map:
exec function BMShowMapList()
{
 if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)
 {
  BattleMapHUD(myHUD).CrossHairMovie.CallShowMapList();
 }
}

exec function BMOpenMap(string MapFile)
{
 WorldInfo.Game.ProcessServerTravel(MapFile);
}
BattleMapGfxHud.uc

First, create two new wrapper functions for calling the HUD’s InitMapList() and ShowMapList():
function CallInitMapList( array <MapItem> Param1 )
{
 ActionScriptVoid("InitMapList");
}

function CallShowMapList()
{
 ActionScriptVoid("ShowMapList");
}
Finally, create two receiver functions called by the HUD to execute our BMOpenMap() command and the Quit command:
function OpenMap(string MapFile)
{
 ConsoleCommand("BMOpenMap " @ MapFile);
}

function CloseMap()
{
 ConsoleCommand("Quit");
}
That’s it!  If everything went smoothly, hitting Esc will now pop up a menu of map choices.  Clicking on a map name will transition to a new map.  Clicking the close button will cleanly exit the UDK.

BattleMap mode source files

Thursday, June 9, 2011

Taking a Second Wind

There won't be a blog post addressing a new feature this coming weekend.  But I didn't want to leave you without something to play with, so I'm addressing a few questions asked in the comments:

Our friends at MonsterLayer.com gave us permission to give away the assets they created for us.  Copy BattleMapItems.upk to your /UDKGame/Content/Misc directory.  It includes:
  • Bed
  • Bedroll
  • Chair
  • Chest
  • Drawer
  • Ladder
  • Small Table
  • Table
Here's a couple of clips of the BattleMap in use from our last session:


And finally, according to the UDK Licensing FAQ, as long as you're not making any money off your creation you are free to package and distribute your project.

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

Sunday, April 17, 2011

D&D BattleMap Using the UDK - Torches and Lighting

This post, I eagerly wanted to get torches back into our BattleMap.  The iconic dungeon torch is oddly comforting for players.  Telling them "you can see 5 squares from where you stand" and actually having a lit torch on the map are two different things.  You'd be surprised to find that they'll huddle around a torch bearer when they can actually see the range of their vision.

The first time we tried to implement this we made the torch radius configurable through the command console.  Not so great for trying to setup a candle or a sunrod, while not affecting every other torch on the map.  This time, we're going to add mouse selection and keyboard commands to modify individual torches.

BattleMapTorch.uc

The first thing we need is an object that looks like fire.  I simply followed the 3D Buzz Video Tutorials on particle emitters.  Literally, click-for-click.  The only thing I did different was turn off the smoke, since it would obscure the map from our top-down view.

Once you have the emitter built, that becomes the base for our torch object.  But, we need to add some features to it.  Drop one onto a map, then open the properties window.  There's a nifty little icon in the upper right hand corner that looks like a wrench.  Choose "Copy To Clipboard", then paste the contents into an editor.  What you see are the properties that would be set in the DefaultProperties section of an emitter.uc object.  That's what we'll start with.  (Not all of those properties are necessary, but it was certainly easier to start with that than trying to figure them all out from scratch.)

Next, we add some network replication functionality.  The BattleMap does work with multiple clients, however we found that using InputDirector is easier.  (See the post on the setup.)

Moving the torch is simply a matter of switching between two states: one that updates the torch's location from the mouse position and one that doesn't.  We turn off collision detection when we're moving it around to avoid it getting stuck on walls or terrain.  Why does stopping the torch also include updating the location?  That has to do with network replication.  For efficiency, synchronization information is not always transmitted.  Some packets are skipped.  (That's why you can be aiming right at someone's head but still miss a shot.)  So, when the torch is no longer moving it may actually appear to be in different locations on clients.  This forces every copy of the torch to be in exactly the same spot.

Lastly, we add a LightAttachment component to the torch to actually create the torch light.  Otherwise, it would just be a flame in the dark.

class BattleMapTorch extends EmitterSpawnable;
var Color LightColor;
var float LightScale;
var PointLightComponent LightAttachment;
var Vector VerticalOffset;
var repnotify Vector TorchLoc,TorchStopLoc;
var float TorchRadius;
replication
{
 if (bNetDirty)
  TorchLoc,TorchStopLoc;
}
// called on clients when TorchStopLoc gets replicated
simulated event ReplicatedEvent( name VarName )
{
 if (VarName == nameof(TorchLoc) )
  SetLocation( TorchLoc );
 else if (VarName == nameof(TorchStopLoc) )
  StopTorch( TorchStopLoc );
 else
  super.ReplicatedEvent( VarName );
}
// this function should be run on both clients and server
simulated function MoveTorch( )
{
 bCollideWorld = false;
 SetCollision(false, false);
 GotoState( 'TorchMoving' );
}
simulated function StopTorch( Vector NewTorchStopLoc )
{
 bCollideWorld = true;
 SetCollision(true, true);
 GotoState('TorchStopped');
 TorchStopLoc = NewTorchStopLoc;
 SetLocation(NewTorchStopLoc);
}
simulated state TorchMoving
{
 simulated event Tick( float DeltaTime )
 {
  // move the torch...
  TorchLoc = BattleMapPlayerController(Owner).MouseOrigin + VerticalOffset;
  SetLocation(TorchLoc);
 }
}
simulated state TorchStopped
{
 simulated event Tick( float DeltaTime )
 {
  // stop the torch...
 }
}
simulated function PostBeginPlay()
{
 LightAttachment = new(self)class'PointLightComponent';
 LightAttachment.SetLightProperties(LightScale, LightColor);
 LightAttachment.SetEnabled(true);
 LightAttachment.Radius = TorchRadius;
 LightAttachment.SetTranslation(vect(0, 0, 1) * 50);
 if ( LightAttachment != None )
 {
  self.AttachComponent(LightAttachment);
 }
 super.PostbeginPlay();
}
DefaultProperties
{
 TorchRadius=600
 VerticalOffset=(X=0,Y=0,Z=50)
 LightColor=(B=0,G=200,R=255,A=255)
 LightScale=4.0
    DrawScale=2.000000
 Begin Object Name=ParticleSystemComponent0
        Template=ParticleSystem'BattleMapAssets.Particles.PS_Torch'
        ReplacementPrimitive=None
        LightingChannels=(bInitialized=True,Dynamic=True)
  SecondsBeforeInactive=0
 End Object
 ParticleSystemComponent=ParticleSystemComponent0
 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 = true
   bStatic=false
   bNoDelete=false
   bMovable=true
}

BattleMapPlayerInput.uc

Now, we need to add functions to our PlayerInput object to manipulate torches.  First is a method to spawn a torch.  We created an all-purpose spawn function, and will use keybind parameters to pass in what to spawn:

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;
  }
 }
}
Next, we want to activate functionality of an object when it's selected.  In this case, we want the torch to start moving when it's clicked.  But each object may do something different.  For example, the triggers we implemented earlier will simply "turn on" -- nothing to continue doing.  So, the function will first stop whatever a currently selected object may be doing (stop moving if we had already selected a torch) then start whatever the newly selected object needs to do.  Replace the BMInteractObject() function with the following:
function string ParseObjectName(Name ObjectName)
{
 local string ObjectType;

 ObjectType = Repl(ObjectName, "BattleMap", "");
 if (InStr(ObjectType, "_") > -1)
  ObjectType = Mid(ObjectType, 0, InStr(ObjectType, "_"));
 return ObjectType;
}

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);
     ObjectSelected = none;
     break;
   }
  }
  //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;
  }
 }
}
As we add more interactive objects to our BattleMap, we'll insert the method to trigger them in this function.  Also, add this declaration to the top of the code along with GridZoomDelta:

var Object ObjectSelected;
Finally, once we have a torch selected we have the ability to modify just that instance of the object.  In this case, we want to be able to modify its light radius.  Another frustrating issue we had recently, due to the summer time change, was seeing the projected map in a lit room.  Even with the lights off, it can be difficult to see what you thought was a brightly lit map in the editor.  So, we wanted the ability to adjust the map's brightness.  Therefore if no object is selected, the default behavior will be to manipulate the lights in a map.  Note that this only works for dynamic lights.  Static lights are... static.  Add the following function:

exec function BMModifyObject(int Modifier)
{
 local BattleMapTorch T;
 local PointLightToggleable L;
 local LightComponent C;

 if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)
 {
  if (ObjectSelected != none)
  {
   switch(Left(ObjectSelected, 14))
   {
    case "BattleMapTorch":
     T = BattleMapTorch(ObjectSelected);
     T.LightAttachment.Radius = T.LightAttachment.Radius * (1 + (0.1 * Modifier));
     break;
   }
  }
  else
  {
   foreach AllActors(class'PointLightToggleable', L)
   {
    C = L.LightComponent;
    C.SetLightProperties(C.Brightness * (1 + (0.25 * Modifier)));
   }
  }
 }
}
DefaultInput.ini

All that's left is to bind keys to the functions we created.  A "T" will spawn a torch.  Left clicking will interact with objects. And the plus/minus keys ("Equals"/"Underscore") will manipulate selected objects.  Replace the "I" binding in our BattleMap Bindings section with the following:

.Bindings=(Name="T",Command="BMSpawn Torch")
.Bindings=(Name="LeftMouseButton",Command="BMInteractObject")
.Bindings=(Name="Equals",Command="BMModifyObject 1")
.Bindings=(Name="Underscore",Command="BMModifyObject -1")

Here's what it looks like in action:



BattleMap mode source files