Gamestudio Links
Zorro Links
Newest Posts
rMomentum always returns 0 when checking for peak/valley
by jcl
Today at 17:38
a little help here?
by 3run
Today at 16:38
How to set a pair trading order right
by jcl
Today at 11:46
Dual Momentum Algorithm - The way Zorro would have done it
by Hredot
Today at 05:21
HIgh FRequency Trading Script -- Highly Profitable
by Yosh
Yesterday at 19:27
Linear Regression in objective function
by jcl
11/22/17 15:40
MT4 vs Zorro
by pascalx
11/22/17 14:00
AUM Magazine
Latest Screens
RPG PARTY
Triton Wing now available on Steam
AckCon'17: Lotter vs The World
Triton Wing
Who's Online
14 registered (Lobo, Hredot, painkiller, 3run, alibaba, Kartoffel, FLD, RealSerious3D, Jerome8911, 1 invisible), 169 Guests and 4 Spiders online.
Key: Admin, Global Mod, Mod
Newest Members
ech87, Lobo, Andy60, AzzoCorp, franzz
17813 Registered Users
Topic Options
Rate This Topic
#468906 - 10/26/17 22:50 [SUB] Key stroke combos as event triggers
txesmi Offline
Serious User

Registered: 06/13/07
Posts: 1050
Loc: Hiporope and its pain
Hi,
I gived a try to this question and turned in a pretty intrincated but very easy to use module.

EXTENDED VERSION
For a simpler version go to next post.

It calls a function when a combo gets completed. It works with collections of scancodes as references. The data is stored in a linked tree manner where nodes hold their next possible scancodes. This way the combo detector fuction only searchs into the smallest scope of possible combos.

The combo detector works in the scope of a previously created set of scancodes and sequences. This means that, unlike the previous version, any stroke of any key not contained into the scancode list of the running set will have no effect into the running set. This way two different sets of combos can be run parallel with no interferences between them. A necessary feature for single machine multiplayer games or an useful feature when you want to run a different set of events on different player states. f.e: cmbOnGround, cmbOnAir, etc.



Source
Download source and examples

The basics
The functions and their parameters are briefly explained into source//combos.h

Code:
void brokenCombo () {
   str_cpy ( strComboLast, "Failed" );
   snd_play ( sndFail, 100, 0 );
}

var cmbEvent () {
   snd_play ( sndCombo, 100, 0 );
}

action actPlayer () {
   // Create a new combo set
   COMBO_SET *cmbSimple = comboSetCreate ( 4, NULL ); // 4 is the timeout lapse in ticks 
   // Add the desired amount of scancodes
   comboScancodeCollectionAdd ( cmbSimple, _vArray( SC_A, SC_S, SC_D ), 3 );
   // Add a combo to play with
   comboAdd ( cmbSimple, _vArray ( SC_S,  SC_A,  SC_D,  SC_S,  SC_A,  SC_A,  SC_S,  SC_A ), 8, cmbEvent ); 
   while ( player ) {
      // Make the system work
      comboSetRun ( cmbSimple, brokenCombo );
      wait(1);
   }
   ent_remove ( me );
   // Remove data 
   comboSetRemove ( cmbSimple );
}



This little example shows the very basics and its usage is as simple as that. Combos can be added at any time. A single combo can be deleted by calling comboAdd with its scancodes sequence and a NULL event. If your combos planning only needs single key strokes combos, that is all you need to know.

The example uses a helper function called _vArray that locally builds an array of an arbitrary (up to 8) number of scancodes in order to pass them as function parameter.

Key releasing virtual scancodes
It needs the FLAG_RELEASE flag in order to call key releasing events. Unlike the previous version, key releasing events are key specific and activated as their negative scancode.

Code:
// key S needs to be released before pressing D in order to complete the combo
comboAdd ( cmbSimple, _vArray ( SC_S, -SC_S,  SC_D,  SC_A ), 4, cmbEvent );
// key D needs to be released after pressing S in order to complete the combo
comboAdd ( cmbSimple, _vArray ( SC_D,  SC_S, -SC_D,  SC_A ), 4, cmbEvent );



