[EXPERIMENTAL] Joystick in AGS - AGS-3.99.106.0-Alpha3-JoystickPR

Started by eri0o, Fri 29/04/2022 23:57:47

Previous topic - Next topic

eri0o

Experimental Editor with AGS with Gamepad support!

Download Editor Here: >>AGS-3.99.106.0-Alpha3-JoystickPR.zip<< | installer | Code Source | GitHub Issue | GitHub PR

First the concepts, joystick means a generic input device comprised by binary (button) and analog (axis) inputs. A Gamepad means something that is vaguely close to a Xbox360 controller.

We can expand the API later, but the basics is joystick connection and disconnection is handled inside the AGS Engine and we can skip things by pressing A,B,X,Y or the symbols in PS controller.



@Alan v.Drake made a beautiful test project!



Playable AgsGameGamepadV4.zip | project | Online https://ericoporto.github.io/agsjs/gamepadtest/




Joystick Static Methods

Joystick.JoystickCount
Code: ags
static readonly int Joystick.JoystickCount
Get the number of connected joysticks. No joysticks should return 0!


Joystick.Joysticks
Code: ags
static readonly Joystick* Joystick.Joysticks[int index]
Gets a joystick by index from the internal engine joystick list.


Joystick Instance Attributes and Methods

Joystick.IsConnected
Code: ags
readonly bool Joystick.IsConnected
True if joystick is connected.


Joystick.Name
Code: ags
readonly String Joystick.Name
joystick name.



Joystick.IsButtonDown
Code: ags
bool Joystick.IsButtonDown(int button)
checks if a joystick button is pressed, by index. DPad is usually mapped separately as a hat.


Joystick.GetAxis
Code: ags
bool Joystick.GetAxis(int axis, optional float deadzone)
get a joystick axis or trigger, trigger only has positive values, by axis number. Values varies from -1.0 to 1.0 for axis, and 0.0 to 1.0 for triggers.


Joystick.GetHat
Code: ags
eJoystick_Hat Joystick.GetHat(int hat)
returns hat value


Joystick.AxisCount
Code: ags
readonly int Joystick.AxisCount
get the number of axis in the joystick


Joystick.ButtonCount
Code: ags
readonly int Joystick.ButtonCount
get the number of buttons in the joystick


Joystick.HatCount
Code: ags
readonly int Joystick.HatCount
get the number of hats in the joystick



Joystick.IsGamepad
Code: ags
readonly bool Joystick.IsGamepad
True if joystick is a valid gamepad connected - this means SDL2 recognized it as a valid GameController and has successfully mapped it's buttons to an Xbox360 gamepad.


Joystick.IsGamepadButtonDown
Code: ags
bool Joystick.IsGamepadButtonDown(eGamepad_Button button)
checks if a gamepad button is pressed, including dpad.

Possible buttons:
  • eGamepad_ButtonA
  • eGamepad_ButtonB
  • eGamepad_ButtonX
  • eGamepad_ButtonY
  • eGamepad_ButtonBack
  • eGamepad_ButtonGuide
  • eGamepad_ButtonStart
  • eGamepad_ButtonLeftStick
  • eGamepad_ButtonRightStick
  • eGamepad_ButtonLeftShoulder
  • eGamepad_ButtonRightShoulder
  • eGamepad_ButtonDpadUp
  • eGamepad_ButtonDpadDown
  • eGamepad_ButtonDpadLeft
  • eGamepad_ButtonDpadRight


Joystick.GetGamepadAxis
Code: ags
float Joystick.GetGamepadAxis(eGamepad_Axis axis, optional float deadzone)
get gamepad axis or trigger, trigger only has positive values. Values varies from -1.0 to 1.0 for axis, and 0.0 to 1.0 for triggers.

You can optionally pass an additional parameter to use as deadzone. If an axis absolute value is smaller than the value of deadzone, it will return 0.0. Default value is AXIS_DEFAULT_DEADZONE, which is for now 0.125, use the name if you need a number.

Possible axis and triggers:
  • eGamepad_AxisLeftX
  • eGamepad_AxisLeftY
  • eGamepad_AxisRightX
  • eGamepad_AxisRightY
  • eGamepad_AxisTriggerLeft
  • eGamepad_AxisTriggerRight


