From 331b65873345cb4a74de90e17027fa449797ecfc Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Tue, 25 Jul 2023 08:13:47 -0500 Subject: [PATCH] feat: Implement InputMapper (#2705) This PR implements a new input mapper for excalibur which is useful for mapping multiple input sources into specific commands. This can be used to enable accessibility for your users adding ways for them to provide different inputs! This examples shows how to map multiple types of input for the same action! ```typescript const moveRight = (amount: number) => { actor.vel.x = 100 * amount } const moveLeft = (amount: number) => { actor.vel.x = -100 * amount } const moveUp = (amount: number) => { actor.vel.y = -100 * amount } const moveDown = (amount: number) => { actor.vel.y = 100 * amount } engine.inputMapper.on(({keyboard}) => keyboard.isHeld(ex.Keys.ArrowRight) ? 1 : 0, moveRight); engine.inputMapper.on(({gamepads}) => gamepads.at(0).isButtonPressed(ex.Buttons.DpadRight) ? 1 : 0, moveRight); engine.inputMapper.on(({gamepads}) => gamepads.at(0).getAxes(ex.Axes.LeftStickX) > 0 ? gamepads.at(0).getAxes(ex.Axes.LeftStickX) : 0, moveRight); ``` --- CHANGELOG.md | 10 ++++++ src/engine/Engine.ts | 9 ++++++ src/engine/Input/InputMapper.ts | 57 +++++++++++++++++++++++++++++++++ src/engine/index.ts | 1 + src/spec/InputMapperSpec.ts | 52 ++++++++++++++++++++++++++++++ 5 files changed, 129 insertions(+) create mode 100644 src/engine/Input/InputMapper.ts create mode 100644 src/spec/InputMapperSpec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fd0d3cd..9498f3d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,16 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Added new `ex.InputMapper` for mapping multiple input sources into actions! This can be useful for providing accessibility into your games and allowing users to map inputs to different game commands. + ```typescript + const moveRight = (amount: number) => { actor.vel.x = 100 * amount } + const moveLeft = (amount: number) => { actor.vel.x = -100 * amount } + const moveUp = (amount: number) => { actor.vel.y = -100 * amount } + const moveDown = (amount: number) => { actor.vel.y = 100 * amount } + engine.inputMapper.on(({keyboard}) => keyboard.isHeld(ex.Keys.ArrowRight) ? 1 : 0, moveRight); + engine.inputMapper.on(({gamepads}) => gamepads.at(0).isButtonPressed(ex.Buttons.DpadRight) ? 1 : 0, moveRight); + engine.inputMapper.on(({gamepads}) => gamepads.at(0).getAxes(ex.Axes.LeftStickX) > 0 ? gamepads.at(0).getAxes(ex.Axes.LeftStickX) : 0, moveRight); + ``` - Added strongly typed events with `ex.EventEmitter` - Added new convenience properties for flipping all the graphics on an Actor * `ex.Actor.graphics.flipHorizontal` - Flips all the graphics horizontally diff --git a/src/engine/Engine.ts b/src/engine/Engine.ts index 95c2371c..86b6fff8 100644 --- a/src/engine/Engine.ts +++ b/src/engine/Engine.ts @@ -44,6 +44,7 @@ import { Clock, StandardClock } from './Util/Clock'; import { ImageFiltering } from './Graphics/Filtering'; import { GraphicsDiagnostics } from './Graphics/GraphicsDiagnostics'; import { Toaster } from './Util/Toaster'; +import { InputMapper } from './Input/InputMapper'; export type EngineEvents = { fallbackgraphicscontext: ExcaliburGraphicsContext2DCanvas, @@ -404,6 +405,11 @@ export class Engine implements CanInitialize, CanUpdate, CanDraw { */ public input: EngineInput; + /** + * Map multiple input sources to specific game actions actions + */ + public inputMapper: InputMapper; + /** * Access Excalibur debugging functionality. * @@ -1127,6 +1133,7 @@ O|===|* >________________>\n\ grabWindowFocus: this._originalOptions?.grabWindowFocus ?? true }); this.input.gamepads.init(); + this.inputMapper = new InputMapper(this.input); // Issue #385 make use of the visibility api // https://developer.mozilla.org/en-US/docs/Web/Guide/User_experience/Using_the_Page_Visibility_API @@ -1200,6 +1207,7 @@ O|===|* >________________>\n\ // Update input listeners this.input.keyboard.update(); this.input.gamepads.update(); + this.inputMapper.execute(); return; } @@ -1219,6 +1227,7 @@ O|===|* >________________>\n\ // Update input listeners this.input.keyboard.update(); this.input.gamepads.update(); + this.inputMapper.execute(); } /** diff --git a/src/engine/Input/InputMapper.ts b/src/engine/Input/InputMapper.ts new file mode 100644 index 00000000..293d8ec6 --- /dev/null +++ b/src/engine/Input/InputMapper.ts @@ -0,0 +1,57 @@ +import { Gamepads } from './Gamepad'; +import { Keyboard } from './Keyboard'; +import { PointerEventReceiver } from './PointerEventReceiver'; + +export interface InputsOptions { + keyboard: Keyboard; + gamepads: Gamepads; + pointers: PointerEventReceiver; +} + +/** + * This allows you to map multiple inputs to specific commands! This is especially useful when + * you need to allow multiple input sources to control a specific action. + */ +export class InputMapper { + + private _handlers = new Map(); + constructor(public inputs: InputsOptions) {} + + /** + * Executes the input map, called internally by Excalibur + */ + execute() { + for (const [input, command] of this._handlers.entries()) { + const results = input(this.inputs); + if (results) { + command(results); + } + } + } + + /** + * This allows you to map multiple inputs to specific commands! This is useful + * + * The inputHandler should return a truthy value if you wish the commandHandler to fire. + * + * Example: + * ```typescript + * const moveRight = (amount: number) => { actor.vel.x = 100 * amount } + * const moveLeft = (amount: number) => { actor.vel.x = -100 * amount } + * const moveUp = (amount: number) => { actor.vel.y = -100 * amount } + * const moveDown = (amount: number) => { actor.vel.y = 100 * amount } + * + * engine.inputMapper.on(({keyboard}) => keyboard.isHeld(ex.Keys.ArrowRight) ? 1 : 0, moveRight); + * engine.inputMapper.on(({gamepads}) => gamepads.at(0).isButtonPressed(ex.Buttons.DpadRight) ? 1 : 0, moveRight); + * engine.inputMapper.on(({gamepads}) => gamepads.at(0).getAxes(ex.Axes.LeftStickX) > 0 ? + * gamepads.at(0).getAxes(ex.Axes.LeftStickX) : 0, moveRight); + * ``` + * @param inputHandler + * @param commandHandler + */ + on( + inputHandler: (inputs: InputsOptions) => TInputHandlerData | false, + commandHandler: (data: TInputHandlerData) => any) { + this._handlers.set(inputHandler, commandHandler); + } +} \ No newline at end of file diff --git a/src/engine/index.ts b/src/engine/index.ts index d91ac8b2..07b386b3 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -115,6 +115,7 @@ export { KeyboardInitOptions, Keyboard } from './Input/Keyboard'; +export * from './Input/InputMapper'; // ex.Util namespaces import * as util from './Util/Index'; diff --git a/src/spec/InputMapperSpec.ts b/src/spec/InputMapperSpec.ts new file mode 100644 index 00000000..ba32d990 --- /dev/null +++ b/src/spec/InputMapperSpec.ts @@ -0,0 +1,52 @@ +import * as ex from '@excalibur'; + +describe('An InputMapper', () => { + it('exists', () => { + expect(ex.InputMapper).toBeDefined(); + }); + + it('can listen to events and map to the command', () => { + const command = jasmine.createSpy('command'); + const keyboard = jasmine.createSpyObj('keyboard', ['isHeld']); + keyboard.isHeld.and.returnValue(true); + const sut = new ex.InputMapper({ + keyboard: keyboard as unknown as ex.Keyboard, + pointers: {} as any, + gamepads: {} as any + }); + + sut.on(({keyboard}) => { + return keyboard.isHeld(ex.Keys.Enter); + }, command); + sut.on(({keyboard}) => { + return keyboard.isHeld(ex.Keys.ArrowRight); + }, command); + + sut.execute(); + + expect(command).toHaveBeenCalledTimes(2); + }); + + it('can listen to events and not map to the command', () => { + const command = jasmine.createSpy('command'); + const keyboard = jasmine.createSpyObj('keyboard', ['isHeld']); + keyboard.isHeld.and.returnValue(false); + const sut = new ex.InputMapper({ + keyboard: keyboard, + pointers: {} as any, + gamepads: {} as any + }); + + sut.on(({keyboard}) => { + return keyboard.isHeld(ex.Keys.Enter); + }, command); + sut.on(({keyboard}) => { + return keyboard.isHeld(ex.Keys.ArrowRight); + }, command); + + sut.execute(); + + expect(command).toHaveBeenCalledTimes(0); + }); + +}); \ No newline at end of file -- 2.51.2