[WIP] Input mapping

Hello! While learning some design patterns that i will need to create a animation state machine, i found another thing that may be easier for me to create and understand better what i am doing. I am creating an Input mapping inspired by the Unreal one (https://docs.unrealengine.com/en-US/InteractiveExperiences/Input/index.html)

I don’t think this is a correct way to do that and i will recreate it in a better way when i learn more.

My plan is to implement key binding, toggle, scale (for axis mapping). Each stuff will be done in its specific class following the SOLID principles (https://medium.com/backticks-tildes/the-s-o-l-i-d-principles-in-pictures-b34ce2f1e898).

Feel free to test it and make suggestions! And don’t laugh :slight_smile:

3 Likes

@knowledgenude, That’s great. I haven’t tried your examples yet though, but I highly recommend using GitHub to share updates like this. That way, we do not have to go through the process of download-extract-save etc.

Best,
QC

1 Like

Thanks for the suggestion! Done https://github.com/knowledgenude/inputmap

3 Likes

So this is the layout of the input map:

The class InputAction is responsible to handle the action buttons like: activate elevator, open door and open menu. In this class there is functions for started() and released(). The down() method is only allowed for modifier keys. Currently any key can be a modifier (do you think it should be limited only for Control, Alt and Shift like in Unreal?).

The class InputAxis (i think i need to find a better name for this one) is responsible to handle continuous pressed keys, commonly used for movement. Each InputAxis should have a scale attached to it and only its scale is returned. So instead of something like if ("InputAxisForMove".down()) doMove(); it would be doMove() {move.y *= "InputAxisForMove"};

Also here in this class, as it have a scale factor, it need some kind of groups. Example:

A group of the “WASD” keys may have its scale determined by the current state of a boolean or a scale property (i.e. for crouch). So if the crouch boolean is active, the movement scale is reduced. The possibility to toggle buttons (not only down()) need to be present to do things like toggle run with “Shift”, etc. but also the toggle i am considering a continuous state, so it fits well in this class.

I’ve updated the system at the Github and the InputAction class is completed (at least its functionality). So i will start to create the InputAxis class. If anyone has some suggestion to improve the performance and the maintaining, please let me know.

1 Like

That’s pretty awesome and useful! Maybe you want to implement it in Armory (or Iron, better ask Lubos) when you are happy with it?

Modifiers usually are only those special keys, I would stick with that because of convention. Maybe you could change the API a bit to resemble more what Unreal does in the link you’ve shared. It’s just an idea:

// Maybe give actions an ID and store them in a static map?
// Then there could be some static method like InputAction.get("action1");
// This makes it easier for later usage in logic nodes
var action1 = new InputAction("action1");

// This input does require pressing the "alt" key, this could be an optional
// second parameter
action1.addKeyboardInput("w", "alt");

// No modifier, with this API it would be possible to set inputs with and without
// modifiers for the same action
action1.addKeyboardInput("up"); 

// You could still add different input types to one action
action1.addMouseInput("left"); 

Each added input would be one way of triggering the action, like a logical OR, while the parameters for each indiviual input mapping would be treated as a logical AND.

Those are only ideas, your system is pretty good already! It’s just meant as inspiration :slight_smile:

2 Likes

Of course!

I will try to change it to the layout you suggested because it looks more practice, thanks for the suggestion!

Please review the new layout here https://github.com/knowledgenude/test-new-inputmap

I am not confident about this extra lines, but they allow to have more than 1 modifier in the InputAction and filter between inputs, so if you press a bind key in the gamepad it will not work with the keyboard and vice-versa:

		var input = null;

		for (k in keyboardInputs) {
			if (keyboard.started(k)) {
				input = "keyboard";
				break;
			}
		}

		switch(input) {
			case "keyboard": {
				if (keyboardModifiers.length == 0) return true;

				for (k in keyboardModifiers) {
					if (!keyboard.down(k)) return false;
				}
			}
		}

		input == null ? return false : return true;
	}
2 Likes

Nice, looks good!

What confuses me a little bit is that both the input and modifier attributes are optional:

public function addKeyboardInput(?input: String, ?modifier: String) {

This allows you to call the function without any parameters which seems a bit weird to me. I would propose this (you’re right that multiple modifiers should be allowed):

public function addKeyboardInput(input: String, ?modifiers: Array<String>) {

Also, modifiers and individual inputs should be bound together, right now (didn’t test it) it seems that if any modifier of one of the “input configurations/entries” isn’t pressed the entire action isn’t executed?

So what you should do is to use a common data structure for those input configurations (or how you call them). Maybe there could be a very small class holding this data (or a typedef which would be slower on static targets), let’s call it KeyboardActionEntry for example:

class KeyboardActionEntry {
	// (default, null) -> make read-only
	public var key(default, null): String;
	public var modifiers(default, null): Array<String>;

	// This is later called in addKeyboardInput(), which creates an empty
	// modifier array if no modifiers are given. This later allows you to
	// iterate through all modifiers without checking if the array is null.
	public function new(key: String, modifiers: Array<String>) {
		this.key = key;
		this.modifiers = modifiers;
	}
}

There might be better ways than using a class, this is what came into my mind first.

In the InputAction class:

public function addKeyboardInput(input: String, ?modifiers: Array<String>) {
	// Create an empty modifier array if not passed
	var mods = modifiers == null ? new Array<String>() : modifiers;

	// keyboardInputs now needs to be declared as Array<KeyboardActionEntry>
	keyboardInputs.push(new KeyboardActionEntry(input, modifiers);
}

Later you then iterate through all entries and check if one of them is actually started/down/released.

Again, just an idea :slight_smile: Depending on whether individual entries/configurations should be edited later it could make more sense to use haxe.ds.Vector instead of arrays for the modifiers, vectors have a fixed size and are at least equally fast as arrays and often faster (depends on the target).

Just play around a bit with all those options, probably you come to better conclusions/solutions.

2 Likes

It is to allow modifiers to be added separately instead of require to add a new input for each new modifier, but i don’t know exactly if this becomes useful in practice yet. It is mainly because i don’t know how azerty keyboards handle inputs like ctrl, shift, alt, so may be useful to set this individually depending on querty or azerty keyboard.

Yes, when you define two modifiers (.i.e. “alt” and “ctrl”) then you will need all modifiers pressed at the same time. It is good for key combinations, but is impossible right now to set a combination like: “alt” works and “gamepad 1 + gamepad2” also works for the same action. I think i should not try to handle the modifiers as the desktop do for now, because it is a bit different. In the desktop for example if you want to execute a shortcut, if you press another modifier key the shortcut will not work, this kind of verification is unnecessary for games and may introduce more delay.

I think is valid to create another class or function just to handle the keys combinations (the modifiers) because they may become complex. Gamepads have a limited number of buttons, they require more key combinations then a keyboard. I will try to change then to vectors, thanks again for the suggestion!

Maybe it will be possible to the InputAction and the InputAxis share this third class for the combinations, i need to see if it will work without cause confusion.

The problem of the current state of the started() and released() functions checking for the input type is its size after adding more verifications like for Koui, Canvas, Virtual Button, etc. i think i will need to check if there is inputs for each input type but i will wait to see how is the performance.

3 Likes

Ok, I probably misunderstood you then. So you want that modifiers are defined per-action, not per-input? Also, above proposals don’t take pressing multiple keys into account, but that can be easily implemented by also using an array/vector for that, just like for modifiers.

I’m not sure if I understand you correctly, but that’s probably something for subclassing/strategies again. You could add methods to the “entry” classes which get checked instead of doing that in the main class so that your system is extendable.

I’m looking forward about your implementation of the InputAxis thing, looks a bit more complicated, I have no idea how I would implement this. Often it’s the best (as you already do) to first think about the API and then start with implementation based on that.

2 Likes

Indeed the InputAxis may be a big challenge but i hope not after i complete this puzzle :slight_smile:

I am trying to create an InputEntry but each way have one limitation. Using Vector i can’t combine string for the key and array for its modifiers. So i switched for this way:

class InputEntry {
	var input: Dynamic = {};

	public function new(type: String, key: String, ?modifiers: Array<String>) {
		this.input.type = type; // "keyboard"
		this.input.key = key; // "p"
		this.input.modifiers = modifiers; // may be like ["shift"] alone or ["ctrl", "alt"] together
		this.input.pressed = false; // execute the action if this or others inputs are true
	}
}

This can be done also as a function but i can’t remove then from the list (they don’t have an ID). Do you think i should go in this way and create a string map for it?

class InputAction {
	public function new() {}

	var keyboardInputs: Array<InputEntry> = [];

	public function addKeyboardInput(key: String, ?modifiers: Array<String>) {
		var mod = modifiers == null ? new Array<String>() : modifiers; 
		keyboardInputs.push(new InputEntry("keyboard", key, modifiers));
	}
}

The input must contain information about its type to don’t fail in the future verifications or the trouble will grow up when add other input types.

1 Like

I think it is almost there, just lack a way to remove inputs. See the new update :slight_smile:

2 Likes

Pretty neat, now I understand why you needed to use dynamic types here. It could be helpful to later implement an interface for Iron’s input classes (or at least use VirtualInput as it is the super class of all input classes, but this would require casting later) to have at least a little bit of type safety. Otherwise you could pass just everything as the first parameter of addInput().

Another idea would be to use different ActionEntry classes for each input type and the user would have to initialize them, @:structInit could help here as well. But that would have to be communicated in the API and wiki and it makes the API more complicated again.

Again, nice work! :slight_smile:

3 Likes

So using the strategies would be:

class InputActionEntry extends VirtualInput
class KeyboardInputEntry exenteds InputActionEntry
class GamepadInputEntry extends InputActionEntry

And then, i create instances of InputActionEntry in InputAction class:

var inputs: Array<InputActionEntry>;

The advantage of extending VirtualInput would be to use the started() and released() method from there for the key and the modifiers array, but this looks complicated. If you confirm this layout i would try to do that, but i have a few questions:

Is possible to have a function addKeyboardInput() in the KeyboardEntry class that can be called from the instance of InputAction ?

Example:

var action1 = new InputAction();
action1.addKeyboardInput(key, modifiers);

The KeyboardEntry instance would be a cast of InputActionEntry stored in the inputs of the InputAction instance. Note that the instances stored in the InputActionEntry instance may or not have the function addKeyboardInput() (because can be a mouse input, gamepad input, etc). This looks dirty to me but would improve the user API a lot.

Ps: the InputActionEntry should not have any information about other types except InputActionEntry.

3 Likes

Extending VirtualInput is a good idea, however VirtualInput doesn’t define the functions you mentioned, they are instead only defined in the subclasses (which might be bad design or there might be a good reason for that, I don’t know). Maybe open a PR for that? As far as I know every input type has these methods and they all have one string parameter so that should work.

Not without manual casting I’m afraid. As you already wrote, it could happen that the instance stored in action1 doesn’t have this attribute.

What you could do (however I’m not sure how intuitive that API would be) is to have pre-defined functions like addKeyboardInput() for the input types you already know and then add a addCustomInput() function that takes some input entry object (this way people can write their own input adapter if they want to, that’s what you wanted right?). The pre-defined methods then could be inlined to call addCustomInput().

3 Likes

I can’t confirm that because i don’t know about how the Input class work, some of them don’t have the same functions, like Pen, GamepadStrick, etc. what i would like is each class separated in a hx file first because scrolling that thing is tedious.

Yes but i also wanted to not need for import the iron.system.Input and put it as a parameter in addInput(). Another solution would be to use resolveClass() and specify the input class name (e.g. “Gamepad”, “Keyboard”), but the path will need to be predefined. I will try to find another way to allow both cases.

– Just ideas that can be discussed later below, no need to waste time reading it now

I also think this names are too much repetitive InputAction, InputActionEntry, KeyboardInputEntry… So what about InputEntry to ControlType and threat the Keyboard together with the Mouse? They can share the same modifiers and that is how games handles it: https://upload-os-bbs.mihoyo.com/upload/2020/10/02/8359669/5d4c6ad3f68e5f663fc801734766dbfd_1681981114234536387.png?x-oss-process=image/resize,s_500/quality,q_80/auto-orient,0/interlace,1/format,png

Another thing i need to think is how to make the InputAction and InputAxis share the same input entry but the InputAxis have more variables that i would not like to see in InputAction

1 Like

@timodriaan , i am trying to understand what is going on in the input related classes but it looks too over bloated and i don’t know from where to start. I can’t see any reference of keyboard, mouse, etc. in the VirtualInput class and vice-versa. Please, if you could give me some light of where to start to clear this classes, or even tell me if there is some reason to don’t touch it (as you said it may not be a bad design for no purpose). For the little that i know, the inputs does not have any reason to extend the VirtualInput class, they have all separated variables and functions and there is no cast happening in the Input class, so i need someone more experienced to clarify this for me:

static var keyboard: Keyboard = null;

public static function getKeyboard(): Keyboard {
	if (!registered) register();
	if (keyboard == null) keyboard = new Keyboard();
	return keyboard;
}

Thanks in advance!

To me it looks like VirtualInput is an adapter to hook other input systems in. I don’t know why they extend virtual input but I think that downVirtual() and upVirtual() can be replaced with down(), started() and released() which then can be overriden, the old functions however should be kept and get @: deprecated for compatibility. But better ask Lubos, Iron is used in Armorpaint and it could break something in Armory as well. But Sensor for example doesn’t implement those methods at all…

Also, the Input class is static only, it probably would be cleaner and more intuitive to make all the input types including VirtualInput a subclass of Input.

The code you pasted just gives you a keyboard object back, it’s a singleton (google that), so only one indiviual object exists when using this code (if there already is a keyboard object, that is returned and now new one is created).

Okay, thanks!

About the current Input class i don’t like it because the mouse class is separated from the keyboard class and have the same name for different keys. Mouse left button is recognized as “left” and keyboard left arrow is also recognized as “left”. I think the right would be “lmb”, “rmb” and share the same class and keys array and maybe even have a list for modifiers already implemented. For the InputMap I think the best way to go is to provide a way to get the started, down and released states directly from the kha.input.KeyCode and then deprecate the armory Input class when it is ready. If you have any suggestion on this feel free to tell me and if i get motivated i can try to make this in the future.

For now i will focus on learning the designs, because i want to create the state machine (but i am really enjoying to create the input map). I’ve changed somethings as you said and created a way to add custom inputs, please see the changes and tell me what is wrong when you have time! It is important for me to know the right way :slight_smile:

Combining the mouse and keyboard class is a bad idea, combining them only leads to confusion. Also, using different identifiers for mouse buttons would break compatibility with most Iron applications if there is no check for that. I would just keep this as it is (of course it’s just my opinion).

The Kha API is very low level (you must register listeners for input events), so the Iron classes are pretty helpful because they represent abstract wrappers around that. It’s possible that I misunderstood what you meant.

Your changes look pretty good to me :slight_smile: If you want to have even less redundant code you could implement the addKeyboardInput() function (same goes for addMouseInput()) like this:

// You can inline the function to replace calls to it with its content during
// compilation (if the Haxe compiler agrees with you :))
public inline function addMouseInput(button: String, ?modifiers: Array<String>): Void {
	var mod = modifiers == null ? new Array<String>() : modifiers;
	addCustomInput(new MouseConfig(button, mod));
}

When you later change the implementation of the InputConfig map (replace it with a different data structure for example), you only need to update addCustomInput() because all other functions rely on that.

Also, if you want you can return the new input configuration so that users of the Haxe API (would be a bit more difficult with nodes) can easily remove an input without having to iterate through all inputs in getInputByKey(), which is faster.

Again, this is just my opinion, there is no such thing as a “right way” (I actually don’t want to tell you that much what to do even if I do that sometimes :sweat_smile:), it’s all about design decisions and different people do it differently.

Another idea: if you’re interested you could implement a Blender UI for that later, this can save a ton of work for users, especially when they are using logic nodes.

2 Likes

Ok, i’ve changed a few things and added a test file for the InputAxis class, waiting for feed backs. If the whole design is a mess (it is not like the unreal one) i also would like to know :slight_smile:

A few things that i already want to implement is deadzone, smooth “acceleration” and rotation to the movement direction.

The mouse moved and gamepad stick will be different from buttons, i don’t know how yet.

2 Likes