CHANGELOG:
  • v1: initial release
  • v2: GetAxis now returns a float
  • v3: GetAxis now has an additional dead_zone parameter. Default value is GAMEPAD_DEFAULT_DEADZONE, which is for now 0.125, use the name if you need a number.
  • v4: Using correspondent A,B,X,Y buttons can skip Speech and Display messages, as long as you Connect to the Gamepad.
  • v5: ditched Gamepad-only approach for a Joystick first approach similar to löve.
  • v6: connection and disconnection handled in-engine, new AGS 4 approach.
  • v7: api renamed get,getcount to instead retrieve from array of joysticks, also fixed save/load game bug

Mehrdad

Wow!. Super useful. Nice job @eri0o
My official site: http://www.pershaland.com/

eri0o

Thanks Mehrdad! I still need to figure more things, but I wanted to throw together a basic one, since with SDL2, we can roll joysticks for all platforms at the same time when support is in the engine itself. :)

Also sketching the API with users because things on the Engine are hard to change.

eri0o

Small update, just switched GetAxis to provide a float value. An example usage is added in the code of the example project as follows:

Code: ags
#define DEAD_ZONE 0.125
float gamepad_mouse_x, gamepad_mouse_y;
bool update_mouse;
bool prev_button_a_pressed;

void repeatedly_execute_always()
{
  if(pad != null)
  {
    if(pad.IsButtonDown(eGamepad_ButtonA) && !prev_button_a_pressed)
    {
      mouse.Click(eMouseLeft);
    }
    
    prev_button_a_pressed = pad.IsButtonDown(eGamepad_ButtonA);
    
    float x_lstick = pad.GetAxis(eGamepad_AxisLeftX);
    float y_lstick = pad.GetAxis(eGamepad_AxisLeftY);
  
    if( x_lstick < -DEAD_ZONE || x_lstick > DEAD_ZONE ||
        y_lstick < -DEAD_ZONE || y_lstick > DEAD_ZONE )
    {
      if(update_mouse) {
        gamepad_mouse_x = IntToFloat(mouse.x);
        gamepad_mouse_y = IntToFloat(mouse.y);
        update_mouse = false;
      }
      
      gamepad_mouse_x += 10.0 * x_lstick;
      gamepad_mouse_y += 10.0 * y_lstick;
      
      mouse.SetPosition(FloatToInt(gamepad_mouse_x, eRoundDown) , 
                        FloatToInt(gamepad_mouse_y, eRoundDown));
    } else {
      update_mouse = true;
    }
  }
}

deadsuperhero

Oh, man! This is exactly what I need right now for my projects. Very exciting to see this, would love to get my games working on SteamDeck!
The fediverse needs great indie game developers! Find me there!

AndreasBlack

#5
Nice! Gonna give this a try later, thanks! I knew it had something to do with the deadzone being off as i wrote in the DM, but my limited coding knowledge could only get it to stay in the middle of the screen previously!

Edit: Something really strange is going on. I get this error: Failed to save room room1.crm; details below
room1.asc(21): Error (line 21): Type mismatch: cannot convert 'int' to 'float'

The project seems to randomly work. Really odd. Say if i edit the line AGS is claiming has an error.

Code: ags
    float x_lstick = pad.GetAxis(eGamepad_AxisLeftX);


Change it to say AxisLeft); leave out the X then i get an error obviously, then edit it back to the correct syntax AxisLeftX); Then still it gives me the error Line 21 again, even tho the syntax is right!

No idea what's causing this. So i can't give this a try in my main project cause i get that error no matter what in that project. Even in the example project as written above i could recreate the error line.

eri0o

#6
I cannot reproduce... Are you sure you are using the new V2 version I provided above and not the previous V1 version that was available before? In V1 GetAxis was giving an integer, but now in V2 I changed to a float, because I felt it was easier to people understand the range available.

If someone else could try too, it would be great.  (roll)

Edit: I may implement an additional axis_deadzone parameter in GetAxis to simplify code for this.

AndreasBlack

#7
I was blind! I didn't see you've also updated the link for the AGSbeta to a "v2". My bad! It works now! Thanks!

The mouse/controller seems stable too, fantastic!

