MODULE: ArrowSelect 0.6.0 (for usage with Keyboard and Gamepad)

Started by eri0o, Sat 03/08/2019 20:38:45

Previous topic - Next topic

eri0o

ArrowSelect version 0.6.0

Select things using arrows keys or joystick hat, module for in point and click games made with Adventure Game Studio.

Requires AGS 3.5.0.14



Get Latest Release arrowselect.scm | GitHub Repo | Demo Windows | Demo Linux

Note: This module doesn't deal with printing things on screen, but if you want to do this, you may find it provides some helpful functions with the Interactives abstraction.

Basic usage

For basic usage with Keyboard, in your global script, add at game_start:

Code: ags

    ArrowSelect.enableKeyboardArrows();


Usage with joystick

If you are using a joystick or gamepad plugin, you will need to implement your own function to deal with. An example for hat is below.

Code: ags

    //pressed a hat
    void pressedPov(int pov){
      if(pov == ePOVCenter){
      } else if(pov == ePOVDown){
        ArrowSelect.moveCursorDirection(eDirectionDown);
      } else if(pov == ePOVLeft){
        ArrowSelect.moveCursorDirection(eDirectionLeft);
      } else if(pov == ePOVRight){
        ArrowSelect.moveCursorDirection(eDirectionRight);
      } else if(pov == ePOVUp){
        ArrowSelect.moveCursorDirection(eDirectionUp);
      } else if(pov == ePOVDownLeft){
        ArrowSelect.moveCursorDirection(eDirectionDownLeft);
      } else if(pov == ePOVDownRight){
        ArrowSelect.moveCursorDirection(eDirectionDownRight);
      } else if(pov == ePOVUpLeft){
        ArrowSelect.moveCursorDirection(eDirectionUpLeft);
      } else if(pov == ePOVUpRight){
        ArrowSelect.moveCursorDirection(eDirectionUpRight);
      }
      return;
    }


What are Interactives ?

Interactives are things on screen that the player can interact with.
These are Objects, Characters, Hotspots, and GUI Controls like buttons and others.
This module only cares for their type, and a position that is similar to the thing center that mouse can click.

Note that some gotchas apply, for example, if you have three different Hotspots areas that map to the same Hotspot, instead of finding out they are different, it will erroneously find a point in the center of them.
So if you have, for example, two TVs in your background, that have the same interaction, create two different hostpots for them and just map the same interaction function to both, otherwise this module will fail.

Code: ags
enum InteractiveType{
  eInteractiveTypeNothing = eLocationNothing,
  eInteractiveTypeObject = eLocationObject,
  eInteractiveTypeCharacter = eLocationCharacter,
  eInteractiveTypeHotspot = eLocationHotspot,
  eInteractiveTypeGUIControl,
  eInteractiveTypeGUI,
};

managed struct Interactive{
  int x;
  int y;
  int ID;
  InteractiveType type;
};


ArrowSelect API

bool ArrowSelect.moveCursorDirection(CharacterDirection dir)
Moves cursor to the interactive available at a direction. Returns true if the cursor is successfully moved.

Interactive* ArrowSelect.getNearestInteractivePointAtDirection(CharacterDirection dir)
Get the nearest interactive available at a direction. Returns null if there is none.

Point* ArrowSelect.getNearestInteractivePointAtDirection(CharacterDirection dir)
Get point of the nearest interactive available at a direction. Returns null if there is none.

bool attribute ArrowSelect.UseMouseAsOrigin
If true, Mouse position is used as origin in getNearestInteractiveAtDirection and related functions. Default is true.

Point* attribute ArrowSelect.Origin
Point used as origin if UseMouseAsOrigin is false.

void filterInteractiveType(InteractiveType interactiveType, InteractiveFilter filter=0)
Filters or not a interactive type for cursor moveCursorDirection and getNearestInteractivePointAtDirection.

bool ArrowSelect.areKeyboardArrowsEnable()
Returns true if regular keyboard arrows are enabled for cursor movements.

bool ArrowSelect.enableKeyboardArrows(bool isKeyboardArrowsEnabled = 1)
Enables or disables (by passing false) regular keyboard arrows handled by this module.

Triangle* ArrowSelect.triangleFromOriginAngleAndDirection(Point* origin, int direction, int spreadAngle=90)
Returns a Triangle instance with one point at the origin points and the two other points separated by spreadAngle, and at the direction angle

