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.)

4 comments:

  1. Hello, I´m spanish and so sorry because my english is very very bad. I do not achieve that it starts.
    But then what version we use, that of August or that of February? Because first you say that of August and then you say "...UnrealScript code starts in \UDK\UDK-2011-02\Development..."
    Thank you

    ReplyDelete
  2. my frontend say warning
    (C:\UDK\UDK-2012-03\Development\Src\BattleMap\Classes\BattleMapGfxHud.uc(34) : Warning, ObjectProperty GFxUI.GFxMoviePlayer:MovieInfo: unresolved reference to 'SwfMovie'BattleMapHud.BMHud'') can you help me?

    en thanks for this blog

    ReplyDelete
  3. Great work and tutorial! I followed this exactly and it mostly works for me, but I have 2 issues: 1 - how would you make the camera more isometric instead of top down? 2 - When i play in game, my player rotates oddly as i move my mouse: almost like if i mouse to the upper part of the screen, my player rotates toward the bottom of the screen. And if i circle the character completely, often the player will pop toward 0,0,0 (ignoring mouse movement for a portion of the screen) or pop in some other direction. would you say this is a swf problem?

    ReplyDelete