MODULE: DoubleClick + KeyListener: detect double clicks and more

Started by Crimson Wizard, Wed 10/02/2016 01:39:17

Previous topic - Next topic

Crimson Wizard

DOWNLOAD DoubleClick 1.0.0 MODULE
DOWNLOAD KeyListener 0.9.7 MODULE

Both modules require AGS 3.2.1 or higher.

Module sources:
DoubleClick
KeyListener
Git clone address: https://ivan-mogilko@bitbucket.org/ivan-mogilko/ags-script-modules.git


Other script modules by me:
Spoiler




Double Click

As of now, AGS does not have built-in mouse double-click events. DoubleClick module solves this by detecting when player made two consecutive clicks within short duration.

Usage is very simple:

Code: ags

function on_mouse_click(MouseButton mb)
{
  if (mb = eMouseLeft)
  {
    if (DoubleClick.Event[eMouseLeft])
    {
      player.SayBackground("Double-clicked!");
    }
    else
    {
      // regular click
    }
  }
}


DoubleClick supports claiming events similar to standart ClaimEvent() function. If you are detecting double clicks in several modules, you may use DoubleClick.ClaimThisEvent() to not let double-click "through" to lower scripts.

DoubleClick.Timeout property is used to set up max timeout (in milliseconds) which may pass between two clicks for them to be considered "double click". Default is 300 ms.



Key Listener

KeyListener is a more sophisticated script module, that keeps track and record of the particular key and mouse button states in your game. Since AGS is limited in which key and mouse events it reports to the script, this module may be a useful workaround.

With KeyListener you do:

  • Setup which keys or mouse buttons to listen (track) and change that list dynamically at any moment in the game;
  • Optionally configure deduction parameters, such as maximal time between two key presses while they still counted as a sequence;
  • Enable and disable listener work at will.

With KeyListener you acquire following information about any of the listened keys and mouse buttons at any time:

  • Is key currently up or down;
  • Was the key just released or pushed;
  • Has the player double clicked a mouse button;
  • How long was the key up or down (in milliseconds);
  • How much times was the key tapped in a sequence;
  • How much times was the key "auto-repeated" while being held down;

NOTE that if you are using KeyListener, you do not need DoubleClick module.


Concepts

On sequencing.
Sequencing is pressing same key (or mouse button) over and over again, when every next press was made within certain time limit since the previous one. KeyListener lets you configure the maximal waiting time (timeout), default value being 300 ms. You are able to know when the key was "sequenced" another once, and how many times it was pressed in sequence already ("sequence length").

On key autorepeats.
In KeyListener terminology, a key's autorepeats start to occur periodically when the key is being held down for some time. This simulates the common operating system behavior, which can be easily observed in any text editor: when you press and hold the key first only one letter is typed, but after small amount of time it starts typing same letter again and again automatically, without you releasing and pressing key again.
You may configure two parameters related to key repeats: the first delay, which has to pass before key begins "repeating", and the (usually shorter) period between repeat count increments.
You may get when autorepeat occurs, and how many times this key was autorepeated.

Signal properties
Since AGS does not support custom callbacks (calling your own functions when something happened in script module), KeyListener tells you about immediate events by setting certain signal properties. These properties are easily distinguished by "Evt*" prefix.
The positive values on signal properties only set for 1 game tick (update), and are reset immediately afterwards. They literally mean that the key event has just occured, and you should react to that. The next tick they will be reset until some key event occurs again.
You will have to check these properties periodically, in the "repeatedly_execute" or "repeatedly_execute_always" functions of GlobalScript or your own custom script to know when the related events occur.

Lifetime of key records
Another important peculiarity is that some properties are kept for just 1 tick more after key state changes, before getting reset. These are KeyUpTime, KeyDownTime and KeyAutoRepeatCount. This is done so to allow you get their final value after being notified about key state change. For example, you may want to know how long the key was held down right after it was released.


Module API

Following are properties of the KeyListener class, that are exposed for use in your scripts:

Code: ags

struct KeyListener
{
  ///////////////////////////////////////////////////////////////////////////
  //
  // Setting up
  // ------------------------------------------------------------------------
  // Functions and properties meant to configure the listener's behavior.
  //
  ///////////////////////////////////////////////////////////////////////////
  
