-
Notifications
You must be signed in to change notification settings - Fork 48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Should fire events instead of using passive model #4
Comments
Note that using events exlusively without control over the event loop has severe data handling drawbacks. If events are introduced, they should be issued at a controlled point in time (when polling) which clears the entire event queue up to that point, and they should not substitute "passive" (it's not really passive, it's active polling) querying of values, particularly not axis values. |
This has been discussed on public-webapps in the past: and it's on my list of features for a next version of the spec: Firefox includes non-standard events that you can enable by flipping a pref (dom.gamepad.non_standard_events.enabled), you'll get GamepadButton{Down,Up} and GamepadAxisMove events. I think spec'ing the button events would be fairly simple and uncontroversial and solves the most important use case--missing button presses that happen between polls. Events for axis values (or analog button values) are harder to get right. They're likely to be spammy and not incredibly useful--most consumers just want to know the current value of an axis. It would be nice to have some sort of event for this so that consumers aren't forced to constantly poll. Maybe we could write some spec text such that we'd fire a "datachanged" event whenever anything changes, but the browser can roll up a series of them into a single event, like if you had:
the browser could compress this to:
|
When thinking about what sort of event api would be useful, I think it is helpful to divide the use cases into categories based on how they expect input. Reactive: input is a cue for something to start happening. Examples: video player, slideshow viewer In this category we mostly get web apps that want to be able to interact or navigate occasionally with a gamepad. These apps want events because the current api forces them to poll continuously even though they rarely process input. For the majority of cases, reactive apps only want to know about:
Tracking slight changes in joystick inputs does not make sense in events, because it produces many more events than a reactive app can reasonably respond to. An additional catch-all event GamepadChanged could be used if necessary but it might need both rate limiting and noise tolerance to be useful. Animated: input is processed continuously and at regular intervals. Examples: games, simulations An event driven api could help these types of applications catch missed button presses and improve timing, but the reality is that events are a poor fit for these types of apps. @pyalot pointed this problem out earlier.
These classes of applications aren't idle, and therefore don't need an event to fire to wake them up. The need to listen for and store events to be used for the next game/simulation loop is a nuisance. What these apps ideally want is a way to get all available information about the gamepad between the current game/simulation loop and the previous one. Specifically that means providing a function that can retrieve for snapshots of gamepad state or gamepad state changes over a specified time interval. An example api change that can accomplish this is to add an optional parameter of type DOMHighResTimeStamp on getGamepads, which will cause each gamepad to instead be returned as a sequence of all gamepad states after the provided time instead of a single gamepad. Providing all gamepad state updates has important benefits:
|
A footnote on events for axes. A common failure mode of this kind of thing is when you have say an X and Y axis and you're plotting say a position on screen from them. So the X axis event arrives first, you draw a line there, then the Y axis event arrives, and you draw a line there. Now you've got a staircase instead of a continous line. Although getting the whole state (as is presently the case) avoids that kind of issue (and it is among the reasons that shouldn't be dismantled), there is also sometimes a case to be made for having the ability to collect the events for axes and then aggregate them yourself (for instance instead of just using the last known, you can do a weighted average). For very twitchy games, this can provide players with a more satisfactory response. |
This problem is easily avoidable if you can get multiple events at once. In your example you get an X axis but have no way of checking if the Y axis event has already fired and is waiting to be processed.
Take the simple case that a controller has a directional pad, and you want to perform different actions for up, diagonal (up + left) , and right. When polling the complete state, just left or just right can (and frequently will) show up in a poll before both show up together unless the are pressed together at exactly the same time. You can't avoid doing the left or right action before diagonal unless you buffer inputs for some arbitrary interval before performing the left or right actions. There are a lot of similar issues like this that need to be tackled by the application and are unlikely to be solved directly by the api.
I am very much in favor of games aggregating events or state provided from the API, and believe this is the ideal way to handle input. The question is how to collect the aggregated events. 1. Poll Spamming (the current way)
note: Timing is much worse than accessing hardware directly in Chrome due to a counter being provided instead of timestamps and internally buffering with a long polling interval. (In my test case 125hz hardware is polled and buffered at 60hz) 2. Event Listeners (same as mouse and keyboard)
3. Query State/Events (my suggested method)
A minimal API to satisfy all possible use cases:
|
Could a model like https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/getCoalescedEvents help here? |
I think there still need to be events to be able to work with user gesture requirements. For example, you still cannot start any media playback with a gamepad on Android, because you never get an input event (crbug.com/454849). Something like getCoalescedEvents doesn't seem to change that. |
There'd still be a root event to get the coalesced events from, and that would be a gesture event. |
Oh, right. Yeah, it makes sense to use a similar mechanism to pointer events for better-than-rAF resolution. |
A similar option is a method where you declare which buttons & axis you're interested in, and it returns an |
Thinking a little more about this, it could just be: gamepad.addEventListener('buttondown', listener);
gamepad.addEventListener('buttonup', listener);
// Event interface:
event.index; // index of button changed
gamepad.addEventListener('axischange', listener);
// Event interface:
event.index; // index of axis changed
const events = event.getCoalescedEvents();
events[0].index; // index of axis changed
events[0].value; // value
gamepad.addEventListener('buttonvaluechange', listener);
// Event interface:
event.index; // index of button changed
const events = event.getCoalescedEvents();
events[0].index; // index of button changed
events[0].value; // value The timing of these events should follow mouse events in Firefox & Chrome, as in:
|
I guess |
On Tue, Jan 2, 2018 at 12:36 PM, Jake Archibald ***@***.***> wrote:
- If an axis or button changes value, an axischange/buttonvaluechange
event will fire in the render steps of the event loop.
Please be aware that if you opportunistically fire events for changed
axes, and you as a programmer react to those events immediately, you will
experience unintended behavior, such as drawing a staircase instead of a
diagonal line (because the X event fires first, at which time the Y value
isn't available yet, and so forth).
|
If you wait until rAF to draw any changes, and you received both axischange events by then, it should correctly handle simultaneous X/Y changes. I think a more interesting question is how to handle that with getCoalescedEvents(). Will the coalesced events for different axes be guaranteed to have the same number of updates at the same time? Perhaps it could combine axes and fire events for them simultaneously to avoid this? I don't know if the underlying APIs tell you about axis associations or not. |
@pyalot maybe I'm not explaining my proposal properly. My proposal:
Whereas your comment suggests I'm proposing firing these events outside of the render steps, and perhaps even firing one event per axis – that isn't what I'm proposing. Take You only need the full 20 event objects in very particular cases, such as a painting application, where you want the full data for all mouse points despite any in-page jank. https://event-timing.glitch.me/ might be useful – it shows how this works with mouse events. I'm proposing |
I'm proposing this event should be fired in the render steps, so it's already in the same phase of the event loop as rAF, and debounced in the same way.
In my proposal the objects look like this: gamepad.addEventListener('axischange', listener);
// Event interface:
event.index; // index of axis changed
const events = event.getCoalescedEvents();
events[0].index; // index of axis changed
events[0].value; // value Each I think needing gamepad.addEventListener('axischange', () => {
doSomethingWithThese(gamepad.axes[0], gamepad.axes[1]);
}); |
On Wed, Jan 3, 2018 at 2:48 PM, Jake Archibald ***@***.***> wrote:
gamepad.addEventListener('axischange', listener);
// Event interface:event.index; // index of axis changedconst events = event.getCoalescedEvents();
events[0].index; // index of axis changed
events[0].value; // value
I'm not sure why you'd call it Coalesced events. It's just fetching the
event queue, the same way "getEvents" is implemented pretty much anywhere
else (USB, SDL, pyglet, xinput, directinput, etc.). You get a list of
events that occured since last time you called that function. It gives you
sufficient information to know what kind of event it was, and what values
it transports.
|
It's also usually the case that these "getEvents" functions support some
rudimentary filters for event types, so you don't have to dig trough all
events to get to the ones that're of interest to you. It's a convenience
function.
…On Thu, Jan 4, 2018 at 11:18 AM, Florian Bösch ***@***.***> wrote:
On Wed, Jan 3, 2018 at 2:48 PM, Jake Archibald ***@***.***>
wrote:
> gamepad.addEventListener('axischange', listener);
>
> // Event interface:event.index; // index of axis changedconst events = event.getCoalescedEvents();
> events[0].index; // index of axis changed
> events[0].value; // value
>
>
I'm not sure why you'd call it Coalesced events. It's just fetching the
event queue, the same way "getEvents" is implemented pretty much anywhere
else (USB, SDL, pyglet, xinput, directinput, etc.). You get a list of
events that occured since last time you called that function. It gives you
sufficient information to know what kind of event it was, and what values
it transports.
|
I'm not calling it that. It already exists for pointer events https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/getCoalescedEvents.
Not really, you get a list of events that were coalesced into that render step event. If you don't call |
On Thu, Jan 4, 2018 at 11:33 AM, Jake Archibald ***@***.***> wrote:
Not really, you get a list of events that were coalesced into that render
step event. If you don't call getCoalescedEvents, those additional events
are gone. If it behaved as you describe, it'd be a massive memory leak –
you'd quickly have millions of events buffered in memory that couldn't be
collected.
That's why these other eventing functions usually let you specify (or have
an implicit) event buffer size.
|
Right. Or there's the pointer events model which doesn't need that, and fits in with the event model already present on the web. |
This sounds promising! Gamepad axis events are pretty similar to pointer move events: they're an analog input that can be highly variable (even if the user isn't touching an analog stick, it's likely to be changing just due to jitter). I suspect that for most gamepad usecases the API that users want is "what state on the gamepad has changed since the last time I looked?" For button presses it's important to have a record of every pressed/released event so that you don't miss quick button presses, but for axes you probably just want the current value, but you probably want to know if it has changed. |
@luser that's exactly what I was aiming for! |
One thing that might make this slightly awkward for the common use case in games is that AIUI most game loops have a process input / update state phase and a render phase. Currently most JS code I've seen just mashes those both into an rAF callback, handling input and updating state first, then rendering. Presumably the slightly better option would be "run a setInterval callback at a fixed rate to do input handling/state updates, do rendering in rAF", but I don't think that matters much for the purposes of this discussion. The Gamepad API currently provides Some notes on native platform APIs: |
Doesn't this proposal make this a bit more formal? You'd use the events as a place to update state, then queue a rAF for the render phase. |
I'd like to quote what I wrote in #15 since it sums up my thoughts on an event based model:
Since there hasn't been any discussion on this in a while, it'd be nice to get some talk going again as there doesn't seem to be a consensus so far. |
thanks @mrmcpowned - our goal right now is to do some cleanup of the underlying model (see #136) of the spec and improve the privacy and security (#120 + #119). In particular, #136 should give us a better foundation on which to specify a better eventing model. However, I don't expect us to focus on the events until we land the PRs I mentioned. |
I have a use case that hasn't been precisely addressed yet: network gaming. When my game detects a change in input state, I want to send that off as soon as possible. I don't want to wait up to another 20ms (or whatever) for the game loop to pick it up and send it off. I currently handle keyboard input outside the game loop and am wondering what to do about gamepads. Latency is already bad enough across the web. Why add to that? Even more important than total latency is variation in latency. Any polling solution (buffered or otherwise) adds to that variation. I believe a proper gamepad solution would have the buttons performing just as well as keyboard keys (and/or mouse buttons) in regards to responsiveness, and can be implemented with the same coding patterns (events and/or polling). Do that and you can stop second-guessing use cases. Thus, while buffering events could solve the button-press-duration-sensitivity problem, it does not solve the responsiveness problem overall. True, most use cases will not need responsiveness tighter than a typical game loop interval, but that is no reason to ignore it either. Personally, I don't see a good use case for analog-change events* and I've built a variety of games. Yet, to be fair, I haven't looked at mouse-versus-gesture cases like others have, so I can't comment much on those details. If a mouse fires positional events, then it makes sense a joystick should too... except for a possibly-important detail: potentiometers (joysticks) versus spinners (mice and trackballs). Sure, the output may look similar in some floating-point-vector ways, but the source and meaning of the data is not similar. Pots have extents, spinners do not. (Ignoring touch pads/screens for now.) Pots are notoriously much more twitchy producing spammy data (that should probably be smoothed as well). Pots also often have centering springs attached which, well, just makes them more weird and noisy. Thus, perhaps, if looking for an excuse to not tie joystick positioning to events, the origin of data from pots would be a good one. * This does not include the case of using analog controls as digital controls, such as faking a button press when an analog value hits 0.5. Such an action is fuzzy to begin with and the use case can tolerate additional slop from the poll timing. Again, personally, if needing an event system for pot changes, I wouldn't be surprised if I needed to do that myself in a loop with a 1ms interval or whatever. Then, I could also cache it, smooth it, or whatever else it needed. |
Pointer events now has a |
Related to #56 |
The new explainer sounds really interesting and was well received when I shared it. Are there plans regarding |
Thanks for sharing the explainer! Yes, I think event coalescing will be necessary and we are currently figuring out how it should work. We want to be able to support use cases where every input frame must be received with minimal latency as well as use cases that can benefit from events but don't want the overhead of handling every input frame. Please take a look at the proposals in this doc. We've also opened a TAG design review and comments are welcome on the pull request. Right now I'm leaning toward Proposal 3 which would keep buttondown/buttonup (with no coalescing) but remove axischange/buttonchange in favor of a unified change event that encapsulates all the changes within an input frame and provides a getCoalescedEvents method to retrieve events for skipped input frames. I think these events, along with the current polling interface, do a good job of covering all the use cases called out in this thread. |
(This is based on an email sent to public-webapps)
The Gamepad API is the only web platform input API that relies on passively polling the input state. Gamepads should fire events like "gamepadbuttondown", "gamepadbuttonup" and "gamepadaxismove". This would have the following benefits:
The text was updated successfully, but these errors were encountered: