Skip to content

Commit

Permalink
Can now switch between gamepad and keyboard
Browse files Browse the repository at this point in the history
  • Loading branch information
hubol committed Feb 10, 2024
1 parent 0b07fa8 commit 27e257f
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 53 deletions.
11 changes: 7 additions & 4 deletions src/igua/core/input.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AsshatInput, KeyboardControls } from "../../lib/game-engine/input/asshat-input";
import { AsshatInput, InputModalityType } from "../../lib/game-engine/input/asshat-input";
import { GamepadControl, GamepadControls, StandardMapping } from "../../lib/game-engine/input/gamepad-controls";
import { MappedGamepad } from "../../lib/game-engine/input/mapped-gamepad";
import { KeyboardControls, MappedKeyboard } from "../../lib/game-engine/input/mapped-keyboard";

const actions = ['MoveLeft', 'MoveRight', 'Duck', 'Jump', 'Interact',
'InventoryMenuToggle', 'PauseMenuToggle', 'MenuEscape',
Expand Down Expand Up @@ -44,10 +45,12 @@ const defaultGamepadControls: GamepadControls<Action> = {
SelectUp: [ button(StandardMapping.Button.PadUp), axisUnit(StandardMapping.Axis.JoystickLeft, [0, -1]), axisUnit(StandardMapping.Axis.JoystickRight, [0, -1]) ]
}

new MappedGamepad(defaultGamepadControls, {} as any, {} as any);

export class IguaInput extends AsshatInput<Action> {
constructor() {
super(keyboardControls);
super([ new MappedKeyboard(keyboardControls), new MappedGamepad(defaultGamepadControls) ]);
}

protected onModalityChanged(from: InputModalityType, to: InputModalityType): void {
// TODO show system message
}
}
69 changes: 35 additions & 34 deletions src/lib/browser/key.ts → src/lib/browser/key-listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,49 +18,50 @@ export type KeyCode = typeof KeyCodes[number];

type KeysState = Partial<Record<KeyCode, number>>;

export const Key = {
export class KeyListener {
private _started = false;
private readonly _state: KeysState = {};

isDown(key: KeyCode) {
return (currentKeysState[key]! & 0b101) as unknown as boolean;
},
return (this._state[key]! & 0b101) as unknown as boolean;
}

isUp(key: KeyCode) {
return !this.isDown(key);
},
justWentDown(key: KeyCode) {
return (currentKeysState[key]! & 0b100) as unknown as boolean;
},
justWentUp(key: KeyCode) {
return (currentKeysState[key]! & 0b010) as unknown as boolean;
}
};

const currentKeysState: KeysState = {};
justWentDown(key: KeyCode) {
return (this._state[key]! & 0b100) as unknown as boolean;
}

function handleKeyDown(event: KeyboardEvent) {
if (event.repeat)
return;
currentKeysState[event.code] |= 0b101;
}
justWentUp(key: KeyCode) {
return (this._state[key]! & 0b010) as unknown as boolean;
}

function handleKeyUp(event: KeyboardEvent) {
currentKeysState[event.code] |= 0b010;
currentKeysState[event.code] &= 0b110;
}
protected onKeyDown(event: KeyboardEvent) {
if (event.repeat)
return;
this._state[event.code] |= 0b101;
}

let startedKeyListener = false;
protected onKeyUp(event: KeyboardEvent) {
this._state[event.code] |= 0b010;
this._state[event.code] &= 0b110;
}

export const KeyListener = {
start() {
if (startedKeyListener)
throw new Error("Cannot start key listener twice!");
document.addEventListener("keydown", handleKeyDown);
document.addEventListener("keyup", handleKeyUp);
startedKeyListener = true;
},
advance() {
if (!startedKeyListener)
throw new Error("Key listener must be started to advance!");
for (const key in currentKeysState) {
currentKeysState[key] &= 0b001;
if (this._started)
return console.error("Attempting to start KeyListener twice!", this);
document.addEventListener("keydown", this.onKeyDown.bind(this));
document.addEventListener("keyup", this.onKeyUp.bind(this));
this._started = true;
}

tick() {
if (!this._started)
return console.error("Attempting to tick() unstarted KeyListener", this);
for (const key in this._state) {
this._state[key] &= 0b001;
}
}
}
}
57 changes: 47 additions & 10 deletions src/lib/game-engine/input/asshat-input.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,73 @@
import { Key, KeyCode, KeyListener } from "../../browser/key";
import { Logging } from "../../logging";
import { Undefined } from "../../types/undefined";

export type KeyboardControls<TAction extends string> = {
[index in TAction]: KeyCode;
export enum InputModalityType {
Keyboard = 'Keyboard',
Gamepad = 'Gamepad',
}

export interface MappedInputModality<TAction extends string> {
readonly type: InputModalityType;
readonly lastEventTimestamp: number;
isDown(action: TAction): boolean;
isUp(action: TAction): boolean;
justWentDown(action: TAction): boolean;
justWentUp(action: TAction): boolean;
start(): void;
tick(): void;
}

export class AsshatInput<TAction extends string> {
constructor(readonly keyboardControls: KeyboardControls<TAction>) {
private _currentModality?: MappedInputModality<TAction>;

constructor(private readonly _modalities: MappedInputModality<TAction>[]) {
console.log(...Logging.componentArgs(this));
}

isDown(action: TAction) {
return Key.isDown(this.keyboardControls[action]);
return this._currentModality?.isDown(action) as boolean;
}

isUp(action: TAction) {
return Key.isUp(this.keyboardControls[action]);
return this._currentModality?.isUp(action) as boolean;
}

justWentDown(action: TAction) {
return Key.justWentDown(this.keyboardControls[action]);
return this._currentModality?.justWentDown(action) as boolean;
}

justWentUp(action: TAction) {
return Key.justWentUp(this.keyboardControls[action]);
return this._currentModality?.justWentUp(action) as boolean;
}

start() {
KeyListener.start();
for (const modality of this._modalities)
modality.start();
}

tick() {
KeyListener.advance();
let latestEventTimestamp = -1;
let modalityWithLatestEventTimestamp = Undefined<MappedInputModality<TAction>>();

for (let i = 0; i < this._modalities.length; i++) {
const modality = this._modalities[i];
modality.tick();
if (modality.lastEventTimestamp > latestEventTimestamp) {
latestEventTimestamp = modality.lastEventTimestamp;
modalityWithLatestEventTimestamp = modality;
}
}

if (modalityWithLatestEventTimestamp !== this._currentModality) {
const fromType = this._currentModality?.type;
this._currentModality = modalityWithLatestEventTimestamp;
const toType = this._currentModality?.type;
if (fromType && toType)
this.onModalityChanged(fromType, toType);
}
}

protected onModalityChanged(from: InputModalityType, to: InputModalityType) {

}
}
22 changes: 17 additions & 5 deletions src/lib/game-engine/input/mapped-gamepad.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
import { sqDistance } from "../../math/vector";
import { vnew } from "../../math/vector-type";
import { Undefined } from "../../types/undefined";
import { InputModalityType, MappedInputModality } from "./asshat-input";
import { GamepadControlType, GamepadControls } from "./gamepad-controls";

export class MappedGamepad<TAction extends string> {
export class MappedGamepad<TAction extends string> implements MappedInputModality<TAction> {
private readonly _keys: string[];
private readonly _actionWasDown: Record<TAction, boolean>;
private readonly _actionIsDown: Record<TAction, boolean>;
private readonly _copyDownFn: CopyStateFn<Record<TAction, boolean>>;
private readonly _applyGamepadToInputFn: ApplyGamepadToInputFn<TAction>;

constructor(readonly controls: GamepadControls<TAction>, private readonly _onConnected: () => void, private readonly _onButtonPressed: () => void) {
constructor(readonly controls: GamepadControls<TAction>) {
this._keys = Object.keys(controls);
this._actionWasDown = this._createActionDownRecord();
this._actionIsDown = this._createActionDownRecord();
this._copyDownFn = compileCopyStateFn(this._keys);
this._applyGamepadToInputFn = compileApplyGamepadToInput(controls, { axisUnitDistance: 0.5, stickDeadZone: 0.3 });
// TODO check if a gamepad is already available and fire _onConnected,
// else subscribe to "gamepadconnected" event
// console.log(this._applyGamepadToInputFn);
}

readonly type = InputModalityType.Gamepad;
lastEventTimestamp = -1;

start(): void {
// nop
}

private _createActionDownRecord() {
Expand All @@ -42,6 +47,13 @@ export class MappedGamepad<TAction extends string> {

this._copyDownFn(this._actionIsDown, this._actionWasDown);
this._applyGamepadToInputFn(gamepad, this._actionIsDown);

for (let i = 0; i < gamepad.buttons.length; i += 1) {
if (gamepad.buttons[i].pressed) {
this.lastEventTimestamp = performance.now();
break;
}
}
}

isDown(action: TAction) {
Expand Down
65 changes: 65 additions & 0 deletions src/lib/game-engine/input/mapped-keyboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { KeyCode, KeyListener } from "../../browser/key-listener";
import { InputModalityType, MappedInputModality } from "./asshat-input";

export type KeyboardControls<TAction extends string> = {
[index in TAction]: KeyCode;
}

export class MappedKeyboard<TAction extends string> implements MappedInputModality<TAction> {
private readonly _keyListener: LimitedKeyListener;

readonly type = InputModalityType.Keyboard;
get lastEventTimestamp() {
return this._keyListener.lastEventTimestamp;
};

constructor(private readonly _controls: KeyboardControls<TAction>) {
const keyCodes = Object.values(_controls as Record<string, KeyCode>);
this._keyListener = new LimitedKeyListener(new Set(keyCodes));
}

isDown(action: TAction): boolean {
return this._keyListener.isDown(this._controls[action]);
}

isUp(action: TAction): boolean {
return this._keyListener.isUp(this._controls[action]);
}

justWentDown(action: TAction): boolean {
return this._keyListener.justWentDown(this._controls[action]);
}

justWentUp(action: TAction): boolean {
return this._keyListener.justWentUp(this._controls[action]);
}

start(): void {
this._keyListener.start();
}

tick(): void {
this._keyListener.tick();
}
}

class LimitedKeyListener extends KeyListener {
lastEventTimestamp = -1;

constructor(private readonly _keyCodes: Set<KeyCode>) {
super();
}

protected onKeyDown(event: KeyboardEvent): void {
if (!this._keyCodes.has(event.code as KeyCode))
return;
this.lastEventTimestamp = performance.now();
super.onKeyDown(event);
}

protected onKeyUp(event: KeyboardEvent): void {
if (!this._keyCodes.has(event.code as KeyCode))
return;
super.onKeyUp(event);
}
}

0 comments on commit 27e257f

Please sign in to comment.