Game Development Community

dev|Pro Game Development Curriculum

Multiple Inputs Bound To Single Action

by Tom Spilman · 08/08/2006 (2:00 pm) · 15 comments

www.sickheadgames.com/images/shbug_03.png
With these changes to the ActionMap and OptionsDlg you can now map more than one input to the same action via the GUI. Once applied you'll get the following behaviors when assigning a new mapping:

  1. If you assign an input to an action without any mappings it displays that one input.
  2. If you assign a different second input to an action it displays the two inputs separated by a comma.
  3. If you have two inputs mapped to an action, the first input is removed and the new input is added.
  4. If you have two inputs mapped to an action and the new input is the same as an existing input, then that one is kept and the second input is cleared.
This has been tested with clean versions of TSE and TGE, but may work with TGB as well.
www.grahamsoftware.com/images/GSDlogo.jpg
Also note that the development of this resource was paid for by Graham Software Development, LLC
who generously allowed us to make it available to the GG community.










Code & Script Changes

1. First in ActionMap::ProcessBind() around line 1063 of ActionMap.cc look for either TORQUE_ALLOW_MULTIPLE_ACTIONMAP_BINDS or the following block of code...

if ( pFnName[0] )
   {
      U32 devMapIndex, nodeIndex;
      while ( findBoundNode( pFnName, devMapIndex, nodeIndex ) )
      {
         dFree( mDeviceMaps[devMapIndex]->nodeMap[nodeIndex].makeConsoleCommand );
         dFree( mDeviceMaps[devMapIndex]->nodeMap[nodeIndex].breakConsoleCommand );
         mDeviceMaps[devMapIndex]->nodeMap.erase( nodeIndex );
      }
   }

Remove it or comment it out.

2. Now replace ActionMap::findBoundNode with this (note that nextBoundNode is a new function and must be added to the header):

bool ActionMap::findBoundNode( const char* function, U32 &devMapIndex, U32 &nodeIndex )
{
   devMapIndex = 0;
   nodeIndex = 0;
   return nextBoundNode( function, devMapIndex, nodeIndex );
}

bool ActionMap::nextBoundNode( const char* function, U32 &devMapIndex, U32 &nodeIndex )
{
	// Loop through all of the existing nodes to find the one mapped to the
	// given function:
   for ( U32 i = devMapIndex; i < mDeviceMaps.size(); i++ )
   {
      const DeviceMap* dvcMap = mDeviceMaps[i];

      for ( U32 j = nodeIndex; j < dvcMap->nodeMap.size(); j++ )
      {
         const Node* node = &dvcMap->nodeMap[j];
			if ( !( node->flags & Node::BindCmd ) && ( dStricmp( function, node->consoleFunction ) == 0 ) )
         {
            devMapIndex = i;
            nodeIndex = j;
				return( true );
         }
		}

      nodeIndex = 0;
   }

   return( false );
}

3. Next we change ActionMap::getBinding to return multiple bindings. For example if W and the Up are both mapped to the same action ActionMap::getBinding will return "keyboard\tW\tkeyboard\tup". So here is the change...

const char* ActionMap::getBinding( const char* command )
{
   char* returnString = Con::getReturnBuffer( 1024 );
   returnString[0] = 0;

   char buffer[256];
   char deviceBuffer[32];
   char keyBuffer[64];
 
   U32 devMapIndex = 0, nodeIndex = 0;
   while ( nextBoundNode( command, devMapIndex, nodeIndex ) )
   {
      const DeviceMap* deviceMap = mDeviceMaps[devMapIndex];

      if ( getDeviceName( deviceMap->deviceType, deviceMap->deviceInst, deviceBuffer ) )
      {
         const Node* node = &deviceMap->nodeMap[nodeIndex];
         const char* modifierString = getModifierString( node->modifiers );

         if ( getKeyString( node->action, keyBuffer ) )
         {
            dSprintf( buffer, sizeof( buffer ), "%s\t%s%s", deviceBuffer, modifierString, keyBuffer );
            if ( returnString[0] )
               dStrcat( returnString, "\t" );
            dStrcat( returnString, buffer );
         }
      }

      ++nodeIndex;
   }

   return returnString;
}

4. Now we move on to optionsDlg.cs. First we change buildFullMapString() to this:

function buildFullMapString( %index )
{
   %name       = $RemapName[%index];
   %cmd        = $RemapCmd[%index];

   %temp = moveMap.getBinding( %cmd );
   if ( %temp $= "" )
      return %name TAB "";

   %mapString = "";

   %count = getFieldCount( %temp );
   for ( %i = 0; %i < %count; %i += 2 )
   {
      if ( %mapString !$= "" )
         %mapString = %mapString @ ", ";

      %device = getField( %temp, %i + 0 );
      %object = getField( %temp, %i + 1 );
      %mapString = %mapString @ getMapDisplayName( %device, %object );
   }

   return %name TAB %mapString; 
}

5. Next we add a new function....

/// This unbinds actions beyond %count associated to the
/// particular moveMap %commmand.
function unbindExtraActions( %command, %count )
{
   %temp = moveMap.getBinding( %command );
   if ( %temp $= "" )
      return;

   %count = getFieldCount( %temp ) - ( %count * 2 );
   for ( %i = 0; %i < %count; %i += 2 )
   {
      %device = getField( %temp, %i + 0 );
      %action = getField( %temp, %i + 1 );
      
      moveMap.unbind( %device, %action );
   }
}