Custom scancodes
This is a new feature that lets you use anything you can imagine as combo member, not only engine scancodes.

Code:
var comboScancodeCustom ( COMBO_SET *_cmbSet, var *_condition );



comboScancodeCustom creates a virtual scancode activated by the return of _condition, adds it to the _cmbSet combo set and returns its value.

The return of the condition function must work as key_... variables do. The function has to return 1 when a certain condition is true and it has to return 0 when not.

Code:
var evn_AD_together () {
   if ( key_a && key_d )
      return 1;
   else
      return 0;
}
...
COMBO_SET *_cmbSet = comboSetCreate ( 4, NULL );
comboScancodeCollectionAdd ( _cmbSet, _vArray( SC_A, SC_D, SC_W, SC_S), 4 );
var _kAandD = comboScancodeCustom ( _cmbSet, evn_AD_together ); // save the returning scancode for combo creation
comboAdd ( cmbSimple, _vArray ( SC_D,  _kAandD, SC_W,  SC_S ), 4, cmbEvent );



The usage of this function can be pretty conflictive when certain key takes effect on more than one event, as in the example above, since A and D are set as valid scancodes on their own. The downloadable pack contains an extended example of a solution for this issue into examples//two_keys.c

Further information
comboRun returns a pointer to an array containing a secuence of scancodes that has been detected as into a combo at that run, so it can be evaluated. This array is allocated as sequence member into the combo set struct.

Code:
str_cpy ( str, "Combo on air: " );
var *_scodes = comboRun ();
for ( ; *_scodes!=0; _scodes++ ) { // the returning array always contains a zero as last member
	str_cat ( str, str_for_key ( NULL, *_scodes ) );
	str_cat ( str, " " );
}
draw_text ( str, 10, 600, COLOR_WHITE );



The behavior of each combo set can be modified by some flags.
FLAG_RELEASE Activates key release virtual scancodes
FLAG_STRICT Makes key releases break combos as key presses do
FLAG_RESTART Makes the system look for a combo that starts by the scancode that made last active combo fail
FLAG_REBUILD Makes the system rebuild the whole scancode sequence retuned by comboRun each run.
FLAG_RETURN Activates the answer to combo events return. f.e: return COMBO_DISABLE in order to remove the event calling branch. More info into source//combos.h

Custom scancodes conditions and combo events can receive a pointer when set at creation time. More info into source//combos.h

The module contains a very useful drawComboSet function in order to debug the module functioning. It also contains all the engine scancodes set as macros in the SC_... form. More info into source//combos.h

I will be pleased of answering any other question (or rant), and don't forget to look at source//combos.h grin

Examples
  • Basic
    A simple setup for simple combos.
  • Two keys
    A demostration of the use of a ghost combo event that works as switch for a custom scancode.
  • Gestures
    A demostration of custom scancodes combos used as mouse gestures detector.

Top
#468920 - 10/27/17 16:33 Re: [SUB] Key stroke combos as event triggers [Re: txesmi]
txesmi Offline
Serious User

Registered: 06/13/07
Posts: 1050
Loc: Hiporope and its pain
Click to reveal..
DEPRECATED

It calls a function when a combo gets completed. It works with collections of scancodes as references to keys. The data is stored in a linked tree manner where key nodes hold their next possible keys. This way the combo detector fuction only searchs into the smallest scope of possible combos.

There are only three public functions:
- comboRun makes the system work.
- comboCreate creates a new combo.
- combosRemove removes all created combos.

Code:
#define KEY_A     30
#define KEY_S     31
#define KEY_D     32

void myEvent ()
{
	printf ( "COMBO!" );
}

void playerCombosCreate ()
{
	var _keys[8] = { KEY_A, KEY_A, KEY_A, KEY_S, KEY_S, KEY_A, KEY_S, KEY_D };
	comboCreate ( _keys, 8, myEvent );
	comboCreate ( vector(KEY_A, KEY_S, NULL), 2, myEvent );
	comboCreate ( KEY_A, KEY_S, KEY_D, myEvent );
	comboCreate ( KEY_A, KEY_D, KEY_A, KEY_D, KEY_A, myEvent );
}

