Sunday, April 17, 2011

D&D BattleMap Using the UDK - Torches and Lighting

This post, I eagerly wanted to get torches back into our BattleMap.  The iconic dungeon torch is oddly comforting for players.  Telling them "you can see 5 squares from where you stand" and actually having a lit torch on the map are two different things.  You'd be surprised to find that they'll huddle around a torch bearer when they can actually see the range of their vision.

The first time we tried to implement this we made the torch radius configurable through the command console.  Not so great for trying to setup a candle or a sunrod, while not affecting every other torch on the map.  This time, we're going to add mouse selection and keyboard commands to modify individual torches.

BattleMapTorch.uc

The first thing we need is an object that looks like fire.  I simply followed the 3D Buzz Video Tutorials on particle emitters.  Literally, click-for-click.  The only thing I did different was turn off the smoke, since it would obscure the map from our top-down view.

Once you have the emitter built, that becomes the base for our torch object.  But, we need to add some features to it.  Drop one onto a map, then open the properties window.  There's a nifty little icon in the upper right hand corner that looks like a wrench.  Choose "Copy To Clipboard", then paste the contents into an editor.  What you see are the properties that would be set in the DefaultProperties section of an emitter.uc object.  That's what we'll start with.  (Not all of those properties are necessary, but it was certainly easier to start with that than trying to figure them all out from scratch.)

Next, we add some network replication functionality.  The BattleMap does work with multiple clients, however we found that using InputDirector is easier.  (See the post on the setup.)

Moving the torch is simply a matter of switching between two states: one that updates the torch's location from the mouse position and one that doesn't.  We turn off collision detection when we're moving it around to avoid it getting stuck on walls or terrain.  Why does stopping the torch also include updating the location?  That has to do with network replication.  For efficiency, synchronization information is not always transmitted.  Some packets are skipped.  (That's why you can be aiming right at someone's head but still miss a shot.)  So, when the torch is no longer moving it may actually appear to be in different locations on clients.  This forces every copy of the torch to be in exactly the same spot.

Lastly, we add a LightAttachment component to the torch to actually create the torch light.  Otherwise, it would just be a flame in the dark.

class BattleMapTorch extends EmitterSpawnable;
var Color LightColor;
var float LightScale;
var PointLightComponent LightAttachment;
var Vector VerticalOffset;
var repnotify Vector TorchLoc,TorchStopLoc;
var float TorchRadius;
replication
{
 if (bNetDirty)
  TorchLoc,TorchStopLoc;
}
// called on clients when TorchStopLoc gets replicated
simulated event ReplicatedEvent( name VarName )
{
 if (VarName == nameof(TorchLoc) )
  SetLocation( TorchLoc );
 else if (VarName == nameof(TorchStopLoc) )
  StopTorch( TorchStopLoc );
 else
  super.ReplicatedEvent( VarName );
}
// this function should be run on both clients and server
simulated function MoveTorch( )
{
 bCollideWorld = false;
 SetCollision(false, false);
 GotoState( 'TorchMoving' );
}
simulated function StopTorch( Vector NewTorchStopLoc )
{
 bCollideWorld = true;
 SetCollision(true, true);
 GotoState('TorchStopped');
 TorchStopLoc = NewTorchStopLoc;
 SetLocation(NewTorchStopLoc);
}
simulated state TorchMoving
{
 simulated event Tick( float DeltaTime )
 {
  // move the torch...
  TorchLoc = BattleMapPlayerController(Owner).MouseOrigin + VerticalOffset;
  SetLocation(TorchLoc);
 }
}
simulated state TorchStopped
{
 simulated event Tick( float DeltaTime )
 {
  // stop the torch...
 }
}
simulated function PostBeginPlay()
{
 LightAttachment = new(self)class'PointLightComponent';
 LightAttachment.SetLightProperties(LightScale, LightColor);
 LightAttachment.SetEnabled(true);
 LightAttachment.Radius = TorchRadius;
 LightAttachment.SetTranslation(vect(0, 0, 1) * 50);
 if ( LightAttachment != None )
 {
  self.AttachComponent(LightAttachment);
 }
 super.PostbeginPlay();
}
DefaultProperties
{
 TorchRadius=600
 VerticalOffset=(X=0,Y=0,Z=50)
 LightColor=(B=0,G=200,R=255,A=255)
 LightScale=4.0
    DrawScale=2.000000
 Begin Object Name=ParticleSystemComponent0
        Template=ParticleSystem'BattleMapAssets.Particles.PS_Torch'
        ReplacementPrimitive=None
        LightingChannels=(bInitialized=True,Dynamic=True)
  SecondsBeforeInactive=0
 End Object
 ParticleSystemComponent=ParticleSystemComponent0
 Begin Object Class=CylinderComponent Name=CollisionCylinder
  CollisionRadius=+0064.000000
  CollisionHeight=+0064.000000
  BlockNonZeroExtent=true
  BlockZeroExtent=true
  BlockActors=true
  CollideActors=true
 End Object
 CollisionComponent=CollisionCylinder
 Components.Add(CollisionCylinder)
   RemoteRole=ROLE_SimulatedProxy
   bAlwaysRelevant=true
   bReplicateMovement=true
   bNetTemporary=false
   bCanBeDamaged = false
   bCollideActors = true
   bBlockActors = true
   bCollideWorld = true
   bStatic=false
   bNoDelete=false
   bMovable=true
}