eri0o

#8
Added a deadzone parameter in GetAxis and updated the description above to explain how it works.

This allows switching the code to a slightly simpler comparison. It will also nudge in the importance of deadzone if someone wants to modify the parameter.

Code: ags
float gamepad_mouse_x, gamepad_mouse_y;
bool update_mouse;
bool prev_button_a_pressed;

void repeatedly_execute_always()
{
  if(pad != null)
  {
    if(pad.IsButtonDown(eGamepad_ButtonA) && !prev_button_a_pressed)
    {
      mouse.Click(eMouseLeft);
    }
    
    prev_button_a_pressed = pad.IsButtonDown(eGamepad_ButtonA);
    
    float x_lstick = pad.GetAxis(eGamepad_AxisLeftX);
    float y_lstick = pad.GetAxis(eGamepad_AxisLeftY);
  
    if( x_lstick != 0.0 || y_lstick != 0.0 )
    {
      if(update_mouse) {
        gamepad_mouse_x = IntToFloat(mouse.x);
        gamepad_mouse_y = IntToFloat(mouse.y);
        update_mouse = false;
      }
      
      gamepad_mouse_x += 8.0 * x_lstick;
      gamepad_mouse_y += 8.0 * y_lstick;
      
      mouse.SetPosition(FloatToInt(gamepad_mouse_x, eRoundDown) , 
                        FloatToInt(gamepad_mouse_y, eRoundDown));
    } else {
      update_mouse = true;
    }
  }
}


Note for the web port:

  • mouse SetPosition is not allowed unless the game enters in fullscreen once (through either alt+enter or by script, but only after the player clicks on the screen once). We would need to support a fake mouse and some way to click in a specific position to circumvent this, but I am not sure how to handle this at this time. This is because a Browser protects you from evil websites that may want to hijack your mouse.
  • after the game is loaded and in focus, you need to hit one button in the joystick at least once, before the browser recognizes it for that tab and we can find it.

Edit:

I ninjad a new update to v3 itself, just a small adjustment to guarantee the float values from GetAxis are always sane.

Crimson Wizard

#9
I see that the gamepad buttons are used as mouse click simulation, but I assume this is done temporary for a test.

Should the goal be to have gamepad buttons handled similar to keyboard key and mouse clicks, that is to have their own respective callback, like on_gamepad_press?

One optional idea is to have gamepad buttons merged with eKey codes. The benefit of that is that you may assign them to the same variables as keys, both in engine API properties, and script variables.

eri0o

QuoteOne optional idea is to have gamepad buttons merged with eKey codes. The benefit of that is that you may assign them to the same variables as keys, both in engine API properties, and script variables.

That is interesting, but I think I prefer to figure some dedicated way to do so, Gamepads have predefined buttons but joysticks don't. But I will keep that approach in mind as a fallback in case things don't work the way I hope.

Things like, multiple Gamepads/Joysticks, I must keep in mind and try to not clash parts that would be helpful to most and the full range of possibilities, like couch co-op.

About on_gamepad_press, I will at some point, but it's not very useful right now (can't skip text) so I will keep using repeatedly execute always at this time. I wonder if for gamepad -> mouse mapping there's something better we could do at this time, like click and holding isn't possible, because there's no way to force a button state in the mouse...

Crimson Wizard

#11
Quote from: eri0o on Mon 02/05/2022 10:54:09
About on_gamepad_press, I will at some point, but it's not very useful right now (can't skip text) so I will keep using repeatedly execute always at this time.

Could you elaborate how is this related to skipping text?

IMO it may be useful just as the key presses in on_key_press are useful: to react to button press without having to deal with their on/off states in the rep-exec. Right now it seems in your examples you're trying to use gamepad to simulate mouse cursor and clicks. But gamepads could be used without mouse cursors as well, similar to how keyboard is used in keyboard controlled games.

