hecrj / coffee

An opinionated 2D game engine for Rust
https://docs.rs/coffee
MIT License
1.08k stars 55 forks source link

Key repeat behavior on macOS #127

Closed JeanMertz closed 4 years ago

JeanMertz commented 4 years ago

I have a small example that moves a circle across the screen based on WASD input.

From experimenting on macOS, it appears that Coffee respects macOS's key repeat preferences:

image

On the one hand, this is obviously a nice thing to have, as it gives the player control over the behaviour. On the other hand, this is unexpected behaviour for games.

Specifically, even with my preferences set to fast/short, there is still a noticeable delay from the "Delay Until Repeat" option. This means that if I press and hold A to move the circle to the left, it will move to the left once on press down, it will then stop moving for a moment even though A is still held down, and then start moving again as soon as the "repeat" kicks in, until I release the A key.

I've experimented with ggez as well, and that game engine behaves as "expected" on macOS (quotations here because the game behaviour differs from default operating system behaviour, but I believe ggez' behaviour is what people expect in a game).


Given that both engines use winit as a dependency, but ggez is still using 0.19 and Coffee uses 0.22, I suspect it's actually a change in behaviour between winit versions, and I would also suspect that change in behaviour is considered to be a fix, since regular macOS windows are expected to behave this way, but I don't think this is expected for games.


Alternatively, Coffee shouldn't treat key input as "text input". There's some investigation for Piston I found here: https://github.com/PistonDevelopers/piston/issues/1220#issuecomment-569185277.


Another reference is to a long-standing issue for Winit to discuss a better input model, see: https://github.com/rust-windowing/winit/issues/753


And finally, looking at ggez, it appears that they've implemented this behavior themselves by tracking is_key_repeated.

Although I'm not quite sure (yet) how that works, as that still doesn't explain why ggez doesn't have a delay between the first key and the repeat, whereas Coffee does.

JeanMertz commented 4 years ago

Here's an example output:

Window(Focused) at SystemTime { tv_sec: 1589730587, tv_nsec: 523265000 }
Keyboard(Input { state: Pressed, key_code: A }) at SystemTime { tv_sec: 1589730588, tv_nsec: 837026000 }
Keyboard(TextEntered { character: 'a' }) at SystemTime { tv_sec: 1589730588, tv_nsec: 837055000 }
Keyboard(Input { state: Pressed, key_code: A }) at SystemTime { tv_sec: 1589730589, tv_nsec: 79325000 }
Keyboard(TextEntered { character: 'a' }) at SystemTime { tv_sec: 1589730589, tv_nsec: 79345000 }
Keyboard(Input { state: Pressed, key_code: A }) at SystemTime { tv_sec: 1589730589, tv_nsec: 129870000 }
Keyboard(TextEntered { character: 'a' }) at SystemTime { tv_sec: 1589730589, tv_nsec: 129894000 }
Keyboard(Input { state: Pressed, key_code: A }) at SystemTime { tv_sec: 1589730589, tv_nsec: 163413000 }
Keyboard(TextEntered { character: 'a' }) at SystemTime { tv_sec: 1589730589, tv_nsec: 163438000 }

Using the following Input type:

struct Test;

impl Input for Test {
    fn update(&mut self, event: input::Event) {
        println!("{:?} at {:?}", event, SystemTime::now());
    }

    fn new() -> Self { Self }
    fn clear(&mut self) {}
}

You can see the first Input at sec 1589730588 and nsec 837026000, then, the next input 242 milliseconds later, and then about every 50 milliseconds.

JeanMertz commented 4 years ago

I guess one approach to solve this problem is to do custom tracking of keyboard state.

It would probably be something like:

But I wonder if this should "just" be handled by the engine instead, similar to what ggez is doing?

JeanMertz commented 4 years ago

Another problem I noticed with this (which would be solved by my above suggestion of keeping state around), is that holding down two keys simultaneously does not trigger the repeat event for one of those keys (which, my guess would be, is the one that was pressed first):

Keyboard(Input { state: Pressed, key_code: D })
Keyboard(Input { state: Pressed, key_code: S })
Keyboard(Input { state: Pressed, key_code: S })
Keyboard(Input { state: Pressed, key_code: S })
Keyboard(Input { state: Pressed, key_code: S })
Keyboard(Input { state: Released, key_code: S })
Keyboard(Input { state: Released, key_code: D })

I removed the TextEntered events here, but what can be observed is that I pressed D and S simultaneously, with D being pressed just a bit faster. Because S was pressed last, repeated events are triggered for that key, whereas only the Released event is triggered for D once I release that one, but no repeast Pressed events are triggered.

JeanMertz commented 4 years ago

Alright, I should have checked the Keyboard implementation more closely. It's already doing exactly what I described above, I just didn't copy it correctly in my implementation.

The reason why I forked the implementation though, is because I wanted to have access to the pressed_keys hash set.

I'll create a PR to see if you are okay exposing those details in the public API.