int ArrowSelect.distanceInteractivePoint(Interactive* s, Point* a)
Retuns the distance between an interactive and a point.

Interactive* ArrowSelect.closestValidInteractivePoint(Interactive* Interactives[], Point* a)
Returns the closest interactive to a point.

Interactive*[] ArrowSelect.getInteractives()
Get a list of all interactives on screen.

bool ArrowSelect.isInteractiveInsideTriangle(Interactive* p, Point* a, Point* b, Point* c)
Returns true if an interactive is inside a triangle defined by three points.

Interactive*[] ArrowSelect.whichInteractivesInTriangle(Interactive* Interactives[], Point* a, Point* b, Point* c)
Returns a list of which triangles are inside a triangle defined by three points.

Implementation details

This is just the detail on how things works on this module

Problem
By using keyboard arrow keys or joystick directional hat, select between
clickable things on screen.

Solution
When the player press an arrow button do as follow:

1 .get the x,y position of each thing on screen,

2 .select only things on arrow button direction (example:at right of current
  cursor position, when player press right arrow button),

3 .calculate distance from cursor to things there, and get what has the smaller
  distance

Solution details
For 2, the key is figuring out the right angle and then create a triangle that
extends to screen border, the things inside the triangle can be figured with the
function below

https://stackoverflow.com/a/9755252/965638
Code: C++

public static bool PointInTriangle(Point p, Point p0, Point p1, Point p2)
{
    var s = p0.Y * p2.X - p0.X * p2.Y + (p2.Y - p0.Y) * p.X + (p0.X - p2.X) * p.Y;
    var t = p0.X * p1.Y - p0.Y * p1.X + (p0.Y - p1.Y) * p.X + (p1.X - p0.X) * p.Y;

   if ((s < 0) != (t < 0))
    return false;

var A = -p1.Y * p2.X + p0.Y * (p2.X - p1.X) + p0.X * (p1.Y - p2.Y) + p1.X * p2.Y;

return A < 0 ?
        (s <= 0 &amp;&amp; s + t >= A) :
        (s >= 0 &amp;&amp; s + t <= A);
}


Author

Made by eri0o

License

Distributed under MIT license. See LICENSE for more information.

artium

Did not try it yet, but looks very nice.

I can think of some use cases for this to make console playing more convenient.
I think the main game-play should still be with a manual moving of the cursor using the analog sticks on the controller. Otherwise, the "point and click" part of the adventure game is missing.
But things like navigating menus, complex RPG style UIs, closeup puzzles could really benefit from it.

I have an improvement suggestion. Let the user of the module to selectively enable which interactives should be part of the system. Then it will be possible to use it with characters, inventory, room exit points etc, but leave the puzzles for manual searching. Maybe someone would like decide that for his game, any interactive that was clicked at least once in the game could be quickly reached using the arrows.

eri0o

#2
I will try to figure out how to filter things out. Each block of the algorithm is also exposed if someone wants to do anything creative with them too. :)

I really want people to try it out because there may be lots of use cases and bugs that I haven't predicted here.

Edit: I need to figure out how to navigate list GUI controls.

Monsieur OUXX

Could you clarify in your first post that you mean keyboard arrow keys? I was very confused at first read, I thought your module was replacing the game's mouse icons with the same icon, just with an added arrow
 

eri0o

Hey Monsieur OUXX, I added a thing on the subject and added keyboard and gamepad mention on the first line, see if it's clearer now. :)

I have an issue I couldn't quite figure out yet which is when there's a clickable GUI on top of some stuff on screen, if someone has ideas or want to fork and pull request on GitHub a solution :P

Crimson Wizard

Quote from: eri0o on Fri 09/08/2019 11:33:20
I have an issue I couldn't quite figure out yet which is when there's a clickable GUI on top of some stuff on screen, if someone has ideas or want to fork and pull request on GitHub a solution :P
What is the problem? Clicking on something, or checking that something is under? There's e.g. Room.ProcessClick that ignores GUI.

eri0o

I need to know before the click if the thing will actually be clickable by ProcessClick, with this information I could invalidate the ocluded interactives.

Edit: I created a issue for this.

Since this problem is more relevant for GUIs and things hidden by GUIs, I will solve by filtering out points that , when hit with GUI.GetAtScreenXY return a value different than null and are not GUI controls, and for GUI Controls, check if the owning GUI matches.

Edit2: Fixed and new release.  8-)

