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

Saturday, March 26, 2011

D&D BattleMap Using the UDK - More Dungeon Less UT

Now that we have an idea of how to setup a top-down camera and a grid overlay, it's time to clean things up and drop the Unreal Tournament functionality.  We don't need an avatar running around while we're displaying a map and we want to be able to move independently of CSG and meshes in the level.

Make a backup copy of the files we've created so far.  You'll have to save the copy outside the /Development/Src directory, otherwise the FrontEnd will try to compile them as well.

I'll outline the changes below if you want to follow along.  Otherwise, links to archives of both versions are below.

BattleMapMain.uc

All we're doing here is changing the object we're extending from, to the base GameInfo class.  We'll no longer be playing an Unreal Tournament match when we start a level:
class BattleMapMain extends GameInfo;

...
BattleMapPawn.uc

Changing the base class again, as well as dropping the first person mesh fix (since there won't be any) and supplying an empty pawn.  We also set a few properties to enable the pawn to move freely around the level.  Essentially, we put the pawn in "flying" mode instead of "walking" mode.  That allows us to move vertically as well as horizontally, and effectively zooms and pans the level.

Replace the entire BattleMapPawn.uc code with the following:

class BattleMapPawn extends Pawn;
DefaultProperties
{
 //this is just an empty skeletal mesh to prevent log errors
 Begin Object Class=SkeletalMeshComponent Name=WPawnSkeletalMeshComponent
 End Object
 Mesh=WPawnSkeletalMeshComponent
 Components.Add(WPawnSkeletalMeshComponent)

 bCollideWorld=false
 bCollideActors=false
 WalkingPhysics=PHYS_Flying
 LandMovementState=PlayerFlying
 AirSpeed=+01200.000000
}
BattleMapHUD.uc

All we're doing here is removing the laser sight code.  Delete the following three lines:
// laser sight from pawn to reticle
bmPlayerController.Pawn.GetActorEyesViewPoint(pawnEyeLocation, pawnEyeRotator);
Draw3DLine(pawnEyeLocation, mouseOrigin, RedColor);
BattleMapPlayerController.uc

A lot of changes here.  First, change the base object:
class BattleMapPlayerController extends PlayerController
 config(Game);
Delete the following blocks.  These manipulated the behavior of the UT pawn:

// This basically locks the camera to the pawn's location, and adds CameraZOffset to the Z of the camera.
simulated event GetPlayerViewPoint( out vector out_Location, out Rotator out_Rotation )
{
...
}

// Force WASD to be NWSE, so use world rotation instead of pawn rotation
state PlayerWalking
{
...
}

// This makes our weapons aim to the pawn's rotation, not the controller's rotation
function Rotator GetAdjustedAimFor(Weapon W, vector StartFireLoc)
{
...
}

// Turn the pawn toward the cursor
function UpdateRotation( float DeltaTime )
{
...
}
Add the following block.  This handles the "flying" mode that we enabled in BattleMapPawn.uc:

// Force WASD to be NWSE, so use world rotation instead of pawn rotation
state PlayerFlying
{
 function PlayerMove(float DeltaTime)
 {
  local vector X,Y,Z;

  //GetAxes(Rotation,X,Y,Z);
  GetAxes(WorldInfo.Rotation,X,Y,Z);

  Pawn.Acceleration = PlayerInput.aForward*X + PlayerInput.aStrafe*Y + PlayerInput.aUp*vect(0,0,1);;
  Pawn.Acceleration = Pawn.AccelRate * Normal(Pawn.Acceleration);

  if ( bCheatFlying && (Pawn.Acceleration == vect(0,0,0)) )
   Pawn.Velocity = vect(0,0,0);
  // Update rotation.
  UpdateRotation( DeltaTime );

  if ( Role < ROLE_Authority ) // then save this move and replicate it
   ReplicateMove(DeltaTime, Pawn.Acceleration, DCLICK_None, rot(0,0,0));
  else
   ProcessMove(DeltaTime, Pawn.Acceleration, DCLICK_None, rot(0,0,0));
 }
}
Finally, replace the DefaultProperties section.  We dropped our CameraZOffice parameter and added a reference to a new camera object we're going to create:
DefaultProperties
{
 //this makes us stop immediately instead of decelerating
 bCheatFlying=true

 InputClass=class'BattleMap.BattleMapPlayerInput'
 CameraClass=class'BattleMap.BattleMapCamera'
}
BattleMapCamera.uc

This is a new class to handle the camera.  The base PlayerController object doesn't include the same functionality as UTPlayerController, so we're moving our top-down camera code here.

All we're doing is moving the camera position and POV after the real UpdateViewTarget "does its thing".  We also want to force ApplyCameraModifiers() which handles things like camera shake.  Because the camera will be so far from the source, the engine may try to skip that processing.

We wrap our changes in a check to see if we're in CinematicMode.  We don't want to change the camera if we are.  This allows us to do things like fly-throughs of a big room before a boss fight:

class BattleMapCamera extends Camera;
var int CameraZOffset;
// let it do it's fancy thing, then just set our own LOC and FOV
function UpdateViewTarget(out TViewTarget OutVT, float DeltaTime)
{
 local vector            Loc, Pos;
 local rotator           Rot;

 super.UpdateViewTarget(OutVT, DeltaTime);
 if (!PCOwner.bCinematicMode)
 {
  Rot = rotator(vect(0,0,-1));
  Loc = PCOwner.Pawn.Location;
  Loc.Z = Loc.Z + CameraZOffset;
  Pos = Loc - Vector(Rot);
  OutVT.POV.Location = Pos;
  OutVT.POV.Rotation = Rot;
  OutVT.POV.FOV = DefaultFOV;
 }

 ApplyCameraModifiers(DeltaTime, OutVT.POV);
}

DefaultProperties
{
 CameraZOffset=6000
 DefaultFOV=35
}
BattleMapPlayerInput.uc

Lastly, we just need to change the base object of our PlayerInput class:
class BattleMapPlayerInput extends PlayerInput within BattleMapPlayerController;

...
Now when you launch the game, the Necropolis level should start behaving more like a D&D dungeon map.  Use the WASD keys to pan the map and Space or C to zoom in and out.  Hit G to turn on the grid and resize it with the mouse wheel.

(Note: If you zoom in using the C key and the camera suddenly shifts and "sticks" for a few seconds, what's happening is that your pawn is dying.  You've hit the Kill-Z for the level, just as if your avatar had run off a cliff.  The camera should reset in a few seconds.  In our own levels, we'll set Kill-Z low enough to compensate for the camera height.)

The original top-down UT mode source files
The new BattleMap mode source files

Tuesday, March 8, 2011

D&D BattleMap Using the UDK - The Grid

Building on our previous post, let's add the iconic D&D grid.  This is significantly easier using the Flash UI.  I'll summarize at the end what we had to do to get it to work using just the UDK.

Flash
In Flash, we simply created the grid using the drawing tools.  Vertical and horizontal lines, using a hairline stroke, spaced 20 units apart, covering the entire scene:


In the ActionScript of our Actions layer, we added a couple of functions that will be called by UnrealScript.  The first will show/hide the grid, the second will scale it.  We hide the grid initially to make positioning easier:
function GridToggle()
{
 GridInst._visible = !GridInst._visible;
}

function GridZoom(Param1:Number)
{
 GridInst._xscale = GridInst._xscale + Param1;
 GridInst._yscale = GridInst._xscale;
 GridInst._x = stage.width*.5 - GridInst._width*.5;
 GridInst._y = stage.height*.5 - GridInst._height*.5;
}

GridInst._visible = false;

From now on, whenever you make a change to the HUD you must:
  1. Publish the movie
  2. Open the UDK Editor and reimport the movie into your package using the Content Browser.
  3. Save the package
  4. Copy the package from the UDKGame\Content\Misc folder to the UDKGame\CookedPC folder.
Since we haven't actually referenced the package from the BM-Necropolis.udk map that we're testing with, the Unreal Frontend doesn't include it when it Cooks.  Believe me, I've lost a lot of time trying to figure out why my ActionScript functions weren't being called when I simply forgot step 4 above.

BattleMapGfxHud.uc
Add two wrapper functions for the ActionScript functions above:
function CallGridToggle()
{
 ActionScriptVoid("GridToggle");
}

function CallGridZoom( float Param1 )
{
 ActionScriptVoid("GridZoom");
}

BattleMapPlayerController.uc
All we need to do here is reference a new Input class that we're going to create to interpret keyboard/mouse commands.  Add this to the DefaultProperties section:
 InputClass=class'BattleMap.BattleMapPlayerInput'

BattleMapPlayerInput.uc
This is a new class that will call our wrapper functions based on input events.  I arbitrarily chose a delta of 5.0% when scaling the grid:
class BattleMapPlayerInput extends UTPlayerInput within BattleMapPlayerController;
var float GridZoomDelta;
exec function BMGridToggle()
{
 BattleMapHUD(myHUD).CrossHairMovie.CallGridToggle();
}

exec function BMGridZoomIn()
{
 BattleMapHUD(myHUD).CrossHairMovie.CallGridZoom(-GridZoomDelta);
}

exec function BMGridZoomOut()
{
 BattleMapHUD(myHUD).CrossHairMovie.CallGridZoom(GridZoomDelta);
}

DefaultProperties
{
 GridZoomDelta = 5.0f
}

I know.  We seem to have a lot of functions just calling each other.  Here's the flow:

Input Event > PlayerInput > GFxMoviePlayer > ActionScript

DefaultInput.ini
Lastly, we need to specify what keyboard/mouse events will trigger those actions.  In UDKGame\Config\DefaultInput.ini, scroll all the way down past "Editor Bindings".  Add a new section:
;-------------------
; BattleMap Bindings
;-------------------
.Bindings=(Name="BMGridToggle",Command="BMGridToggle")
.Bindings=(Name="BMGridZoomIn",Command="BMGridZoomIn")
.Bindings=(Name="BMGridZoomOut",Command="BMGridZoomOut")
-Bindings=(Name="G",Command="GBA_SwitchToBestWeapon")
-Bindings=(Name="MouseScrollUp",Command="GBA_PrevWeapon")
-Bindings=(Name="MouseScrollDown",Command="GBA_NextWeapon")
.Bindings=(Name="G",Command="BMGridToggle")
.Bindings=(Name="MouseScrollUp",Command="BMGridZoomOut")
.Bindings=(Name="MouseScrollDown",Command="BMGridZoomIn")

This is doing three things:
  • Maps Input Binding commands with UnrealScript (PlayerInput) functions (I just happened to have named them the same)
  • Unbinds previously specified inputs from their default commands
  • Binds those inputs to our new commands
Essentially, we use the "G" key to toggle the grid and the mouse wheel to scale the grid.

The result is:



For reference, to implement this just using the UDK and UnrealScript, we had to:
  • Paint a grid texture
  • Model a grid mesh (essentially, a 2D plane)
  • Create a BattleMapGridActor in UnrealScript which instantiated the grid mesh, applied the texture, and included several functions for moving ("scaling" by moving up and down, and positioning horizontally)
  • Include numerous functions in BattleMapPlayerInput for spawning, moving and snapping the grid
It was a huge amount of code, compared to the few changes above.  I wonder what else we can move from UnrealScript to Flash...

Thursday, March 3, 2011

D&D BattleMap Using the UDK - Fixed HUD!

After a very long wait...



...we finally have our UDK BattleMap fixed.  It has been shackled to the August version of the UDK since the versions from September forward replaced the HUD with ScaleForm.  That change broke the code that provided mouse coordinates.  At long last, we have have it working again with a custom ScaleForm HUD. This thread on epicgames.com provided the answer.  To prove it, here's an updated version of our very first video.  Note the custom cursor provided by our friend at monsterlayer.com.



So while we FINALLY begin the process of documenting how we created our D&D Battlemap, I'll give you a teaser and show you how we did the mod above.

But first, a couple of disclaimers:
  • This isn't going to be a step-by-step.  There are far too many well written UDK tutorials out there, so I would just be giving myself carpal tunnel syndrome.  I'll explain the code, but you should be able to find walkthroughs on installing the UDK, compiling scripts and launching a custom game.
  • We compiled bits of code from countless blogs, articles, and forum posts to get our code working.  And, embarrassingly, we recorded very few sources.  We have absolutely no intention of taking credit for anyone else's work.  So, if you see a bit of your own code in here, a flaming accusatory email isn't necessary.  Tell us it's yours and we'll be happy to give you full credit for it.
  • We were teaching ourselves UnrealScript for the first time as we were attempting this project.  There are probably simpler and more effective ways of doing what we did.  We're not experts, so don't point and laugh.  Tell us how to do it better.  We love constructive criticism.
  • I apologize for the way the code is formatted in the blog post.  Hopefully, it'll be readable once you paste it into an editor.  I'll try to find a better want to represent it for the following posts.
So, that being said, how do you play UDK top down like Alien Swarm?  First, you'll need Flash CS5 and Allar's AWESOME Blog.  I didn't use his Foreclosure UI, but his tutorials for installing Flash and setting up ScaleForm were invaluable.

Flash
This ActionScript does the magic that fixed our BattleMap:

import flash.external.ExternalInterface;
Mouse.hide();
startDrag("CursorInst", true);
var mouseListener:Object = new Object();
mouseListener.onMouseMove = function()
{
 CursorInst._x = _root._xmouse;
 CursorInst._y = _root._ymouse;

 ExternalInterface.call( "ReceiveMouseCoords", _root._xmouse, _root._ymouse );

 updateAfterEvent();
}

Mouse.addListener(mouseListener);

Even though this is just a quick demo it's the foundation of our BattleMap, so all our UnrealScript code starts in \UDK\UDK-2011-02\Development\Src\BattleMap\Classes.

BattleMapMain.uc

No explaination needed here.  This sets up the other classes:

class BattleMapMain extends UTGame;
DefaultProperties
{
 HUDType=class'BattleMap.BattleMapHUD'
 PlayerControllerClass=class'BattleMap.BattleMapPlayerController'
 DefaultPawnClass=class'BattleMap.BattleMapPawn'


 // don't wait for game start, spawn immediately
 bDelayedStart=false
}

BattleMapPawn.uc

This creates our avatar in the game.  We had to override the BecomeViewTarget() function to skip creating the first person meshes:

class BattleMapPawn extends UTPawn
 config(Game);

// we're skipping right over UTPawn.BecomeViewTarget down to Pawn.BecomeViewTarget
// since UTPawn version sets first person meshes (arms, gun, etc)
simulated event BecomeViewTarget( PlayerController PC )
{
 if (PhysicsVolume != None)
 {
  PhysicsVolume.NotifyPawnBecameViewTarget(self, PC);
 }

 // if we don't normally replicate health, but will want to do so now to this client, force an update
 if (!bReplicateHealthToAll && WorldInfo.NetMode != NM_Client)
 {
  PC.ForceSingleNetUpdateFor(self);
 }
}

// have to set bOwnerNoSee=false so that we can see our own pawn
DefaultProperties
{
 Begin Object Name=WPawnSkeletalMeshComponent
  bOwnerNoSee=false
 End Object
}
BattleMapGfxHud.uc

The wrapper for our custom ScaleForm HUD and receives its mouse coordinates:

class BattleMapGFxHud extends GFxMoviePlayer;
var int MouseX;
var int MouseY;

function Initialize()
{
 Start();
 Advance(0.0f);
}

// This is called from Flash.
function ReceiveMouseCoords( float x, float y )
{
 MouseX = x;
 MouseY = y;
}

DefaultProperties
{
 bDisplayWithHudOff = false

    // Path to your package/flash file here.
 MovieInfo = SwfMovie'BattleMapHud.BMHud'
}
BattleMapHUD.uc

Here we're instantiating the HUD, converting the mouse's 2D coordinates into 3D space and passing the vector on to our PlayerController:
class BattleMapHUD extends UDKHUD;
var BattleMapGfxHud CrosshairMovie;
var class<BattleMapGfxHud> CrosshairMovieClass;

simulated function PostBeginPlay()
{
 super.PostBeginPlay();

 if (CrosshairMovie == none)
 {
  CrosshairMovie = new CrosshairMovieClass;
  CrosshairMovie.Initialize();
 }

 SizeX = 1024.0f;
 SizeY = 764.0f;
}

singular event Destroyed()
{
 if( CrosshairMovie != none )
 {
  CrosshairMovie.Close( true );
  CrosshairMovie = none;
 }

 Destroy();
}

function vector2D GetMouseCoordinates()
{
 local Vector2D mousePos;

 mousePos.X = CrosshairMovie.MouseX;
 mousePos.Y = CrosshairMovie.MouseY;

 return mousePos;
}

event PostRender()
{
 local BattleMapPlayerController bmPlayerController;
 local vector startTrace;
 local Vector endTrace;
 local Vector mouseOrigin;
 local Vector mouseDirection;
 local string StringMessage;
 local Vector pawnEyeLocation;
 local Rotator pawnEyeRotator;

 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 += bmPlayerController.CameraZOffset;
  endTrace = startTrace + (mouseDirection * 5000);
  Trace(mouseOrigin, mouseDirection, endTrace, startTrace, true);

        // laser sight from pawn to reticle
  bmPlayerController.Pawn.GetActorEyesViewPoint(pawnEyeLocation, pawnEyeRotator);
        Draw3DLine(pawnEyeLocation, mouseOrigin, RedColor);
 }
 bmPlayerController.SetMouseOrigin(mouseOrigin);

 StringMessage = "NetMode=" @ WorldInfo.NetMode @ "\nPlayerController=" @ PlayerOwner @ "\nControllerRotation=" @ bmPlayerController.Rotation @ "\nPawn=" @ PlayerOwner.Pawn @ "\nPawnRotation=" @ bmPlayerController.Pawn.Rotation @ "\nPawnEyeRotation=" @ pawnEyeRotator;
 //`Log(StringMessage);

 Canvas.DrawColor = GreenColor;
 Canvas.SetPos( 10, 10 );
 Canvas.DrawText( StringMessage, false, , , TextRenderInfo );
}

DefaultProperties
{
 CrosshairMovieClass = class'BattleMapGfxHud'
}
BattleMapPlayerController.uc

Finally, we move the camera to a top down position, convert the WASD keys to NWSE, and make the pawn aim for the cursor:

class BattleMapPlayerController extends UTPlayerController
 config(Game);

var int CameraZOffset;
var Vector2D MousePosition;
var Vector MouseOrigin;

simulated function SetMouseOrigin(Vector NewMouseOrigin)
{
 MouseOrigin = NewMouseOrigin;
 ServerMouseOrigin(NewMouseOrigin);
}

reliable server function ServerMouseOrigin(Vector NewMouseOrigin)
{
 MouseOrigin = NewMouseOrigin;
}

// This basically locks the camera to the pawn's location, and adds CameraZOffset to the Z of the camera.
simulated event GetPlayerViewPoint( out vector out_Location, out Rotator out_Rotation )
{
 // this right here is just terrible.  but after spending days trying to figure out why the server
 // thought the client was in 3rd person, but the client thought it was in 1st, I just hacked it.
 bBehindView = true;

    // let's just ignore all the fancy crap
    if(Pawn == None)
        super.GetPlayerViewPoint(out_Location, out_Rotation);
    else
        out_Location = Pawn.Location;
    out_Location.Z += CameraZOffset;
    out_Rotation = rotator(vect(0,0,-1));
}

// Force WASD to be NWSE, so use world rotation instead of pawn rotation
state PlayerWalking
{
 function PlayerMove( float DeltaTime )
 {
  local vector   X,Y,Z, NewAccel;
  local eDoubleClickDir DoubleClickMove;
  local rotator   OldRotation;
  local bool    bSaveJump;

  if( Pawn == None )
  {
   GotoState('Dead');
  }
  else
  {
   // Changed this:
   //GetAxes(Pawn.Rotation,X,Y,Z);
   GetAxes(WorldInfo.Rotation,X,Y,Z);

   // Update acceleration.
   NewAccel = PlayerInput.aForward*X + PlayerInput.aStrafe*Y;
   NewAccel.Z = 0;
   NewAccel = Pawn.AccelRate * Normal(NewAccel);

   DoubleClickMove = PlayerInput.CheckForDoubleClickMove( DeltaTime/WorldInfo.TimeDilation );
   // Update rotation.
   OldRotation = Rotation;
   UpdateRotation( DeltaTime );
   bDoubleJump = false;

   if( bPressedJump && Pawn.CannotJumpNow() )
   {
    bSaveJump = true;
    bPressedJump = false;
   }
   else
   {
    bSaveJump = false;
   }

   if( Role < ROLE_Authority ) // then save this move and replicate it
   {
    ReplicateMove(DeltaTime, NewAccel, DoubleClickMove, OldRotation - Rotation);
   }
   else
   {
    ProcessMove(DeltaTime, NewAccel, DoubleClickMove, OldRotation - Rotation);
   }
   bPressedJump = bSaveJump;
  }
 }
}

// This makes our weapons aim to the pawn's rotation, not the controller's rotation
function Rotator GetAdjustedAimFor(Weapon W, vector StartFireLoc)
{
 local Vector pawnEyeLocation;
 local Rotator pawnEyeRotator;

 if(Pawn != None)
 {
  //return Pawn.Rotation;
  Pawn.GetActorEyesViewPoint(pawnEyeLocation, pawnEyeRotator);
  return Rotator(MouseOrigin - pawnEyeLocation);
 }
    else return Rotation;
}

// Turn the pawn toward the cursor
function UpdateRotation( float DeltaTime )
{
 local Rotator NewRotation, ViewRotation;  //DeltaRot

 ViewRotation = Rotation;
 if (Pawn!=none)
 {
  ViewRotation = Rotator(MouseOrigin - Pawn.Location + (Pawn.EyeHeight * vect(0,0,1)));
  Pawn.SetDesiredRotation(ViewRotation);
 }

 //// Calculate Delta to be applied on ViewRotation
 //DeltaRot.Yaw = PlayerInput.aTurn;
 //DeltaRot.Pitch = PlayerInput.aLookUp;

 //ProcessViewRotation( DeltaTime, ViewRotation, DeltaRot );
 //SetRotation(ViewRotation);
 SetRotation(ViewRotation);

 ViewShake( deltaTime );
 NewRotation = ViewRotation;
 NewRotation.Roll = Rotation.Roll;

 if ( Pawn != None )
  Pawn.FaceRotation(NewRotation, deltatime);
}

// Make sure our custom HUD is used
reliable client function ClientSetHUD(class<HUD> newHUDType)
{
 if ( myHUD != None )
 {
  myHUD.Destroy();
 }

 //myHUD = (newHUDType != None) ? Spawn(newHUDType, self) : None;
 myHUD = (newHUDType != None) ? Spawn(class'BattleMap.BattleMapHUD', self) : None;
}

DefaultProperties
{
 CameraZOffset=480  
}

Seems rather simple now.  To get it running though, we need to make a few .INI changes.

DefaultEngine.ini

Add the following parameters to the appropriate sections in the INI.  Use whatever screen resolution is appropriate for you.  The dimensions below is the native resolution of our projector:

[UnrealEd.EditorEngine]
+ModEditPackages=BattleMap


[SystemSettings]
Fullscreen=True
ResX=1680
ResY=1050

DefaultGame.ini

This ensures that our custom code is run when using "Play From Here" in the UDK Editor:

[Engine.GameInfo]
+DefaultMapPrefixes=(Prefix="BM",bUsesCommonPackage=FALSE,GameType="BattleMap.BattleMapMain")

Almost there.  To get a quick map going, copy Content\Maps\VCTF-Necropolis.udk to Content\Maps\BM-Necropolis.udk.

Now, compile the code and cook the assets with the Unreal Frontend.  After it completes, you'll need to copy the package with your custom HUD (ours is Content\Misc\BattleMapHud.upk) to the CookedPC folder.

And lastly, add the following to the Use Url box before launching:

BM-Necropolis.udk?game=BattleMap.BattleMapMain?Listen=true

There you go!  Hopefully everything went smoothly and you're now playing Unreal top down.  The code works in multiplayer as well.  The target I was shooting at above is another game client connected.  Don't forget to try out the vehicles.  (And if anyone creates a Car Wars, AutoDuel or Darkwind clone, dibs on beta testing.)

Thursday, December 30, 2010

D&D BattleMap Using the UDK - The Setup

So, if you've seen this video you know we do actually use this to play D&D:



But how do we get that UDK map onto the table?  First, it starts with an Optoma Pro350W projector. 


My wife had one requirement when we started this project: I had to be able to completely break down the entire setup and put her dining room back to a presentable state.  That meant no permanent fixtures.  We explored various ways of positioning the projector on top of cabinets, building an overhanging stand for it or (gasp!) hanging it from the chandelier.  Then, one of our brilliant players came up with the idea of the mirror -- that way we wouldn't be restricted to aiming the projector at the table.  That's a cheap $5 Wal-mart kids' room mirror that I removed the framing and backing from.  It's plastic, very light weight, and hung with binder clips and picture wire from white hooks.  (My wife's first compromise about "no permanent fixtures".)

The projector itself sits on a heavy duty Redmond Laptop Stand, so that we could adjust the angle if necessary.  That sits on a decorative shelf.  (My patient wife's second compromise.)


And those are the only two modifications we made to the room.  After D&D night, I can remove every trace of the battlemap setup, save for the decorative shelf and the camouflaged hooks.

Ok, I said "only two" and you're probably thinking, "what is that huge statue looming over the room?!"  Yes, well, that's a statue from the old Wizards of the Coast brick-and-mortar stores that used to be in the malls in our area.  When the stores were closing, I bought a set.  That was very early in our marriage.  (My advice to young lovers: buy all your mid-life crisis toys early, cause you're not getting them later when you want them...)  So, that's been "grandfathered" in, and doesn't count.


Makes a great conversation piece, though.  And scares the pizza delivery guys.  :)

Anyway, where we used to have a battlemat or paper maps, we now use a sheet of bright white vinyl from a fabric store to project the map onto.


No grid marks, so we can position the map anyway/anywhere we want to.  And it's tough as leather, so we don't worry about drink stains or die rolls on the table.

Ok, so we now have the projector setup but what's it connected to?  You may notice that our DM's laptop is running Excel, not the UDK...


It's prohibitively more expensive to buy a gaming capable laptop than a productivity laptop.  That is an inexpensive HP G56 laptop.  It runs the UDK at a laughable 2 fps.  But even if it could run it, it's not practical: we use an Excel based combat tracker.  Flipping projected screens between the UDK and Excel would kill the immersion and probably cause seizures in at least one of us.  So, what's driving the projector?



My monster of a gaming rig sporting dual GTX-285s sits in the very next room.  All it took was:
  • 50 feet of VGA extension cables
  • Input Director, to remotely control my rig from the DM's laptop
(Just a side note about Input Director: this is one of the most useful applications I have ever downloaded.  If you have more than one computer turned on at any one time time, it'll save you time, desk space, and body strain.  I love this thing.  I used to think dual-boxing gamers were ambidextrous, ADD, narcissistic, power gamers.  I've tried it using Input Director and it's shockingly easy.)

The last thing we did was replace the light switch in the room with a dimmer.  When building a map, it can be difficult to determine how bright to light it.  Using a dimmer, we could adjust the light in the room without having to rebuild the map on the fly.  (And it didn't count against my "no permanent fixtures" limitation, since it benefited the room during normal use.)

And that's all the hardware we needed.  The room can be completely returned to normal after all the monsters have been slain.  The next blog will start explaining the UDK code itself.

D&D BattleMap Using the UDK

If you've come here from the YouTube videos, then you know what this is about.  If not, let me give you some background.  I'm sure many D&D DMs reach the same conclusion we did: battlemaps are difficult to make interesting.
  • Wet erase vinyl maps require some time and artistic talent to be more than just room-defining walls.  Plus, you can't really prepare them beforehand if the evening's adventure will be in more than one location.
  • Scanning maps from a module, scaling, printing, cutting and taping uses up a lot of material that gets thrown away at the end of the evening.
  • Dungeon tiles are great if the adventure you are running used them.  Otherwise, they can be clunky to try to fit together to match your design.
Which left us with various digital map tools.  But if we're sitting around the table with laptops staring at a networked map, we loose the immersion of using the miniatures.  (More on that later.)

It was during this period of battlemap exploration that we came across the UDK.  We're a group of IT industry professionals who play D&D.  Surely, we could spend some of our gaming time creating our own game, right?  But what game to make?  Soon afterward, Alien Swarm entered the picture and we had our epiphany: if we made a top-down game, why couldn't we just overlay that with a grid and have a fully animated, realistic, 3D battlemap?

Well, we did.  And came up with this:

The next blog will talk about our setup and how we actually use it to play.  Then we'll explain the code we wrote to build it.  Until then, check out some of the other videos.  We're kinda proud of them.  :)

D&D BattleMap Using the UDK - The Setup