Log In  

I've got two big frustrations with gamepad support in HTML exported games and the BBS. The following is based on testing in Chrome on Windows and Mac using an XBox 360 Wireless Controller and a Nintendo Switch Pro Controller. I'm using this test app for everything following.

Cart #netties_joy_test-1 | 2021-01-26 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

D-pad does not function as direction inputs

When exported to HTML or uploaded to the BBS, d-pads are either ignored or treated as extra pause buttons. (It seems they are ignored on the HTML export and behave as pause buttons on the BBS?) The gamepad browser API supports other controller layouts but is designed around common modern controllers, and it has a concept of the "standard gamepad" canonical device, which allocates buttons 12, 13, 14 and 15 to up, down, left and right. I would be great if the D-pad was supported here, by mapping these buttons to player direction inputs, either in addition to or instead of the analog stick.

Controller assignment cannot be changed in the browser

Controller assignment cannot be changed. Not even by unplugging and replugging controllers. I think controllers are assigned indexes by the browser when it first encounters them, and you need to restart the whole browser to get them re-indexed. This is generally untenable. PICO-8 games have some capacity to work around this, since they can see up to eight controllers, but they are hampered because they can't actually tell the difference between a player using the keyboard and one using a joystick, which renders the common best practice in this area rather player-unfriendly: to prompt for player input and then assign the first active controller as player 1, etc. Even if they can work around it, I think something could and should be done at the HTML exporter and BBS level, if not in the core of PICO-8 itself.

In depth

PICO-8 has a joystick model based on the classic consoles and arcade machines, where a fixed number of joysticks (or at least joystick ports) were present, and clearly identified by position. This is a fine model in many cases, but it begins to break down when where there are many controllers, controllers of different types, or wireless controllers, all connected to one machine. It might well be that all the controllers are not equally suitable for a given game. It might be that it's not easy to figure out which one is player 1, it might be that it's not easy or even possible to remove unwanted controllers.

As a single example, whenever I try to play an HTML-exported single player PICO-8 game, I find that none of my gamepads is player 1. Why? Well it turns out my keyboard also registers itself as a gamepad, and a pretty useless one at that. (It acts as a single-axis wheel... don't ask.) And since it's always there, it always ends up being gamepad #1. I can't unplug the keyboard, because... it's my keyboard, I need it to use the computer.

Now, there's a best practice to handle this situation! Games can wait on the title screen for a player to press a button on a controller, and then make that controller player 1. This works pretty well. But PICO-8 struggles at it because it deliberately abstracts away controllers entirely from the game code. The game doesn't know that
you're using a controller or playing with the keyboard. If you follow this method there's a risk a player unfamiliar with PICO-8 will first hit one of player 2's keyboard controls, activating player 2's awkward controls and deactivating player 1's. In trying to make the game behave better on gamepads you risk making it more confusing on keyboard. Trapped inside PICO-8's machine abstraction, there's not a lot a game can do about this.

And of course, most developers don't even try. If you're just developing a single player game, PICO-8 encourages you not even to think about other players - if you never specify a player index to BTN or BTNP you'll only ever see player 1. And honestly, maybe they shouldn't have to! It would be nice if they didn't have to worry about it. It's hard to force everybody to fix their games to handle a problem many of them will never encounter.

I'm not sure I want to advocate for any specific intervention here. Right now I just want to highlight the problem and say that it is pretty painful for me. A full controller remapping interface in the browser might be helpful but is probably overkill. I think mapping gamepads to player 1, 2, 3 etc. on first observed button press would probably be an improvement from where we are currently. I.e. after PICO-8 has loaded, the first time any unassigned controller presses a button, it gets assigned to the first unassigned player slot, starting at 1. (The main problem I can see with this is if you want one player on keyboard and one on gamepad, although that's already a problem at the moment.) It might also be helpful to have some way as a player or as a developer to force all gamepads to be player 1, to handle the most common case of single-player games.

P#86818 2021-01-26 22:42

