Page 1 of 1

Tip: Your input system should not be an opaque abstraction

Posted: Sat Mar 14, 2026 4:42 pm
by celes
Almost every engine out there has this sort of high level "input actions" API that they encourage using instead of you polling individual devices such as the keyword and gamepad.

In godot, they literally call it input actions, and every godot game uses it, so it's far from an unpopular idea!

But I did realize something. It's something that bit me in carrot survivors, and couldn't quite fix without a pile of hacks. This time, when encountering the same issue I decided to tackle it properly.

Consider this simple (heh) requirement:

"When using a joystick for movement, the character attacks in the direction of movement, unless they're using the right joystick, in that case the charcter attacks in the direction the right joystick is aiming. Oh, and if they're using a mouse forget all this, the direction of attack is the direction the mouse is pointing at from the player."

Now, you still want to go through some generic input layer for the most part, but you can see how the above requirement throws a wrench into the whole thing. The truth is, if you want your game's user experience to be good, you cannot really "abstract away" input devices. Sure, you can do this for the jump key, spacebar or some gamepad button, but anything more complex than that and you need to start delving into specifics.

The issue I'm dealing with rn is close to the one I described above. None of this is rocket science, it's just hard work and lots of iteration to figure out what people need to play in various ways (remember! the more input styles you support, the more accessible your game will be). But one particular aspect annoyed me: How do I know which input mode I'm in?

As an owner of a steam controller, I know how annoying it is when games use mose movement as an indicator that the player is using keyboard and mouse, so I'm not gonna do that. Furthermore, for Iris Blade, there's two keyboard input styles: One that uses the mouse, with Left and Right buttons to attack, and one that uses only keyboard, with the J and K keys for attacks instead[1]. Using keyboard input as a predictor that the player is playing with their mouse is thus not possible.

Having discarded mouse movement and keyboard input, the only reliable indicator that the player wants to attack with the mouse (and thus, we should use mouse position to direct their attack) is the mouse keys themselves... Again, no shit Sherlock, I could've been a bit less roundabout-y about this huh?

But circling back to the original input actions talk, I have an input action defined for the attack action, which is conveniently bound to Left mouse button, the J keyboard key and the X gamepad button all at once. I have a great API for this:

Code: Select all

if (Input.IsActionJustPressed(InputAction.Attack) {
   ...
}
Problem is, if my code has any sort of special logic that depends on the input device, as I described above, I need to undo that, and... check each input device manually? Now that's bad! I was planning on using the input action system to allow configurable key remappings and now I won't be able to... unless?

Actually the solution was simpler than I originally thought:

Code: Select all

if (Input.IsActionJustPressed(InputAction.Attack, out var actionBinding) {
   if (actionBinding.IsMouse()) {
       // mouse aim logic
   }
   else {
       // aim where the character is facing based on movement
   }
}
By adding this optional output parameter, I can check whether the action was pressed, but still get to peek at the internals of the event that produced the action to do custom logic. Now that was such a simple change that I'm very mad no other engine has it. :akko_nope:


[1] I'm planning on making this configurable, but that's a story for another day.

Re: Tip: Your input system should not be an opaque abstraction

Posted: Mon Mar 16, 2026 3:04 am
by palas
Oh the input, so important to nail down in any game, yet rarely talked about.
As an owner of a steam controller, I know how annoying it is when games use mouse movement as an indicator that the player is using keyboard and mouse, so I'm not gonna do that.
Very interesting, I've never considered this aspect. So it's not possible to differentiate between someone moving their cursor with a mouse vs steam controller?
Using keyboard input as a predictor that the player is playing with their mouse is thus not possible.
A bit confused by this. Even if the player uses the mouse to attack, they are still using the keyboard for movement. Could attack action be bound to both mouse and keyboard at the same time? That way, both styles are combined in one.
Problem is, if my code has any sort of special logic that depends on the input device, as I described above, I need to undo that, and... check each input device manually?
Ha! Before you showed your API, I've pictured something like:

Code: Select all

if Input.Pressed(InputAction.Attack) is action {
  match action { 
    ActionType::Mouse => {},
    ActionType::Controller => {}
  }
}
I'm very mad no other engine has it. :akko_nope:
Most likely another classic instance of "we do whatever is most simple for a beginner, any slightly more complicated use case is the user's problem".
But, surely every half competent engine has a way to tell whether a mouse or a controller is used, and process each one separately? This is such a fundamental feature after all.

Re: Tip: Your input system should not be an opaque abstraction

Posted: Mon Mar 16, 2026 9:53 am
by celes
palas wrote: Mon Mar 16, 2026 3:04 am Very interesting, I've never considered this aspect. So it's not possible to differentiate between someone moving their cursor with a mouse vs steam controller?
Well, for your average 3d third person games, camera controls with mouse are a lot more precise than joystick, so what many people do is set up the steam controller (or the steam deck) so that the right trackpad acts as a mouse, but the rest of the device acts as a controller. To the game, this looks as if the player is mostly playing with a controller but using the mouse to aim.

If the game supports this well it leads to a super pleasing experience, but in many games they implement very poor input device heuristics and for example using the mouse for aim prevents using the left joystick for movement until the mouse stops moving for a few seconds. You should never assume the players are gonna be playing the game exactly as you are!
palas wrote: Mon Mar 16, 2026 3:04 am A bit confused by this. Even if the player uses the mouse to attack, they are still using the keyboard for movement. Could attack action be bound to both mouse and keyboard at the same time? That way, both styles are combined in one.
Well the thing is, I have keyboard and keyboard&mouse as two separate input schemes, and I want to implement a feature (attack in the direction the mouse is pointing at) that should only work in one of the two cases. If you are using keyboard-only and the mouse happens to be left of your character, your attacks will always go to the left which would be bad :blobcatgiggle:
palas wrote: Mon Mar 16, 2026 3:04 am Ha! Before you showed your API, I've pictured something like:
Hehe, good one! Pretty much the same thing I came up with. Actually, I'm using that DUnion source generator you shared a while back for this, and the `.IsMouse()` in my code was auto-generated from the union, but I could've used at `.Match(...)` as well!

I've found the source generator really useful even if I've only used it in a few places. Input was one of them because it's very useful to describe input bindings. I still miss proper discriminated unions in C# but at least I get something that's really close and not half bad.
palas wrote: Mon Mar 16, 2026 3:04 am Most likely another classic instance of "we do whatever is most simple for a beginner, any slightly more complicated use case is the user's problem".
But, surely every half competent engine has a way to tell whether a mouse or a controller is used, and process each one separately? This is such a fundamental feature after all.
When I had this issue in Godot, I had to fall back to the non "input action" API, so checking the keyboard, mouse and gamepad devices directly without going through the abstraction layer. I'm not sure if it's any better now but all their "is_action_*" methods return a boolean, and judging by GDScript's limitations, it's unlikely they return anything else anytime soon ^^''