@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,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.
|