  // Enables or disables KeyListener, without cancelling tracked keys settings
  import static attribute bool Enabled;
  
  /// Commences or ceases to listen keycode
  import static void ListenKey(eKeyCode keycode, bool enable = true);
  import static void ListenMouse(bool enable = true);
  /// Stops listening all keycodes
  import static void StopListenAllKeys();
  
  /// Resets all listener parameters to default values
  import static void ResetToDefaults();
  
  /// Get/set minimal time (in milliseconds) for a held down key to be down to be considered "repeating"
  import static attribute int KeyAutoRepeatDelay;
  /// Get/set period (in milliseconds) between each "repeating" key count
  import static attribute int KeyAutoRepeatPeriod;
  /// Get/set maximal period (in milliseconds) between two key taps while they still considered a sequence
  import static attribute int KeySequenceTimeout;
  
  
  ///////////////////////////////////////////////////////////////////////////
  //
  // Event signals
  // ------------------------------------------------------------------------
  // Properties meant to signal user about keys events.
  //
  ///////////////////////////////////////////////////////////////////////////

  /// Gets if key was just pushed
  readonly import static attribute bool EvtKeyPushed[];
  readonly import static attribute bool EvtMousePushed[];
  /// Gets if key was just released after being held down
  readonly import static attribute bool EvtKeyReleased[];
  readonly import static attribute bool EvtMouseReleased[];
  /// Gets if key command was repeated for a held down key
  readonly import static attribute bool EvtKeyAutoRepeated[];
  readonly import static attribute bool EvtMouseAutoRepeated[];
  /// Gets if key was tapped another time in sequence
  readonly import static attribute bool EvtKeySequenced[];
  readonly import static attribute bool EvtMouseSequenced[];
  /// Gets if key was tapped twice in sequence just now
  readonly import static attribute bool EvtKeyDoubleTap[];
  readonly import static attribute bool EvtMouseDoubleClick[];
  
  
  ///////////////////////////////////////////////////////////////////////////
  //
  // Key records
  // ------------------------------------------------------------------------
  // Properties meant to tell gathered information about keys the listener
  // is listening to.
  //
  ///////////////////////////////////////////////////////////////////////////

  /// Gets if key is currently held down
  readonly import static attribute bool IsKeyDown[];
  readonly import static attribute bool IsMouseDown[];
  /// Gets how many times the pressed down key command was repeated
  readonly import static attribute int  KeyAutoRepeatCount[];
  readonly import static attribute int  MouseAutoRepeatCount[];
  /// Gets how many times the key was tapped in sequence
  readonly import static attribute int  KeySequenceLength[];
  readonly import static attribute int  MouseSequenceLength[];
  /// Gets how long (in milliseconds) the key was not pressed
  readonly import static attribute int  KeyUpTime[];
  readonly import static attribute int  MouseUpTime[];
  /// Gets how long (in milliseconds) the key was held down
  readonly import static attribute int  KeyDownTime[];
  readonly import static attribute int  MouseDownTime[];
};



Examples of use

Example 1: Some simple actions.
Spoiler

Code: ags

function game_start()
{
  // We are going to track spacebar and the mouse buttons
  KeyListener.ListenKey(eKeySpace);
  KeyListener.ListenMouse();
  // Start tracking
  KeyListener.Enabled = true;
}

Code: ags

function repeatedly_execute()
{
  if (KeyListener.EvtMouseDoubleClick[eMouseLeft])
  {
    // player double clicked with mouse; do something
  }
  
  if (KeyListener.EvtMousePushed[eMouseLeft] && KeyListener.MouseSequenceLength[eMouseLeft] == 2)
  {
    // this is essentially the same as above
  }
  
  if (KeyListener.IsKeyDown[eKeySpace] && KeyListener.KeyDownTime[eKeySpace] > 1000)
  {
    // player is holding space bar for more than 1 second
  }
}

[close]

Example 2: displaying particular key records on GUI.
Spoiler

Code: ags

int key;
function game_start()
{
  key = eKeyA;
  KeyListener.ListenKey(key);
  KeyListener.Enabled = true;
}