action actPlayer ()
{
	combosRemove ();
	playerCombosCreate ();
	player = me;
	while ( player )
	{
		var *_scancodes = comboRun ( NULL );
		wait(1);
	}
	combosRemove ();
}



So it needs to:
- create some combos with any of the versions of comboCreate
- make the system run by calling comboRun inside a loop
- remember to remove stored data when finished
You can deactivate a combo by calling comboCreate with its scancode sequence and a NULL event and reactivate it with a valid event. You can also empty the combos memory at any time, (fixed)but not inside a combo event! If your needs, wait a frame as in engine events. You will need to ensure COMBO_LENGTH_MAX is almost twice the longer scancode sequence of your project.

comboRun returns a pointer to an array containing a secuence of scancodes that has been detected as into a combo at that run, so it can be evaluated.

Code:
str_cpy ( str, "Combo on air: " );
var *_scodes = comboRun ();
for ( ; *_scodes!=-1; _scodes++ )
{
	str_cat ( str, str_for_key ( NULL, *_scodes ) );
	str_cat ( str, " " );
}
draw_text ( str, 10, 600, COLOR_WHITE );



The module internaly incorporates a 'key released' virtual scancode, so it states a bunch of ways of calling the same event as combinations of the scancodes secuence and this virtual scancode. (fixed)So the return of comboRun may contain some zeros interleaved. The resulting data tree of creating DAD, ADD and AA key strokes combos, drawn with a recursive function:



'rls' means that 'key release' virtual scancode I spoke about.

Code:
TEXT *txtChain = { string = ( "-", "" ); }
STRING *str = "";
FONT *fnt = "Courier#16";
var drawComboChildren ( COMBO *_cmb, var _posX, var _posY )
{
	var _posYbak = _posY;
	if ( _cmb == comboRoot )
	{
		draw_text ( "ROOT", _posX, _posY, COLOR_WHITE );
		_posX += str_width("ROOT",fnt) + 24;
	}
	COMBO *_cmbChild = _cmb->child;
	for ( ; _cmbChild!=NULL; _cmbChild=_cmbChild->next )
	{
		if ( _posYbak == _posY )
			draw_text ( (txtChain->pstring)[0], _posX-16, _posY, COLOR_WHITE );
		else
			draw_text ( (txtChain->pstring)[1], _posX-16, _posY, COLOR_WHITE );
		if ( _cmbChild->key == 280 )
			str_cpy ( str, "mouse left" );
		else if ( _cmbChild->key == 281 )
			str_cpy ( str, "mouse right" );
		else if ( _cmbChild->key == 0 )
			str_cpy ( str, "rls" );
		else
			str_for_key ( str, _cmbChild->key );
		if ( _cmbChild->event )
		{
			char *_name;
			engine_getscriptinfo ( _cmbChild->event, &_name );
			str_cat ( str, " -> " );
			str_cat ( str, _name );
		}
		draw_text ( str, _posX, _posY, COLOR_WHITE );
		var _posMax = drawComboChildren ( _cmbChild, _posX+str_width(str,fnt)+24, _posY );
		_posY = maxv ( _posY+16,_posMax );
	}
	return _posY;
}


Each node of the tree is a COMBO data struct

Code:
typedef struct COMBO {
	var key;
	struct COMBO *child;
	void event ( var *_result, void *_ptr );
	struct COMBO *next;
} COMBO;


that holds a linked list of the next possible key strokes. I set three combos but there are only two branches borning from the root since ADD and AA share their first key.

comboRun has two code parts. The first looks for changes on key strokes and if any, executes the second part just once. comboRun has a static pointer to a COMBO struct that serves as combo state. It begins been the root but may climb the data tree step by step when any of its borning branches coincide with the actual key stroke change. If not, it goes back to the root and starts again.

It is possible to make the event be executed when last key is released by adding NULL as last member in the scancodes sequence.

Code:
comboCreate ( KEY_A, KEY_S, NULL, evnAS );