Edit3: Added a way to filter things out as per artium request. I also added enter as click just on the demo global script for ease of testing it out.

Edit4: Added support for list box!



Edit5: Now intermediary points of a slider are available too! Also fixed a crash that happened in the previous version.



Edit6: Added support for InvWindow and Inventory Items!


Babar

Hi eri0o!
morgan directed me to this for a problem I was having. I'll download and investigate it tomorrow, but I am curious, does this also cater to dialogue options (I care more about default, it seems with your GUI handling, it probably does custom).
The ultimate Professional Amateur

Now, with his very own game: Alien Time Zone

eri0o

Hey Babar!

I actually never ever used the default dialog options thing from AGS, so I would say no. I would need to investigate if it's possible. Basically the problem is figuring out if it's on or not, and once this is figured out, what's the minimal list of points where the cursor needs to be.

Stranga

Hi there eri0o :) Is there a way to disable the player from using the mouse and just control the cursor via keyboard?

eri0o

Hi Stranga,

That is a super interesting use case. I looked into and is not really easy to accomplish now since it currently picks the origin from the mouse cursor position. I need to do some changes to allow for that.

Edit:



Created a new version! I made that the demo game can allow a fake cursor (you can check it by clicking the red orb, but you should really look the demo project source code)

The gist is in the room script:

Code: ags
function on_key_press(eKeyCode keycode) 
{
  if(!global_bool_fake_cursor) return;
  
  if(keycode == eKeyReturn) 
  {
    _doFakeClick();
    return;
  }
  
  Interactive* interactive;
  Point* p;
    
  p = new Point;
  p.x = gFakeCursor.X;
  p.y = gFakeCursor.Y;
  ArrowSelect.Origin = p;
  
  if (keycode == eKeyDownArrow) {
    interactive = ArrowSelect.getNearestInteractiveAtDirection(eDirectionDown);
  } else if (keycode == eKeyUpArrow) {
    interactive = ArrowSelect.getNearestInteractiveAtDirection(eDirectionUp);
  } else if (keycode == eKeyLeftArrow) {
    interactive = ArrowSelect.getNearestInteractiveAtDirection(eDirectionLeft);
  } else if (keycode == eKeyRightArrow) {
    interactive = ArrowSelect.getNearestInteractiveAtDirection(eDirectionRight);
  } else return;
  
  if(interactive == null) return;
    
  gFakeCursor.X = interactive.x;
  gFakeCursor.Y = interactive.y;
}


Unfortunately by going this route, other things in the "game" ui that relies on mouse position breaks. If you can write your GUI code avoiding it - like avoiding YPopPosition and other mouse dependent code in the GUI, then sure, it will work.


I think this module need a good revamp on the API eventually so it's simpler to understand how to use but for now I think I will keep adding things in it and leave the revamped API whenever a 1.0.0 version happens someday in the future.

Stranga

This is perfect!  :grin: Exactly what I need! Thank you very much eri0o! Most of my games have been coded around using any mouse functions so this fine.

Babar

Hey again eri0o! I'm still abusing the module for purposes it hasn't been intended. It turned out that as you said, it doesn't work with the default dialog, but I don't have the know-how to figure out how to make it.

My question this time is is if there is some way to simply have the cursor iterate through all the interactives, rather than the player having to visually figure out which direction key they need to press. So for example, just continuously pressing a key (down?) would just take you through all the items on the screen?
The ultimate Professional Amateur

Now, with his very own game: Alien Time Zone

eri0o

Hi Babar,

The module has a thing it calls interactives and allows getting a list of all of them - it retrieves a dynamic array of interactive pointers:

Code: ags
 interactives = ArrowSelect.getInteractives();


If you disable the module default handling of keyboard you can then use this to implement your own logic.

The dialog doesn't give information for implementing keyboard handling, so if you want to do it, you need to do on your own. Luckily the second example in AGS Manual is exactly keyboard handling for dialogues.

Edit: I have a small modified version of this module in the works, but I plan to change the API to be more consistent. I also plan on making key usage to be similar to how things are in Controlz, where you simply set the desired directions to boolean. I also wanted to have a possibility to pass an angle (in case someone is using Maths.ArcTan2 to get analogue direction from a gamepad). Anyway, I haven't yet managed to get to it - I ended up modifying it's logic on my game, so I can't really just pull it out of it like I used to.

SMF spam blocked by CleanTalk