Hold and Time-Context button events
by Jeff Raab · 02/03/2009 (4:01 am) · 11 comments
I had released a resource that simulated this behavior in script a while back, and i've found that recently, while it works, it's honestly not good enough, clunky, and hackish.
So this evening, I set out to implement the same thing in the engine itself.
7 hours later, I'm done, and I figure this is decent enough that people may enjoy it as a resource, so here it is.
All the changes happen in sim/actionMap.cpp/h and you'll be making a new file as well.
First and foremost, make a new file called holdButtonEvent.cpp in the sim folder and put this code into it:
save it, and then open up actionMap.h
at the top, under the other header definitions, add:
then, underneath the line:
in the ActionMap class, add:
Then, further down, in the Flags enum, add a comma to the last define, so it looks like
and then add:
Below that.
Then, at the bottom of the Node struct definition, below
add:
Further down, under the line:
add:
Finally, at the bottom of the file, before the #endif
add:
Next, open up actionMap.cpp, and find the lines
Below that, add:
And again after
Next, below the lines
Then, after the define of
Add:
Next, find the line that reads:
Next, after the lines
Then find the line that reads:
Then, below the definition:
Save everything, and recompile it.
To use, simply create a normal keybind setup, but instaed of .bind or .bindCmd, use:
moveMap.bindContext(keyboard, ",", HoldTester, TapTester, 10000);
to do a time-context based keybind, or
moveMap.bindHold(keyboard, ".", HoldTester);
For a hold-based.
To get it to return the time held, simply change it to read:
moveMap.bindHold(keyboard, ".", HoldTester, true);
and add a argument to your function definition to get the time, such as:
for context binds, the format is device, action, Holding function(called after the minimum hold-time is passed while keeping the key pressed), Tap function(called when releasing the key, but before we pass the minimum hold time, and the Minimum hold time(in milliseconds).
For hold binds, you simply put your device, action, and function call.
This doesn't work for move events, such as the mouse axis, though you could probably get that in with some work.
Hope this is helpful.
-Jeff
So this evening, I set out to implement the same thing in the engine itself.
7 hours later, I'm done, and I figure this is decent enough that people may enjoy it as a resource, so here it is.
All the changes happen in sim/actionMap.cpp/h and you'll be making a new file as well.
First and foremost, make a new file called holdButtonEvent.cpp in the sim folder and put this code into it:
#include "sim/actionMap.h"
#include "platform/platform.h"
holdButtonEvent::holdButtonEvent(StringTableEntry func, F32 minHoldTime, ActionMap::Node* button, bool holdOnly)
{
consoleFunctionHeld = func;
mMinHoldTime = minHoldTime;
mButton = button;
mEventValue = 1.0; //default
startTime = 0;
mHoldOnly = holdOnly;
breakEvent = false;
active = false;
didHold = false;
returnHoldTime = false;
}
void holdButtonEvent::processTick()
{
if(active)
{
F32 currTime = Sim::getCurrentTime();
static const char *argv[2];
//see if this key even is still active
if(!breakEvent)
{
//are we only checking if it's holding?
if(mHoldOnly)
{
//yes, we are, and since it's held, we fire off our function
if(returnHoldTime)
{
argv[0] = consoleFunctionHeld;
argv[1] = Con::getFloatArg(mEventValue);
argv[2] = Con::getFloatArg((currTime - startTime));
Con::execute(3, argv);
}
else
{
argv[0] = consoleFunctionHeld;
argv[1] = Con::getFloatArg(mEventValue);
Con::execute(2, argv);
}
}
//if we don't care if we're just holding, check our time
//have we passed our min limit?
else if((currTime - startTime) >= mMinHoldTime)
{
//holy crap, we have, fire off our hold function
didHold = true;
argv[0] = consoleFunctionHeld;
argv[1] = Con::getFloatArg(mEventValue);
Con::execute(2, argv);
}
//otherwise we haven't yet, so keep our active status
return;
}
//hmm, apparently not, so see if we tapped the key instead
else
{
if(!mHoldOnly && !didHold)
{
//yes, we tapped and we care, so fire off the tap function.
argv[0] = mButton->consoleFunction;
argv[1] = Con::getFloatArg(mEventValue);
Con::execute(2, argv);
}
//otherwise we don't care and we're done, so reset everything
active = false;
startTime = 0;
breakEvent = false;
didHold = false;
}
}
}
void holdButtonEvent::advanceTime(F32 deltaTime)
{
}
void holdButtonEvent::interpolateTick(F32 delta)
{
}save it, and then open up actionMap.h
at the top, under the other header definitions, add:
//hold/time-context stuff -JR #ifndef _ITICKABLE_H_ #include "core/iTickable.h" #endif class holdButtonEvent; //-JR
then, underneath the line:
typedef SimObject Parent;
in the ActionMap class, add:
//hold/time-context stuff -JR friend class holdButtonEvent;
Then, further down, in the Flags enum, add a comma to the last define, so it looks like
BindCmd = BIT(5), ///< Bind a console command to this.
and then add:
//hold/time-context stuff -JR Held = BIT(6)
Below that.
Then, at the bottom of the Node struct definition, below
char *breakConsoleCommand; ///< Console command to execute when we break this command.
add:
//Hold/time-context stuff -JR holdButtonEvent* holdEvent; // -JR
Further down, under the line:
bool processBindCmd(const char *device, const char *action, const char *makeCmd, const char *breakCmd);
add:
//hold/time/context stuff -JR bool processHoldBind(const char *device, const char *action, const char *holdFunc, const char *tapFunc, const U32 holdTime, const bool holdOnly, const bool returnHoldTime = false);
Finally, at the bottom of the file, before the #endif
add:
//hold/time-context stuff -JR
class holdButtonEvent : public ITickable
{
ActionMap::Node* mButton; ///< our button we're holding
F32 mMinHoldTime; ///< minimum time to qualify as 'held'. If we hold less than this,
///< it's a 'press', otherwise it's a 'held'
public:
F32 startTime; ///< Our timestamp when we first pressed.
F32 mEventValue; ///< Event value from our key event.
StringTableEntry consoleFunctionHeld; ///< Console function to call with new values if we held over
///< a certain time.
bool mHoldOnly; ///< does this only care if we're holding?
///< true means that it only fires a function while holding
///< false time-contexts it
bool breakEvent; ///< Button is no longer being pressed!
bool didHold; ///< did we, at some point in the process, hold the button?
bool active; ///< do we be tickin?
bool returnHoldTime; ///< Do we return back our time held?
holdButtonEvent(StringTableEntry func, F32 minHoldTime, ActionMap::Node* button, bool holdOnly);
virtual void interpolateTick( F32 delta );
virtual void processTick();
virtual void advanceTime( F32 timeDelta );
};
//-JRNext, open up actionMap.cpp, and find the lines
if (getKeyString(rNode.action, objectbuffer) == false)
continue;at around 122.Below that, add:
//hold/time-context stuff -JR const char* command; if(rNode.flags & Node::BindCmd) command = "bindCmd"; else if(rNode.flags & Node::Held) command = "held"; else command = "bind"; //-JRnext, below the lines
else
dStrcat(lineBuffer, ", """);at line 186, add://hold/time-context stuff -JR
}
else if (rNode.flags & Node::Held) {
dStrcat(lineBuffer, ", ");
dStrcat(lineBuffer, rNode.consoleFunction);
dStrcat(lineBuffer, ", ");
dStrcat(lineBuffer, rNode.holdEvent->consoleFunctionHeld);
//-JR Then below the lines if (getKeyString(rNode.action, keybuffer) == false)
continue;at line 223, add://hold/time-context stuff -JR const char* command; if(rNode.flags & Node::BindCmd) command = "bindCmd"; else if(rNode.flags & Node::Held) command = "held"; else command = "bind"; //-JR
And again after
else
dStrcat(finalBuffer, ", """);on line 286, add://hold/time-context stuff -JR
}
else if (rNode.flags & Node::Held) {
dStrcat(finalBuffer, ", ");
dStrcat(finalBuffer, rNode.consoleFunction);
dStrcat(finalBuffer, ", ");
dStrcat(finalBuffer, rNode.holdEvent->consoleFunctionHeld);
//-JRNext, below the lines
return( returnString ); }at line 674, add:
//hold/time-context stuff -JR
if ( mapNode->flags & Node::Held )
{
S32 bufferLen = dStrlen( mapNode->consoleFunction ) + dStrlen( mapNode->holdEvent->consoleFunctionHeld ) + 2;
char* returnString = Con::getReturnBuffer( bufferLen );
dSprintf( returnString, bufferLen, "%st%s",
( mapNode->consoleFunction ? mapNode->consoleFunction : "" ),
( mapNode->holdEvent->consoleFunctionHeld ? mapNode->holdEvent->consoleFunctionHeld : "" ) );
return( returnString );
}
//-JRThen, after the define of
bool ActionMap::processBind(const U32 argc, const char** argv, SimObject* object)
Add:
//hold/time-context stuff -JR
bool ActionMap::processHoldBind(const char *device, const char *action, const char *holdFunc, const char *tapFunc, const U32 holdTime, const bool holdOnly, const bool retHoldTime)
{
U32 deviceType;
U32 deviceInst;
if(!getDeviceTypeAndInstance(device, deviceType, deviceInst))
{
Con::printf("processBindCmd: unknown device: %s", device);
return false;
}
// Ok, we now have the deviceType and instance. Create an event descriptor
// for the bind...
//
EventDescriptor eventDescriptor;
if (createEventDescriptor(action, &eventDescriptor) == false) {
Con::printf("Could not create a description for binding: %s", action);
return false;
}
// SI_POV == SI_MOVE, and the POV works fine with bindCmd, so we have to add these manually.
if( ( eventDescriptor.eventCode == SI_XAXIS ) ||
( eventDescriptor.eventCode == SI_YAXIS ) ||
( eventDescriptor.eventCode == SI_ZAXIS ) ||
( eventDescriptor.eventCode == SI_RXAXIS ) ||
( eventDescriptor.eventCode == SI_RYAXIS ) ||
( eventDescriptor.eventCode == SI_RZAXIS ) ||
( eventDescriptor.eventCode == SI_SLIDER ) ||
( eventDescriptor.eventCode == SI_XPOV ) ||
( eventDescriptor.eventCode == SI_YPOV ) ||
( eventDescriptor.eventCode == SI_XPOV2 ) ||
( eventDescriptor.eventCode == SI_YPOV2 ) )
{
Con::warnf( "ActionMap::processBindCmd - Cannot use 'bindCmd' with a move event type. Use 'bind' instead." );
return false;
}
// Event has now been described, and device determined. we need now to extract
// any modifiers that the action map will apply to incoming events before
// calling the bound function...
//
// DMMTODO
F32 deadZoneBegin = 0.0f;
F32 deadZoneEnd = 0.0f;
F32 scaleFactor = 1.0f;
// Ensure that the console function is properly specified?
//
// DMMTODO
// Create the full bind entry, and place it in the map
//
// DMMTODO
Node* pBindNode = getNode(deviceType, deviceInst,
eventDescriptor.flags,
eventDescriptor.eventCode);
pBindNode->flags = Node::Held;
pBindNode->deadZoneBegin = deadZoneBegin;
pBindNode->deadZoneEnd = deadZoneEnd;
pBindNode->scaleFactor = scaleFactor;
pBindNode->consoleFunction = StringTable->insert(dStrdup(tapFunc));
pBindNode->holdEvent = new holdButtonEvent(StringTable->insert(dStrdup(holdFunc)), holdTime, pBindNode, holdOnly);
pBindNode->holdEvent->returnHoldTime = retHoldTime;
return true;
}
//-JRNext, find the line that reads:
enterBreakEvent(pEvent, pNode);at around 1265, and replace it with:
//filter to prevent Hold buttons from being eaten -JR
if(!(pNode->flags & Node::Held))
enterBreakEvent(pEvent, pNode);
//-JRNext, after the lines
Con::evaluate(pNode->makeConsoleCommand); }at line 1309, add:
//held/time-context stuff -JR
else if(pNode->flags & Node::Held)
{
//check if we're already holding, if not, start our timer
if(!pNode->holdEvent->active){
pNode->holdEvent->active = true;
pNode->holdEvent->startTime = Sim::getCurrentTime();
pNode->holdEvent->mEventValue = value;
}
}
//-JRThen find the line that reads:
else if (pEvent->action == SI_BREAK)
{at about line 1433 and below that, add:/hold/time-context stuff -JR
const Node* button = findNode( pEvent->deviceType, pEvent->deviceInst,
pEvent->modifier, pEvent->objInst );
if(button != NULL) {
if(button->flags == Node::Held)
{
if(!button->holdEvent->breakEvent)
button->holdEvent->breakEvent = true;
return true;
}
}
//-JRThen, below the definition:
ConsoleMethod( ActionMap, bindCmd, void, 6, 6, "actionMap.bindCmd( device, action, makeCmd, breakCmd )" )on line 1676, add:
//hold/time-context -JR
ConsoleMethod( ActionMap, bindContext, void, 7, 7, "actionMap.bindCmd( device, action, holdFunction, tapFunction, holdTime)" )
{
object->processHoldBind( argv[2], argv[3], argv[4], argv[5], dAtof(argv[6]), false );
}
ConsoleMethod( ActionMap, bindHold, void, 5, 6, "actionMap.bindCmd( device, action, holdFunction, returnHoldTime)" )
{
if(argc == 6)
object->processHoldBind( argv[2], argv[3], argv[4], "", 0, true, argv[5]);
else
object->processHoldBind( argv[2], argv[3], argv[4], "", 0, true);
}
//-JRSave everything, and recompile it.
To use, simply create a normal keybind setup, but instaed of .bind or .bindCmd, use:
moveMap.bindContext(keyboard, ",", HoldTester, TapTester, 10000);
to do a time-context based keybind, or
moveMap.bindHold(keyboard, ".", HoldTester);
For a hold-based.
To get it to return the time held, simply change it to read:
moveMap.bindHold(keyboard, ".", HoldTester, true);
and add a argument to your function definition to get the time, such as:
function HoldTester(%val, %time)
{
echo("Held down for " @ %time @ " milliseconds!");
}for context binds, the format is device, action, Holding function(called after the minimum hold-time is passed while keeping the key pressed), Tap function(called when releasing the key, but before we pass the minimum hold time, and the Minimum hold time(in milliseconds).
For hold binds, you simply put your device, action, and function call.
This doesn't work for move events, such as the mouse axis, though you could probably get that in with some work.
Hope this is helpful.
-Jeff
About the author
#2
02/04/2009 (12:06 pm)
isn't this feature in the engine already? ... like in the mission editor you hold down right mouse button to pan the cam... though i can't remember of the bat if right click is used for something else.
#3
Context essentially lets you get two functions in one key.
Lets say, for example, you wanted to let your player crouch.
Normally, there's 2 crouch functions: a toggle, where tapping the key activates and deactivates the crouch, and a hold, where as long as the button is pressed, they can crouch.
bindContext would let you define both with a single keybind.
So you could tap the crouch button to toggle it on/off, but if you held it down, you'd only stay crouched as long as you're pressing it.
This is more or less designed to let you reduce the number of keys used for your game(even better in the instance of using a gamepad or whatnot, where you're limited in usage anyways)
bindHold, however, is specifically designed to onlydo a function so long as the key is held down.
Torque technically has this built in, but it felt a little clunky for what I needed. I'm also working on a slight mod of this to read back how long the key was held down for. I'd wanted to include it initially, but ran into some bugs that I wanted to squash first.
With the counter-return, you could explicitly track how long the button was held down automatically, without needing schedules and whatnot.
So lets say you have a jump button that when held down, charges your jump. Longer it's held, stronger the jump.
By default, you'd need a schedule to constantly check if the key is still being held down, which clutters the script.
This, you simply define the function, and it's automatically called as long as the key is held down, and when I get the time-return added, it'll tell you exactly how long it's been held for.
@Mikael I think that's just different ActionMaps at play. One actionmap for your game keybinds, and one actionmap for the editor.
02/04/2009 (4:34 pm)
@Daniel, sure.Context essentially lets you get two functions in one key.
Lets say, for example, you wanted to let your player crouch.
Normally, there's 2 crouch functions: a toggle, where tapping the key activates and deactivates the crouch, and a hold, where as long as the button is pressed, they can crouch.
bindContext would let you define both with a single keybind.
So you could tap the crouch button to toggle it on/off, but if you held it down, you'd only stay crouched as long as you're pressing it.
This is more or less designed to let you reduce the number of keys used for your game(even better in the instance of using a gamepad or whatnot, where you're limited in usage anyways)
bindHold, however, is specifically designed to onlydo a function so long as the key is held down.
Torque technically has this built in, but it felt a little clunky for what I needed. I'm also working on a slight mod of this to read back how long the key was held down for. I'd wanted to include it initially, but ran into some bugs that I wanted to squash first.
With the counter-return, you could explicitly track how long the button was held down automatically, without needing schedules and whatnot.
So lets say you have a jump button that when held down, charges your jump. Longer it's held, stronger the jump.
By default, you'd need a schedule to constantly check if the key is still being held down, which clutters the script.
This, you simply define the function, and it's automatically called as long as the key is held down, and when I get the time-return added, it'll tell you exactly how long it's been held for.
@Mikael I think that's just different ActionMaps at play. One actionmap for your game keybinds, and one actionmap for the editor.
#4
02/05/2009 (1:11 am)
At first I didn't get it, but now I see.. very cool resource! I can't wait for the hold-time feature! Very nice!
#5
Hope you guys find this stuff useful. Lemme know if you have any problems with it.
02/05/2009 (4:33 pm)
Updated for the time-return.Hope you guys find this stuff useful. Lemme know if you have any problems with it.
#6
02/08/2009 (12:23 pm)
hello im having some trouble getting this to work in my modified tge 1.5.2. Whenever i try to add a bindcontext it renames them to held in the config cs and then doesn't do anything with the buttons
#7
I messed up the dump command for when the config file is being made, total oversight on my part.
The fix is to take the lines that read:
and replace it with
Both times it appears in the dumpActionMap() function.
That should make it work now.
02/08/2009 (5:56 pm)
My apologies, it was a mistake on my part.I messed up the dump command for when the config file is being made, total oversight on my part.
The fix is to take the lines that read:
else if(rNode.flags & Node::Held)
command = "held";and replace it with
else if(rNode.flags & Node::Held)
{
if(rNode.holdEvent->mHoldOnly)
command = "bindHold";
else
command = "bindContext";
}Both times it appears in the dumpActionMap() function.
That should make it work now.
#8
02/08/2009 (7:15 pm)
ah ha thank you very much I'm about to test it now.. i saw that line and experimented with it but didn't know what i was doing :)
#9
But this is VERY useful (eg. Energy build Weapons). I'm gonna use this for sure!
02/10/2009 (4:08 am)
hehe... When I first read the description I was like what the hell... But this is VERY useful (eg. Energy build Weapons). I'm gonna use this for sure!
#10
02/10/2009 (11:20 am)
Admittedly, looking back at it, the description is kind of lousy. I'll look to rework it with a better description later today.
#11
Also, I had to add stuff to dumpActionMap to get bindContext to dump correctly:
EDIT: bindHold seems to work fine.
EDIT: Managed to get it working by changing the 'button' parameter to a StringTableEntry representing the correct function. But I think I must just be missing something with the original code :P.
EDIT: Also, not sure if this was intended or not, but I'm getting no callback when I release a buttonHold or the hold part of a buttonContext.
EDIT: Managed to get that part working ;P.
06/19/2010 (7:17 pm)
I'm not able to use this - I've set up the source as described and added a bindContext to test. Whenever I press the key (lshift, but I also tried with G), I get a crash stemming from this in holdButtonEvent::processTick:if(!mHoldOnly && !didHold)
{
//yes, we tapped and we care, so fire off the tap function.
argv[0] = mButton->consoleFunction;
argv[1] = Con::getFloatArg(mEventValue);
Con::execute(2, argv);
}mButton seems to point to garbage, so the console is breaking. I checked the constructor of holdButtonEvent and mButton is being set correctly - but from the first processTick onwards, it's junk. Any ideas?Also, I had to add stuff to dumpActionMap to get bindContext to dump correctly:
dStrcat(finalBuffer, ", "); dStrcat(finalBuffer, Con::getFloatArg(rNode.holdEvent->mMinHoldTime));Also when writing to lineBuffer.
EDIT: bindHold seems to work fine.
EDIT: Managed to get it working by changing the 'button' parameter to a StringTableEntry representing the correct function. But I think I must just be missing something with the original code :P.
EDIT: Also, not sure if this was intended or not, but I'm getting no callback when I release a buttonHold or the hold part of a buttonContext.
EDIT: Managed to get that part working ;P.

Torque Owner Daniel Buckmaster
T3D Steering Committee