Skip to content

Commit

Permalink
feat(platform): add Keyboard Input device
Browse files Browse the repository at this point in the history
  • Loading branch information
RuggeroVisintin committed Dec 26, 2023
1 parent 8fc00ef commit 8bb496e
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 55 deletions.
31 changes: 5 additions & 26 deletions src/ecs/components/InputComponent.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
import { Type } from "../../core";
import { KeyEvent, KeyStatus, KeyboardDevice } from "../../platform/inputs";
import { BaseComponent } from "./BaseComponent";

export enum KeyStatus {
Up,
Down
};

interface KeyEvent {
status: KeyStatus,
code: string
}

type OnInputEventTriggeredCallback = (event: KeyEvent) => void;

@Type('InputComponent')
Expand All @@ -19,27 +10,15 @@ export class InputComponent extends BaseComponent {

constructor() {
super();

window.addEventListener("keydown", (e) => this.onKeyDown(e), true);

window.addEventListener("keyup", (e) => this.onKeyUp(e), true);
}

private onKeyDown(event: KeyboardEvent): void {
if (!this.onInputEventCb) return;

this.onInputEventCb({
code: event.code,
status: KeyStatus.Down,
})
public update(inputDevice: KeyboardDevice): void {
inputDevice.pushInputListener((e) => this.onKeyUpdate(e));
}

private onKeyUp(event: KeyboardEvent): void {
private onKeyUpdate(event: KeyEvent): void {
if (!this.onInputEventCb) return;

this.onInputEventCb({
code: event.code,
status: KeyStatus.Up,
})
this.onInputEventCb(event);
}
}
18 changes: 18 additions & 0 deletions src/ecs/systems/InputSystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ISystem } from "./ISystem";
import { InputComponent } from '../components';
import { KeyboardDevice } from "../../platform";

export class InputSystem implements ISystem {
public readonly components: InputComponent[] = [];

constructor(private readonly inputDevice: KeyboardDevice) {}

public update(): void {
this.components.forEach(inputComponent => inputComponent.update(this.inputDevice));
this.inputDevice.update();
}

public registerComponent(component: InputComponent): void {
this.components.push(component);
}
}
3 changes: 2 additions & 1 deletion src/ecs/systems/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './ISystem';
export * from './RenderSystem';
export * from './PhysicsSystem';
export * from './PhysicsSystem';
export * from './InputSystem';
3 changes: 2 additions & 1 deletion src/platform/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './gfx';
export * from './gfx';
export * from './inputs';
52 changes: 52 additions & 0 deletions src/platform/inputs/KeyboardDevice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export enum KeyStatus {
Up,
Down
};

export interface KeyEvent {
status: KeyStatus,
code: string
}

type InputListenerCallback = (event: KeyEvent) => void;

export class KeyboardDevice {
private _listeners: InputListenerCallback[] = [];
private _keyStatusMap: Record<string, KeyStatus> = {};

get listeners(): InputListenerCallback[] {
// Copy to avoid modifying existing items from getter
return [
...this._listeners
];
}

public constructor() {
window.addEventListener("keydown", (e) => this.onKeyDown(e), true);
window.addEventListener("keyup", (e) => this.onKeyUp(e), true);
}

public pushInputListener(callback: InputListenerCallback): void {
this._listeners.push(callback);
}

public update(): void {
Object.entries(this._keyStatusMap).forEach(([key, value]) => {
this._listeners.forEach(listener => listener({
code: key,
status: value
}))
})

// Empty listeners after every update
this._listeners = [];
}

private onKeyDown(e: KeyboardEvent): void {
this._keyStatusMap[e.code] = KeyStatus.Down;
}

private onKeyUp(e: KeyboardEvent): void {
this._keyStatusMap[e.code] = KeyStatus.Up;
}
}
1 change: 1 addition & 0 deletions src/platform/inputs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './KeyboardDevice';
39 changes: 12 additions & 27 deletions test/unit/ecs/components/InputComponent.test.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,22 @@
import { InputComponent, KeyStatus } from "../../../../src";
import { InputComponent, KeyEvent, KeyStatus, KeyboardDevice } from "../../../../src";

describe('ecs/components/InputComponent', () => {
const inputComponent = new InputComponent();
let inputDevice = new KeyboardDevice();
let inputComponent = new InputComponent();

beforeEach(() => {
inputDevice = new KeyboardDevice();
inputComponent = new InputComponent();
})

describe('.update()', () => {
it('Should invoke the onInputEventCb when a key is pressed down', () => {
const onInputCb = jest.fn();

inputComponent.onInputEventCb = onInputCb;

const event = new KeyboardEvent('keydown', { code: 'KeyA' });
window.dispatchEvent(event);
it('Should push a listener in the inputDevice', () => {
const onInputCb = jest.fn((e: KeyEvent) => { });

expect(onInputCb).toHaveBeenCalledWith({
status: KeyStatus.Down,
code: 'KeyA'
});
});

it('Should invoke the onInputEventCb when a key is released', () => {
const onInputCb = jest.fn();

inputComponent.onInputEventCb = onInputCb;
inputComponent.update(inputDevice);

const event = new KeyboardEvent('keyup', { code: 'KeyA' });
window.dispatchEvent(event);

expect(onInputCb).toHaveBeenCalledWith({
status: KeyStatus.Up,
code: 'KeyA'
});
expect(inputDevice.listeners.length).toBe(1);
});

it.todo('Should push an InputListener into the InputSystem');
})
})
48 changes: 48 additions & 0 deletions test/unit/ecs/systems/InputSystem.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { InputComponent, InputSystem, KeyStatus, KeyboardDevice } from "../../../../src";

describe('ecs/systems/InputSystem', () => {
const inputDevice = new KeyboardDevice();
let inputSystem = new InputSystem(inputDevice);

beforeEach(() => {
inputSystem = new InputSystem(inputDevice);
})

describe('.registerComponent()', () => {
it('Should register the component into the InputSystem components list', () => {
const testInputComponent = new InputComponent();
inputSystem.registerComponent(testInputComponent);

expect(inputSystem.components).toContain(testInputComponent);
})
});

describe('.update()', () => {
it('Should update each component registered into the system', () => {
const inputComponent = new InputComponent();
const spyUpdate = jest.spyOn(inputComponent, 'update');

inputSystem.registerComponent(inputComponent);
inputSystem.update();

expect(spyUpdate).toHaveBeenCalled();
});

it('Should trigger the inputDevice update', () => {
const fakeCb = jest.fn();
const inputComponent = new InputComponent();
inputComponent.onInputEventCb = fakeCb;

const event = new KeyboardEvent('keydown', { code: 'KeyA' });
window.dispatchEvent(event);

inputSystem.registerComponent(inputComponent);
inputSystem.update();

expect(fakeCb).toHaveBeenCalledWith({
code: 'KeyA',
status: KeyStatus.Down
});
})
})
})
87 changes: 87 additions & 0 deletions test/unit/platform/inputs/KeyboardDevice.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { KeyEvent, KeyStatus, KeyboardDevice } from "../../../../src";

describe('platform/inputs/KeyboardDevice', () => {
let keyboardDevice = new KeyboardDevice();

beforeEach(() => {
keyboardDevice = new KeyboardDevice();
})

describe('.pushInputListener()', () => {
it('Should register a new inputListener in the listener list', () => {
const callback = jest.fn((event: KeyEvent) => {});

keyboardDevice.pushInputListener(callback);

expect(keyboardDevice.listeners).toContain(callback);
});
})

describe('.update()', () => {
it('Should invoke the listener when a button is pressed down', () => {
const callback = jest.fn((event: KeyEvent) => {});

keyboardDevice.pushInputListener(callback);

const event = new KeyboardEvent('keydown', { code: 'KeyA' });
window.dispatchEvent(event);

keyboardDevice.update();

expect(callback).toHaveBeenCalledWith({
code: 'KeyA',
status: KeyStatus.Down
});
});


it('Should invoke the listener when a button is released', () => {
const callback = jest.fn((event: KeyEvent) => {});

keyboardDevice.pushInputListener(callback);

const event = new KeyboardEvent('keyup', { code: 'KeyA' });
window.dispatchEvent(event);

keyboardDevice.update();

expect(callback).toHaveBeenCalledWith({
code: 'KeyA',
status: KeyStatus.Up
});
});

it('Should invoke the listener with only the latest status of the key', () => {
const callback = jest.fn((event: KeyEvent) => { });

keyboardDevice.pushInputListener(callback);

const keyDownEvent = new KeyboardEvent('keydown', { code: 'KeyA' });
const keyUpEvent = new KeyboardEvent('keyup', { code: 'KeyA' });

window.dispatchEvent(keyDownEvent);
window.dispatchEvent(keyUpEvent);

keyboardDevice.update();

expect(callback).toHaveBeenCalledWith({
code: 'KeyA',
status: KeyStatus.Up
});

expect(callback).not.toHaveBeenCalledWith({
code: 'KeyA',
status: KeyStatus.Down
})
});

it('Should cleanup all listeners after an update', () => {
const callback = jest.fn((event: KeyEvent) => {});

keyboardDevice.pushInputListener(callback);
keyboardDevice.update();

expect(keyboardDevice.listeners).toBeEmpty();
})
})
})

0 comments on commit 8bb496e

Please sign in to comment.