I've made the following changes to the HTML file exported by PICO-8 and it behaves much better for me. Controllers are mapped when any button is pressed, first-come first-served. They are unmapped when disconnected. D-pad performs directional control. I also changed it so that the shoulder buttons (4, 5, 6, 7) don't open the pause menu but the central buttons (8, 9, which are "back" and "start" on the XBox controller and "-" and "+" on the Switch) do pause. (I haven't tested it more extensively than connecting and disconnected the controllers and mashing a lot of buttons, but it seems a good start.)

EDIT - I've since rewritten this, fixed a bug related to the menu button and bundled it into a plate. Go have a look at https://www.lexaloffle.com/bbs/?tid=41355 instead of using this.

diff --git a/./jtest_bad.html b/./jtest.html
index e55866f..fcf9451 100755
--- a/./jtest_bad.html
+++ b/./jtest.html
@@ -31,6 +31,8 @@
     var pico8_gamepads = {};
     pico8_gamepads.count = 0;

+    var pico8_gamepads_mapping = [];
     // When pico8_gpio is defined, reading and writing to gpio pins will read and write to these values
     var pico8_gpio = new Array(128);

@@ -591,31 +593,60 @@
         if (!gps) return;

         pico8_gamepads.count = gps.length;
+        pico8_gamepads_active = 0;
+        while (gps.length > pico8_gamepads_mapping.length) {
+            pico8_gamepads_mapping.push(null);
+        }

         for (var i = 0; i < gps.length && i < max_players; i++) {
             var gp = gps[i];
+            if (gp && !gp.connected) {
+                // Unmap disconnected controllers
+                pico8_gamepads_mapping[i] = null;
+            }
             if (gp && gp.axes && gp.buttons)
-                pico8_buttons[i] = 0;
+                var button_state = 0;

-                if (gp.axes[0] && gp.axes[0] < -threshold) pico8_buttons[i] |= 0x1;
-                if (gp.axes[0] && gp.axes[0] > threshold) pico8_buttons[i] |= 0x2;
-                if (gp.axes[1] && gp.axes[1] < -threshold) pico8_buttons[i] |= 0x4;
-                if (gp.axes[1] && gp.axes[1] > threshold) pico8_buttons[i] |= 0x8;
+                if (gp.axes[0] && gp.axes[0] < -threshold) button_state |= 0x1;
+                if (gp.axes[0] && gp.axes[0] > threshold) button_state |= 0x2;
+                if (gp.axes[1] && gp.axes[1] < -threshold) button_state |= 0x4;
+                if (gp.axes[1] && gp.axes[1] > threshold) button_state |= 0x8;

                 // buttons: first 4 are O/X; (almost) everything else taken to be menu button
                 // ref: https://w3c.github.io/gamepad/#remapping (er.. that mapping doesn't agree with xbox, buffalo snes)
                 for (j = 0; j < gp.buttons.length; j++)
                 if (gp.buttons[j].value > 0 || gp.buttons[j].pressed)
-                    if (j < 4)
-                        pico8_buttons[i] |= (0x10 << (((j+1)/2)&1)); // 0 1 1 0 0 1 -- A,X are O,X on xbox controller
-                    else
-                    {
-                        if (j >= 6 && j <= 8) // PICO-8 0.2.0g: seems usually 6,7,8 are menu buttons (?) // others might be easy to accidentally bump
-                            pico8_buttons[0] |= 0x40; // menu button
+                    if (j < 4) {
+                        button_state |= (0x10 << (((j+1)/2)&1)); // 0 1 1 0 0 1 -- A,X are O,X on xbox controller
+                    } else if (j >= 8 && j <= 9) {
+                        pico8_buttons[0] |= 0x40; // menu button
+                    } else if (j == 12) {
+                        button_state |= 0x4;
+                    } else if (j == 13) {
+                        button_state |= 0x8;
+                    } else if (j == 14) {
+                        button_state |= 0x1;
+                    } else if (j == 15) {
+                        button_state |= 0x2;
+                if (button_state != 0 && pico8_gamepads_mapping[i] === null) {
+                    // Allocate gamepad to first unmapped player slot
+                    var allocated_players = pico8_gamepads_mapping.filter(function(x) { return x != null; });
+                    var sorted_players = Array.from(allocated_players).sort();
+                    for (var desired = 0; desired < sorted_players.length; ++desired) {
+                        if (desired != sorted_players[desired]) {
+                            pico8_gamepads_mapping[i] = desired;
+                            break;
+                        }
+                    }
+                    pico8_gamepads_mapping[i] = sorted_players.length;
+                }
+                if (pico8_gamepads_mapping[i] !== null) {
+                    pico8_buttons[pico8_gamepads_mapping[i]] = button_state;
+                }

I'm not sure what license the code that PICO-8 spits out falls under, so I can't technically say this is MIT or CC0 licensed, since it is derivative of that, but so far as is possible I allow any use, re-use, redistribution, repackaging or reimagination of my changes. I expect that means that anyone who already owns PICO-8 can do with it whatever they can already do with exported HTML and I hope it means that Zep can take as little or as much of it as is useful.

P#86820 2021-01-27 00:28 ( Edited 2021-01-31 23:04)

This is great, thanks so much @weeble! I've merged your changes into both the upcoming 0.2.2 default template (along with a CC0 notice), and it's also currently live on the BBS player.

Mapping on activity is an excellent idea, and it's nice that cart authors don't really need to consider it. I did find myself sometimes instinctively using axes instead of buttons as a first action though; is there any reason to exclude axis 0,1 activity? Your keyboard registers as a single axis wheel -- perhaps that is going to produce unwanted axis signals?

I've included axis 0 & 1 activity in the current BBS player (at the end of p8convert*_gamepad_to_button_state), but will roll that back if it turns out to be a problem.

    any_button |= button_state;

One other change I made was to defer to the old layout code for gamepads with no layout mapping (gamepad.mapping != "standard"), as in that case any DPAD & menu button indexes will likely be off anyway. This way, at least there is still a menu button somewhere even if it is in the wrong place. I don't know how common unmapped gamepads are, but on my ubuntu dev machine no gamepad layout mappings ever show up under firefox or Chrome, even though most of them do under Windows on the same machine (XBox360, iBUFFALO snes-like, a nes-like, a generic elecom dual-stick).

var gamepad_states = gps.map(function (gp) {
    return (gp.mapping == "standard") ?
        p8_convert_standard_gamepad_to_button_state(gp, axis_threshold, button_threshold) :
        p8_convert_unmapped_gamepad_to_button_state(gp, axis_threshold, button_threshold);
P#87126 2021-02-02 18:41 ( Edited 2021-02-02 18:43)

I think in general a button press is more clearly an expression of user intent. I think there's a risk with wheels and paddles that they might exceed the deadzone simply sitting there unused. That said, it seems less likely to be an issue. (For my particular case I think it would be okay because you have to actually put the keyboard into a different mode before it actually sends gyroscope readings, but I haven't looked terribly hard at what it sends the rest of the time.)

It doesn't seem to be working for me on the BBS in Chrome on Windows, but I can't seem to get Chrome to show the code to debug it. I see this in the console:

VM9246:3209 Uncaught TypeError: Cannot read property 'mapping' of null
    at VM9246:3209
    at Array.map (<anonymous>)
    at p8_update_gamepads (VM9246:3208)
(anonymous) @ VM9246:3209
p8_update_gamepads @ VM9246:3208
requestAnimationFrame (async)
(anonymous) @ VM9246:3241

Sounds like that's probably in the code you quoted and somehow there's a null in the gps array? I am not sure why that would be.

P#87129 2021-02-02 19:29 ( Edited 2021-02-02 19:32)

I guess the gp in "gps.map(function (gp) .." can sometimes be null? Try it now -- I changed:

return (gp.mapping == "standard") ?


return (gp && gp.mapping == "standard") ?
P#87130 2021-02-02 19:43

Ah! I see. "Array indices for which there is no connected Gamepad with the corresponding index should return null." Chrome retains a slot for every gamepad that it has seen, even if they were gone before a particular window was opened. I guess that also means that the test for "gp && !gp.connected" is insufficient to unmap disconnected gamepads. Instead of "gp && !gp.connected" we should check for "!gp || !gp.connected" so as to reliably correctly unmap them. (I want to investigate this further. I did explicitly test disconnection. I wonder if Chrome does actually return a non-null controller object with .connected=false in that circumstance, despite what the spec says.)

P#87132 2021-02-02 19:53

Awesome it's working for me!

P#87133 2021-02-02 19:57

Eyyy, nice catch. Subtle!

Regardless of browser behaviour, it would never be wrong to unmap a null gamepad, right? Seems safe to me to move to "!gp || !gp.connected" even if it makes no apparent difference.

P#87134 2021-02-02 21:12 ( Edited 2021-02-04 08:24)

[Please log in to post a comment]