6. Finally we replace OptRemapInputCtrl::onInputEvent() with this new one:

function OptRemapInputCtrl::onInputEvent( %this, %device, %action )
{
   //error( "** onInputEvent called - device = " @ %device @ ", action = " @ %action @ " **" );
   Canvas.popDialog( RemapDlg );

   // Test for the reserved keystrokes:
   if ( %device $= "keyboard" )
   {
      // Cancel...
      if ( %action $= "escape" )
      {
         // Do nothing...
         return;
      }
   }

   %cmd  = $RemapCmd[%this.index];
   %name = $RemapName[%this.index];

   // Grab the friendly display name for this action
   // which we'll use when prompting the user below.
   %mapName = getMapDisplayName( %device, %action );
   
   // Get the current command this action is mapped to.
   %prevMap = moveMap.getCommand( %device, %action );

   // If nothing was mapped to the previous command 
   // mapping then it's easy... just bind it.
   if ( %prevMap $= "" )
   {
      unbindExtraActions( %cmd, 1 );
      moveMap.bind( %device, %action, %cmd );
      OptRemapList.setRowById( %this.index, buildFullMapString( %this.index ) );
      return;
   }

   // If the previous command is the same as the 
   // current then they hit the same input as what
   // was already assigned.
   if ( %prevMap $= %cmd )
   {
      unbindExtraActions( %cmd, 0 );
      moveMap.bind( %device, %action, %cmd );
      OptRemapList.setRowById( %this.index, buildFullMapString( %this.index ) );
      return;   
   }

   // Look for the index of the previous mapping.
   %prevMapIndex = findRemapCmdIndex( %prevMap );
   
   // If we get a negative index then the previous 
   // mapping was to an item that isn't included in
   // the mapping list... so we cannot unmap it.
   if ( %prevMapIndex == -1 )
   {
      MessageBoxOK( "Remap Failed", "\"" @ %mapName @ "\" is already bound to a non-remappable command!" );
      return;
   }

   // Setup the forced remapping callback command.
   %callback = "redoMapping(" @ %device @ ", \"" @ %action @ "\", \"" @
                              %cmd @ "\", " @ %prevMapIndex @ ", " @ %this.index @ ");";
   
   // Warn that we're about to remove the old mapping and
   // replace it with another.
   %prevCmdName = $RemapName[%prevMapIndex];
   MessageBoxYesNo( "Warning",
      "\"" @ %mapName @ "\" is already bound to \""
      @ %prevCmdName @ "\"!\nDo you wish to replace this mapping?",
       %callback, "" );
}

And that's it... Enjoy!

www.sickheadgames.com/stuff/garagegamesimages/multioptions.png

About the author

Tom is a programmer and co-owner of Sickhead Games, LLC.


#1
08/05/2006 (10:23 am)
Nice one Tom!

Dropped in without a hitch.

Thanks.

- Tim
#2
08/08/2006 (2:35 pm)
thanks, i will use this a lot!
#3
08/08/2006 (8:31 pm)
Awesome. My solution was doubling up the controls (which didn't work very well), this is much better!
Thanks alot.
#4
08/09/2006 (12:22 am)
Fantastic! Thanks Tom!
#5
08/09/2006 (8:30 am)
Great resource Tom, keep em coming!
#6
08/09/2006 (9:44 am)
OMG it finally happened, woot!
I had a bounty out on this feature in a forum thread somewhere, let me know if you would like to collect on that.
Regards,
Dreamer
#7
08/09/2006 (11:39 am)
@Dreamer - Naw don't worry about it... enjoy.
#8
08/10/2006 (7:11 am)
Well I love the idea of this, unfortunately the application is giving me hell. Why don't I see 2 mappings? It still looks exactly the same and performs the same. I followed your code to the letter and it just isn't there.
#9
08/10/2006 (10:56 am)
@Ron - I really couldn't say why it isn't working for you. What version of Torque are you integrating with?
#10
08/10/2006 (11:10 am)
OK, I am a retard. I somehow didn't get everything in STEP 1!!! Sheesh. This thing works wonderfully. Thank you very much. Now back to making the joystick axis/pov input remap too in GUI.
#11
08/10/2006 (12:13 pm)
I found one interesting thing, if you remap a selection using a different device each time you can add way more that 2 devices. For example, keyboard, mouse, and joystick.
#12
08/12/2006 (8:39 am)
I know this simple question but not a code. I am more of artist but I am learning. How you add to the header? note that nextBoundNode is a new function and must be added to the header.
#13
08/16/2006 (12:02 pm)
way cool, thx for sharing! :)
#14
08/16/2006 (9:18 pm)
Michael,

The header is the .h file. So open actionMap.h and add this under bool findBoundNode
bool nextBoundNode( const char* function, U32 &devMapIndex, U32 &nodeIndex );
#15
08/17/2006 (8:51 pm)
Has anyone succeeded in getting this to work in TGB? I can't seem to find the buildFullMapString function anywhere...