Any examples of action-based keybindings?


(Nolan) #1

I’ve seen various examples of axis-based keybindings. I assume these are used for emulating gamepads, or for situations where you’re moving an entity along an axis and need some sort of non-binary indicator of how far along the axis the control is.

I think what I want is an action. I want to perform some action when a key is pressed, optionally repeating it if the key is held.

Is there an example of configuring and using those types of bindings? I’ve looked through the examples but can’t immediately find one. Everything seems to use an axis, and while I don’t think that’s what I want, I might be wrong. :slight_smile:
Thanks!


(Khionu Sybiern) #2
"quit": [ [ Key(Ctrl), Key(Q) ], [ Key(Ctrl), Key(E) ] ]

So, basically, it’s a vec of key combos.


(Nolan) #3

Cool, and at that point, the handling code is more or less identical as it is for axis? Thanks!


(Khionu Sybiern) #4

There’s a way to get whether an action is triggered from the Input event channel.


(Théo Degioanni) #5

The recommended way to test for actions is the InputHandler. It will give you useful features to work with axis and actions.


(Nolan) #6

OK, think I’m making some progress. Here is my InputSystem:

struct InputSystem(Option<ReaderId<InputEvent<String>>>);

impl<'s> System<'s> for InputSystem {
    type SystemData = (
        WriteStorage<'s, Transform>,
        ReadStorage<'s, Player>,
        Read<'s, InputHandler<String, String>>,
        Read<'s, EventChannel<InputEvent<String>>>,
    );

    fn run(&mut self, (mut transforms, player, input, channel): Self::SystemData) {
        /*for (transform, _) in (&mut transforms, &player).join() { // Leaving this here to remind myself how to get the player
        }*/
        for event in channel.read(self.0.as_mut().unwrap()) {
        }
    }

    fn setup(&mut self, res: &mut amethyst::ecs::Resources) {
        Self::SystemData::setup(res);
        self.0 = Some(res.fetch_mut::<EventChannel<InputEvent<String>>>().register_reader());
    }
}

Then I have a Bundle that creates my game:

struct GameBundle;

impl<'a, 'b> bundle::SystemBundle<'a, 'b> for GameBundle {
    fn build(self, builder: &mut DispatcherBuilder<'a, 'b>) -> bundle::Result<()> {
        builder.add(AccessibilitySystem, "accessibility_system", &[]);
        builder.add(LevelSystem, "level_system", &["accessibility_system"]);
        builder.add(WrapSystem, "wrap_system", &[]);
        builder.add(InputSystem, "input_system", &[]);
        Ok(())
    }
}

This reports:

error[E0277]: the trait bound `for<'c> fn(std::option::Option<shrev::storage::ReaderId<amethyst_input::event::InputEvent<std::string::String>>>) -> InputSystem {InputSystem::{{construct
or}}}: shred::system::System<'c>` is not satisfied
   --> src/main.rs:151:17                                                                                                                                                                
    |                                                                                                                                                                                    
151 |         builder.add(InputSystem, "input_system", &[]);                                                                                                                             
    |                 ^^^ the trait `for<'c> shred::system::System<'c>` is not implemented for `fn(std::option::Option<shrev::storage::ReaderId<amethyst_input::event::InputEvent<std::st
ring::String>>>) -> InputSystem {InputSystem::{{constructor}}}`
                                                                                                                                                                                         

What am I getting wrong?

Presumably, once this is done, I can match action names in the iterator. Is this the recommended approach?


(Khionu Sybiern) #7

You only need the InputHandler<String, String>. It’s a wrapper for an EventChannel.


(Nolan) #8

Thanks, I’m still confused. So now I have:

struct InputSystem(Option<ReaderId<InputHandler<String, String>>>);

impl<'s> System<'s> for InputSystem {
    type SystemData = (
        WriteStorage<'s, Transform>,
        ReadStorage<'s, Player>,
        Read<'s, InputHandler<String, String>>,
        Read<'s, InputHandler<String, String>>,
    );

    fn run(&mut self, (mut transforms, player, input, channel): Self::SystemData) {
        /*for (transform, _) in (&mut transforms, &player).join() {
        }*/
        for event in channel.read(self.0.as_mut().unwrap()) {
        }
    }

    fn setup(&mut self, res: &mut amethyst::ecs::Resources) {
        Self::SystemData::setup(res);
        self.0 = Some(res.fetch_mut::<InputHandler<String, String>>().register_reader());
    }
}

struct GameBundle;

impl<'a, 'b> bundle::SystemBundle<'a, 'b> for GameBundle {
    fn build(self, builder: &mut DispatcherBuilder<'a, 'b>) -> bundle::Result<()> {
        builder.add(AccessibilitySystem, "accessibility_system", &[]);
        builder.add(LevelSystem, "level_system", &["accessibility_system"]);
        builder.add(WrapSystem, "wrap_system", &[]);
        builder.add(InputSystem, "input_system", &[]);
        Ok(())
    }
}

error[E0599]: no method named `read` found for type `shred::res::data::Read<'_, amethyst_input::input_handler::InputHandler<std::string::String, std::string::String>>` in the current sc
ope
   --> src/main.rs:134:30                                                                                                                                                                
    |                                                                                                                                                                                    
