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