It is possible to force a key be released before pressing next one in order to complete the combo by adding NULL between scancodes.

Code:
comboCreate ( KEY_A, NULL, KEY_S, evnAS ); // Releasing A is needed for AS combo completion



Source
[spoiler]
combos.h
Code:
#ifndef _COMBOS_H_
#define _COMBOS_H_

#define COMBO_TIMEOUT        4
#define COMBO_LENGTH_MAX     16

typedef struct COMBO {
	var key;
	struct COMBO *child;
	void event ( var *_result, void *_ptr );
	struct COMBO *next;
} COMBO;

COMBO *comboRoot = { key = NULL; child = NULL; event = NULL; next = NULL; }

var comboRun ( void *_ptr );
void comboCreate ( var *_keys, int _count, void *_event );
void comboCreate ( var _key1, var _key2, var _key3, void *_event );
void comboCreate ( var _key1, var _key2, var _key3, var _key4, void *_event );
void comboCreate ( var _key1, var _key2, var _key3, var _key4, var _key5, void *_event );
void comboCreate ( var _key1, var _key2, var _key3, var _key4, var _key5, var _key6, void *_event );
void comboCreate ( var _key1, var _key2, var _key3, var _key4, var _key5, var _key6, var _key7, void *_event );
void combosRemove ();

#include "combos.c"
#endif



combos.c
Code:
void comboRemove ( COMBO *_cmb ) {
	COMBO *_cmbChild = _cmb->child;
	while ( _cmbChild )
	{
		COMBO *_cmbNext = _cmbChild->next;
		comboRemove ( _cmbChild );
		_cmbChild = _cmbNext;
	}
	sys_free ( _cmb );
}

void combosRemove () {
	COMBO *_cmbChild = comboRoot->child;
	while ( _cmbChild )
	{
		COMBO *_cmbNext = _cmbChild->next;
		comboRemove ( _cmbChild );
		_cmbChild = _cmbNext;
	}
	comboRoot->child = NULL;
}

COMBO *comboChildFind ( COMBO *_cmb, var _key ) {
	COMBO *_cmbChild = _cmb->child;
	for ( ; _cmbChild!=NULL; _cmbChild=_cmbChild->next ) {
		if ( _cmbChild->key < _key )
			return NULL;
		if ( _cmbChild->key == _key )
			return _cmbChild;
	}
	return NULL;
}

COMBO *comboChildAdd ( COMBO *_cmb, var _key, void *_event ) {
	COMBO *_cmbNew = sys_malloc ( sizeof(COMBO) );
	_cmbNew->key = _key;
	_cmbNew->child = NULL;
	_cmbNew->event = _event;
	_cmbNew->next = NULL;
	COMBO *_cmbChild = _cmb->child;
	if ( !_cmbChild ) {                                     // first child
		_cmb->child = _cmbNew;
	} else if ( _cmbChild->key < _key ) {                   // lowest key on childhood
		_cmbNew->next = _cmbChild;
		_cmb->child = _cmbNew;
	} else {                                                // insert sorted by key, lower first
		for ( ; _cmbChild->next!=NULL; _cmbChild=_cmbChild->next ) {
			COMBO *_cmbNext = _cmbChild->next;
			if ( _cmbNext->key > _key )
				continue;
			_cmbChild->next = _cmbNew;
			_cmbNew->next = _cmbNext;
			break;
		}
		if ( _cmbChild->next == NULL )
			_cmbChild->next = _cmbNew;
	}
	return _cmbNew;
}

void comboSet ( var *_keys, int _count, void *_event ) {
	int _i = 0;
	for ( ; _i<_count-1; _i+=1 )                            // check viability
		if ( _keys[_i] == _keys[_i+1] )
			return;
	COMBO *_cmb = comboRoot;                                // actual combo branch, root init
	int _i = 0;
	for ( ; _i<_count; _i+=1 ) {
		COMBO *_cmbT = comboChildFind ( _cmb, *_keys );      // search into the actual combo branch
		if ( _cmbT )                                         // succesful, so the child becomes the parent 
			_cmb = _cmbT;
		else                                                 // failure, new one is needed
			_cmb = comboChildAdd ( _cmb, *_keys, NULL );
		_keys ++;                                            // advance a position in the array
	}
	_cmb->event = _event;
}