BattleMapPlayerInput.uc

Now, we need to add functions to our PlayerInput object to manipulate torches.  First is a method to spawn a torch.  We created an all-purpose spawn function, and will use keybind parameters to pass in what to spawn:

exec function BMSpawn(string ObjectToSpawn)
{
 if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)
 {
  switch(ObjectToSpawn)
  {
   case "Torch":
    Spawn(class'BattleMapTorch',Outer,,MouseOrigin,,,true );
    break;
  }
 }
}
Next, we want to activate functionality of an object when it's selected.  In this case, we want the torch to start moving when it's clicked.  But each object may do something different.  For example, the triggers we implemented earlier will simply "turn on" -- nothing to continue doing.  So, the function will first stop whatever a currently selected object may be doing (stop moving if we had already selected a torch) then start whatever the newly selected object needs to do.  Replace the BMInteractObject() function with the following:
function string ParseObjectName(Name ObjectName)
{
 local string ObjectType;

 ObjectType = Repl(ObjectName, "BattleMap", "");
 if (InStr(ObjectType, "_") > -1)
  ObjectType = Mid(ObjectType, 0, InStr(ObjectType, "_"));
 return ObjectType;
}

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);
     ObjectSelected = none;
     break;
   }
  }
  //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;
  }
 }
}
As we add more interactive objects to our BattleMap, we'll insert the method to trigger them in this function.  Also, add this declaration to the top of the code along with GridZoomDelta:

var Object ObjectSelected;
Finally, once we have a torch selected we have the ability to modify just that instance of the object.  In this case, we want to be able to modify its light radius.  Another frustrating issue we had recently, due to the summer time change, was seeing the projected map in a lit room.  Even with the lights off, it can be difficult to see what you thought was a brightly lit map in the editor.  So, we wanted the ability to adjust the map's brightness.  Therefore if no object is selected, the default behavior will be to manipulate the lights in a map.  Note that this only works for dynamic lights.  Static lights are... static.  Add the following function:

exec function BMModifyObject(int Modifier)
{
 local BattleMapTorch T;
 local PointLightToggleable L;
 local LightComponent C;

 if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)
 {
  if (ObjectSelected != none)
  {
   switch(Left(ObjectSelected, 14))
   {
    case "BattleMapTorch":
     T = BattleMapTorch(ObjectSelected);
     T.LightAttachment.Radius = T.LightAttachment.Radius * (1 + (0.1 * Modifier));
     break;
   }
  }
  else
  {
   foreach AllActors(class'PointLightToggleable', L)
   {
    C = L.LightComponent;
    C.SetLightProperties(C.Brightness * (1 + (0.25 * Modifier)));
   }
  }
 }
}
DefaultInput.ini

All that's left is to bind keys to the functions we created.  A "T" will spawn a torch.  Left clicking will interact with objects. And the plus/minus keys ("Equals"/"Underscore") will manipulate selected objects.  Replace the "I" binding in our BattleMap Bindings section with the following:

.Bindings=(Name="T",Command="BMSpawn Torch")
.Bindings=(Name="LeftMouseButton",Command="BMInteractObject")
.Bindings=(Name="Equals",Command="BMModifyObject 1")
.Bindings=(Name="Underscore",Command="BMModifyObject -1")

Here's what it looks like in action:



BattleMap mode source files

32 comments:

  1. This is a fantastic project, thanks for sharing the how to.

    I have been trying to follow this for my own group but have hit a problem. I have UDK, Flash (with Scaleform) installed and setup correct;y I think. When I launch the map BM_Necropolis, the level launches in the top down view OK. NWAD, C and space keys all work as expected however G causes an application crash.

    I have built a BMHud.swf containing my grid and the actionscript code for toggling and zooming. I imported the .swf file into a package called BattleMapHud.upk and ensured a copy is placed in UDK_GAME/CookedPC after cooking the level. I have also ensured the key bindings are in place. What am I missing, please help!

    The one thing I am unsure about is the AS code you reference at the start of the Fixed Hud post, where does it go? In the BMHud.swf file? ANy assistance would be much appreciated.

    DrZ

    ReplyDelete
  2. Typically, ActionScript is put into its own layer. Mine has three layers:

    * Actions
    * Cursor
    * Grid

    All the ActionScript is in the Actions layer.

    You can also add debug statements to your ActionScript using the trace() function. Inside your GridToggle() function, add:

    trace("toggling grid");

    Then open up the Config\UDKEngine.ini and look for:

    Suppress=DevGFxUI

    Put a semicolon in front to comment it out. Now when you play your map and hit "G", check the Launch.log.

    Also, check your GfxHud.uc object and make sure you're referencing the correct movie:

    MovieInfo = SwfMovie'BattleMapHud.BMHud'

    Let me know if you determine what's wrong.

    ReplyDelete
  3. i had the same problem, till i realized the folder tha contains the .swf had the wrong name (has to be BattleMapHud), stupidest mistake, but it happens, good luck solving ur problem :)

    ReplyDelete
  4. Ah OK. Got the G key to no longer crash, however no Grid is displayed when I toggle G. The folder that contained the source .swf was called BattleMap, changing it to BattleMapHud seems to have stopped the crash.

    I'll add the trace to see if that gives a clue as to why the Grid isn't showing.

    I also didn't realise I had to provide my own cursor image in a separate layer in the .swf. I'll give that a go too.

    Thanks for the assistance. DrZ

    ReplyDelete
  5. try moving the mouse wheel a bit and see if that makes the grid to appear, that happens to me too, takes a few turns of the wheel but it finally shows

    ReplyDelete
  6. I added my own cursor image to a separate layer called Cursor, its displayed on the map but its static and doesn't move with the mouse. The MousePosition and MouseOrigin values in the hud update ok though when the mouse is moved.

    Still no joy with the grid, I tried toggling the mouse wheel a few notches but no dice. Any other thoughts?

    ReplyDelete
  7. Doh!, the grid wasn't showing because I hadn't maximised the window. G key still not working and mouse wheel also not working. Cursor image is also static.

    I have my GRID set to 1440x900 (resolution of test monitor). Thanks again for any assistance. DrZ

    ReplyDelete
  8. If the mouse position is updating in UnrealScript, then at least your map communicating with your Flash. After the mouse event listener, are you dragging the cursor symbol?

    startDrag("CursorInst", true);

    As for the grid, try putting a trace() statement in the function that toggles the grid. That will narrow down whether the issue is in ActionScript or UnrealScript.

    ReplyDelete
  9. Here's the AS code block before the GridToggle() and GridZoom() functions.

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

    I have added the trace() statement to the GridToggle() function, what am I looking for in the Launch.log file?

    ReplyDelete
  10. Here's the output in Launch.log:

    [0033.29] DevGFxUI: toggling grid

    [0044.62] ScriptWarning: Invalid user index (255) specified for ClearReadProfileSettingsCompleteDelegate()
    OnlineSubsystemSteamworks Transient.OnlineSubsystemSteamworks_0
    Function OnlineSubsystemSteamworks.OnlineSubsystemSteamworks:ClearReadProfileSettingsCompleteDelegate:00FE

    ReplyDelete
  11. Ok, perfect. Your ActionScript grid toggle function is getting called. Do you have this in the function as well?

    GridInst._visible = !GridInst._visible;

    And do you have the grid placed on your scene? Upper left corner of the symbol lined up with the upper left corner of the scene?

    ReplyDelete
  12. if u are new to flash maybe u forgot to name ur grid and cursor instances... once you turned your grid and cursor to movieclip objects, select one of them and on the right side of the screen youll see its properties, you need to fill in the instance name with the appropiate one (GridInst and CursorInst)

    ReplyDelete
  13. btw, i just noticied this when i got the torch working, when im on the left side of the map, the cursor position is correct, but as i move to the right its kinda "left behind" im not sure how to explain this, its like the cursor image its not where it HAS to be...

    for instance, the cursor image its in one place and the torch its several units to the right, im not sure that im making myself clear enough, ill try to post a picture tomorrow to show you guys.

    and a million thanks again for this, its just an incredible amazing work

    ReplyDelete
  14. Yes, naming the movieclips to CursorInst and GridInst, did the trick, thank you. :) Both Grid and cursor are working now. This is really cool.

    Now to move on to adding my particle system and torch. Thanks for the help guys, keep up the great work.

    DrZ prepares to make his first map.

    ReplyDelete
  15. And make sure when u make the particle system to name and save it in the same package thats on the code, otherwise it wont work:
    Package: BattleMapAssets
    Group: Particles
    Name: PS_Torch

    ReplyDelete
  16. Yes, I spotted that when reviewing the code. All is now working including triggered doors and torches. :) This is bloody marvellous!

    A couple of questions:

    First, I have made the grid 1440x900 (each of the vertices 20px apart) and set the following settings under [SystemSettingsPIB] in DefaultEngine.ini to:

    Fullscreen=True
    ResX=1440
    ResY=900

    However there is a small border (less the 2-3cm) around the edge of the entire map before the grid starts/ends. Hopefully you can visualise what I am describing.

    Is this deliberate or is there a way to ensure the grid overlays the entire map?

    Secondly, could this explain why I am having difficulty aligning the grid with the edges of the example map from 'Making it Interactive'? I cannot seem to size the grid to align properly. Also should the zoom function also zoom the grid scale?

    Thanks for the continued assistance.

    DrZ

    ReplyDelete
  17. i had the same problem for some reason, fixed it moving a bit upwards and to the left outside the scene on the flash file, and its not exactly scaled probably cause the material on the map, i can bet its a math problem involving measures on the ground and i suck at them, i just apply the material on the right scale (cant remember if it was 2 or 4) when i make the maps so i dont need the grid except when they are on terrains like grass, rock, etc.

    ReplyDelete
  18. Hmm, shifting it out to the left of the scene and up seems to align it better but its still not quite true. I wonder if the widescreen resolution is what's throwing it out?

    ReplyDelete
  19. 1st: Got this up and running! So Awesome, Keep Up the Blog Posts!

    2nd: I also had the Torch/Cursor mismatch problem that Gerar Orpheo mentioned. This is a problem with mismatching screen resolution values. I fixed this by:

    - Choosing a screen res, and setting the Flash Artboard to that resolution (I chose 1280x800).

    - Open "BattleMapHud.uc" and make sure the values near the top (in the simulated function) are set correctly:
    SizeX = 1280.0f;
    SizeY = 800.0f;

    - Set your resolution (and/or fullscreen option) in the "DefaultEngineUDK.ini", the "DefaultEngine.ini", and "UDKEngine.ini", look for the "ResX=" and "ResY=" options, and make sure you're not setting the Mobile resolution.

    Actually, there's a shortcut for that last step. Open the Game via the UDKGame shortcut. [Tab] to open the console and type "setres 1280x800w" (my settings... use 'f' at the end for fullscreen). This will setup the *ini files (and even setup the Editor Resolution).

    ReplyDelete
  20. A couple of questions for our gracious blogger host, ZPN:

    How did you accomplish the hide/reveal of the areas past the set of doors (in purple light)? Dynamic Lights triggered by the Door Trigger? I've been warned away from using too may Dynamic Lights.

    2nd, I was wondering if we could get a "delete torch" function in example code.

    Thanks Again!

    ReplyDelete
  21. Thanks a lot Joshua, that seems to have fixed it for me too, appreciate the help, i had missed the resolution code on BattleMapHud.uc

    ReplyDelete
  22. and ill get a bit in the middle now to answer Joshua the lights trick, they used a toglable light that is triggered with the same trigger that opens the doors, or at least thats what they did on the previous video.

    ReplyDelete
  23. Thanks, Joshua. Good catch on the differing resolutions. And yes, revealing portions of the map is done with togglable point lights. We use them quite liberally throughout our maps and have yet to experience any negative issues from it. Look for deleting objects (torches) in the next post.

    ReplyDelete
  24. Hey guys, i was messing around building Tomb of Horrors today and i noticed that when i make a wall with a brush, the torch light filtrates to the other room, the wall isnt stopping the light from passing, so whenever theres a secret door and a room on the other side, its not that secret, any ideas how can i fix that?, i tried with lightmass volumes, but it doesnt work, the torch light (and just that particular light) keeps filtrating, thanks in advance for your help

    ReplyDelete
  25. Gerar,

    I would check 3 things.

    First, make sure you have Grid Snapping turned on and that your brushes are snapped to the grid (r-click on on of the brush's vertexes to snap it to the grid, then drag to position). Sometimes a super tiny gap will leak light (although this mostly happens with Lightmass).

    2nd, there is a known bug in the "default properties" of CSG brushes. To resolve this, select all brush surfaces (Shift+S). Then, hit F5 for lighting/texture properties. Go to the bottom and in the Lightmass box, right click on the Root "Lightmass Settings" entry (the one with the collapsible arrow next to it). You should see a "Reset to Defaults" option. Select that. Make sure that any sub-entries under LightmassSettings are _no longer bolded_.

    Third, select your light. F4 for properties. Look under Light> Light Component> Light Component> Lighting Channels. Make sure the BSP lighting channel is checked.


    If I'd have to guess, I'd say it's #2, but check.

    Can you tell I'm taking a UDK lighting and textures course?

    ReplyDelete
  26. Oops, forgot some stuff in #3. Select a surface on the brush in question. Right click on it, and choose Select Surfaces> Matching Brush. F5 for Surface Properties. Look under the Lighting Channels. Make sure they match your light in question.

    ReplyDelete
  27. Thanks for you help Joshua, sadly it didn't fixed the problem, the torch light keeps filtrating over the wall, the other lights act just fine, im posting a pic so you can see the problem and see if you or our great hosts can think of a different solution

    http://img716.imageshack.us/img716/7339/lightingissue.jpg

    ReplyDelete
  28. Hey guys, I hope all is well. I have been busy making a decent encounter map and all is going extremely well, I can't thank you enough for the guide, scripts and assistance.

    I am however hitting an issue with a Cinematic I have prepared in Matinee that is supposed to fire when the player spawns or the level is loaded. Its a pretty simple camera cinematic of a tour of the immediate encounter area which then pulls back to the overhead view (pretty much like some of the examples from the Youtube videos). Problem is whilst the cinematic works it doesn't fire correctly at the start of a level. In addition I am finding the camera starting position changes after a level is loaded and I am back in the editor and Matinee.

    Am I right is thinking this is related to the scripted changes for the camera position or am I missing something. Any help would be much appreciated.

    ReplyDelete
  29. Nevermind, I figured it out. I forgot to Toggle Cinematic Mode in my Kinsmet setup, its now working. :)

    Alos figured out why the movement track for the camera was changing after a level is loaded, I had the Movement Track set to 'Relative to Initial', changing it to 'World Frame' did the trick.

    ReplyDelete
  30. You should make a post about spawning actors (monsters). Seems like you could copy/paste a chunk from BattleMapTorch and modify it to suit.

    But thus far, I am damn impressed. Major kudos to you.

    ReplyDelete
  31. Dr.Zeuss i had the same problem with the cinematics, thanks for the fix

    ReplyDelete