@series-inc/rundot-3d-engine 0.6.9 → 0.6.10
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/SKILL.md +366 -0
- package/package.json +3 -2
package/SKILL.md
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
# Rundot 3D Engine
|
|
2
|
+
|
|
3
|
+
The Rundot 3D Engine (`@series-inc/rundot-3d-engine`) is a Three.js-based game engine with ECS architecture, Rapier physics, and StowKit asset integration.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { VenusGame, GameObject, Component } from "@series-inc/rundot-3d-engine"
|
|
9
|
+
import { MeshRenderer, RigidBodyComponentThree, RigidBodyType, ColliderShape } from "@series-inc/rundot-3d-engine/systems"
|
|
10
|
+
|
|
11
|
+
class MyGame extends VenusGame {
|
|
12
|
+
protected async onStart(): Promise<void> {
|
|
13
|
+
// Load assets
|
|
14
|
+
const stowkit = StowKitSystem.getInstance()
|
|
15
|
+
const buildJson = (await import("../prefabs/build.json")).default
|
|
16
|
+
await stowkit.loadFromBuildJson(buildJson, {
|
|
17
|
+
fetchBlob: (path) => fetch(path).then(r => r.blob()),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// Create a player
|
|
21
|
+
const player = new GameObject("Player")
|
|
22
|
+
player.position.set(0, 1, 0)
|
|
23
|
+
|
|
24
|
+
// Add mesh
|
|
25
|
+
const meshObj = new GameObject("PlayerMesh")
|
|
26
|
+
meshObj.addComponent(new MeshRenderer("player_mesh"))
|
|
27
|
+
player.add(meshObj)
|
|
28
|
+
|
|
29
|
+
// Add physics
|
|
30
|
+
player.addComponent(new RigidBodyComponentThree({
|
|
31
|
+
type: RigidBodyType.DYNAMIC,
|
|
32
|
+
shape: ColliderShape.CAPSULE,
|
|
33
|
+
radius: 0.5,
|
|
34
|
+
height: 2,
|
|
35
|
+
lockRotationX: true,
|
|
36
|
+
lockRotationY: true,
|
|
37
|
+
lockRotationZ: true,
|
|
38
|
+
}))
|
|
39
|
+
|
|
40
|
+
// Add lighting
|
|
41
|
+
const light = new THREE.DirectionalLight(0xffffff, 1)
|
|
42
|
+
light.position.set(5, 10, 5)
|
|
43
|
+
this.scene.add(light)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected preRender(deltaTime: number): void {
|
|
47
|
+
// Per-frame logic
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
protected async onDispose(): Promise<void> {
|
|
51
|
+
// Cleanup
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
MyGame.create()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Core Architecture
|
|
59
|
+
|
|
60
|
+
### VenusGame — Game Base Class
|
|
61
|
+
|
|
62
|
+
Manages renderer, scene, camera, and all engine systems.
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
class MyGame extends VenusGame {
|
|
66
|
+
protected getConfig(): VenusGameConfig {
|
|
67
|
+
return {
|
|
68
|
+
backgroundColor: 0x87CEEB,
|
|
69
|
+
antialias: true,
|
|
70
|
+
shadowMapEnabled: true,
|
|
71
|
+
shadowMapType: "vsm", // or "pcf_soft"
|
|
72
|
+
toneMapping: "aces", // "aces" | "linear" | "none"
|
|
73
|
+
toneMappingExposure: 1.0,
|
|
74
|
+
audioEnabled: true,
|
|
75
|
+
cameraType: "perspective", // or "orthographic"
|
|
76
|
+
orthoSize: 10, // half-height for ortho camera
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
protected async onStart(): Promise<void> { /* init game */ }
|
|
81
|
+
protected preRender(deltaTime: number): void { /* per-frame logic */ }
|
|
82
|
+
protected async onDispose(): Promise<void> { /* cleanup */ }
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Static access** (after initialization):
|
|
87
|
+
- `VenusGame.scene` — Three.js scene
|
|
88
|
+
- `VenusGame.camera` — active camera
|
|
89
|
+
- `VenusGame.renderer` — WebGL renderer
|
|
90
|
+
- `VenusGame.instance` — game instance
|
|
91
|
+
|
|
92
|
+
**Lifecycle order:** Physics → Tweens → Components → `preRender()` → `render()`
|
|
93
|
+
|
|
94
|
+
**IMPORTANT:** Use the `deltaTime` parameter in `preRender()`. Never call `this.clock.getDelta()`.
|
|
95
|
+
|
|
96
|
+
### GameObject — Entity Class
|
|
97
|
+
|
|
98
|
+
Extends `THREE.Object3D`. Auto-added to scene on creation.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const obj = new GameObject("MyObject")
|
|
102
|
+
obj.position.set(0, 1, 0)
|
|
103
|
+
obj.addComponent(new MyComponent())
|
|
104
|
+
|
|
105
|
+
// Hierarchy
|
|
106
|
+
const child = new GameObject("Child")
|
|
107
|
+
obj.add(child)
|
|
108
|
+
|
|
109
|
+
// Component access
|
|
110
|
+
const comp = obj.getComponent(MyComponent)
|
|
111
|
+
obj.hasComponent(MyComponent)
|
|
112
|
+
obj.removeComponent(MyComponent)
|
|
113
|
+
|
|
114
|
+
// Enable/disable
|
|
115
|
+
obj.setEnabled(false) // triggers onDisabled() on all components
|
|
116
|
+
obj.setEnabled(true) // triggers onEnabled()
|
|
117
|
+
|
|
118
|
+
// Cleanup
|
|
119
|
+
obj.dispose() // disposes all components and children
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Component — Behavior Base Class
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
class MyComponent extends Component {
|
|
126
|
+
protected onCreate(): void {
|
|
127
|
+
// Called when added to GameObject — init here
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public update(deltaTime: number): void {
|
|
131
|
+
// Called every frame
|
|
132
|
+
this.gameObject.position.x += deltaTime
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public lateUpdate(deltaTime: number): void {
|
|
136
|
+
// Called after all update() calls — camera follow, UI updates
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public onEnabled(): void { /* GameObject enabled */ }
|
|
140
|
+
public onDisabled(): void { /* GameObject disabled */ }
|
|
141
|
+
|
|
142
|
+
protected onCleanup(): void {
|
|
143
|
+
// Called on removal or dispose — cleanup listeners, resources
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Access from Component:**
|
|
149
|
+
- `this.gameObject` — the attached GameObject
|
|
150
|
+
- `this.scene` — the Three.js scene
|
|
151
|
+
- `this.getComponent(Type)` — get sibling component
|
|
152
|
+
|
|
153
|
+
## Systems
|
|
154
|
+
|
|
155
|
+
### MeshRenderer — Static Mesh Display
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { MeshRenderer } from "@series-inc/rundot-3d-engine/systems"
|
|
159
|
+
|
|
160
|
+
// ALWAYS use child GameObject pattern
|
|
161
|
+
const renderer = new MeshRenderer("asset_name", castShadow?, receiveShadow?, isStatic?, materialOverride?)
|
|
162
|
+
const meshObj = new GameObject("Mesh")
|
|
163
|
+
meshObj.addComponent(renderer)
|
|
164
|
+
parentGameObject.add(meshObj)
|
|
165
|
+
|
|
166
|
+
// Check if loaded
|
|
167
|
+
renderer.isLoaded()
|
|
168
|
+
renderer.getBounds()
|
|
169
|
+
renderer.setVisible(false)
|
|
170
|
+
renderer.setMaterial(customMaterial)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Static meshes** (`isStatic: true`) skip per-frame matrix updates — use for non-moving objects.
|
|
174
|
+
|
|
175
|
+
### SkeletalRenderer — Animated Characters
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { SkeletalRenderer } from "@series-inc/rundot-3d-engine/systems"
|
|
179
|
+
|
|
180
|
+
const skelRenderer = new SkeletalRenderer("character_mesh")
|
|
181
|
+
const meshObj = new GameObject("CharacterMesh")
|
|
182
|
+
meshObj.addComponent(skelRenderer)
|
|
183
|
+
character.add(meshObj)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### RigidBodyComponentThree — Physics
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import { RigidBodyComponentThree, RigidBodyType, ColliderShape } from "@series-inc/rundot-3d-engine/systems"
|
|
190
|
+
|
|
191
|
+
// Dynamic (affected by physics)
|
|
192
|
+
new RigidBodyComponentThree({
|
|
193
|
+
type: RigidBodyType.DYNAMIC,
|
|
194
|
+
shape: ColliderShape.BOX, // BOX, SPHERE, CAPSULE
|
|
195
|
+
size: new THREE.Vector3(1, 1, 1), // box dimensions
|
|
196
|
+
radius: 0.5, // sphere/capsule radius
|
|
197
|
+
height: 2, // capsule height
|
|
198
|
+
mass: 1.0,
|
|
199
|
+
friction: 0.5,
|
|
200
|
+
restitution: 0.8, // bounciness
|
|
201
|
+
linearDamping: 0.5,
|
|
202
|
+
angularDamping: 0.5,
|
|
203
|
+
lockRotationX/Y/Z: true, // lock rotation axes
|
|
204
|
+
fitToMesh: true, // auto-size from mesh bounds
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// Static (immovable)
|
|
208
|
+
new RigidBodyComponentThree({ type: RigidBodyType.STATIC, shape: ColliderShape.BOX, size: ... })
|
|
209
|
+
|
|
210
|
+
// Kinematic (script-controlled)
|
|
211
|
+
new RigidBodyComponentThree({ type: RigidBodyType.KINEMATIC, ... })
|
|
212
|
+
|
|
213
|
+
// Trigger (sensor, no physics response)
|
|
214
|
+
new RigidBodyComponentThree({ type: RigidBodyType.STATIC, shape: ColliderShape.BOX, isSensor: true })
|
|
215
|
+
rb.registerOnTriggerEnter((other) => console.log("entered", other.name))
|
|
216
|
+
rb.registerOnTriggerExit((other) => console.log("exited", other.name))
|
|
217
|
+
|
|
218
|
+
// Velocity & forces (dynamic only)
|
|
219
|
+
rb.setVelocity(new THREE.Vector3(0, 5, 0))
|
|
220
|
+
rb.applyImpulse(new THREE.Vector3(10, 0, 0))
|
|
221
|
+
rb.applyForce(new THREE.Vector3(0, -9.8, 0))
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### AnimationGraphComponent — State Machine Animations
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { AnimationGraphComponent } from "@series-inc/rundot-3d-engine/systems"
|
|
228
|
+
|
|
229
|
+
const config = {
|
|
230
|
+
parameters: {
|
|
231
|
+
speed: { type: "float", default: 0 },
|
|
232
|
+
isGrounded: { type: "bool", default: true },
|
|
233
|
+
},
|
|
234
|
+
states: {
|
|
235
|
+
idle: { animation: "idle" },
|
|
236
|
+
walk: { animation: "walk" },
|
|
237
|
+
run: { animation: "run" },
|
|
238
|
+
locomotion: {
|
|
239
|
+
tree: {
|
|
240
|
+
parameter: "speed",
|
|
241
|
+
children: [
|
|
242
|
+
{ animation: "idle", threshold: 0 },
|
|
243
|
+
{ animation: "walk", threshold: 1 },
|
|
244
|
+
{ animation: "run", threshold: 2 },
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
transitions: [
|
|
250
|
+
{ from: "idle", to: "walk", when: { speed: 1 } },
|
|
251
|
+
{ from: "walk", to: "run", when: { speed: 2 } },
|
|
252
|
+
{ from: "attack", to: "idle", exitTime: 1.0 }, // after anim finishes
|
|
253
|
+
],
|
|
254
|
+
initialState: "idle",
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const animGraph = new AnimationGraphComponent(model, config)
|
|
258
|
+
character.addComponent(animGraph)
|
|
259
|
+
|
|
260
|
+
// Drive transitions via parameters
|
|
261
|
+
animGraph.setParameter("speed", 2)
|
|
262
|
+
animGraph.setState("attack") // direct state change
|
|
263
|
+
animGraph.getCurrentState()
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### StowKitSystem — Asset Loading
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
import { StowKitSystem } from "@series-inc/rundot-3d-engine/systems"
|
|
270
|
+
|
|
271
|
+
const stowkit = StowKitSystem.getInstance()
|
|
272
|
+
|
|
273
|
+
// Load from build.json
|
|
274
|
+
await stowkit.loadFromBuildJson(buildJson, {
|
|
275
|
+
fetchBlob: (path) => fetch(path).then(r => r.blob()),
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
// Access assets
|
|
279
|
+
const mesh = await stowkit.getMesh("name") // async
|
|
280
|
+
const mesh = stowkit.getMeshSync("name") // sync (null if not loaded)
|
|
281
|
+
const tex = await stowkit.getTexture("name")
|
|
282
|
+
const clip = await stowkit.getAnimation("walk", "character_mesh")
|
|
283
|
+
const audio = await stowkit.getAudio("sfx_click")
|
|
284
|
+
const skinned = await stowkit.getSkinnedMesh("character", 1.0)
|
|
285
|
+
|
|
286
|
+
// Clone with shadow settings
|
|
287
|
+
const clone = await stowkit.cloneMesh("name", castShadow, receiveShadow)
|
|
288
|
+
|
|
289
|
+
// GPU instancing
|
|
290
|
+
await stowkit.registerMeshForInstancing("coin_batch", "coin_mesh", true, true, 500)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Other Systems
|
|
294
|
+
|
|
295
|
+
- **AudioSystem** — 2D/3D positional audio with AudioListener management
|
|
296
|
+
- **InputManager** — keyboard, mouse, touch, and gamepad input
|
|
297
|
+
- **LightingSystem** — directional, point, and spot lights with shadow management
|
|
298
|
+
- **NavigationSystem** — A* pathfinding on navigation meshes
|
|
299
|
+
- **ParticleSystem** — GPU particle effects
|
|
300
|
+
- **TweenSystem** — property animation and easing
|
|
301
|
+
- **UISystem** — HTML-based UI overlay system
|
|
302
|
+
- **PrefabSystem** — load and instantiate prefab hierarchies from JSON
|
|
303
|
+
- **SplineSystem** — Catmull-Rom spline paths for cameras, movement, etc.
|
|
304
|
+
|
|
305
|
+
## Patterns
|
|
306
|
+
|
|
307
|
+
### Separation of Concerns
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
// Logic/physics on parent
|
|
311
|
+
const character = new GameObject("Character")
|
|
312
|
+
character.addComponent(new CharacterController())
|
|
313
|
+
character.addComponent(new RigidBodyComponentThree({ type: RigidBodyType.DYNAMIC, shape: ColliderShape.CAPSULE }))
|
|
314
|
+
|
|
315
|
+
// Visual as child (can offset independently)
|
|
316
|
+
const visual = new GameObject("Visual")
|
|
317
|
+
visual.addComponent(new SkeletalRenderer("character_mesh"))
|
|
318
|
+
character.add(visual)
|
|
319
|
+
visual.position.y = -1
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Cache Component References
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
class MyComponent extends Component {
|
|
326
|
+
private meshRenderer?: MeshRenderer
|
|
327
|
+
|
|
328
|
+
protected onCreate(): void {
|
|
329
|
+
this.meshRenderer = this.getComponent(MeshRenderer) // cache in onCreate
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
public update(deltaTime: number): void {
|
|
333
|
+
// use cached ref — don't search every frame
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Factory Pattern
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
class EnemyFactory {
|
|
342
|
+
static create(type: string): GameObject {
|
|
343
|
+
const enemy = new GameObject(`Enemy_${type}`)
|
|
344
|
+
enemy.addComponent(new EnemyAI())
|
|
345
|
+
const meshObj = new GameObject("Mesh")
|
|
346
|
+
meshObj.addComponent(new MeshRenderer(`enemy_${type}`))
|
|
347
|
+
enemy.add(meshObj)
|
|
348
|
+
return enemy
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Common Mistakes
|
|
354
|
+
|
|
355
|
+
- **Don't create GameObjects in `update()`** — causes memory leaks. Create once, reuse or pool.
|
|
356
|
+
- **Don't access `this.gameObject` in constructor** — use `onCreate()` instead.
|
|
357
|
+
- **Don't forget `dispose()`** — always dispose GameObjects when done.
|
|
358
|
+
- **Don't add multiple components of same type** to one GameObject — use child GameObjects.
|
|
359
|
+
- **Don't call `this.clock.getDelta()`** — use the `deltaTime` parameter.
|
|
360
|
+
- **Don't bypass MeshRenderer** by loading meshes directly from StowKitSystem — use the component pattern.
|
|
361
|
+
|
|
362
|
+
## Dependencies
|
|
363
|
+
|
|
364
|
+
- `three` (peer, >=0.180.0)
|
|
365
|
+
- `@dimforge/rapier3d` — Rapier physics engine
|
|
366
|
+
- `@series-inc/stowkit-reader` + `@series-inc/stowkit-three-loader` — asset loading
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@series-inc/rundot-3d-engine",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
"files": [
|
|
91
91
|
"dist",
|
|
92
92
|
"docs",
|
|
93
|
-
"scripts"
|
|
93
|
+
"scripts",
|
|
94
|
+
"SKILL.md"
|
|
94
95
|
]
|
|
95
96
|
}
|