Input Handling System
Relevant source files
The following files were used as context for generating this wiki page:The Input Handling System in Farm Siege is responsible for capturing and processing all user inputs from keyboards, gamepads, and mouse clicks. It employs a dual-strategy approach to manage different types of interactions. For continuous, state-based game actions like character movement and abilities, it uses a centralized InputMap class. For discrete, event-based UI interactions such as button clicks or text entry, it uses a component-based EventListener. This separation allows for a clean distinction between real-time game controls and user interface logic.
Core Components
The system is primarily built around two key classes: InputMap and EventListener.
| Component | File | Description | Use Cases |
|---|---|---|---|
InputMap | lib/InputMap.ts | Maps physical inputs (keyboard, gamepad) to abstract game actions. Manages input state (pressed/released) and is polled every frame. | Character movement, firing, using items. |
EventListener | lib/components/logical/EventListener.ts | A logical component that creates an invisible interactive area on the canvas to listen for discrete events like clicks and key presses. | UI buttons, text input fields. |
Game Action Mapping (InputMap)
The InputMap class is the heart of the real-time game control system. It abstracts raw hardware inputs into a consistent set of logical actions that the game logic can easily consume. It supports both keyboard and gamepad inputs, treating them interchangeably based on its configuration.
Sources: lib/InputMap.ts, src/components/CharacterContainer.ts
Configuration
An InputMap is instantiated with a configuration object that defines the mapping between logical action names and their corresponding physical inputs.
// src/components/CharacterContainer.ts:33-58
this.inputMap = new InputMap({
up: {
keys: ['KeyW', 'ArrowUp', GamepadStickDirections.LeftStickUp]
},
left: {
keys: ['KeyA', 'ArrowLeft', GamepadStickDirections.LeftStickLeft]
},
down: {
keys: ['KeyS', 'ArrowDown', GamepadStickDirections.LeftStickDown]
},
right: {
keys: ['KeyD', 'ArrowRight', GamepadStickDirections.LeftStickRight]
},
use: {
keys: ['Space', GamepadButtons.ButtonB],
overrides: ['place']
},
place: {
keys: ['KeyV', GamepadButtons.ButtonA],
overrides: ['use']
},
fire: {
keys: ['KeyC', GamepadButtons.ButtonX],
singlePress: true
}
});
The configuration supports several properties:
keys: An array of strings representing keyboardevent.codevalues orGamepadButtons/GamepadStickDirectionsenum members.overrides: An array of other action names that should be ignored if this action is active. In the example, pressingusewill preventplacefrom being active simultaneously.singlePress: A boolean that, whentrue, treats the action as a single-shot event. The key is removed from the active list immediately after being read once, preventing it from being triggered continuously while held down.
Sources: lib/InputMap.ts:41-47, src/components/CharacterContainer.ts:33-58
Input Sources
InputMap gathers input from two primary browser APIs:
- Keyboard Events: It attaches
keydownandkeyuplisteners to thewindowobject to track which keys are currently pressed. It maintains an internalactiveKeysarray. - Gamepad API: On every poll, it calls
navigator.getGamepads()to get the current state of all connected gamepads. It reads button presses and analog stick positions, respecting ananalogDeadzoneof0.2to prevent stick drift.
Sources: lib/InputMap.ts:121-158, lib/InputMap.ts:172-176, lib/InputMap.ts:109
Data Flow
The following diagram illustrates how physical inputs are processed by InputMap and consumed by game components.
This diagram shows the flow from physical user input through browser APIs to the InputMap instance, which is then read by the game logic.
Sources: lib/InputMap.ts, src/components/CharacterContainer.ts
Usage
The CharacterContainer component polls the inputMap.pressed getter within its onTick method. The returned object contains the logical action names as keys and their intensity (0 to 1) as values. This value is then used to calculate character movement and trigger actions.
// src/components/CharacterContainer.ts:84-101
protected onTick(_ctx: PropsContext<CharacterContainerProps>, timeDifference: number): void {
// ...
// Read inputs
const inputs = this.inputMap!.pressed;
if (!effectsStore.content.gameOver.active) {
let moveX = 0;
let moveY = 0;
if (inputs.up) {
moveY -= inputs.up * values.character.movementSpeed * (timeDifference / 1000);
}
if (inputs.down) {
moveY += inputs.down * values.character.movementSpeed * (timeDifference / 1000);
}
// ...
characterStore.move(moveX, moveY);
if (inputs.use) {
// ...
}
// ...
}
}
Sources: src/components/CharacterContainer.ts:84-101
UI and Discrete Event Handling (EventListener)
For UI elements like buttons and text fields, the EventListener component provides a way to handle discrete events like mouse clicks and individual key presses within a specific area of the canvas.
Sources: lib/components/logical/EventListener.ts, lib/Canvas.ts, src/components/Input.ts
Event Propagation
The event handling process for UI components begins in the Canvas class.
- The main
Canvasinstance attaches native DOM event listeners (click,keypress,keydown,keyup) to thewindowor<canvas>element. - When an event fires,
Canvascallsthis.root.propagateEvent()on the root component of the game. - The event is propagated down the component tree.
- An
EventListenercomponent checks if the event's coordinates are within its defined bounds. - If the event is a match, the
EventListenerexecutes the corresponding callback function (e.g.,onClick,onKeypress) passed to it via props.
The sequence diagram below shows this flow for a click event.
This diagram shows how a user click is captured by the canvas and propagated through the component tree to the correct EventListener.
Sources: lib/Canvas.ts:36-51, lib/components/logical/EventListener.ts
Text Input
The Input component is a prime example of using EventListener for complex interactions. It uses onKeypress to append characters to its internal text state and onKeydown to handle special keys like Backspace, which are not always captured by onKeypress in all browsers. It also listens for Enter to trigger its onEnter callback.
// src/components/Input.ts:134-159
{
component: new EventListener(),
position: (): Coordinates => new Coordinates(0, 0),
props: (ctx: PropsContext<InputProps>): EventListenerProps => ({
size: new Dimensions(ctx.props.width, 48),
onKeypress: (event: KeyboardEvent): void => {
event.preventDefault();
switch (event.key) {
case 'Enter':
this.onEnterCallback(this.text);
this.text = '';
return;
// ...
}
// Key press
if (!this.limitReached) {
this.text += event.key;
}
},
// Additional Keydown event since Backspace is not passed to onKeypress in all browsers
onKeydown: (event: KeyboardEvent): void => {
if (event.key === 'Backspace') {
event.preventDefault();
this.text = this.text.slice(0, this.text.length - 1);
}
}
})
}
Sources: src/components/Input.ts:134-159
Input Visualizations
The game includes several components under src/components/inputButtons/ that are purely visual. These are used in the "How to Play" section to show the player which physical buttons correspond to game actions. For example, Instructions3.ts displays animated KeyboardSpaceButton and ControllerBButton components to teach the player how to harvest crops.
Sources: src/components/instructions/Instructions3.ts:89-102, src/screens/HelpScreen.ts
Conclusion
The Farm Siege input system effectively separates concerns between real-time game control and UI interaction. The InputMap provides a robust, polling-based solution for handling fluid character actions from multiple device types, while the EventListener component offers a flexible, event-driven model for building an interactive user interface on the HTML canvas. This dual architecture is well-suited for a game that combines fast-paced action with menu-based navigation and text entry.