@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.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/Readme.md +53 -0
  3. package/dist/classes/bindings/toggle.d.ts +122 -0
  4. package/dist/classes/bindings/trigger.d.ts +79 -0
  5. package/dist/classes/bindings/value.d.ts +104 -0
  6. package/dist/classes/entity.d.ts +83 -0
  7. package/dist/classes/eventBus.d.ts +94 -0
  8. package/dist/classes/gameEngine.d.ts +57 -0
  9. package/dist/classes/input/gamepad.d.ts +94 -0
  10. package/dist/classes/input/keyboard.d.ts +66 -0
  11. package/dist/classes/input/mouse.d.ts +80 -0
  12. package/dist/classes/input/readers/gamepad.d.ts +77 -0
  13. package/dist/classes/input/readers/keyboard.d.ts +60 -0
  14. package/dist/classes/input/readers/mouse.d.ts +45 -0
  15. package/dist/classes/loggers/consoleBackend.d.ts +29 -0
  16. package/dist/classes/loggers/nullBackend.d.ts +14 -0
  17. package/dist/engines/scene.d.ts +11 -0
  18. package/dist/interfaces/action.d.ts +20 -0
  19. package/dist/interfaces/binding.d.ts +144 -0
  20. package/dist/interfaces/entity.d.ts +9 -0
  21. package/dist/interfaces/game.d.ts +26 -0
  22. package/dist/interfaces/input.d.ts +181 -0
  23. package/dist/interfaces/logger.d.ts +88 -0
  24. package/dist/managers/binding.d.ts +185 -0
  25. package/dist/managers/entity.d.ts +70 -0
  26. package/dist/managers/game.d.ts +20 -0
  27. package/dist/managers/input.d.ts +56 -0
  28. package/dist/managers/level.d.ts +55 -0
  29. package/dist/sage.d.ts +20 -0
  30. package/dist/sage.es.js +2208 -0
  31. package/dist/sage.es.js.map +1 -0
  32. package/dist/sage.umd.js +2 -0
  33. package/dist/sage.umd.js.map +1 -0
  34. package/dist/utils/capabilities.d.ts +2 -0
  35. package/dist/utils/graphics.d.ts +10 -0
  36. package/dist/utils/logger.d.ts +66 -0
  37. package/dist/utils/physics.d.ts +2 -0
  38. package/dist/utils/version.d.ts +5 -0
  39. package/docs/architecture.md +129 -0
  40. package/docs/behaviors.md +706 -0
  41. package/docs/binding_system.md +820 -0
  42. package/docs/design/input.md +86 -0
  43. package/docs/entity_system.md +538 -0
  44. package/docs/eventbus.md +225 -0
  45. package/docs/getting_started.md +264 -0
  46. package/docs/images/sage_logo.png +0 -0
  47. package/docs/images/sage_logo_shape.png +0 -0
  48. package/docs/overview.md +38 -0
  49. package/docs/physics_system.md +686 -0
  50. package/docs/scene_system.md +513 -0
  51. package/package.json +69 -0
  52. package/src/classes/bindings/toggle.ts +261 -0
  53. package/src/classes/bindings/trigger.ts +211 -0
  54. package/src/classes/bindings/value.ts +227 -0
  55. package/src/classes/entity.ts +256 -0
  56. package/src/classes/eventBus.ts +259 -0
  57. package/src/classes/gameEngine.ts +125 -0
  58. package/src/classes/input/gamepad.ts +388 -0
  59. package/src/classes/input/keyboard.ts +189 -0
  60. package/src/classes/input/mouse.ts +276 -0
  61. package/src/classes/input/readers/gamepad.ts +179 -0
  62. package/src/classes/input/readers/keyboard.ts +123 -0
  63. package/src/classes/input/readers/mouse.ts +133 -0
  64. package/src/classes/loggers/consoleBackend.ts +135 -0
  65. package/src/classes/loggers/nullBackend.ts +51 -0
  66. package/src/engines/scene.ts +112 -0
  67. package/src/images/sage_logo.svg +172 -0
  68. package/src/images/sage_logo_shape.svg +146 -0
  69. package/src/interfaces/action.ts +30 -0
  70. package/src/interfaces/binding.ts +191 -0
  71. package/src/interfaces/entity.ts +21 -0
  72. package/src/interfaces/game.ts +44 -0
  73. package/src/interfaces/input.ts +221 -0
  74. package/src/interfaces/logger.ts +118 -0
  75. package/src/managers/binding.ts +729 -0
  76. package/src/managers/entity.ts +252 -0
  77. package/src/managers/game.ts +111 -0
  78. package/src/managers/input.ts +233 -0
  79. package/src/managers/level.ts +261 -0
  80. package/src/sage.ts +119 -0
  81. package/src/types/global.d.ts +11 -0
  82. package/src/utils/capabilities.ts +16 -0
  83. package/src/utils/graphics.ts +148 -0
  84. package/src/utils/logger.ts +225 -0
  85. package/src/utils/physics.ts +16 -0
  86. package/src/utils/version.ts +11 -0