void comboVariants ( var *_keys, var *_keys2, int _index, int _count, void *_event ) {
	memcpy ( _keys2+_index, _keys, (_count-_index)*sizeof(var) );
	comboSet ( _keys2, _count, _event );
	int _i = _index;
	for ( ; _i<_count-1; _i+=1 ) {
		int _offset = _i - _index;
		memcpy ( _keys2+_i, _keys+_offset, sizeof(var) );
		_keys2[_i+1] = NULL;
		memcpy ( _keys2+_i+2, _keys+1+_offset, (_count-(_i+1))*sizeof(var) );
		comboVariants ( _keys+1+_offset, _keys2, _index+2+_offset, _count+1, _event );
	}
}

void comboCreate (  var *_keys, int _count, void *_event ) {
	var _keys2[COMBO_LENGTH_MAX];
	comboVariants ( _keys, _keys2, 0, _count, _event );
}

void comboCreate (  var _key1, var _key2, var _key3, void *_event ) {
	var _keys2[COMBO_LENGTH_MAX];
	comboVariants ( &_key1, _keys2, 0, 3, _event );
}

void comboCreate (  var _key1, var _key2, var _key3, var _key4, void *_event ) {
	var _keys2[COMBO_LENGTH_MAX];
	comboVariants ( &_key1, _keys2, 0, 4, _event );
}

void comboCreate (  var _key1, var _key2, var _key3, var _key4, var _key5, void *_event ) {
	var _keys2[COMBO_LENGTH_MAX];
	comboVariants ( &_key1, _keys2, 0, 5, _event );
}

void comboCreate (  var _key1, var _key2, var _key3, var _key4, var _key5, var _key6, void *_event ) {
	var _keys2[COMBO_LENGTH_MAX];
	comboVariants ( &_key1, _keys2, 0, 6, _event );
}

void comboCreate (  var _key1, var _key2, var _key3, var _key4, var _key5, var _key6, var _key7, void *_event ) {
	var _keys2[COMBO_LENGTH_MAX];
	comboVariants ( &_key1, _keys2, 0, 7, _event );
}

var *comboRun ( void *_ptr ) {
	static var _result[COMBO_LENGTH_MAX] = { -1 };
	static int _index = 0;
	static var _keyLast = 0;
	static var _t = total_ticks;
	static COMBO *_cmb = comboRoot;                         // actual combo branch, root init
	var _key = key_lastpressed;
	if ( !key_pressed(_key) )                               // force a 'not pressed key' event
		_key = 0;
	if ( ( _t < total_ticks ) && ( _cmb != comboRoot ) ) {  // restart climbing if last change was a while ago
		_cmb = comboRoot;   
		_result[0] = -1;
		_index = 0;
	}
	if ( _key == _keyLast )                                 // nothing to do when changes are scarce
		return _result;
	_keyLast = _key;
	_cmb = comboChildFind ( _cmb, _key );                   // search into the actual combo branch
	if ( _cmb ) {                                           // succesful, so the child becomes the parent 
		if ( _key != 0 ) {                                   // update active combo only when a true key has been pressed
			_t = total_ticks + COMBO_TIMEOUT;
			_result[_index] = _key;
			_result[_index+1] = -1;
			_index += 1;
		}            
		if ( _cmb->event )                                   // at the end, we have come this far for this
			_cmb->event ( _result, _ptr );
	} else {                                                // failure, check if failed key starts a new combo
		if ( _cmb != comboRoot )
			_cmb = comboChildFind ( comboRoot, _key );        
		if ( _cmb ) {                                        // succesful, so the child becomes the parent 
			_t = total_ticks + COMBO_TIMEOUT;
			_result[0] = _key;
			_result[1] = -1;
			_index = 1;
			if ( _cmb->event )               
				_cmb->event ( _result, _ptr );
		} else {                                             // failure, back to the root
			_cmb = comboRoot;
			_t = total_ticks;
			_result[0] = -1;
			_index = 0;
		}
	}
	if ( !comboRoot->child ) {                              // reset local stuff when comboRoot is empty
		_cmb = comboRoot;
		_t = total_ticks;
		_index = 0;
		_result[0] = -1;
	}
	return _result;
}