function repeatedly_execute() 
{
  Label1.Text = String.Format("Key is down: %d", KeyListener.IsKeyDown[key]);
  Label2.Text = String.Format("Key just pushed: %d", KeyListener.EvtKeyPushed[key]);
  Label3.Text = String.Format("Key just released: %d", KeyListener.EvtKeyReleased[key]);
  Label4.Text = String.Format("Key uptime: %d", KeyListener.KeyUpTime[key]);
  Label5.Text = String.Format("Key downtime: %d", KeyListener.KeyDownTime[key]);
  Label6.Text = String.Format("Key repeats: %d", KeyListener.KeyAutoRepeatCount[key]);
  Label7.Text = String.Format("Key taps: %d", KeyListener.KeySequenceLength[key]);
  Label8.Text = String.Format("Key double tap: %d", KeyListener.EvtKeyDoubleTap[key]);
  Label9.Text = String.Format("Key sequenced: %d", KeyListener.EvtKeySequenced[key]);
  Label10.Text = String.Format("Key repeated: %d", KeyListener.EvtKeyAutoRepeated[key]);
}

[close]


The module is currently in BETA stage and needs extensive testing.
Comments, critism, suggestions and bug reports are welcome.

Crimson Wizard

#1
Shortly after I posted this module here, I realized its program interface was somewhat raw and inconsistent. I took time to improve it to make it look better and more convenient to use.
There are also some changes to the state recording logic.

New version of the module 0.9.5:
https://bitbucket.org/ivan-mogilko/ags-script-modules/downloads/KeyListener_0.9.5.scm
Source code location is the same.

Changes 0.9.0 -> 0.9.5:

* The signal properties of the KeyListener class now share similar naming convention that makes them stand-out and easier to find and remember. All of them start with "Evt" prefix now (meaning - obviously - "Event").
Code: ags

  /// Gets if key was just pushed
  readonly import static attribute bool EvtKeyPushed[];
  readonly import static attribute bool EvtMousePushed[];
  /// Gets if key was just released after being held down
  readonly import static attribute bool EvtKeyReleased[];
  readonly import static attribute bool EvtMouseReleased[];
  /// Gets if key command was repeated for a held down key
  readonly import static attribute bool EvtKeyAutoRepeated[];
  readonly import static attribute bool EvtMouseAutoRepeated[];
  /// Gets if key was tapped another time in sequence
  readonly import static attribute bool EvtKeySequenced[];
  readonly import static attribute bool EvtMouseSequenced[];
  /// Gets if key was tapped twice in sequence just now
  readonly import static attribute bool EvtKeyDoubleTap[];
  readonly import static attribute bool EvtMouseDoubleClick[];


* Key sequencing is now broken by any other key of same type (keyboard or mouse) pressed. This means that a sequential taps are now only remembered for only one key and one mouse button at a time.

* Mouse buttons now register for (and unregister from) listening all at once. This was done not for convenience though, but for technical reasons; there are few unfortunate quirks in how AGS reports mouse clicks in script that caused trouble unless all mouse buttons are recorded at once, and I felt it would be unnecessary hassle to try overcoming them. Besides, unlike keyboard keys, mouse buttons are few in numbers.

* There are few more convenience fixes; for example EvtMouseDoubleClick now does not require to check if the mouse was "just released" as well - it does so internally.

* Properties and constants related to automatic key repeating received "Auto" prefix to their names to make it more clear that they refer to Automatic Repeat (as opposed to repeatedly pressed by user).


NOTE: will update the first post to reflect these changes shortly.


EDIT: had to upload another fixed version (0.9.6) after finding a bug.

Monsieur OUXX

Cool. I had some home-made primitive version of this for my own purpose (some of it embedded in newer unpublished versions of StandaloneClick) but I was spending too much time maintaining it for the limited features it offered.

This is cool and should be considered by newbies for every new game.

EDIT: you should consider adding events related to drag and drop : mouse being held and how much it has been moved along X and Y since previous game loop.

This could come very handy for fighting games as well.

 

arj0n


Crimson Wizard

Thank you.