My meaning in general is not to try to simulate anything with gamepad (this was the plugin's approach, because there was no other way) but instead integrate gamepad as a part of the normal engine input and script. Have events for gamepad similar to how there are events for mouse and keys.

eri0o

on_ events don't happen when something is blocking. Also they are not good when you need to get something continuously, like keep the character walking.

I also think I am handling wrong the pointers in my code, I think ideally objects that represent the same index should get the same pointer - right now this is countered because you can't connect to the same joystick twice, and that is only factory function that produces gamepads. But in the engine I could theoretically pass the pointer of the pressed joystick in an on_ event so you could inspect which joystick/gamepad had the button pressed, which would be easy if the pointers were guaranteed to match. I am not sure the best approach right now to guarantee such uniqueness.

Crimson Wizard

#13
Quote from: eri0o on Mon 02/05/2022 11:25:19
on_ events don't happen when something is blocking. Also they are not good when you need to get something continuously, like keep the character walking.

Ok, but so do events for mouse clicks and keys, and yet they exist and are useful.

Skipping text and skippable blocking actions should be done in the engine IMO. In other words, whenever there's an event for mouse or keys there should be a corresponding event for gamepad. If a mouse or keys are checked in the engine, so should gamepad be checked as well, and so on. Well, at least this is how I thought this will be done. Is your goal for the moment to use gamepad to simulate other inputs in rep-exec-always?

AndreasBlack

Just tried the new V3 and i'd made sure i've downloaded all the links this time  :-D In V3 i've noticed the character seems to get stuck around the first or second walk frame before walking away, sorta like running on a treadmill a millisecond (Thumbleweed template). Not sure what that is about ??? works fine otherwise, to the untrained eye, at least. Have you tried adding a view with one frame and added audio into the frame and see if it stutters all over? Say forexample the player wants a fist animation view and adds the sound in the view and pushes the button or holds down the button, does it stutter? In my mind, i don't think it should, if it does! Thanks





eri0o

#15
Quote from: AndreasBlack on Mon 02/05/2022 12:11:27
Just tried the new V3 and i'd made sure i've downloaded all the links this time  :-D In V3 i've noticed the character seems to get stuck around the first or second walk frame before walking away, sorta like running on a treadmill a millisecond (Thumbleweed template). Not sure what that is about ??? works fine otherwise, to the untrained eye, at least. Have you tried adding a view with one frame and added audio into the frame and see if it stutters all over? Say forexample the player wants a fist animation view and adds the sound in the view and pushes the button or holds down the button, does it stutter? In my mind, i don't think it should, if it does! Thanks

Sorry, but I don't think I added any handling of views here, is this something in your own code? Or is it something that happens when you click for the character to walk using the button of the gamepad to press a button on the mouse?

QuoteSkipping text and skippable blocking actions should be done in the engine IMO

Yes, but I think it needs it's own functions and place to store things. One problem is one can have two joysticks or more connected, and then they can save the game and load and have zero, there are things regarding the gamepad that seem to belong more in a config file than in an save state. But these depends on how much information is meant to be stored in these.

I will write down the bits of the problems I am trying to figure out, since, I do not have been able to figure the roadmap yet, maybe some of the problems I am thinking are just in my head and may not be that relevant for now. I revised a bit the notes on the original issue in GitHub, I will try to update there with things on my mind.

I really need people to play around with this and come up with the problems, that arise. :/

eri0o

I will be looking in to add in the next version a way to properly skip texts for this, but it may not be the final form we endup going, but I need something materialized to think.

eri0o

Now you can use A,B,X or Y buttons to skip Display and Speak messages. This is still in development, I am trying to figure it out the API for being able to say which button can be used and stuff like that - for now, if keyboard can skip text, the gamepad will be able to skip too.

For now, the controller has to be connect first by using the script API before it can be used as such. I updated the top post to v4 which is the release that includes this. If you downloaded the v4 yesterday, you need to download again as I just found a mistake at that.

AndreasBlack

It happens when i click walk with a button on the controller, yes. Thumbleweed template with own game/sprites ofc. They never behave like that without the controller using the mouse tho. Walks normally then.
Normal walking views added with 8 frame sprites right left up down, etc. Nothing special going on, so i was a bit surprised by it tbh. But focus on solving the big picture, i should never complain anymore, not qualified. Great job so far!  (nod)

eri0o

@AndreasBlack, Can you try V4 that is up and see if it still happens? I previously wrongly handled button press when the button was released too, so this may have been a problem, but I fixed in the latest version.

SMF spam blocked by CleanTalk