@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,706 @@
|
|
|
1
|
+
# Behavior System Guide
|
|
2
|
+
|
|
3
|
+
## Introduction
|
|
4
|
+
|
|
5
|
+
In the SAGE engine, behaviors are the "personality modules" that give entities their abilities and define how they interact with the world. Think of behaviors like special power discs from Tron - each one gives your entity unique capabilities that can be combined in creative ways.
|
|
6
|
+
|
|
7
|
+
This guide will walk you through creating and using behaviors, from basic concepts to advanced techniques. It serves as a detailed complement to the [Entity System Guide](entity_system.md), diving deeper into the behavior implementation specifics.
|
|
8
|
+
|
|
9
|
+
## Behaviors: The Building Blocks of Functionality
|
|
10
|
+
|
|
11
|
+
A behavior in SAGE is a reusable component that:
|
|
12
|
+
- Has a unique name
|
|
13
|
+
- Listens for specific events
|
|
14
|
+
- Processes those events to update entity state
|
|
15
|
+
- May perform updates each frame
|
|
16
|
+
- Can emit new events for others to react to
|
|
17
|
+
|
|
18
|
+
While the [Entity System Guide](entity_system.md) provides an overview of how behaviors fit into the larger picture, this guide focuses specifically on implementing and combining behaviors effectively.
|
|
19
|
+
|
|
20
|
+
## Behavior Lifecycle
|
|
21
|
+
|
|
22
|
+
When a behavior is attached to an entity, it goes through a specific lifecycle:
|
|
23
|
+
|
|
24
|
+
1. **Initialization**: The behavior is instantiated and receives a reference to its parent entity
|
|
25
|
+
2. **Subscription**: The behavior subscribes to any events it's interested in
|
|
26
|
+
3. **Active Phase**: The behavior responds to events and executes update methods each frame
|
|
27
|
+
4. **Detachment/Cleanup**: When removed, the behavior unsubscribes from events and cleans up any resources
|
|
28
|
+
|
|
29
|
+
All behaviors automatically handle this lifecycle, but it's important to understand it when developing custom behaviors.
|
|
30
|
+
|
|
31
|
+
## Creating Your First Behavior
|
|
32
|
+
|
|
33
|
+
Let's create a simple behavior that makes an entity take damage and respond to healing:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { GameEntityBehavior, GameEvent } from '@skewedaspect/sage';
|
|
37
|
+
|
|
38
|
+
// Define what state properties this behavior needs
|
|
39
|
+
interface HealthState {
|
|
40
|
+
currentHealth: number;
|
|
41
|
+
maxHealth: number;
|
|
42
|
+
isAlive: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class HealthBehavior extends GameEntityBehavior<HealthState> {
|
|
46
|
+
// The unique name of this behavior
|
|
47
|
+
name = 'HealthBehavior';
|
|
48
|
+
|
|
49
|
+
// The event types this behavior listens for
|
|
50
|
+
eventSubscriptions = ['entity:damage', 'entity:heal'];
|
|
51
|
+
|
|
52
|
+
// Handle incoming events
|
|
53
|
+
processEvent(event: GameEvent, state: HealthState): boolean {
|
|
54
|
+
if (event.type === 'entity:damage') {
|
|
55
|
+
const damage = event.payload?.amount || 0;
|
|
56
|
+
|
|
57
|
+
// Reduce health by damage amount
|
|
58
|
+
state.currentHealth = Math.max(0, state.currentHealth - damage);
|
|
59
|
+
|
|
60
|
+
// Check if entity is now dead
|
|
61
|
+
if (state.currentHealth === 0 && state.isAlive) {
|
|
62
|
+
state.isAlive = false;
|
|
63
|
+
|
|
64
|
+
// Emit a death event for others to respond to
|
|
65
|
+
this.$emit({
|
|
66
|
+
type: 'entity:died',
|
|
67
|
+
payload: {
|
|
68
|
+
causeOfDeath: 'damage',
|
|
69
|
+
finalBlow: event.payload // Include info about what killed it
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return true; // We handled this event
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (event.type === 'entity:heal') {
|
|
78
|
+
const healAmount = event.payload?.amount || 0;
|
|
79
|
+
|
|
80
|
+
// Only heal if alive
|
|
81
|
+
if (state.isAlive) {
|
|
82
|
+
// Add health, but don't exceed max
|
|
83
|
+
state.currentHealth = Math.min(
|
|
84
|
+
state.maxHealth,
|
|
85
|
+
state.currentHealth + healAmount
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Let others know healing occurred
|
|
89
|
+
this.$emit({
|
|
90
|
+
type: 'entity:healed',
|
|
91
|
+
payload: { amount: healAmount }
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return true; // We handled this event
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return false; // We didn't handle this event
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Optional: update method runs each frame
|
|
102
|
+
update(dt: number, state: HealthState): void {
|
|
103
|
+
// We could add regeneration over time here if desired
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Core Behavior Methods
|
|
109
|
+
|
|
110
|
+
Every behavior must implement these key methods or properties:
|
|
111
|
+
|
|
112
|
+
### 1. The `name` Property
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
name = 'MovementBehavior';
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Each behavior needs a unique name that identifies it. This name is used when:
|
|
119
|
+
- Detaching behaviors from entities
|
|
120
|
+
- Debugging behavior issues
|
|
121
|
+
- Managing behavior interactions
|
|
122
|
+
|
|
123
|
+
### 2. The `eventSubscriptions` Property
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
eventSubscriptions = ['input:jump', 'physics:collision'];
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
This array tells the system what events your behavior wants to listen for. When any of these events occur, your `processEvent` method will be called.
|
|
130
|
+
|
|
131
|
+
### 3. The `processEvent` Method
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
processEvent(event: GameEvent, state: any): boolean {
|
|
135
|
+
// Handle the event
|
|
136
|
+
if (event.type === 'input:jump') {
|
|
137
|
+
// Process jump input
|
|
138
|
+
state.velocity.y = state.jumpPower;
|
|
139
|
+
return true; // We handled this event
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return false; // We didn't handle this event
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
This method is where your behavior responds to events. It receives:
|
|
147
|
+
- The event object with type and payload
|
|
148
|
+
- The entity's state object
|
|
149
|
+
- Returns `true` if the event was handled, `false` otherwise
|
|
150
|
+
|
|
151
|
+
The return value is important - if you return `true`, no other behaviors will process this event. This allows for "event filtering" where one behavior can intercept events before they reach others.
|
|
152
|
+
|
|
153
|
+
### 4. The `update` Method (Optional)
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
update(dt: number, state: any): void {
|
|
157
|
+
// Update entity state based on time
|
|
158
|
+
state.position.x += state.velocity.x * dt;
|
|
159
|
+
state.position.y += state.velocity.y * dt;
|
|
160
|
+
|
|
161
|
+
// Apply gravity
|
|
162
|
+
state.velocity.y -= 9.8 * dt;
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
This method runs every frame and lets your behavior apply continuous changes to the entity. The `dt` parameter represents the time (in seconds) since the last update, which helps create smooth, frame-rate independent movement.
|
|
167
|
+
|
|
168
|
+
## Using Your Behavior
|
|
169
|
+
|
|
170
|
+
To use this behavior, you need to:
|
|
171
|
+
|
|
172
|
+
1. Include it in an entity definition
|
|
173
|
+
2. Ensure the entity state includes the properties it needs
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
// Register an entity definition with our behavior
|
|
177
|
+
gameEngine.managers.entityManager.registerEntityDefinition({
|
|
178
|
+
type: 'character:hero',
|
|
179
|
+
defaultState: {
|
|
180
|
+
name: 'Frodo',
|
|
181
|
+
currentHealth: 100, // Required by HealthBehavior
|
|
182
|
+
maxHealth: 100, // Required by HealthBehavior
|
|
183
|
+
isAlive: true, // Required by HealthBehavior
|
|
184
|
+
position: { x: 0, y: 0, z: 0 }
|
|
185
|
+
},
|
|
186
|
+
behaviors: [
|
|
187
|
+
HealthBehavior, // Add our health behavior
|
|
188
|
+
MovementBehavior // Add other behaviors as needed
|
|
189
|
+
]
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Create the entity with additional initial state
|
|
193
|
+
const hero = gameEngine.managers.entityManager.createEntity('character:hero', { currentHealth: 80 });
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Integrating with the Entity System
|
|
197
|
+
|
|
198
|
+
While we've seen how to create individual behaviors, the real power comes from how they integrate with the entity system. As described in the [Entity System Guide](entity_system.md), behaviors are the functional building blocks that power entities.
|
|
199
|
+
|
|
200
|
+
Here's how behaviors connect with the broader entity architecture:
|
|
201
|
+
|
|
202
|
+
1. **Entity Definitions**: When you register an entity type, you specify which behaviors it should have
|
|
203
|
+
|
|
204
|
+
2. **State Sharing**: All behaviors attached to the same entity share access to that entity's state object
|
|
205
|
+
|
|
206
|
+
3. **Event Flow**: When an event targets an entity, it flows through all behaviors in the order they were attached
|
|
207
|
+
|
|
208
|
+
4. **Behavior Dependencies**: Some behaviors may depend on others (like an animation behavior responding to a movement behavior's changes)
|
|
209
|
+
|
|
210
|
+
## Behavior Composition
|
|
211
|
+
|
|
212
|
+
The true power of behaviors comes from combining them. Let's see how multiple behaviors can work together:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// A behavior for inventory management
|
|
216
|
+
class InventoryBehavior extends GameEntityBehavior<{ items: any[] }> {
|
|
217
|
+
name = 'InventoryBehavior';
|
|
218
|
+
eventSubscriptions = ['item:pickup', 'item:use'];
|
|
219
|
+
|
|
220
|
+
processEvent(event: GameEvent, state: any): boolean {
|
|
221
|
+
if (event.type === 'item:pickup') {
|
|
222
|
+
state.items.push(event.payload.item);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (event.type === 'item:use' && event.payload.itemIndex < state.items.length) {
|
|
227
|
+
const item = state.items[event.payload.itemIndex];
|
|
228
|
+
|
|
229
|
+
// Emit an event that something else might listen for
|
|
230
|
+
this.$emit({
|
|
231
|
+
type: 'item:effect',
|
|
232
|
+
payload: { item }
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// If it's a one-time use item, remove it
|
|
236
|
+
if (item.consumable) {
|
|
237
|
+
state.items.splice(event.payload.itemIndex, 1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// A behavior to handle potion effects
|
|
248
|
+
class PotionEffectBehavior extends GameEntityBehavior {
|
|
249
|
+
name = 'PotionEffectBehavior';
|
|
250
|
+
eventSubscriptions = ['item:effect'];
|
|
251
|
+
|
|
252
|
+
processEvent(event: GameEvent, state: any): boolean {
|
|
253
|
+
if (event.type === 'item:effect' && event.payload.item.type === 'potion') {
|
|
254
|
+
const potion = event.payload.item;
|
|
255
|
+
|
|
256
|
+
if (potion.effect === 'healing') {
|
|
257
|
+
// Re-emit as a healing event that HealthBehavior will handle
|
|
258
|
+
this.$emit({
|
|
259
|
+
type: 'entity:heal',
|
|
260
|
+
payload: { amount: potion.power }
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Now, when a character uses a healing potion:
|
|
273
|
+
1. `InventoryBehavior` handles the item use and emits an `item:effect` event
|
|
274
|
+
2. `PotionEffectBehavior` receives that event, sees it's a healing potion, and emits `entity:heal`
|
|
275
|
+
3. `HealthBehavior` receives the `entity:heal` event and increases health accordingly
|
|
276
|
+
|
|
277
|
+
All this happens without any of these behaviors needing to know about each other directly!
|
|
278
|
+
|
|
279
|
+
## Advanced Behavior Techniques
|
|
280
|
+
|
|
281
|
+
### Dynamic Behavior Attachment
|
|
282
|
+
|
|
283
|
+
Behaviors can be attached or detached at runtime:
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
// Give a character temporary invincibility
|
|
287
|
+
const invincibilityBehavior = new InvincibilityBehavior();
|
|
288
|
+
hero.attachBehavior(invincibilityBehavior);
|
|
289
|
+
|
|
290
|
+
// Later, remove the behavior
|
|
291
|
+
setTimeout(() => {
|
|
292
|
+
hero.detachBehavior('InvincibilityBehavior');
|
|
293
|
+
console.log("Your star power has worn off!");
|
|
294
|
+
}, 10000);
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
This dynamic ability to modify entity capabilities allows for:
|
|
298
|
+
- Power-ups and temporary abilities
|
|
299
|
+
- Status effects (like being stunned or poisoned)
|
|
300
|
+
- Progressive character development (learning new skills)
|
|
301
|
+
- Context-sensitive behaviors (different abilities in water vs. land)
|
|
302
|
+
|
|
303
|
+
### State Dependencies
|
|
304
|
+
|
|
305
|
+
When designing behaviors, be explicit about what state properties they need:
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
// This behavior needs very specific state structure
|
|
309
|
+
class AiBehavior extends GameEntityBehavior<{
|
|
310
|
+
target: { id: string; } | null;
|
|
311
|
+
aiState: 'idle' | 'chase' | 'attack' | 'flee';
|
|
312
|
+
lastDecisionTime: number;
|
|
313
|
+
position: { x: number; y: number; z: number; };
|
|
314
|
+
}> {
|
|
315
|
+
name = 'AiBehavior';
|
|
316
|
+
// ...implementation...
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Using TypeScript interfaces as shown above creates a contract that:
|
|
321
|
+
- Documents what state your behavior requires
|
|
322
|
+
- Provides type safety and autocomplete when accessing state properties
|
|
323
|
+
- Makes it clear to other developers what your behavior depends on
|
|
324
|
+
|
|
325
|
+
### Behavior Initialization and Cleanup
|
|
326
|
+
|
|
327
|
+
For behaviors that need to set up resources or perform cleanup:
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
class NetworkBehavior extends GameEntityBehavior {
|
|
331
|
+
name = 'NetworkBehavior';
|
|
332
|
+
private connection: WebSocket | null = null;
|
|
333
|
+
|
|
334
|
+
// Called when attached to an entity
|
|
335
|
+
onAttached(): void {
|
|
336
|
+
super.onAttached();
|
|
337
|
+
|
|
338
|
+
// Set up network connection
|
|
339
|
+
this.connection = new WebSocket('wss://game-server.example.com');
|
|
340
|
+
this.connection.onmessage = this.handleMessage.bind(this);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Called when detached from an entity
|
|
344
|
+
onDetached(): void {
|
|
345
|
+
// Clean up network resources
|
|
346
|
+
if (this.connection) {
|
|
347
|
+
this.connection.close();
|
|
348
|
+
this.connection = null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
super.onDetached();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private handleMessage(msg: MessageEvent): void {
|
|
355
|
+
// Process network messages
|
|
356
|
+
// ...
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Always remember to call the parent class methods (`super.onAttached()` and `super.onDetached()`) to ensure proper behavior lifecycle management.
|
|
362
|
+
|
|
363
|
+
### Communication Patterns
|
|
364
|
+
|
|
365
|
+
Behaviors can communicate in several ways:
|
|
366
|
+
|
|
367
|
+
1. **Direct State Modification**: Behaviors that share an entity can access the same state
|
|
368
|
+
2. **Event Broadcasting**: Emit events that other behaviors respond to
|
|
369
|
+
3. **Entity References**: Store references to other entities in state
|
|
370
|
+
|
|
371
|
+
Each approach has different tradeoffs:
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
// Direct state modification - simple but creates tight coupling
|
|
375
|
+
class MovementBehavior extends GameEntityBehavior {
|
|
376
|
+
update(dt: number, state: any): void {
|
|
377
|
+
state.position.x += state.velocity.x * dt;
|
|
378
|
+
// Animation system can read directly from state
|
|
379
|
+
state.isMoving = Math.abs(state.velocity.x) > 0.1;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
class AnimationBehavior extends GameEntityBehavior {
|
|
384
|
+
update(dt: number, state: any): void {
|
|
385
|
+
// Directly reads state set by movement behavior
|
|
386
|
+
if (state.isMoving) {
|
|
387
|
+
playAnimation('walk');
|
|
388
|
+
} else {
|
|
389
|
+
playAnimation('idle');
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Event broadcasting - more decoupled but less direct
|
|
395
|
+
class MovementBehavior extends GameEntityBehavior {
|
|
396
|
+
update(dt: number, state: any): void {
|
|
397
|
+
const oldX = state.position.x;
|
|
398
|
+
state.position.x += state.velocity.x * dt;
|
|
399
|
+
|
|
400
|
+
// Emit event only when movement state changes
|
|
401
|
+
const isMovingNow = Math.abs(state.velocity.x) > 0.1;
|
|
402
|
+
if (isMovingNow !== state.isMoving) {
|
|
403
|
+
state.isMoving = isMovingNow;
|
|
404
|
+
this.$emit({
|
|
405
|
+
type: 'entity:movementChanged',
|
|
406
|
+
payload: { isMoving: state.isMoving }
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
class AnimationBehavior extends GameEntityBehavior {
|
|
413
|
+
eventSubscriptions = ['entity:movementChanged'];
|
|
414
|
+
|
|
415
|
+
processEvent(event: GameEvent, state: any): boolean {
|
|
416
|
+
if (event.type === 'entity:movementChanged') {
|
|
417
|
+
if (event.payload.isMoving) {
|
|
418
|
+
playAnimation('walk');
|
|
419
|
+
} else {
|
|
420
|
+
playAnimation('idle');
|
|
421
|
+
}
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Behavior Priorities
|
|
430
|
+
|
|
431
|
+
The order behaviors are attached matters! Events are processed in attachment order, and any behavior can stop further processing by returning `true` from `processEvent`.
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
// Shield behavior might intercept damage before it reaches health
|
|
435
|
+
const entityDef = {
|
|
436
|
+
type: 'character:tank',
|
|
437
|
+
initialState: { /* ... */ },
|
|
438
|
+
behaviors: [
|
|
439
|
+
ShieldBehavior, // First chance to handle damage
|
|
440
|
+
ArmorBehavior, // Second chance to reduce damage
|
|
441
|
+
HealthBehavior // Finally, apply any remaining damage
|
|
442
|
+
]
|
|
443
|
+
};
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
This ordering enables powerful patterns like:
|
|
447
|
+
- Damage reduction chains (shields → armor → health)
|
|
448
|
+
- Input filtering (disable player control when stunned)
|
|
449
|
+
- Event transformation (convert raw inputs to game commands)
|
|
450
|
+
|
|
451
|
+
## Specialized Behavior Types
|
|
452
|
+
|
|
453
|
+
Beyond basic behaviors, you can create specialized types for common tasks:
|
|
454
|
+
|
|
455
|
+
### Physics Behaviors
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
class PhysicsBodyBehavior extends GameEntityBehavior {
|
|
459
|
+
name = 'PhysicsBodyBehavior';
|
|
460
|
+
private body: PhysicsBody | null = null;
|
|
461
|
+
|
|
462
|
+
onAttached(): void {
|
|
463
|
+
super.onAttached();
|
|
464
|
+
|
|
465
|
+
// Create physics body in the physics engine
|
|
466
|
+
this.body = physicsEngine.createBody({
|
|
467
|
+
type: 'dynamic',
|
|
468
|
+
position: this.entity.state.position,
|
|
469
|
+
mass: this.entity.state.mass || 1
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
update(dt: number, state: any): void {
|
|
474
|
+
if (this.body) {
|
|
475
|
+
// Sync physics engine position to entity
|
|
476
|
+
const pos = this.body.getPosition();
|
|
477
|
+
state.position.x = pos.x;
|
|
478
|
+
state.position.y = pos.y;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
onDetached(): void {
|
|
483
|
+
// Remove body from physics engine
|
|
484
|
+
if (this.body) {
|
|
485
|
+
physicsEngine.removeBody(this.body);
|
|
486
|
+
this.body = null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
super.onDetached();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Input Behaviors
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
class PlayerInputBehavior extends GameEntityBehavior {
|
|
498
|
+
name = 'PlayerInputBehavior';
|
|
499
|
+
eventSubscriptions = ['input:keydown', 'input:keyup', 'input:gamepad'];
|
|
500
|
+
|
|
501
|
+
processEvent(event: GameEvent, state: any): boolean {
|
|
502
|
+
if (event.type === 'input:keydown') {
|
|
503
|
+
// Map keyboard input to game actions
|
|
504
|
+
switch (event.payload.key) {
|
|
505
|
+
case 'ArrowLeft':
|
|
506
|
+
state.movement = 'left';
|
|
507
|
+
this.$emit({ type: 'player:move', payload: { direction: 'left' } });
|
|
508
|
+
return true;
|
|
509
|
+
case 'ArrowRight':
|
|
510
|
+
state.movement = 'right';
|
|
511
|
+
this.$emit({ type: 'player:move', payload: { direction: 'right' } });
|
|
512
|
+
return true;
|
|
513
|
+
case 'Space':
|
|
514
|
+
this.$emit({ type: 'player:jump' });
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (event.type === 'input:keyup') {
|
|
520
|
+
// Handle key release
|
|
521
|
+
switch (event.payload.key) {
|
|
522
|
+
case 'ArrowLeft':
|
|
523
|
+
case 'ArrowRight':
|
|
524
|
+
state.movement = 'none';
|
|
525
|
+
this.$emit({ type: 'player:stopMove' });
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### AI Behaviors
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
class SimpleEnemyAI extends GameEntityBehavior {
|
|
539
|
+
name = 'SimpleEnemyAI';
|
|
540
|
+
eventSubscriptions = ['entity:spotted', 'entity:lostTarget'];
|
|
541
|
+
|
|
542
|
+
processEvent(event: GameEvent, state: any): boolean {
|
|
543
|
+
if (event.type === 'entity:spotted' && event.payload.type === 'player') {
|
|
544
|
+
state.target = event.payload.id;
|
|
545
|
+
state.aiState = 'chase';
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (event.type === 'entity:lostTarget') {
|
|
550
|
+
state.target = null;
|
|
551
|
+
state.aiState = 'patrol';
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
update(dt: number, state: any): void {
|
|
559
|
+
switch (state.aiState) {
|
|
560
|
+
case 'idle':
|
|
561
|
+
// Just stand around
|
|
562
|
+
break;
|
|
563
|
+
|
|
564
|
+
case 'patrol':
|
|
565
|
+
// Move along patrol path
|
|
566
|
+
this.updatePatrolBehavior(dt, state);
|
|
567
|
+
break;
|
|
568
|
+
|
|
569
|
+
case 'chase':
|
|
570
|
+
// Chase target if we have one
|
|
571
|
+
if (state.target) {
|
|
572
|
+
this.chaseTarget(dt, state);
|
|
573
|
+
}
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private updatePatrolBehavior(dt: number, state: any): void {
|
|
579
|
+
// Patrol logic implementation
|
|
580
|
+
// ...
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private chaseTarget(dt: number, state: any): void {
|
|
584
|
+
// Chase target logic
|
|
585
|
+
// ...
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
## Testing Behaviors
|
|
591
|
+
|
|
592
|
+
Behaviors are highly testable in isolation:
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
import { expect } from 'chai';
|
|
596
|
+
import { HealthBehavior } from './behaviors/health';
|
|
597
|
+
|
|
598
|
+
describe('HealthBehavior', () => {
|
|
599
|
+
it('should reduce health when damaged', () => {
|
|
600
|
+
const behavior = new HealthBehavior();
|
|
601
|
+
const state = { currentHealth: 100, maxHealth: 100, isAlive: true };
|
|
602
|
+
|
|
603
|
+
// Mock the $emit method
|
|
604
|
+
behavior.$emit = () => {};
|
|
605
|
+
|
|
606
|
+
// Process a damage event
|
|
607
|
+
behavior.processEvent({
|
|
608
|
+
type: 'entity:damage',
|
|
609
|
+
payload: { amount: 30 }
|
|
610
|
+
}, state);
|
|
611
|
+
|
|
612
|
+
expect(state.currentHealth).to.equal(70);
|
|
613
|
+
expect(state.isAlive).to.be.true;
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it('should emit death event when health reaches zero', () => {
|
|
617
|
+
const behavior = new HealthBehavior();
|
|
618
|
+
const state = { currentHealth: 30, maxHealth: 100, isAlive: true };
|
|
619
|
+
let emittedEvent = null;
|
|
620
|
+
|
|
621
|
+
// Mock the $emit method
|
|
622
|
+
behavior.$emit = (event) => { emittedEvent = event; };
|
|
623
|
+
|
|
624
|
+
// Process a lethal damage event
|
|
625
|
+
behavior.processEvent({
|
|
626
|
+
type: 'entity:damage',
|
|
627
|
+
payload: { amount: 50 }
|
|
628
|
+
}, state);
|
|
629
|
+
|
|
630
|
+
expect(state.currentHealth).to.equal(0);
|
|
631
|
+
expect(state.isAlive).to.be.false;
|
|
632
|
+
expect(emittedEvent).to.not.be.null;
|
|
633
|
+
expect(emittedEvent.type).to.equal('entity:died');
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// More tests...
|
|
637
|
+
});
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
## Designing Event-Driven Behavior Systems
|
|
641
|
+
|
|
642
|
+
When creating a complex game with many behaviors, consider these design patterns:
|
|
643
|
+
|
|
644
|
+
### Event Vocabulary
|
|
645
|
+
|
|
646
|
+
Develop a consistent pattern for event naming:
|
|
647
|
+
|
|
648
|
+
```
|
|
649
|
+
category:action
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
Examples:
|
|
653
|
+
- `player:move` - Player initiated movement
|
|
654
|
+
- `entity:damage` - Entity received damage
|
|
655
|
+
- `item:pickup` - Item was picked up
|
|
656
|
+
- `scene:loaded` - Scene finished loading
|
|
657
|
+
|
|
658
|
+
This consistency makes it easier to understand event flows and predict event names.
|
|
659
|
+
|
|
660
|
+
### Behavior Composition Patterns
|
|
661
|
+
|
|
662
|
+
Several patterns emerge when combining behaviors:
|
|
663
|
+
|
|
664
|
+
1. **Filter Chain**: Behaviors process events in sequence, with each potentially modifying or blocking the event
|
|
665
|
+
- Example: Input → ActionValidation → ActionExecution
|
|
666
|
+
|
|
667
|
+
2. **Observer Pattern**: Multiple independent behaviors respond to the same events
|
|
668
|
+
- Example: Achievement, Sound, and Animation behaviors all listening for "enemy:defeated"
|
|
669
|
+
|
|
670
|
+
3. **Command Pattern**: Behaviors emit high-level command events that other behaviors execute
|
|
671
|
+
- Example: AI emits "movement:request" that MovementBehavior handles
|
|
672
|
+
|
|
673
|
+
4. **State Machine**: Behaviors implement state transitions and state-specific logic
|
|
674
|
+
- Example: Character states like "idle", "walking", "jumping", "attacking"
|
|
675
|
+
|
|
676
|
+
## Best Practices
|
|
677
|
+
|
|
678
|
+
1. **Single Responsibility**: Each behavior should do one thing well
|
|
679
|
+
2. **Clear Dependencies**: Be explicit about what state properties a behavior needs
|
|
680
|
+
3. **Use TypeScript Interfaces**: Define interfaces for your state requirements
|
|
681
|
+
4. **Document Event Types**: Keep a registry of event types and their payload formats
|
|
682
|
+
5. **Behavior Composition**: Solve complex problems by combining simple behaviors
|
|
683
|
+
6. **Avoid Direct References**: Prefer events over direct references between behaviors
|
|
684
|
+
7. **Test in Isolation**: Write unit tests for individual behaviors
|
|
685
|
+
8. **Watch Event Performance**: Monitor high-frequency events that might impact performance
|
|
686
|
+
|
|
687
|
+
## Common Behavior Types
|
|
688
|
+
|
|
689
|
+
Some behaviors you might want to implement in your games:
|
|
690
|
+
|
|
691
|
+
- **MovementBehavior**: Handle entity position and velocity
|
|
692
|
+
- **HealthBehavior**: Manage damage and healing
|
|
693
|
+
- **InputBehavior**: Process player inputs
|
|
694
|
+
- **AiBehavior**: Control NPC decision making
|
|
695
|
+
- **CombatBehavior**: Handle attack and defense logic
|
|
696
|
+
- **PhysicsBehavior**: Interface with the physics engine
|
|
697
|
+
- **AnimationBehavior**: Control visual animations
|
|
698
|
+
- **SoundBehavior**: Manage entity sound effects
|
|
699
|
+
- **InteractionBehavior**: Handle entity interactions with the world
|
|
700
|
+
- **InventoryBehavior**: Manage collected items
|
|
701
|
+
|
|
702
|
+
## Conclusion
|
|
703
|
+
|
|
704
|
+
Behaviors are the heart of the SAGE entity system. By creating small, focused behaviors and combining them in different ways, you can build complex game objects with minimal code duplication. Remember that the key to good behavior design is composition - build simple behaviors that do one thing well, then combine them to create rich, interactive entities.
|
|
705
|
+
|
|
706
|
+
For more information on how behaviors fit into the overall entity architecture, see the [Entity System Guide](entity_system.md). To understand the event system that powers behavior communication, check out the [Event Bus Guide](eventbus.md).
|