134 |         for event in channel.read(self.0.as_mut().unwrap()) {                                                                                                                      
    |                              ^^^^                                                                                                                                                  
                                                                                                                                                                                         
error[E0599]: no method named `register_reader` found for type `shred::res::FetchMut<'_, amethyst_input::input_handler::InputHandler<std::string::String, std::string::String>>` in the c
urrent scope
   --> src/main.rs:140:71                                                                                                                                                                
    |                                                                                                                                                                                    
140 |         self.0 = Some(res.fetch_mut::<InputHandler<String, String>>().register_reader());                                                                                          
    |                                                                       ^^^^^^^^^^^^^^^                                                                                              
                                                                                                                                                                                         
error[E0277]: the trait bound `for<'c> fn(std::option::Option<shrev::storage::ReaderId<amethyst_input::input_handler::InputHandler<std::string::String, std::string::String>>>) -> InputS
ystem {InputSystem::{{constructor}}}: shred::system::System<'c>` is not satisfied
   --> src/main.rs:151:17                                                                                                                                                                
    |                                                                                                                                                                                    
151 |         builder.add(InputSystem, "input_system", &[]);                                                                                                                             
    |                 ^^^ the trait `for<'c> shred::system::System<'c>` is not implemented for `fn(std::option::Option<shrev::storage::ReaderId<amethyst_input::input_handler::InputHandl
er<std::string::String, std::string::String>>>) -> InputSystem {InputSystem::{{constructor}}}`
                                                                                                                                                                                         

Looking at InputHandler, I see how I can test if an action is down. Instead I want to fire once when it is pressed, which is why I assume I need an event.

Thanks.


(Khionu Sybiern) #9

Now you have two InputHandler’s.

For detecting the initial fire, just track the state. Don’t trigger the action on your end unless the state wasn’t already set to triggered.

  • Both State and InputHandler say the action isn’t triggered => return
  • State isn’t triggered and InputHandler is => trigger
  • State and InputHandler are triggered => post-trigger, still held
  • State is triggered but InputHandler is => reset trigger state

(Nolan) #10

Huh, interesting. I can think of lots of actions that I’d only want to trigger once, either on initial press or release. Lots of frameworks have the option of firing an event only on keydown or keyup, and they all push that state tracking to the higher-level handler that only fires the keyup/keydown event. This approach requires me to track local state for each and every instance of one of these actions, which seems very high-ceremony for such a common pattern. In reading the book section about event channels, I assumed I could grab a channel in my input-related systems, then intercept the action-related input events there. Are there any advantages to tracking that state myself that I’m missing?

Thanks.


(Khionu Sybiern) #11

Flexibility is it, really. But the thing is, EventChannel and InputHandler are not event-driven. ECS is a data-driven development paradigm, so things are done a bit differently.


(Théo Degioanni) #12

But it would be nice if the InputHandler had a higher-level event channel. Maybe we could look into that.


(Nolan) #13

Thanks, that would be nice. For now I’m tracking the state of my actions, but it feels a bit kludgy. Even if an ECS is technically data-oriented, Amethyst does have an event system, and it makes sense to me that an ECS might need to use events to reduce coupling between systems.

What is the channel in InputHandler sending to? I’m still not clear why I can’t just grab it like any other resource. Is it just sending to itself? If so, why would I want a channel for that? Not trying to be snarky, I just don’t understand why an EventChannel is needed at all here if the intent is not to expose these events to other systems. I’m also indirectly trying to understand when I might want an EventChannel in my own systems. Input seemed like the ideal use case–let the InputSystem focus on indicating when an action is pressed and released, then put code in my own system that reacts to those higher-level concepts, and doesn’t care whether the action comes from an InputHandler, some sort of headless testing system pretending to be a player, etc. Instead of using a generic event abstraction, I’m directly calling methods on InputSystem and tracking state locally. If I was designing something similar, I’d use events, but that they aren’t used here makes me wonder if I’m doing it wrong.

Thanks.


(Théo Degioanni) #14

There is no EventChannel needed here. I think there is a misunderstanding.
InputHandler represents the state of the input, that’s it.
Alternatively, you can get raw input events using an Event channel, but you won’t get all the features of the InputHandler (namely actions, axis, etc…).
But I reckon the InputHandler could also provide its information in the shape of event. It’s just something that is not implemented yet.


(Nolan) #15

Gotcha. What are the various ActionPressed/ActionReleased events for, then? I assumed they were fired once when an action began being triggered or was released.


(Théo Degioanni) #16

Oh. I had never seen that.

Okay then yes, scrap what I said earlier.
You don’t need to get the input handler in your system, just register the InputBundle.
Then, you can use the event channel EventChannel<InputEvent<String>> and listen to the ActionPressed events.

The mistake in your previous code is that you registered InputSystem instead of InputSystem(None).


(Nolan) #17

Yep, there we go, now things work as I’d expect. :slight_smile: Thanks!

I now have a few commands implemented. My only confusion is how to implement quitting the game from an action. The book examples I’ve seen seem to do it from the state. Should I be handling quit/pause from the state itself, and game actions from within the system?

Thanks for all the help so far.


(Théo Degioanni) #18

Yes doing it from the state could be a good option.
Input handling from systems is usually good to be done locally. If you have global kinds of inputs, you do it from states, and then if you have for example a player movement system, you handle the player movement inputs there.