Quantcast
Channel: ZomBPir8Ninja
Viewing all articles
Browse latest Browse all 18

D&D BattleMap Using the UDK – Placeable Zones

$
0
0
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

Viewing all articles
Browse latest Browse all 18

Trending Articles