Quote from: Monsieur OUXX on Tue 01/03/2016 15:23:03
EDIT: you should consider adding events related to drag and drop : mouse being held and how much it has been moved along X and Y since previous game loop.
I am working on drag & drop module now; I had one I picked out from my ancient project, but it uses such awful workarounds that I decided to completely rewrite it.

Crimson Wizard

A very small update, just some cosmetic changes + safety fixes.
KeyListener 0.9.7

Crimson Wizard

DoubleClick - is a stripped version of KeyListener that only detects mouse double clicks.
If you do not need full functionality of KeyListener, that could be a better option.

Download module: https://bitbucket.org/ivan-mogilko/ags-script-modules/downloads/DoubleClick_1.0.0.scm
Source: https://bitbucket.org/ivan-mogilko/ags-script-modules/src/05c8084142e7e50cc0e9a4390db88d4405812fba/ui/DoubleClick/?at=master

Usage is very simple:

Code: ags

function on_mouse_click(MouseButton mb)
{
  if (mb = eMouseLeft)
  {
    if (DoubleClick.Event[eMouseLeft])
    {
      player.SayBackground("Double-clicked!");
    }
    else
    {
      // regular click
    }
  }
}


DoubleClick supports claiming events similar to standart ClaimEvent() function. If you are detecting double clicks in several modules, you may use DoubleClick.ClaimThisEvent() to not let double-click "through" to lower scripts.

DoubleClick.Timeout property is used to set up max timeout (in milliseconds) which may pass between two clicks for them to be considered "double click". Default is 300 ms.

lorenzo

This is going to be useful, I'll try it later. Thank you, Crimson Wizard!

Monsieur OUXX

I think I'm going to fork this module and merge the DoubleClick module with the (unfinished?) DragDrop module from https://bitbucket.org/ivan-mogilko/ags-script-modules/

The goal is to have some sort of unified listener module that lets you listen to almost every possible mouse event. The ultime goal is to require zero function in the global script to listen to control clicks. The clicks are meant to be handled directly inside the module that also manages the GUI controls that come with it (for example: the script for handling the click on "Save" in the custom Save GUI is entirely within the module that comes with it. Nothing in the global script).

I already have such a module, called "StandaloneClick" (it works pretty well, the concept is bulletproof : I even have events like "hover" and stuff), but since it's your module that's used in the Thumbleweed module, I'd rather build on yours. Also yours is more modern and overall better.
 

Crimson Wizard

Quote from: Monsieur OUXX on Sun 05/08/2018 15:56:32
I think I'm going to fork this module and merge the DoubleClick module with the (unfinished?) DragDrop module from https://bitbucket.org/ivan-mogilko/ags-script-modules/

Sure. The repository is one for all of my modules, so all up-to-date versions may be found there at once.
DragDrop module is finished and working, unless I don't know something? (Although I was constantly worrying that description I wrote is too complicated)

Monsieur OUXX

 

AndreasBlack

Hey Crimson,

I' m using your timer alot now. in fact i think i'll probably never look back at the AGS timer. (laugh)

Can this module be used with AGS controller somehow? Would be totally cool. Once again i'm trying to recreate a classic fighting game a little bit, and there are moments where a button is held down, or fast tapped (count in milliseconds would be useful with the keylistener) to execute a different attack.
I was thinking perhaps i could do something like "ifkeypressed || gamepad.buttonisdown".? something in that fashion? But i'm not sure it would actually work, since there are no mentionings of the ags controller in the actual script. It probably weren't even released when this module was created, probably.  :)



Crimson Wizard

Quote from: AndreasBlack on Sat 14/11/2020 02:07:16
Can this module be used with AGS controller somehow?

This module was written only for builtin AGS input (mouse & keyboard), but it may be easily expanded to use controller plugin, provided there is a way to know if plugin is enabled, and that plugin can report pressed button state, as the logic will be exactly the same.

Matti

I just tried out the Double-Click module and I don't get it to work. Shouldn't the double-click be detected even if there's a blocking function in progress?

I tried using the on_mouse_click code from your first post and as long as the regular click triggers some blocking command the double click is not being detected.

I tried something like this but it didn't work either:

