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

5 comments:

  1. Thanks very much. Very cool.

    I have the token functionality working (along with Torches, Effects and Zones) and I can interact with them as expected however Blood is no longer working properly.

    I can spawn Blood, however whilst the object is created (I can see the BattleBlood object on the HUD if I hover the mouse over it) the graphic for the Blood decal is never displayed. I can select, move and place the Blood but again nothing is displayed? Any ideas what I have done wrong?

    I have double checked the name of the Blood decal object is correct. Mines in the BattleMapAssets package (BattleMapAssets'Materials'Blood_Decal) so am not sure what's going on.

    Also for Tokens I can only seem to create Tokens sequentially meaning if I create 4 P Tokens (P1 - P4) for the players, the first Monster Token I can create is M5 and not M1.

    Also I couldn't find a way to reset the Token numbering, I can switch a Token from P to M using the [ ] keys but the numbering seems fixed. It would be nice to be able to change the number of a token as well as its P or M class.

    ReplyDelete
  2. I think I found the error with Blood not being visible when spawned.

    The DefaultBloodSize var is never set a default value in BattleMapPlayerInput.uc. I added the following to the Default properties region of the file:

    DefaultBloodSize = 256.0

    All is working now.

    Also in the future would it be possible to create alternate coloured versions of the Blood decal and add [] functionality to the ModifyObject routines so that we can spawn water puddles, green slime and other puddle effects in addition to Blood?

    ReplyDelete
  3. Hey guys, i got a question, are the beds, desks and chest decals? or u modelled them 3d? they look great.

    ps: i have yet to test the new updates, havent got the time, but great job :)

    ReplyDelete
  4. @DrZeuss: Yep, I missed the DefaultBloodSize in DefaultProperties. Good find. I added it to the post. And yes, right now the tokens all use the same 1-up counter. When we implement a UI for BattleMap assets, we'll include a way to enter your own text.

    @Gerar: The bed, desk and chest are 3D models.

    ReplyDelete
  5. ZomBPir8Ninja - Did you model the bed, desk and chest 3D meshes and import them into UDK or are they already available in a existing UDK package?

    If the latter, could you point us to where to download them from? Thanks

    ReplyDelete