@@ -0,0 +1,686 @@
1
+ # Physics System Guide
2
+
3
+ ## Introduction
4
+
5
+ The physics system in SAGE is the force that brings your game world to life, making objects move, collide, and interact in realistic ways. Powered by Havok Physics through BabylonJS, SAGE provides a robust physics implementation that balances realism with performance.
6
+
7
+ This guide will walk you through how to use physics in your SAGE games, from basic concepts to advanced techniques.
8
+
9
+ ## Core Concepts
10
+
11
+ Physics in games simulates the fundamental forces that govern how objects interact:
12
+
13
+ - **Gravity**: Pulls objects downward
14
+ - **Collisions**: Determines how objects interact when they touch
15
+ - **Forces**: Push, pull, and otherwise affect objects
16
+ - **Constraints**: Limit how objects can move relative to one another
17
+
18
+ In SAGE, physics simulation is handled automatically as part of the game loop, but you have control over how entities interact with the physics system.
19
+
20
+ ## Getting Started with Physics
21
+
22
+ SAGE initializes the physics system automatically when you create the game engine:
23
+
24
+ ```typescript
25
+ import { createGameEngine } from '@skewedaspect/sage';
26
+
27
+ async function initGame() {
28
+ const canvas = document.getElementById('gameCanvas') as HTMLCanvasElement;
29
+
30
+ // Physics is initialized here
31
+ const gameEngine = await createGameEngine(canvas);
32
+
33
+ // The physics system is accessible through
34
+ const physics = gameEngine.physics;
35
+ }
36
+ ```
37
+
38
+ ## Physical Objects
39
+
40
+ To make an entity interact with the physics system, you need to:
41
+
42
+ 1. Create a visual representation (mesh)
43
+ 2. Add a physics body (via PhysicsAggregate)
44
+ 3. Connect the physics body to your entity state
45
+
46
+ Here's a behavior that handles physics for an entity:
47
+
48
+ ```typescript
49
+ import { GameEntityBehavior, GameEvent } from '@skewedaspect/sage';
50
+ import {
51
+ PhysicsAggregate,
52
+ PhysicsShapeType,
53
+ Vector3
54
+ } from '@babylonjs/core';
55
+
56
+ // Define what state a physical entity needs
57
+ interface PhysicalObjectState {
58
+ position: { x: number, y: number, z: number };
59
+ mass: number;
60
+ restitution: number; // "bounciness"
61
+ friction: number;
62
+ isStatic: boolean;
63
+ shape: 'box' | 'sphere' | 'capsule' | 'cylinder' | 'mesh';
64
+ dimensions: { width?: number, height?: number, depth?: number, radius?: number };
65
+
66
+ // Properties to be set by the behavior
67
+ physicsAggregate?: any;
68
+ mesh?: any;
69
+ }
70
+
71
+ export class PhysicsBehavior extends GameEntityBehavior<PhysicalObjectState> {
72
+ name = 'PhysicsBehavior';
73
+ eventSubscriptions = ['scene:loaded', 'physics:applyForce', 'physics:setVelocity'];
74
+
75
+ processEvent(event: GameEvent, state: PhysicalObjectState): boolean {
76
+ if (event.type === 'scene:loaded') {
77
+ // Create physical representation when scene is available
78
+ this.createPhysicalObject(event.payload.scene, state);
79
+ return true;
80
+ }
81
+
82
+ if (event.type === 'physics:applyForce' && state.physicsAggregate) {
83
+ const force = event.payload.force;
84
+ const contactPoint = event.payload.contactPoint ||
85
+ new Vector3(state.position.x, state.position.y, state.position.z);
86
+
87
+ // Apply a force at a specific point
88
+ state.physicsAggregate.body.applyForce(
89
+ new Vector3(force.x, force.y, force.z),
90
+ new Vector3(contactPoint.x, contactPoint.y, contactPoint.z)
91
+ );
92
+ return true;
93
+ }
94
+
95
+ if (event.type === 'physics:setVelocity' && state.physicsAggregate) {
96
+ const velocity = event.payload.velocity;
97
+
98
+ // Set linear velocity directly
99
+ state.physicsAggregate.body.setLinearVelocity(
100
+ new Vector3(velocity.x, velocity.y, velocity.z)
101
+ );
102
+ return true;
103
+ }
104
+
105
+ return false;
106
+ }
107
+
108
+ // Called every frame to sync physics state back to entity
109
+ update(_dt: number, state: PhysicalObjectState): void {
110
+ if (state.physicsAggregate && !state.isStatic) {
111
+ // Get position from physics engine
112
+ const physicsPosition = state.physicsAggregate.transformNode.position;
113
+
114
+ // Update entity state with physics position
115
+ state.position.x = physicsPosition.x;
116
+ state.position.y = physicsPosition.y;
117
+ state.position.z = physicsPosition.z;
118
+ }
119
+ }
120
+
121
+ // Create the physical representation
122
+ createPhysicalObject(scene, state: PhysicalObjectState): void {
123
+ // Create mesh based on shape
124
+ const mesh = this.createMesh(scene, state);
125
+ state.mesh = mesh;
126
+
127
+ // Set initial position
128
+ mesh.position.x = state.position.x;
129
+ mesh.position.y = state.position.y;
130
+ mesh.position.z = state.position.z;
131
+
132
+ // Determine physics shape
133
+ let shapeType: PhysicsShapeType;
134
+ switch (state.shape) {
135
+ case 'box':
136
+ shapeType = PhysicsShapeType.BOX;
137
+ break;
138
+ case 'sphere':
139
+ shapeType = PhysicsShapeType.SPHERE;
140
+ break;
141
+ case 'capsule':
142
+ shapeType = PhysicsShapeType.CAPSULE;
143
+ break;
144
+ case 'cylinder':
145
+ shapeType = PhysicsShapeType.CYLINDER;
146
+ break;
147
+ case 'mesh':
148
+ shapeType = PhysicsShapeType.MESH;
149
+ break;
150
+ default:
151
+ shapeType = PhysicsShapeType.BOX;
152
+ }
153
+
154
+ // Create physics body
155
+ const physicsAggregate = new PhysicsAggregate(
156
+ mesh,
157
+ shapeType,
158
+ {
159
+ mass: state.isStatic ? 0 : state.mass,
160
+ restitution: state.restitution,
161
+ friction: state.friction
162
+ },
163
+ scene
164
+ );
165
+
166
+ state.physicsAggregate = physicsAggregate;
167
+ }
168
+
169
+ // Create appropriate mesh based on shape
170
+ createMesh(scene, state: PhysicalObjectState): any {
171
+ // Implementation would create the right shape of mesh
172
+ // based on the state.shape and state.dimensions
173
+ // For brevity, implementation details omitted
174
+
175
+ // Example for a box:
176
+ // return MeshBuilder.CreateBox('box', {
177
+ // width: state.dimensions.width || 1,
178
+ // height: state.dimensions.height || 1,
179
+ // depth: state.dimensions.depth || 1
180
+ // }, scene);
181
+ }
182
+ }
183
+ ```
184
+
185
+ ## Using the Physics Behavior
186
+
187
+ To use the physics behavior with an entity:
188
+
189
+ ```typescript
190
+ // Register an entity with physics
191
+ gameEngine.managers.entityManager.registerEntityDefinition({
192
+ type: 'prop:crate',
193
+ defaultState: {
194
+ position: { x: 0, y: 5, z: 0 }, // Start 5 units above origin
195
+ mass: 1.5,
196
+ restitution: 0.4,
197
+ friction: 0.8,
198
+ isStatic: false,
199
+ shape: 'box',
200
+ dimensions: { width: 1, height: 1, depth: 1 }
201
+ },
202
+ behaviors: [
203
+ PhysicsBehavior,
204
+ VisualRepresentationBehavior // For handling appearance
205
+ ]
206
+ });
207
+
208
+ // Create the entity with additional initial state
209
+ const crate = gameEngine.managers.entityManager.createEntity('prop:crate', { position: { x: 10, y: 5, z: 0 } });
210
+ ```
211
+
212
+ ## Collision Detection
213
+
214
+ One of the most important features of a physics system is collision detection. Here's how to handle collisions in SAGE:
215
+
216
+ ```typescript
217
+ import { ActionManager, ExecuteCodeAction } from '@babylonjs/core';
218
+
219
+ // A behavior that responds to collisions
220
+ class CollisionBehavior extends GameEntityBehavior<{ health: number }> {
221
+ name = 'CollisionBehavior';
222
+ eventSubscriptions = ['entity:physicsMeshCreated'];
223
+
224
+ processEvent(event: GameEvent, state: any): boolean {
225
+ if (event.type === 'entity:physicsMeshCreated') {
226
+ const mesh = event.payload.mesh;
227
+ this.setupCollisionHandling(mesh, state);
228
+ return true;
229
+ }
230
+ return false;
231
+ }
232
+
233
+ setupCollisionHandling(mesh, state): void {
234
+ if (!mesh) return;
235
+
236
+ // Create action manager if it doesn't exist
237
+ if (!mesh.actionManager) {
238
+ mesh.actionManager = new ActionManager(mesh.getScene());
239
+ }
240
+
241
+ // Register intersection event with any other mesh
242
+ mesh.actionManager.registerAction(
243
+ new ExecuteCodeAction(
244
+ { trigger: ActionManager.OnIntersectionEnterTrigger },
245
+ (evt) => {
246
+ // Get the other mesh
247
+ const otherMesh = evt.source;
248
+
249
+ // Emit collision event
250
+ this.$emit({
251
+ type: 'entity:collision',
252
+ payload: {
253
+ otherMeshId: otherMesh.id,
254
+ velocity: mesh.physicsAggregate?.body.getLinearVelocity(),
255
+ collisionPoint: evt.additionalData?.contactPoint
256
+ }
257
+ });
258
+
259
+ // For a damage-causing collision, we might:
260
+ const impactForce = this.calculateImpactForce(
261
+ mesh.physicsAggregate?.body,
262
+ otherMesh.physicsAggregate?.body
263
+ );
264
+
265
+ if (impactForce > 5) { // Threshold for damage
266
+ // Take damage proportional to impact
267
+ const damageTaken = Math.floor(impactForce * 0.5);
268
+ state.health -= damageTaken;
269
+
270
+ // Emit damage event
271
+ this.$emit({
272
+ type: 'entity:damage',
273
+ payload: {
274
+ amount: damageTaken,
275
+ source: 'collision',
276
+ otherMeshId: otherMesh.id
277
+ }
278
+ });
279
+ }
280
+ }
281
+ )
282
+ );
283
+ }
284
+
285
+ calculateImpactForce(bodyA, bodyB) {
286
+ if (!bodyA || !bodyB) return 0;
287
+
288
+ // Get velocities
289
+ const velA = bodyA.getLinearVelocity();
290
+ const velB = bodyB.getLinearVelocity();
291
+
292
+ // Calculate relative velocity magnitude (simplified)
293
+ const relVelocity = new Vector3(
294
+ velA.x - velB.x,
295
+ velA.y - velB.y,
296
+ velA.z - velB.z
297
+ );
298
+
299
+ return relVelocity.length();
300
+ }
301
+ }
302
+ ```
303
+
304
+ ## Physics Constraints
305
+
306
+ Physics constraints allow you to create complex mechanical connections between objects:
307
+
308
+ ```typescript
309
+ import { PhysicsConstraint, DistanceConstraint } from '@babylonjs/core';
310
+
311
+ // A behavior that creates a swinging pendulum
312
+ class PendulumBehavior extends GameEntityBehavior {
313
+ name = 'PendulumBehavior';
314
+ eventSubscriptions = ['entity:physicsMeshCreated', 'entity:attachToPoint'];
315
+
316
+ processEvent(event: GameEvent, state: any): boolean {
317
+ if (event.type === 'entity:physicsMeshCreated') {
318
+ // Store the mesh for later constraint setup
319
+ state.mesh = event.payload.mesh;
320
+ return true;
321
+ }
322
+
323
+ if (event.type === 'entity:attachToPoint') {
324
+ if (!state.mesh) return false;
325
+
326
+ const point = event.payload.point;
327
+ const scene = state.mesh.getScene();
328
+
329
+ // Create a fixed point in space
330
+ const pivotPoint = MeshBuilder.CreateSphere(
331
+ 'pivot',
332
+ { diameter: 0.1 },
333
+ scene
334
+ );
335
+ pivotPoint.position = new Vector3(point.x, point.y, point.z);
336
+
337
+ // Make the pivot static (immovable)
338
+ const pivotAggregate = new PhysicsAggregate(
339
+ pivotPoint,
340
+ PhysicsShapeType.SPHERE,
341
+ { mass: 0 },
342
+ scene
343
+ );
344
+
345
+ // Create a distance constraint (rope)
346
+ const ropeLength = event.payload.length || 5;
347
+ const constraint = new DistanceConstraint({
348
+ pivotA: new Vector3(0, 0, 0), // Local to pivot
349
+ pivotB: new Vector3(0, 0, 0), // Local to object
350
+ maxDistance: ropeLength
351
+ });
352
+
353
+ // Connect the constraint
354
+ constraint.attachAll(
355
+ true, // Be collision-enabled
356
+ pivotAggregate.body, // The fixed point
357
+ state.mesh.physicsAggregate.body // The swinging object
358
+ );
359
+
360
+ // Store the constraint
361
+ state.constraint = constraint;
362
+
363
+ return true;
364
+ }
365
+
366
+ return false;
367
+ }
368
+ }
369
+
370
+ // Usage example - create a wrecking ball
371
+ gameEngine.managers.entityManager.registerEntityDefinition({
372
+ type: 'prop:wreckingBall',
373
+ initialState: {
374
+ position: { x: 0, y: 5, z: 0 },
375
+ mass: 5000, // Very heavy!
376
+ restitution: 0.2,
377
+ friction: 0.8,
378
+ isStatic: false,
379
+ shape: 'sphere',
380
+ dimensions: { radius: 2 }
381
+ },
382
+ behaviors: [
383
+ PhysicsBehavior,
384
+ PendulumBehavior,
385
+ VisualRepresentationBehavior
386
+ ]
387
+ });
388
+
389
+ // Create and set up the wrecking ball
390
+ const wreckingBall = gameEngine.managers.entityManager.createEntity('prop:wreckingBall');
391
+
392
+ // Attach it to a high point
393
+ gameEngine.eventBus.publish({
394
+ type: 'entity:attachToPoint',
395
+ targetID: wreckingBall.id,
396
+ payload: {
397
+ point: { x: 0, y: 20, z: 0 }, // 20 units up
398
+ length: 15 // 15 unit long cable
399
+ }
400
+ });
401
+
402
+ // Give it a push
403
+ gameEngine.eventBus.publish({
404
+ type: 'physics:applyForce',
405
+ targetID: wreckingBall.id,
406
+ payload: {
407
+ force: { x: 50000, y: 0, z: 0 } // Strong push!
408
+ }
409
+ });
410
+ ```
411
+
412
+ ## Ragdoll Physics
413
+
414
+ For more complex character physics, such as ragdoll effects:
415
+
416
+ ```typescript
417
+ class RagdollBehavior extends GameEntityBehavior {
418
+ name = 'RagdollBehavior';
419
+ eventSubscriptions = ['entity:die', 'entity:characterMeshCreated'];
420
+
421
+ // This implementation would be complex
422
+ // Main concepts:
423
+ // 1. Create physics body for each character limb
424
+ // 2. Connect limbs with constraints (joints)
425
+ // 3. Keep ragdoll disabled until needed
426
+ // 4. On death, enable ragdoll and disable character controller
427
+
428
+ processEvent(event: GameEvent, state: any): boolean {
429
+ if (event.type === 'entity:characterMeshCreated') {
430
+ // Set up the ragdoll structure
431
+ this.setupRagdoll(event.payload.meshes, state);
432
+ return true;
433
+ }
434
+
435
+ if (event.type === 'entity:die') {
436
+ // Enable ragdoll physics when character dies
437
+ this.enableRagdoll(state);
438
+ return true;
439
+ }
440
+
441
+ return false;
442
+ }
443
+
444
+ // Implementation details omitted for brevity
445
+ // Would involve creating separate physics bodies for
446
+ // head, torso, arms, legs, etc. and connecting them
447
+ // with ball-socket constraints
448
+ }
449
+ ```
450
+
451
+ ## Performance Considerations
452
+
453
+ Physics simulation is computationally expensive. Here are some tips for optimizing physics in your game:
454
+
455
+ 1. **Use Simple Collision Shapes**: Wherever possible, use primitive shapes (boxes, spheres) instead of complex meshes.
456
+
457
+ 2. **Static Objects**: Make non-moving objects static (mass = 0).
458
+
459
+ 3. **Sleep Parameters**: Objects that haven't moved in a while can be "put to sleep" by the physics engine.
460
+
461
+ 4. **Physics Layers**: Group objects into physics layers and control which layers interact with each other.
462
+
463
+ ```typescript
464
+ // Setting up selective collision filtering
465
+ function setupCollisionGroups(physics) {
466
+ // Define collision groups
467
+ const PLAYER_GROUP = 1;
468
+ const ENEMY_GROUP = 2;
469
+ const ENVIRONMENT_GROUP = 4;
470
+ const PROJECTILE_GROUP = 8;
471
+
472
+ // Setup examples:
473
+
474
+ // Player collides with environment and enemies, but not other players
475
+ playerAggregate.body.setCollisionFilteringGroups(
476
+ PLAYER_GROUP, // My group
477
+ ENVIRONMENT_GROUP | ENEMY_GROUP // Groups I collide with
478
+ );
479
+
480
+ // Projectiles collide with enemies and environment, but not other projectiles
481
+ projectileAggregate.body.setCollisionFilteringGroups(
482
+ PROJECTILE_GROUP,
483
+ ENVIRONMENT_GROUP | ENEMY_GROUP
484
+ );
485
+ }
486
+ ```
487
+
488
+ 5. **Level of Detail**: Use simpler physics for distant objects.
489
+
490
+ ## Common Physics Patterns
491
+
492
+ ### Character Controller
493
+
494
+ A common pattern for character movement that balances physics realism with responsive controls:
495
+
496
+ ```typescript
497
+ class CharacterControllerBehavior extends GameEntityBehavior {
498
+ name = 'CharacterControllerBehavior';
499
+ eventSubscriptions = ['input:move', 'input:jump', 'entity:physicsMeshCreated'];
500
+
501
+ processEvent(event: GameEvent, state: any): boolean {
502
+ if (event.type === 'entity:physicsMeshCreated') {
503
+ // Configure physics for character control
504
+ this.setupCharacterPhysics(event.payload.mesh, state);
505
+ return true;
506
+ }
507
+
508
+ if (event.type === 'input:move' && state.controller) {
509
+ const direction = event.payload.direction;
510
+
511
+ // Apply movement forces
512
+ this.moveCharacter(direction, state);
513
+ return true;
514
+ }
515
+
516
+ if (event.type === 'input:jump' && state.controller) {
517
+ // Check if on ground
518
+ if (this.isGrounded(state)) {
519
+ // Apply jump impulse
520
+ state.physicsAggregate.body.applyImpulse(
521
+ new Vector3(0, state.jumpForce, 0),
522
+ state.physicsAggregate.transformNode.position
523
+ );
524
+ }
525
+ return true;
526
+ }
527
+
528
+ return false;
529
+ }
530
+
531
+ setupCharacterPhysics(mesh, state) {
532
+ // Character-specific physics setup
533
+ // Often uses a capsule shape and special friction settings
534
+
535
+ // Store controller state
536
+ state.controller = {
537
+ isGrounded: false,
538
+ lastGroundCheckTime: 0,
539
+ currentVelocity: { x: 0, y: 0, z: 0 }
540
+ };
541
+ }
542
+
543
+ moveCharacter(direction, state) {
544
+ // Use forces for ground movement, but in a controlled way
545
+ const physicsBody = state.physicsAggregate.body;
546
+
547
+ // Get current velocity
548
+ const velocity = physicsBody.getLinearVelocity();
549
+
550
+ // Calculate target velocity based on input
551
+ const targetVelocity = {
552
+ x: direction.x * state.moveSpeed,
553
+ z: direction.z * state.moveSpeed,
554
+ };
555
+
556
+ // Calculate required force to reach target velocity
557
+ const force = {
558
+ x: (targetVelocity.x - velocity.x) * state.acceleration,
559
+ z: (targetVelocity.z - velocity.z) * state.acceleration
560
+ };
561
+
562
+ // Apply horizontal forces
563
+ physicsBody.applyForce(
564
+ new Vector3(force.x, 0, force.z),
565
+ state.physicsAggregate.transformNode.position
566
+ );
567
+ }
568
+
569
+ isGrounded(state) {
570
+ // Implementation would use raycasting to check if
571
+ // the character is standing on something
572
+
573
+ // For performance, we don't check every frame
574
+ const now = performance.now();
575
+ if (now - state.controller.lastGroundCheckTime < 100) {
576
+ return state.controller.isGrounded;
577
+ }
578
+
579
+ // Use BabylonJS raycasting for ground check
580
+ const origin = state.physicsAggregate.transformNode.position.clone();
581
+ const direction = new Vector3(0, -1, 0);
582
+ const length = state.height * 0.55; // Slightly more than half height
583
+
584
+ const ray = new Ray(origin, direction, length);
585
+ const hit = state.mesh.getScene().pickWithRay(ray);
586
+
587
+ state.controller.isGrounded = hit.hit;
588
+ state.controller.lastGroundCheckTime = now;
589
+
590
+ return state.controller.isGrounded;
591
+ }
592
+
593
+ update(dt: number, state: any): void {
594
+ if (state.controller) {
595
+ // Check grounding status regularly
596
+ this.isGrounded(state);
597
+
598
+ // Apply drag to prevent sliding
599
+ if (state.controller.isGrounded) {
600
+ const physicsBody = state.physicsAggregate.body;
601
+ const velocity = physicsBody.getLinearVelocity();
602
+
603
+ // Apply horizontal drag
604
+ physicsBody.applyForce(
605
+ new Vector3(
606
+ -velocity.x * state.groundFriction,
607
+ 0,
608
+ -velocity.z * state.groundFriction
609
+ ),
610
+ state.physicsAggregate.transformNode.position
611
+ );
612
+ }
613
+ }
614
+ }
615
+ }
616
+ ```
617
+
618
+ ### Vehicle Physics
619
+
620
+ For implementing vehicles with wheels, suspension, etc.:
621
+
622
+ ```typescript
623
+ class VehicleBehavior extends GameEntityBehavior {
624
+ name = 'VehicleBehavior';
625
+ eventSubscriptions = ['input:accelerate', 'input:brake', 'input:turn'];
626
+
627
+ // This would be a complex implementation
628
+ // Key concepts:
629
+ // 1. Create chassis as main body
630
+ // 2. Create wheels as separate physics objects
631
+ // 3. Connect wheels with physics constraints that simulate suspension
632
+ // 4. Apply forces to wheels for acceleration/braking
633
+ // 5. Apply steering by rotating wheel constraints
634
+
635
+ // Implementation details omitted for brevity
636
+ }
637
+ ```
638
+
639
+ ## Debugging Physics
640
+
641
+ To debug physics in your game:
642
+
643
+ ```typescript
644
+ import { PhysicsViewer } from '@babylonjs/core';
645
+
646
+ function setupPhysicsDebugging(scene) {
647
+ // Create a physics viewer
648
+ const physicsViewer = new PhysicsViewer(scene);
649
+
650
+ // Show physics impostors for all objects
651
+ for (const mesh of scene.meshes) {
652
+ if (mesh.physicsAggregate) {
653
+ physicsViewer.showImpostor(mesh.physicsAggregate.body, mesh);
654
+ }
655
+ }
656
+
657
+ // Toggle visibility with a key
658
+ window.addEventListener('keydown', (e) => {
659
+ if (e.key === 'p') {
660
+ physicsViewer.isEnabled = !physicsViewer.isEnabled;
661
+ }
662
+ });
663
+
664
+ return physicsViewer;
665
+ }
666
+ ```
667
+
668
+ ## Best Practices
669
+
670
+ 1. **Match Visual and Physical Representations**: Keep your visual meshes and physics bodies aligned.
671
+
672
+ 2. **Use Continuous Collision Detection** for fast-moving objects to prevent "tunneling" through thin obstacles.
673
+
674
+ 3. **Physics Time Step**: Be aware of the physics time step - too small is inefficient, too large is unstable.
675
+
676
+ 4. **Avoid Stacking Many Objects**: Physics stacks are computationally expensive and can become unstable.
677
+
678
+ 5. **Use Physics Materials**: Apply different friction/restitution settings for different surfaces (ice, mud, etc.).
679
+
680
+ ## Conclusion
681
+
682
+ The physics system brings your virtual world to life, making objects interact in believable ways. By understanding how to use physics effectively in SAGE, you can create games with satisfying tactile feedback, from bouncing balls to epic destruction.
683
+
684
+ Physics is a deep topic, and this guide only scratches the surface. As you become more comfortable with the basics, explore the BabylonJS and Havok documentation for advanced techniques to push your physics simulation to the next level.
685
+
686
+ Remember, with great physics comes great responsibility - a lightsaber swinging with realistic physics can be a joy to use, but only if the Wookiee in the next room isn't accidentally sliced in half by the player's enthusiastic flailing.