Code: ags
function hDoor_Interact()
{
  if (DoubleClick.Event[eMouseLeft]) 
  { 
    player.ChangeRoom(6, 280, 155, eDirectionLeft);
  }
  else
  {
    player.Walk(282, 173, eBlock); 
    player.ChangeRoom(6, 280, 155, eDirectionLeft);
  }
}


Maybe I'm just a bit stupid here, but how would I go about having the player leave the room with a double click and skip the walk command?

Crimson Wizard

Quote from: Matti on Fri 18/12/2020 15:15:52
I just tried out the Double-Click module and I don't get it to work. Shouldn't the double-click be detected even if there's a blocking function in progress?

I tried using the on_mouse_click code from your first post and as long as the regular click triggers some blocking command the double click is not being detected.

on_mouse_click and on_key_pressed are not called by AGS during blocking commands. You should try this in repeteadly_execute_always instead. I cannot guarantee this right now though, might need some testing, because i don't remember if engine updates input state during blocking actions at all. If that does not work either you might need to switch to non-blocking actions and restructure your script logic.


PS. I am not even sure if object and hotspot interaction can be run during blocking action and not prevented by the engine.


EDIT:
Quote from: Matti on Fri 18/12/2020 15:15:52
Maybe I'm just a bit stupid here, but how would I go about having the player leave the room with a double click and skip the walk command?

Sorry, I skipped this line at first. No, you cannot cancel the blocking walk, this is simply not supported by AGS at the moment. (EDIT: turns out you can, see below) If you want that kind of mechanic you have to use non-blocking movement at all times (except cutscenes perhaps).

eri0o

Matti, have you tried doing SkipUntilCharacterStops(player.ID) if a double click is detected in the repeatedly_execute_always? I have not tried and that function appears to be one of the old ones, so not sure if this actually works - but it should be like skipping a cutscene if the character is doing a blocking walk.

Untested!!!
Code: ags
void repeatedly_execute_always()
{
  if(DoubleClick.Event[eMouseLeft]) 
  {
      SkipUntilCharacterStops(player.ID)
  }
}

Crimson Wizard

Redacted: nevermind, got the wrong test.

UPDATE: yes I confirm, SkipUntilCharacterStops works in the rep-exec-always during blocking walk, and it makes it fast-forward (character ends up in the final destination).
I did not test with DoubleClick, but with simple IsKeyPressed for now, so whether double click will be detected remains a question.

Unfortunately, ChangeRoom does not work when called from rep-exec-always, so it has to be called from another place.

Matti

Cool, I didn't know about SkipUntilCharacterStops.

Anyway, I decided that I don't want to have the walking in my game be blocking anyway, so the doubleclick won't be a problem I guess. Now I just have to figure out how to make non-blocking walking and leaving the room consecutive/queued  ;)

rongel

Hi!

I have a simple question (hopefully) about the module: Can I use it so that a key press is only registered once, even if the key is hold down for a while? I have a ridiculously simple pause system in my game:

Code: ags
if(KeyListener.EvtKeyPushed[eKeySpace]) {
    
    Display("GAME PAUSED %s",
    "[[Press any key to continue");
  }


It works just the way I want (any key removes the pause), except that if I hold the spacebar down, the pause will soon toggle automatically off. So I want the pause to stay on, even if the player is holding down the spacebar, and that another key press is needed to switch it off. 

I asked something like this for my menu system earlier, which worked. But I'm hoping I could use the Keylistener to manipulate how the spacebar works in this situation. Any ideas? And thanks for the module CW!
Dreams in the Witch House on Steam & GOG

Crimson Wizard

Normally you would not need this module for such thing, you'd simply start pause by space press and unpause by anything else in on_key_press function.

But with Display command it's different, in AGS key presses won't be registered at all while Display is active on screen. What you may probably do is to set special key to skip Display right before starting one and set it back to your normal defaults right after.

Something like
Code: ags

int old_skip_display = game.skip_display;
int old_skip_key = game.skip_speech_specific_key;
game.skip_display = eSkipKey;
game.skip_speech_specific_key = eKeyCodeA;
Display("GAME PAUSED %s",
    "[[Press any key to continue");
game.skip_display = old_skip_display;
game.skip_speech_specific_key = old_skip_key;

SMF spam blocked by CleanTalk