Edited by txesmi (11/03/17 03:36)

Top
#468922 - 10/27/17 17:19 Re: [SUB] Key stroke combos as event triggers [Re: txesmi]
painkiller Online
Serious User

Registered: 08/23/09
Posts: 1434
Loc: Spain
great contribution txesmi! wink
_________________________
3D Gamestudio A8 Pro
AMD FX 8350 4.00 Ghz
16GB RAM
Gigabyte GeForce GTX 960 4GB

Top
#468925 - 10/27/17 22:39 Re: [SUB] Key stroke combos as event triggers [Re: painkiller]
DriftWood Offline
Newbie

Registered: 07/03/14
Posts: 46
Light years beyond, thanks!

Can you now deal with a key_timer as combos are time sensitive? Also, the remaining questions are - two keys at once. And charging a single key for a time before moving to the next key.

Two keys is an assumption as the engine checks each frame and one at a time, again needing a timer and lastpressed check.

You're Still, a Master and this is AUM worthy - Hail @txesmi.

Top
#468937 - 10/28/17 11:24 Re: [SUB] Key stroke combos as event triggers [Re: DriftWood]
txesmi Offline
Serious User

Registered: 06/13/07
Posts: 1050
Loc: Hiporope and its pain
Thank you all!

Originally Posted By: DriftWood
Can you now deal with a key_timer as combos are time sensitive?

The header defines a macro as time out length. I thought it would be enough. Do you mean a specific time lapse for each key on each combo? It would break the module I guess.

Originally Posted By: DriftWood
two keys at once.

Impossible to manage with key_lastpressed, I guess. Maybe a similar system can be done by saving pointers to key_... variables instead of scancodes but not as a variant of this module.

Originally Posted By: DriftWood
And charging a single key for a time before moving to the next key.

Maybe I can add a layer of complexity with the use of first or last bits of scancodes vars as working flags for different functionalities but it would encrease the complexity in an exponential way. The main idea was to keep its usage and execution simple. I will think around a bit.

This last question might be solved by adding a 'specific key release' virtual scancode as the negative of the key itself. '-A, A' could work as the trigger you are asking for. I going to study its viability.

I am thinking on adding a virtual KEY_ANY scancode. So you can set a generic combo and manage its answer inside the event. But I am not sure it is possible.

Top
#469029 - 11/03/17 02:19 Re: [SUB] Key stroke combos as event triggers [Re: txesmi]
txesmi Offline
Serious User

Registered: 06/13/07
Posts: 1050
Loc: Hiporope and its pain
I did a huge update of the module. It went a couple of steps further. Download above.

Top
#469055 - 11/04/17 20:47 Re: [SUB] Key stroke combos as event triggers [Re: txesmi]
DriftWood Offline
Newbie

Registered: 07/03/14
Posts: 46
Great work!

Top
#469114 - 11/05/17 20:23 Re: [SUB] Key stroke combos as event triggers [Re: DriftWood]
txesmi Offline
Serious User

Registered: 06/13/07
Posts: 1050
Loc: Hiporope and its pain
thank you!

a mouse gesture detector setup for this module is incoming wink

Top
#469230 - 11/09/17 09:51 Re: [SUB] Key stroke combos as event triggers [Re: txesmi]
txesmi Offline
Serious User

Registered: 06/13/07
Posts: 1050
Loc: Hiporope and its pain
Added the promised mouse gestures detector setup to the examples folder.

Salud!

Top


Moderator:  HeelX, Lukas, Rei_Ayanami, VeT 

Gamestudio download | chip programmers | Zorro platform | shop | resources | magazine | manual | support faq | bugs

oP group Germany GmbH | Birkenstr. 25-27 | 63549 Ronneburg / Germany | info (at) 3dgamestudio.net