Tuesday, April 5, 2011

D&D BattleMap Using the UDK - Making It Interactive

So far, we're able to view a UDK map top-down and overlay it with a grid.  That's great for replacing hand drawn or tiled maps - we can show a tremendous amount of detail.  But to really impress our players, we want to be able to move objects on an interactive map.  Fortunately, the UDK provides that capability with Kismet and Matinee.  However, we have two problems.

Once we position a room and overlay the grid, we don't want to have to move around to activate triggers.  (Remember, positioning the camera actually involves moving our invisible avatar.)  Besides, our pawn is an empty mesh with no Collision Model so it wouldn't activate triggers anyway.  What we need is the ability to trigger actions with keyboard input.

There are probably a dozen or more ways to accomplish this, but one goal we had from the beginning was to keep the map and script separate.  We could easily script our own Kismet actions, but then the map builder would have to have our scripts installed beforehand to use its custom actions.  Not the ideal solution.  We want to be able to have anyone make any map, then launch it with our scripts to turn it into a D&D battlemap.  So, we're going to try to use default Kismet triggers.

BattleMapPlayerController.uc

First, we're going to add a new variable to our PlayerController to store what the mouse cursor is floating over.  The functionality we're about to implement could all be done in the PlayerInput object when a key is pressed, but as long as we're tracing the mouse position every tick anyway we might as well store what our trace hit so we can use it later and save ourselves some cycles.  Add this to the top:
var name ObjectUnderMouse;
Then update the MouseOrigin functions so that the HUD can pass in the object that it traced:
simulated function SetMouseOrigin(Vector NewMouseOrigin, name NewObjectUnderMouse)
{
 // avoid spamming if the cursor isn't moving
 if (MouseOrigin != NewMouseOrigin)
 {
  MouseOrigin = NewMouseOrigin;
  ObjectUnderMouse = NewObjectUnderMouse;
  ServerMouseOrigin(NewMouseOrigin, NewObjectUnderMouse);
 }
}

reliable server function ServerMouseOrigin(Vector NewMouseOrigin, name NewObjectUnderMouse)
{
 MouseOrigin = NewMouseOrigin;
 ObjectUnderMouse = NewObjectUnderMouse;
}
BattleMapHUD.uc

Replace the PostRender event with the following.  We're now capturing the actor that was hit by the trace and passing it to our PlayerController.  If nothing was hit, we just pass it the base WorldInfo so we don't have to test for nulls.

(We also did an additional bit of cleanup here and fixed the starting point of the trace since we changed our Camera object last blog post.)
event PostRender()
{
 local BattleMapPlayerController bmPlayerController;
 local vector startTrace;
 local Vector endTrace;
 local Vector mouseOrigin;
 local Vector mouseDirection;
 local string StringMessage;
 local TraceHitInfo hitInfo;
 local Actor traceHit;

 super.PostRender();

 StringMessage = "";
 bmPlayerController = BattleMapPlayerController(PlayerOwner);
 bmPlayerController.MousePosition = GetMouseCoordinates();
 //Deproject the mouse from screen coordinate to world coordinate and store World Origin and Dir.
 Canvas.DeProject(bmPlayerController.MousePosition, mouseOrigin, mouseDirection);
 if (bmPlayerController.Pawn != none)
 {
  startTrace = bmPlayerController.Pawn.Location;
  startTrace.Z += BattleMapCamera(bmPlayerController.PlayerCamera).CameraZOffset;
  endTrace = startTrace + (mouseDirection * 20000);
  traceHit = Trace(mouseOrigin, mouseDirection, endTrace, startTrace, true, , hitInfo);
 }
 if (traceHit == none)
  traceHit = WorldInfo;
 // save final mouse 3d position (on ground)
 bmPlayerController.SetMouseOrigin(mouseOrigin, traceHit.Name);

 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 );
}
BattleMapPlayerInput.uc

This is where the magic happens and is what keeps us from having to create custom Kismet actions.  Add the following function to our PlayerInput object.  This will get called when a key is pressed.  Essentially, this function loops through all the Trigger actors in the map and finds the one we moused over.  Then it manually activates all the actions linked to the Trigger.  This gives us a tremendous amount of flexibility.
exec function BMInteractObject()
{
 local Trigger T;
 local int i, j, k;

 // activate each Kismet event attached to this trigger
 if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)
  foreach DynamicActors(class'Trigger', T)
   if (T.Name == ObjectUnderMouse)
    for (i=0; i<T.GeneratedEvents.Length; i++)
     for (j=0; j<T.GeneratedEvents[i].OutputLinks.Length; j++)
      for (k=0; k<T.GeneratedEvents[i].OutputLinks[j].Links.Length; k++)
       T.GeneratedEvents[i].OutputLinks[j].Links[k].LinkedOp.ForceActivateInput(T.GeneratedEvents[i].OutputLinks[j].Links[k].InputLinkIdx);
}

DefaultInput.ini

Finally, we just need to connect our function with a key.  In the BattleMap Bindings section, add these two lines to make the "i" key interact with the map:
.Bindings=(Name="BMInteractObject",Command="BMInteractObject")
.Bindings=(Name="I",Command="BMInteractObject")

Now, anything you can animate in your map using Kismet and Matinee can be triggered on command.  Things like opening doors, turning on lights, activating traps, etc.

This shows how quickly and easily you can whip together a map:



BattleMap mode source files

2 comments:

  1. Hey man,

    I'm a student in an Advanced UDK Workshop @ the Art Institute of Portland. I'm just getting into the code side (past kismet into UC) and your blog has _already_ proven invaluable, in that my proposed project (similar to an old school Hex Crawl map combined with a Final Fantasy III overland map) does not seem totally improbable. THANKS!

    I'll be sure to post back with what my instructor and I come up with in class. Keep up the awesome posts!

    ReplyDelete
  2. Excellent! Glad we could help. I'd love to hear how your project turns out. And if during your studies you figure out ways to improve what we've done here, let us know. Good luck!

    ReplyDelete