@skewedaspect/sage 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/Readme.md +53 -0
- package/dist/classes/bindings/toggle.d.ts +122 -0
- package/dist/classes/bindings/trigger.d.ts +79 -0
- package/dist/classes/bindings/value.d.ts +104 -0
- package/dist/classes/entity.d.ts +83 -0
- package/dist/classes/eventBus.d.ts +94 -0
- package/dist/classes/gameEngine.d.ts +57 -0
- package/dist/classes/input/gamepad.d.ts +94 -0
- package/dist/classes/input/keyboard.d.ts +66 -0
- package/dist/classes/input/mouse.d.ts +80 -0
- package/dist/classes/input/readers/gamepad.d.ts +77 -0
- package/dist/classes/input/readers/keyboard.d.ts +60 -0
- package/dist/classes/input/readers/mouse.d.ts +45 -0
- package/dist/classes/loggers/consoleBackend.d.ts +29 -0
- package/dist/classes/loggers/nullBackend.d.ts +14 -0
- package/dist/engines/scene.d.ts +11 -0
- package/dist/interfaces/action.d.ts +20 -0
- package/dist/interfaces/binding.d.ts +144 -0
- package/dist/interfaces/entity.d.ts +9 -0
- package/dist/interfaces/game.d.ts +26 -0
- package/dist/interfaces/input.d.ts +181 -0
- package/dist/interfaces/logger.d.ts +88 -0
- package/dist/managers/binding.d.ts +185 -0
- package/dist/managers/entity.d.ts +70 -0
- package/dist/managers/game.d.ts +20 -0
- package/dist/managers/input.d.ts +56 -0
- package/dist/managers/level.d.ts +55 -0
- package/dist/sage.d.ts +20 -0
- package/dist/sage.es.js +2208 -0
- package/dist/sage.es.js.map +1 -0
- package/dist/sage.umd.js +2 -0
- package/dist/sage.umd.js.map +1 -0
- package/dist/utils/capabilities.d.ts +2 -0
- package/dist/utils/graphics.d.ts +10 -0
- package/dist/utils/logger.d.ts +66 -0
- package/dist/utils/physics.d.ts +2 -0
- package/dist/utils/version.d.ts +5 -0
- package/docs/architecture.md +129 -0
- package/docs/behaviors.md +706 -0
- package/docs/binding_system.md +820 -0
- package/docs/design/input.md +86 -0
- package/docs/entity_system.md +538 -0
- package/docs/eventbus.md +225 -0
- package/docs/getting_started.md +264 -0
- package/docs/images/sage_logo.png +0 -0
- package/docs/images/sage_logo_shape.png +0 -0
- package/docs/overview.md +38 -0
- package/docs/physics_system.md +686 -0
- package/docs/scene_system.md +513 -0
- package/package.json +69 -0
- package/src/classes/bindings/toggle.ts +261 -0
- package/src/classes/bindings/trigger.ts +211 -0
- package/src/classes/bindings/value.ts +227 -0
- package/src/classes/entity.ts +256 -0
- package/src/classes/eventBus.ts +259 -0
- package/src/classes/gameEngine.ts +125 -0
- package/src/classes/input/gamepad.ts +388 -0
- package/src/classes/input/keyboard.ts +189 -0
- package/src/classes/input/mouse.ts +276 -0
- package/src/classes/input/readers/gamepad.ts +179 -0
- package/src/classes/input/readers/keyboard.ts +123 -0
- package/src/classes/input/readers/mouse.ts +133 -0
- package/src/classes/loggers/consoleBackend.ts +135 -0
- package/src/classes/loggers/nullBackend.ts +51 -0
- package/src/engines/scene.ts +112 -0
- package/src/images/sage_logo.svg +172 -0
- package/src/images/sage_logo_shape.svg +146 -0
- package/src/interfaces/action.ts +30 -0
- package/src/interfaces/binding.ts +191 -0
- package/src/interfaces/entity.ts +21 -0
- package/src/interfaces/game.ts +44 -0
- package/src/interfaces/input.ts +221 -0
- package/src/interfaces/logger.ts +118 -0
- package/src/managers/binding.ts +729 -0
- package/src/managers/entity.ts +252 -0
- package/src/managers/game.ts +111 -0
- package/src/managers/input.ts +233 -0
- package/src/managers/level.ts +261 -0
- package/src/sage.ts +119 -0
- package/src/types/global.d.ts +11 -0
- package/src/utils/capabilities.ts +16 -0
- package/src/utils/graphics.ts +148 -0
- package/src/utils/logger.ts +225 -0
- package/src/utils/physics.ts +16 -0
- package/src/utils/version.ts +11 -0
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
# Binding System
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The SAGE Binding System is a flexible and powerful approach to handling user input in your games. It creates an abstraction layer between physical input devices (keyboard, mouse, gamepad) and the game actions they trigger, making your code more modular and easier to maintain.
|
|
6
|
+
|
|
7
|
+
## Core Concepts
|
|
8
|
+
|
|
9
|
+
### Actions
|
|
10
|
+
|
|
11
|
+
Actions are the "what" in your game's input system - they represent game events that can be triggered by user input. Actions are divided into two types:
|
|
12
|
+
|
|
13
|
+
- **Digital Actions**: Boolean (on/off) values, like jumping or interacting
|
|
14
|
+
- **Analog Actions**: Numeric values (typically ranging from -1 to 1 or 0 to 1), like throttle control or camera rotation
|
|
15
|
+
|
|
16
|
+
Actions are defined by a name and type, and analog actions can optionally specify minimum and maximum values:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// Digital action example
|
|
20
|
+
{
|
|
21
|
+
name: 'jump',
|
|
22
|
+
type: 'digital'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Analog action example
|
|
26
|
+
{
|
|
27
|
+
name: 'throttle',
|
|
28
|
+
type: 'analog',
|
|
29
|
+
minValue: 0, // No throttle
|
|
30
|
+
maxValue: 1 // Full throttle
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Bindings
|
|
35
|
+
|
|
36
|
+
Bindings are the "how" in your input system - they connect physical inputs to actions. SAGE supports multiple binding types that process input differently:
|
|
37
|
+
|
|
38
|
+
- **Trigger Bindings**: Fire once when a button is pressed or released
|
|
39
|
+
- **Toggle Bindings**: Toggle between on/off states each time a button is pressed (like crouch)
|
|
40
|
+
- **Value Bindings**: Continuously translate input values (like joystick position) to action values
|
|
41
|
+
|
|
42
|
+
Each binding connects a specific input from a device (like a keyboard key or gamepad button) to an action, with options to customize how the input translates to the action.
|
|
43
|
+
|
|
44
|
+
### Contexts
|
|
45
|
+
|
|
46
|
+
Contexts allow you to organize bindings into groups that can be activated or deactivated together. This is particularly useful for handling different input modes, such as:
|
|
47
|
+
|
|
48
|
+
- Gameplay controls
|
|
49
|
+
- Menu navigation
|
|
50
|
+
- Vehicle controls
|
|
51
|
+
- Dialog interactions
|
|
52
|
+
|
|
53
|
+
Contexts can be exclusive (only one active at a time) or non-exclusive (multiple can be active simultaneously).
|
|
54
|
+
|
|
55
|
+
## Basic Setup
|
|
56
|
+
|
|
57
|
+
### Initializing the Binding System
|
|
58
|
+
|
|
59
|
+
The binding system relies on the SAGE event bus and works alongside the input manager:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { GameEventBus } from './src/classes/eventBus.ts';
|
|
63
|
+
import { BindingManager } from './src/managers/binding.ts';
|
|
64
|
+
|
|
65
|
+
// Create event bus and binding manager
|
|
66
|
+
const eventBus = new GameEventBus();
|
|
67
|
+
const bindingManager = new BindingManager(eventBus);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Registering Actions
|
|
71
|
+
|
|
72
|
+
Before you can create bindings, you need to register the actions they will trigger:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// Register a digital action
|
|
76
|
+
bindingManager.registerAction({
|
|
77
|
+
name: 'quit',
|
|
78
|
+
type: 'digital'
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Register an analog action
|
|
82
|
+
bindingManager.registerAction({
|
|
83
|
+
name: 'throttle',
|
|
84
|
+
type: 'analog',
|
|
85
|
+
minValue: 0, // No throttle
|
|
86
|
+
maxValue: 1 // Full throttle
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Creating and Registering Bindings
|
|
91
|
+
|
|
92
|
+
Here's how to create a binding that triggers the "quit" action when the Escape key is pressed:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { KeyboardValueReader } from './src/classes/input/readers/keyboard.ts';
|
|
96
|
+
import { TriggerBinding } from './src/classes/bindings/trigger.ts';
|
|
97
|
+
|
|
98
|
+
// Create a trigger binding for the Escape key
|
|
99
|
+
const quitBinding = new TriggerBinding(
|
|
100
|
+
{ name: 'quit', type: 'digital' }, // The action
|
|
101
|
+
'keyboard-main', // Device ID
|
|
102
|
+
new KeyboardValueReader('Escape'), // Input source
|
|
103
|
+
{ edgeMode: 'rising' } // Options (trigger on key press)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Register the binding
|
|
107
|
+
bindingManager.registerBinding(quitBinding);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Using Binding Definitions
|
|
111
|
+
|
|
112
|
+
You can also create bindings using definitions, which is useful for creating bindings programmatically or from configuration:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// Define a binding definition object
|
|
116
|
+
const jumpBindingDefinition = {
|
|
117
|
+
type: 'trigger',
|
|
118
|
+
action: 'jump', // Action name as a string
|
|
119
|
+
deviceID: 'keyboard-main',
|
|
120
|
+
reader: {
|
|
121
|
+
type: 'keyboard',
|
|
122
|
+
sourceType: 'key',
|
|
123
|
+
sourceKey: 'Space'
|
|
124
|
+
},
|
|
125
|
+
options: {
|
|
126
|
+
edgeMode: 'rising'
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Register the binding from the definition
|
|
131
|
+
bindingManager.registerBindingFromDefinition(jumpBindingDefinition);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Handling Actions
|
|
135
|
+
|
|
136
|
+
Once actions and bindings are set up, you listen for action events on the event bus:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// Subscribe to the quit action
|
|
140
|
+
eventBus.subscribe('action:quit', (event) => {
|
|
141
|
+
console.log('Quit action triggered!');
|
|
142
|
+
game.quit();
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Binding Types in Detail
|
|
147
|
+
|
|
148
|
+
### Trigger Binding
|
|
149
|
+
|
|
150
|
+
The trigger binding fires a one-time event when a button state changes:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// Create a trigger binding for the Space key to jump
|
|
154
|
+
const jumpBinding = new TriggerBinding(
|
|
155
|
+
{ name: 'jump', type: 'digital' },
|
|
156
|
+
'keyboard-main',
|
|
157
|
+
new KeyboardValueReader('Space'),
|
|
158
|
+
{
|
|
159
|
+
edgeMode: 'rising', // 'rising' = when pressed, 'falling' = when released
|
|
160
|
+
threshold: 0.5 // For analog inputs, the activation threshold
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Toggle Binding
|
|
166
|
+
|
|
167
|
+
The toggle binding switches between on and off states with each activation:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
// Create a toggle binding for the C key to toggle crouch
|
|
171
|
+
const crouchBinding = new ToggleBinding(
|
|
172
|
+
{ name: 'crouch', type: 'digital' },
|
|
173
|
+
'keyboard-main',
|
|
174
|
+
new KeyboardValueReader('KeyC'),
|
|
175
|
+
{
|
|
176
|
+
initialState: false, // Start in "off" state
|
|
177
|
+
onValue: true, // Value when toggled on
|
|
178
|
+
offValue: false // Value when toggled off
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Value Binding
|
|
184
|
+
|
|
185
|
+
The value binding continuously passes input values to an action:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
// Create a value binding for the right trigger for throttle control
|
|
189
|
+
const throttleBinding = new ValueBinding(
|
|
190
|
+
{ name: 'throttle', type: 'analog', minValue: 0, maxValue: 1 },
|
|
191
|
+
'gamepad-0',
|
|
192
|
+
new GamepadValueReader('button', '7'), // Right trigger
|
|
193
|
+
{
|
|
194
|
+
scale: 1.0, // Full value when pressed
|
|
195
|
+
deadzone: 0.05, // Ignore small values
|
|
196
|
+
emitOnChange: true // Only emit when value changes
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Working with Contexts
|
|
202
|
+
|
|
203
|
+
### Registering Contexts
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// Register contexts for different game states
|
|
207
|
+
bindingManager.registerContext('gameplay', true); // true = exclusive
|
|
208
|
+
bindingManager.registerContext('menu', true); // also exclusive
|
|
209
|
+
bindingManager.registerContext('always', false); // false = non-exclusive
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Assigning Bindings to Contexts
|
|
213
|
+
|
|
214
|
+
When creating bindings, you can specify their context:
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// Create a binding that only works in the gameplay context
|
|
218
|
+
const jumpBinding = new TriggerBinding(
|
|
219
|
+
{ name: 'jump', type: 'digital' },
|
|
220
|
+
'keyboard-main',
|
|
221
|
+
new KeyboardValueReader('Space'),
|
|
222
|
+
{
|
|
223
|
+
context: 'gameplay',
|
|
224
|
+
edgeMode: 'rising'
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Activating and Deactivating Contexts
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// Start in gameplay mode
|
|
233
|
+
bindingManager.activateContext('gameplay');
|
|
234
|
+
|
|
235
|
+
// When opening a menu, switch to menu mode
|
|
236
|
+
bindingManager.activateContext('menu'); // This automatically deactivates 'gameplay' since both are exclusive
|
|
237
|
+
|
|
238
|
+
// Return to gameplay
|
|
239
|
+
bindingManager.activateContext('gameplay');
|
|
240
|
+
|
|
241
|
+
// Deactivate a specific context
|
|
242
|
+
bindingManager.deactivateContext('menu');
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Implementing Character Movement with WASD Controls
|
|
246
|
+
|
|
247
|
+
Here's a complete example of implementing WASD movement controls using digital actions:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
// 1. Register movement actions (using digital actions for directional movement)
|
|
251
|
+
bindingManager.registerAction({
|
|
252
|
+
name: 'move_forward',
|
|
253
|
+
type: 'digital'
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
bindingManager.registerAction({
|
|
257
|
+
name: 'move_backward',
|
|
258
|
+
type: 'digital'
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
bindingManager.registerAction({
|
|
262
|
+
name: 'move_left',
|
|
263
|
+
type: 'digital'
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
bindingManager.registerAction({
|
|
267
|
+
name: 'move_right',
|
|
268
|
+
type: 'digital'
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// 2. Create WASD keyboard bindings using trigger bindings
|
|
272
|
+
// Trigger bindings fire once when pressed/released, which is appropriate for digital actions
|
|
273
|
+
const wBinding = new TriggerBinding(
|
|
274
|
+
bindingManager.getAction('move_forward'),
|
|
275
|
+
'keyboard-main',
|
|
276
|
+
new KeyboardValueReader('KeyW'),
|
|
277
|
+
{ edgeMode: 'both' } // Fire on both press and release
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const sBinding = new TriggerBinding(
|
|
281
|
+
bindingManager.getAction('move_backward'),
|
|
282
|
+
'keyboard-main',
|
|
283
|
+
new KeyboardValueReader('KeyS'),
|
|
284
|
+
{ edgeMode: 'both' }
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
const aBinding = new TriggerBinding(
|
|
288
|
+
bindingManager.getAction('move_left'),
|
|
289
|
+
'keyboard-main',
|
|
290
|
+
new KeyboardValueReader('KeyA'),
|
|
291
|
+
{ edgeMode: 'both' }
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const dBinding = new TriggerBinding(
|
|
295
|
+
bindingManager.getAction('move_right'),
|
|
296
|
+
'keyboard-main',
|
|
297
|
+
new KeyboardValueReader('KeyD'),
|
|
298
|
+
{ edgeMode: 'both' }
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// 3. Register all bindings
|
|
302
|
+
bindingManager.registerBinding(wBinding);
|
|
303
|
+
bindingManager.registerBinding(sBinding);
|
|
304
|
+
bindingManager.registerBinding(aBinding);
|
|
305
|
+
bindingManager.registerBinding(dBinding);
|
|
306
|
+
|
|
307
|
+
// 4. Handle the movement actions
|
|
308
|
+
eventBus.subscribe('action:move_forward', (event) => {
|
|
309
|
+
// True when key is pressed, false when released
|
|
310
|
+
if (event.payload.value === true) {
|
|
311
|
+
player.velocity.z = -player.speed; // Move forward (negative Z in many 3D systems)
|
|
312
|
+
} else {
|
|
313
|
+
// Only zero out velocity if player isn't also moving backward
|
|
314
|
+
if (!player.isMovingBackward) {
|
|
315
|
+
player.velocity.z = 0;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Similar handlers for the other actions
|
|
321
|
+
// ...existing code...
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Alternative: Using Binding Definitions for WASD Controls
|
|
325
|
+
|
|
326
|
+
You can also set up WASD controls using binding definitions, which is more concise:
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
// Define all actions at once
|
|
330
|
+
const movementActions = [
|
|
331
|
+
{ name: 'move_forward', type: 'digital' },
|
|
332
|
+
{ name: 'move_backward', type: 'digital' },
|
|
333
|
+
{ name: 'move_left', type: 'digital' },
|
|
334
|
+
{ name: 'move_right', type: 'digital' }
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
// Register all actions
|
|
338
|
+
movementActions.forEach(action => bindingManager.registerAction(action));
|
|
339
|
+
|
|
340
|
+
// Define all bindings at once using binding definitions
|
|
341
|
+
const wasdBindings = [
|
|
342
|
+
{
|
|
343
|
+
type: 'trigger', // Use trigger binding for digital actions
|
|
344
|
+
action: 'move_forward',
|
|
345
|
+
deviceID: 'keyboard-main',
|
|
346
|
+
reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'KeyW' },
|
|
347
|
+
options: { edgeMode: 'both' } // Fire on both press and release
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
type: 'trigger',
|
|
351
|
+
action: 'move_backward',
|
|
352
|
+
deviceID: 'keyboard-main',
|
|
353
|
+
reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'KeyS' },
|
|
354
|
+
options: { edgeMode: 'both' }
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
type: 'trigger',
|
|
358
|
+
action: 'move_left',
|
|
359
|
+
deviceID: 'keyboard-main',
|
|
360
|
+
reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'KeyA' },
|
|
361
|
+
options: { edgeMode: 'both' }
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
type: 'trigger',
|
|
365
|
+
action: 'move_right',
|
|
366
|
+
deviceID: 'keyboard-main',
|
|
367
|
+
reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'KeyD' },
|
|
368
|
+
options: { edgeMode: 'both' }
|
|
369
|
+
}
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
// Register all bindings in one go
|
|
373
|
+
wasdBindings.forEach(binding => bindingManager.registerBinding(binding));
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Integration with Entity System
|
|
377
|
+
|
|
378
|
+
The entity system in SAGE allows you to define required actions at the entity level, creating a clean way to associate input controls with specific game objects.
|
|
379
|
+
|
|
380
|
+
### Defining Entity Actions
|
|
381
|
+
|
|
382
|
+
When defining an entity, you can specify the actions it requires:
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
import { GameEntityDefinition } from './src/interfaces/entity.ts';
|
|
386
|
+
|
|
387
|
+
// Define a player entity with movement behaviors
|
|
388
|
+
const playerEntityDefinition: GameEntityDefinition = {
|
|
389
|
+
type: 'player',
|
|
390
|
+
defaultState: {
|
|
391
|
+
position: { x: 0, y: 0, z: 0 },
|
|
392
|
+
velocity: { x: 0, y: 0, z: 0 },
|
|
393
|
+
speed: 5,
|
|
394
|
+
isMovingForward: false,
|
|
395
|
+
isMovingBackward: false,
|
|
396
|
+
isMovingLeft: false,
|
|
397
|
+
isMovingRight: false
|
|
398
|
+
},
|
|
399
|
+
behaviors: [
|
|
400
|
+
PlayerMovementBehavior,
|
|
401
|
+
PlayerAnimationBehavior
|
|
402
|
+
],
|
|
403
|
+
// Define actions needed by this entity (digital actions for directional movement)
|
|
404
|
+
actions: [
|
|
405
|
+
{ name: 'move_forward', type: 'digital' },
|
|
406
|
+
{ name: 'move_backward', type: 'digital' },
|
|
407
|
+
{ name: 'move_left', type: 'digital' },
|
|
408
|
+
{ name: 'move_right', type: 'digital' },
|
|
409
|
+
{ name: 'jump', type: 'digital' }
|
|
410
|
+
]
|
|
411
|
+
};
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Creating Player Movement Behavior
|
|
415
|
+
|
|
416
|
+
The entity system works with behaviors that respond to action events:
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
import { GameEntityBehavior } from './src/classes/entity.ts';
|
|
420
|
+
|
|
421
|
+
class PlayerMovementBehavior extends GameEntityBehavior<{
|
|
422
|
+
position: { x: number, y: number, z: number },
|
|
423
|
+
velocity: { x: number, y: number, z: number },
|
|
424
|
+
speed: number,
|
|
425
|
+
isMovingForward: boolean,
|
|
426
|
+
isMovingBackward: boolean,
|
|
427
|
+
isMovingLeft: boolean,
|
|
428
|
+
isMovingRight: boolean
|
|
429
|
+
}> {
|
|
430
|
+
initialize(entity) {
|
|
431
|
+
// Subscribe to movement actions
|
|
432
|
+
this.eventBus.subscribe('action:move_forward', (event) => {
|
|
433
|
+
const isActive = event.payload.value === true;
|
|
434
|
+
entity.state.isMovingForward = isActive;
|
|
435
|
+
|
|
436
|
+
if (isActive) {
|
|
437
|
+
entity.state.velocity.z = -entity.state.speed;
|
|
438
|
+
} else if (!entity.state.isMovingBackward) {
|
|
439
|
+
entity.state.velocity.z = 0;
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
this.eventBus.subscribe('action:move_backward', (event) => {
|
|
444
|
+
const isActive = event.payload.value === true;
|
|
445
|
+
entity.state.isMovingBackward = isActive;
|
|
446
|
+
|
|
447
|
+
if (isActive) {
|
|
448
|
+
entity.state.velocity.z = entity.state.speed;
|
|
449
|
+
} else if (!entity.state.isMovingForward) {
|
|
450
|
+
entity.state.velocity.z = 0;
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
this.eventBus.subscribe('action:move_left', (event) => {
|
|
455
|
+
const isActive = event.payload.value === true;
|
|
456
|
+
entity.state.isMovingLeft = isActive;
|
|
457
|
+
|
|
458
|
+
if (isActive) {
|
|
459
|
+
entity.state.velocity.x = -entity.state.speed;
|
|
460
|
+
} else if (!entity.state.isMovingRight) {
|
|
461
|
+
entity.state.velocity.x = 0;
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
this.eventBus.subscribe('action:move_right', (event) => {
|
|
466
|
+
const isActive = event.payload.value === true;
|
|
467
|
+
entity.state.isMovingRight = isActive;
|
|
468
|
+
|
|
469
|
+
if (isActive) {
|
|
470
|
+
entity.state.velocity.x = entity.state.speed;
|
|
471
|
+
} else if (!entity.state.isMovingLeft) {
|
|
472
|
+
entity.state.velocity.x = 0;
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
this.eventBus.subscribe('action:jump', () => {
|
|
477
|
+
// Implement jump logic
|
|
478
|
+
if (entity.state.onGround) {
|
|
479
|
+
entity.state.velocity.y = entity.state.jumpForce;
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
update(entity, deltaTime) {
|
|
485
|
+
// Apply velocity to position
|
|
486
|
+
entity.state.position.x += entity.state.velocity.x * deltaTime;
|
|
487
|
+
entity.state.position.y += entity.state.velocity.y * deltaTime;
|
|
488
|
+
entity.state.position.z += entity.state.velocity.z * deltaTime;
|
|
489
|
+
|
|
490
|
+
// Apply physics like gravity, friction, etc.
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
## Saving and Loading Bindings
|
|
496
|
+
|
|
497
|
+
The binding system provides methods to export and import configurations, allowing for customizable controls that persist between game sessions.
|
|
498
|
+
|
|
499
|
+
### Exporting Configuration
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
// Get current configuration as a serializable object
|
|
503
|
+
const savedConfig = bindingManager.exportConfiguration();
|
|
504
|
+
|
|
505
|
+
// You can now save this object however you prefer
|
|
506
|
+
localStorage.setItem('gameControls', JSON.stringify(savedConfig));
|
|
507
|
+
// or
|
|
508
|
+
saveToFile('controls.json', JSON.stringify(savedConfig));
|
|
509
|
+
// or
|
|
510
|
+
await api.saveUserPreferences(savedConfig);
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Importing Configuration
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
// Load the configuration from your storage
|
|
517
|
+
const loadedConfig = JSON.parse(localStorage.getItem('gameControls'));
|
|
518
|
+
|
|
519
|
+
// Apply the configuration to the binding manager
|
|
520
|
+
bindingManager.importConfiguration(loadedConfig);
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Complete Example: Saving and Loading Controls
|
|
524
|
+
|
|
525
|
+
Here's a complete example of how to implement customizable controls with saving/loading:
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
class GameControlsManager {
|
|
529
|
+
private bindingManager: BindingManager;
|
|
530
|
+
|
|
531
|
+
constructor(eventBus: GameEventBus) {
|
|
532
|
+
this.bindingManager = new BindingManager(eventBus);
|
|
533
|
+
this.setupDefaultControls();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Set up default control scheme
|
|
537
|
+
setupDefaultControls() {
|
|
538
|
+
// Register default actions
|
|
539
|
+
const defaultActions = [
|
|
540
|
+
{ name: 'move_forward', type: 'digital' },
|
|
541
|
+
{ name: 'move_backward', type: 'digital' },
|
|
542
|
+
{ name: 'move_left', type: 'digital' },
|
|
543
|
+
{ name: 'move_right', type: 'digital' },
|
|
544
|
+
{ name: 'jump', type: 'digital' },
|
|
545
|
+
{ name: 'interact', type: 'digital' },
|
|
546
|
+
{ name: 'pause', type: 'digital' }
|
|
547
|
+
];
|
|
548
|
+
|
|
549
|
+
// Register all default actions
|
|
550
|
+
defaultActions.forEach(action => this.bindingManager.registerAction(action));
|
|
551
|
+
|
|
552
|
+
// Create contexts
|
|
553
|
+
this.bindingManager.registerContext('gameplay', true);
|
|
554
|
+
this.bindingManager.registerContext('menu', true);
|
|
555
|
+
|
|
556
|
+
// Define default bindings
|
|
557
|
+
const defaultBindings = [
|
|
558
|
+
// Movement - WASD
|
|
559
|
+
{
|
|
560
|
+
type: 'value',
|
|
561
|
+
action: 'move_forward',
|
|
562
|
+
deviceID: 'keyboard-main',
|
|
563
|
+
reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'KeyW' },
|
|
564
|
+
context: 'gameplay',
|
|
565
|
+
options: { onValue: true, offValue: false }
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
type: 'value',
|
|
569
|
+
action: 'move_backward',
|
|
570
|
+
deviceID: 'keyboard-main',
|
|
571
|
+
reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'KeyS' },
|
|
572
|
+
context: 'gameplay',
|
|
573
|
+
options: { onValue: true, offValue: false }
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
type: 'value',
|
|
577
|
+
action: 'move_left',
|
|
578
|
+
deviceID: 'keyboard-main',
|
|
579
|
+
reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'KeyA' },
|
|
580
|
+
context: 'gameplay',
|
|
581
|
+
options: { onValue: true, offValue: false }
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
type: 'value',
|
|
585
|
+
action: 'move_right',
|
|
586
|
+
deviceID: 'keyboard-main',
|
|
587
|
+
reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'KeyD' },
|
|
588
|
+
context: 'gameplay',
|
|
589
|
+
options: { onValue: true, offValue: false }
|
|
590
|
+
},
|
|
591
|
+
// Jump - Space
|
|
592
|
+
{
|
|
593
|
+
type: 'trigger',
|
|
594
|
+
action: 'jump',
|
|
595
|
+
deviceID: 'keyboard-main',
|
|
596
|
+
reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'Space' },
|
|
597
|
+
context: 'gameplay',
|
|
598
|
+
options: { edgeMode: 'rising' }
|
|
599
|
+
},
|
|
600
|
+
// Interact - E
|
|
601
|
+
{
|
|
602
|
+
type: 'trigger',
|
|
603
|
+
action: 'interact',
|
|
604
|
+
deviceID: 'keyboard-main',
|
|
605
|
+
reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'KeyE' },
|
|
606
|
+
context: 'gameplay',
|
|
607
|
+
options: { edgeMode: 'rising' }
|
|
608
|
+
},
|
|
609
|
+
// Pause - Escape (works in both gameplay and menu contexts)
|
|
610
|
+
{
|
|
611
|
+
type: 'trigger',
|
|
612
|
+
action: 'pause',
|
|
613
|
+
deviceID: 'keyboard-main',
|
|
614
|
+
reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'Escape' },
|
|
615
|
+
options: { edgeMode: 'rising' }
|
|
616
|
+
}
|
|
617
|
+
];
|
|
618
|
+
|
|
619
|
+
// Register all default bindings
|
|
620
|
+
defaultBindings.forEach(binding => this.bindingManager.registerBindingFromDefinition(binding));
|
|
621
|
+
|
|
622
|
+
// Start in gameplay context
|
|
623
|
+
this.bindingManager.activateContext('gameplay');
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Load control configuration from storage
|
|
627
|
+
loadControls() {
|
|
628
|
+
try {
|
|
629
|
+
const savedConfig = localStorage.getItem('gameControls');
|
|
630
|
+
if (savedConfig) {
|
|
631
|
+
const config = JSON.parse(savedConfig);
|
|
632
|
+
this.bindingManager.importConfiguration(config);
|
|
633
|
+
console.log('Custom controls loaded successfully');
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
636
|
+
} catch (err) {
|
|
637
|
+
console.error('Failed to load custom controls:', err);
|
|
638
|
+
}
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Save current control configuration to storage
|
|
643
|
+
saveControls() {
|
|
644
|
+
try {
|
|
645
|
+
const config = this.bindingManager.exportConfiguration();
|
|
646
|
+
localStorage.setItem('gameControls', JSON.stringify(config));
|
|
647
|
+
console.log('Controls saved successfully');
|
|
648
|
+
return true;
|
|
649
|
+
} catch (err) {
|
|
650
|
+
console.error('Failed to save controls:', err);
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Reset to default controls
|
|
656
|
+
resetToDefaults() {
|
|
657
|
+
// Clear existing bindings and re-setup defaults
|
|
658
|
+
localStorage.removeItem('gameControls');
|
|
659
|
+
// Re-initialize binding manager
|
|
660
|
+
this.bindingManager.importConfiguration({
|
|
661
|
+
actions: [],
|
|
662
|
+
bindings: [],
|
|
663
|
+
contexts: []
|
|
664
|
+
});
|
|
665
|
+
this.setupDefaultControls();
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Rebind a specific action to a new key
|
|
669
|
+
rebindAction(actionName: string, newKey: string, context?: string) {
|
|
670
|
+
// Remove existing bindings for this action in this context
|
|
671
|
+
this.bindingManager.unregisterBindings(actionName, context || null);
|
|
672
|
+
|
|
673
|
+
// Create new binding
|
|
674
|
+
const action = this.bindingManager.getAction(actionName);
|
|
675
|
+
if (!action) {
|
|
676
|
+
console.error(`Cannot rebind: action "${actionName}" not found`);
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Create and register the new binding
|
|
681
|
+
const bindingDef = {
|
|
682
|
+
type: action.type === 'digital' ? 'value' : 'value',
|
|
683
|
+
action,
|
|
684
|
+
deviceID: 'keyboard-main',
|
|
685
|
+
reader: {
|
|
686
|
+
type: 'keyboard',
|
|
687
|
+
sourceType: 'key',
|
|
688
|
+
sourceKey: newKey
|
|
689
|
+
},
|
|
690
|
+
context,
|
|
691
|
+
options: action.type === 'digital'
|
|
692
|
+
? { onValue: true, offValue: false }
|
|
693
|
+
: {}
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
this.bindingManager.registerBindingFromDefinition(bindingDef);
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Get the binding manager for direct access if needed
|
|
701
|
+
getBindingManager() {
|
|
702
|
+
return this.bindingManager;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Usage:
|
|
707
|
+
const controls = new GameControlsManager(eventBus);
|
|
708
|
+
|
|
709
|
+
// On game startup, try to load saved controls or fall back to defaults
|
|
710
|
+
if (!controls.loadControls()) {
|
|
711
|
+
console.log('No saved controls found, using defaults');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// When user changes a keybinding in settings menu
|
|
715
|
+
controls.rebindAction('jump', 'KeyF', 'gameplay');
|
|
716
|
+
controls.saveControls();
|
|
717
|
+
|
|
718
|
+
// When user wants to reset all controls
|
|
719
|
+
controls.resetToDefaults();
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
### Storage Considerations
|
|
723
|
+
|
|
724
|
+
The binding system is deliberately agnostic about how you store configurations. It simply provides methods to convert the binding state to and from plain JavaScript objects. This gives you the flexibility to:
|
|
725
|
+
|
|
726
|
+
- Save to browser storage like localStorage
|
|
727
|
+
- Save to files on disk
|
|
728
|
+
- Store in databases
|
|
729
|
+
- Sync with cloud services
|
|
730
|
+
- Embed in user profiles
|
|
731
|
+
|
|
732
|
+
## Advanced Usage: Multiple Input Methods
|
|
733
|
+
|
|
734
|
+
One of the strengths of the binding system is support for multiple input devices. Here's how to set up the same action with different input methods:
|
|
735
|
+
|
|
736
|
+
```typescript
|
|
737
|
+
// Jump action with keyboard
|
|
738
|
+
const keyboardJump = new TriggerBinding(
|
|
739
|
+
{ name: 'jump', type: 'digital' },
|
|
740
|
+
'keyboard-main',
|
|
741
|
+
new KeyboardValueReader('Space')
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
// Jump action with gamepad
|
|
745
|
+
const gamepadJump = new TriggerBinding(
|
|
746
|
+
{ name: 'jump', type: 'digital' },
|
|
747
|
+
'gamepad-0',
|
|
748
|
+
new GamepadValueReader('button', '0') // A/Cross button
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
// Movement with gamepad analog stick (using digital actions for clarity)
|
|
752
|
+
// Map left stick up to move_forward action
|
|
753
|
+
const gamepadMoveForward = new ValueBinding(
|
|
754
|
+
{ name: 'move_forward', type: 'digital' },
|
|
755
|
+
'gamepad-0',
|
|
756
|
+
new GamepadValueReader('axis', '1'), // Left stick Y-axis
|
|
757
|
+
{
|
|
758
|
+
deadzone: 0.2, // Prevent drift
|
|
759
|
+
threshold: -0.5, // Activate when stick pushed upward (negative Y)
|
|
760
|
+
invert: true // Invert because pushing up gives negative values
|
|
761
|
+
}
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
// Map left stick down to move_backward action
|
|
765
|
+
const gamepadMoveBackward = new ValueBinding(
|
|
766
|
+
{ name: 'move_backward', type: 'digital' },
|
|
767
|
+
'gamepad-0',
|
|
768
|
+
new GamepadValueReader('axis', '1'), // Left stick Y-axis
|
|
769
|
+
{
|
|
770
|
+
deadzone: 0.2, // Prevent drift
|
|
771
|
+
threshold: 0.5 // Activate when stick pushed downward (positive Y)
|
|
772
|
+
}
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
// Register all bindings
|
|
776
|
+
bindingManager.registerBinding(keyboardJump);
|
|
777
|
+
bindingManager.registerBinding(gamepadJump);
|
|
778
|
+
bindingManager.registerBinding(gamepadMoveForward);
|
|
779
|
+
bindingManager.registerBinding(gamepadMoveBackward);
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
### Supporting Multiple Input Devices with Binding Definitions
|
|
783
|
+
|
|
784
|
+
Using binding definitions makes it easier to support multiple input methods:
|
|
785
|
+
|
|
786
|
+
```typescript
|
|
787
|
+
// Define action once
|
|
788
|
+
bindingManager.registerAction({ name: 'jump', type: 'digital' });
|
|
789
|
+
|
|
790
|
+
// Define bindings for multiple devices
|
|
791
|
+
const jumpBindings = [
|
|
792
|
+
{
|
|
793
|
+
// Keyboard binding
|
|
794
|
+
type: 'trigger',
|
|
795
|
+
action: 'jump', // Action name as a string
|
|
796
|
+
deviceID: 'keyboard-main',
|
|
797
|
+
reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'Space' },
|
|
798
|
+
context: 'gameplay'
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
// Gamepad A button binding
|
|
802
|
+
type: 'trigger',
|
|
803
|
+
action: 'jump', // Action name as a string
|
|
804
|
+
deviceID: 'gamepad-0',
|
|
805
|
+
reader: { type: 'gamepad', sourceType: 'button', sourceKey: '0' },
|
|
806
|
+
context: 'gameplay'
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
// Mouse left button binding
|
|
810
|
+
type: 'trigger',
|
|
811
|
+
action: 'jump', // Action name as a string
|
|
812
|
+
deviceID: 'mouse-main',
|
|
813
|
+
reader: { type: 'mouse', sourceType: 'button', sourceKey: '0' },
|
|
814
|
+
context: 'gameplay'
|
|
815
|
+
}
|
|
816
|
+
];
|
|
817
|
+
|
|
818
|
+
// Register all bindings
|
|
819
|
+
jumpBindings.forEach(binding => bindingManager.registerBindingFromDefinition(binding));
|
|
820
|
+
```
|