@genex-ai/cli-demo 0.90.0-dev.227 → 0.91.0-dev.228
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/README.md +4 -1
- package/dist/index.js +296 -66
- package/package.json +1 -1
- package/templates/controllers/NOTICE.md +6 -4
- package/templates/controllers/character/player-character.ts +217 -0
- package/templates/skills/genex-ai-character/SKILL.md +54 -8
- package/templates/skills/genex-ai-image/SKILL.md +5 -1
- package/templates/skills/genex-ai-menu/SKILL.md +14 -6
- package/templates/skills/genex-ai-skybox/SKILL.md +19 -6
- package/templates/skills/genex-game-director/SKILL.md +29 -11
- package/templates/skills/genex-game-director/references/routing-map.md +3 -3
- package/templates/skills/genex-threejs-character-controller/SKILL.md +103 -51
- package/templates/skills/genex-threejs-character-controller/references/animations.md +62 -50
- package/templates/skills/genex-threejs-embed-auth/SKILL.md +5 -3
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +10 -8
- package/templates/skills/genex-threejs-multiplayer/references/host-physics.md +8 -7
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: genex-threejs-character-controller
|
|
3
|
-
description: Add Genex's tuned ECCTRL-derived physics character controller with `npx genex controller character`: dynamic-capsule movement, follow camera, touch input,
|
|
3
|
+
description: Add Genex's tuned ECCTRL-derived physics character controller with `npx genex controller character`: dynamic-capsule movement, follow camera, touch input, and one loader that resolves the player's body — the game's own generated character by default, the profile VRM avatar as the fallback. Use for every on-foot player or third-person movement request.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Genex Three.js Character Controller
|
|
@@ -19,24 +19,47 @@ animation retargeting, capsule auto-fit, foot IK) and the 12-clip core
|
|
|
19
19
|
~1.3 MB) into `public/assets/`. Need more — swords, pistols, magic, climbing,
|
|
20
20
|
swimming, emotes? Install exactly what the game uses with
|
|
21
21
|
`npx genex controller anims <tags|clip names…>` (see Animations below). The
|
|
22
|
-
|
|
23
|
-
**your** avatar when you're signed in,
|
|
24
|
-
otherwise a bundled CC0 default (attribution in `src/controllers/NOTICE.md`).
|
|
25
|
-
At runtime the character plays as the **visiting player's own** picked avatar
|
|
26
|
-
(`user.avatarUrl` from the embed identity — see the wiring below); the baked
|
|
27
|
-
file is only the fallback for local dev and load failures. The copied files are then owned by the game —
|
|
22
|
+
copied files are then owned by the game —
|
|
28
23
|
edit them freely; re-running skips existing files unless `--force`. Do not write
|
|
29
24
|
a character controller from scratch and do not swap in a kinematic-controller
|
|
30
25
|
tutorial: this one is a real dynamic body that pushes crates, rides moving
|
|
31
26
|
platforms, climbs stairs and slides on too-steep slopes out of the box.
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
## The player's body: the game's own character
|
|
29
|
+
|
|
30
|
+
**The player wears the character this game generated for itself.** That is the
|
|
31
|
+
default for any game where a human body appears on screen — third-person
|
|
32
|
+
obviously, and first-person too the moment remotes, a look-down body, a shadow,
|
|
33
|
+
a death cam, or a menu portrait shows one. Load `$genex-ai-character` and start
|
|
34
|
+
it EARLY, with your first art actions, so it lands around the v0 preview
|
|
35
|
+
instead of after it.
|
|
36
|
+
|
|
37
|
+
The profile VRM avatar is the **fallback**, and the command bakes a neutral CC0
|
|
38
|
+
copy to `public/assets/avatar.vrm` (attribution in `src/controllers/NOTICE.md`)
|
|
39
|
+
so a fresh game always has a working body. It is used when:
|
|
40
|
+
|
|
41
|
+
- the generated character hasn't landed yet — say in one plain line that this
|
|
42
|
+
body is temporary; or
|
|
43
|
+
- it genuinely could not be made (out of credits, generation failed, email
|
|
44
|
+
unverified) — say so in one line, record `Player character: VRM — <reason>`
|
|
45
|
+
in DESIGN.md, and keep building.
|
|
46
|
+
|
|
47
|
+
At runtime the fallback lane loads the **visiting player's own** picked avatar
|
|
48
|
+
(`user.avatarUrl` from the embed identity); the baked file covers local dev and
|
|
49
|
+
load failures. Games whose player is not a person at all — a car, a ship, an
|
|
50
|
+
RTS cursor, a board — generate that object with `npx genex model` instead and
|
|
51
|
+
never enter this lane.
|
|
52
|
+
|
|
53
|
+
**You never write two boot paths for this.** `loadPlayerCharacter()` decides at
|
|
54
|
+
runtime from `public/assets/meshy-character.json` and returns one shape either
|
|
55
|
+
way, so the character arriving mid-build is a file drop
|
|
56
|
+
(`npx genex controller character --character <id>`) and not an edit to your
|
|
57
|
+
most load-bearing code. Wire the block under "Minimal wiring" once, at hour 0.
|
|
58
|
+
|
|
36
59
|
For a full custom LOCOMOTION SET beyond the UAL packs (a weapon-hold 8-way
|
|
37
|
-
shooter kit, a signature gait), see
|
|
38
|
-
motion install --set rifle` drops the proven rifle set + its own
|
|
39
|
-
`genex motion gen` builds new sets from text.
|
|
60
|
+
shooter kit, a signature gait), see `$genex-ai-character`'s motion section:
|
|
61
|
+
`npx genex motion install --set rifle` drops the proven rifle set + its own
|
|
62
|
+
runtime, and `genex motion gen` builds new sets from text.
|
|
40
63
|
|
|
41
64
|
Before generating a Meshy character, discuss two or three visual directions.
|
|
42
65
|
When the user names a visual reference, inspect references before writing the
|
|
@@ -64,12 +87,12 @@ that model in R2. Before rigging, ask the user to approve a separate
|
|
|
64
87
|
rigged and animated. (For these approvals, use your question tool when you
|
|
65
88
|
have one; if you have none, a short numbered list in chat.)
|
|
66
89
|
|
|
67
|
-
|
|
68
|
-
accepted only when their skeleton signature matches the active
|
|
69
|
-
revision; this lane does no runtime retargeting. The shared
|
|
70
|
-
dynamic controller stays authoritative for collision, grounding,
|
|
71
|
-
world translation
|
|
72
|
-
translates the visual root.
|
|
90
|
+
The generated character is a **same-rig Meshy-native lane**. Its animation-only
|
|
91
|
+
GLBs are accepted only when their skeleton signature matches the active
|
|
92
|
+
character revision; this lane does no runtime retargeting. The shared
|
|
93
|
+
ECCTRL-derived dynamic controller stays authoritative for collision, grounding,
|
|
94
|
+
facing, and world translation — identical in both lanes. The animation layer
|
|
95
|
+
poses the visual rig; it never translates the visual root.
|
|
73
96
|
|
|
74
97
|
Meshy manifests bypass browser cache, so newly installed actions must work
|
|
75
98
|
after preview without asking the player to disable cache. `playOneShot()`
|
|
@@ -101,10 +124,11 @@ fork as a migration strategy. Install a fresh copy elsewhere and port only the n
|
|
|
101
124
|
| `character/character-animations.ts` | `CharacterAnimations` | animation state machine, directional profiles, speed-matched cadence, `playOneShot`, procedural fallback |
|
|
102
125
|
| `character/animation-packs.ts` | `loadCharacterClips` | loads the core + installed UAL packs and retargets them to the active VRM |
|
|
103
126
|
| `character/meshy/meshy-loader.ts` | `loadMeshyCharacter` | loads a Meshy manifest, exact-signature model/clips, locomotion slots, and fallbacks |
|
|
127
|
+
| `character/player-character.ts` | `loadPlayerCharacter`, `loadRemotePlayerCharacter` | **the player's body** — the game's generated character when it has one, the avatar fallback when it doesn't, one shape either way |
|
|
104
128
|
| `character/motion-actions.ts` | `MotionActionDriver` | applies only validated planar trajectories through the physics controller, never the visual root |
|
|
105
|
-
| `character/vrm/*` | `loadVrm`, `retargetClips`, `capsuleFromModel`, `FootIK` | load
|
|
129
|
+
| `character/vrm/*` | `loadVrm`, `retargetClips`, `capsuleFromModel`, `FootIK` | the FALLBACK lane's parts: load a VRM avatar, retarget library clips onto its humanoid rig, auto-fit the capsule (works on both lanes), ground the feet |
|
|
106
130
|
|
|
107
|
-
## Minimal wiring
|
|
131
|
+
## Minimal wiring
|
|
108
132
|
|
|
109
133
|
```ts
|
|
110
134
|
import { PhysicsWorld } from "./controllers/shared/physics-world.ts";
|
|
@@ -115,25 +139,27 @@ import { characterPresets } from "./controllers/character/presets.ts";
|
|
|
115
139
|
import { FollowCamera } from "./controllers/character/follow-camera.ts";
|
|
116
140
|
import { createAimCue } from "./controllers/character/aim-cue.ts";
|
|
117
141
|
import { KeyboardInput } from "./controllers/character/keyboard-input.ts";
|
|
118
|
-
import {
|
|
142
|
+
import { loadPlayerCharacter } from "./controllers/character/player-character.ts";
|
|
119
143
|
import { capsuleFromModel } from "./controllers/character/vrm/capsule-fit.ts";
|
|
120
144
|
import { waitForPlayer } from "@genex-ai/embed-sdk";
|
|
121
145
|
|
|
122
146
|
const physics = await PhysicsWorld.create(); // nothing RAPIER-related may run before this resolves
|
|
123
147
|
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
148
|
+
// The player's body — ONE call, both lanes. It plays as the character THIS
|
|
149
|
+
// GAME generated whenever `public/assets/meshy-character.json` is present
|
|
150
|
+
// (with that character's exact-rig clips and locomotion profile), and falls
|
|
151
|
+
// back to the visiting player's own profile avatar when it isn't — retargeting
|
|
152
|
+
// the bundled core library plus any packs installed by `genex controller
|
|
153
|
+
// anims`. `user.avatarUrl` comes from the embed identity
|
|
154
|
+
// ($genex-threejs-embed-auth boots before this) and is used only in that
|
|
155
|
+
// fallback lane; the baked `./assets/avatar.vrm` covers local dev and load
|
|
156
|
+
// failures. WRITE THIS ONCE: when the generated character lands mid-build,
|
|
157
|
+
// `genex controller character --character <id>` drops the manifest in and the
|
|
158
|
+
// next reload swaps the body. Nothing below changes.
|
|
131
159
|
const { user } = await waitForPlayer(); // from "@genex-ai/embed-sdk"
|
|
132
|
-
const
|
|
133
|
-
.catch(() => loadVrm("./assets/avatar.vrm")); // network failure → bundled fallback
|
|
134
|
-
const clips = await loadCharacterClips(vrm);
|
|
160
|
+
const player = await loadPlayerCharacter({ avatarUrl: user.avatarUrl });
|
|
135
161
|
|
|
136
|
-
const fit = capsuleFromModel(
|
|
162
|
+
const fit = capsuleFromModel(player.scene); // collider fits THIS body's bounds
|
|
137
163
|
const character = new CharacterController(physics.world, camera, {
|
|
138
164
|
...characterPresets["default"].options,
|
|
139
165
|
...fit,
|
|
@@ -141,11 +167,15 @@ const character = new CharacterController(physics.world, camera, {
|
|
|
141
167
|
userData: { controller: { excludeVehicleRay: true } }, // car wheels must never drive on the player
|
|
142
168
|
});
|
|
143
169
|
scene.add(character.root);
|
|
144
|
-
character.root.add(
|
|
145
|
-
|
|
170
|
+
character.root.add(player.scene); // parent the body under the character root
|
|
171
|
+
player.scene.position.y = fit.modelOffsetY; // root = capsule CENTER; drop the model so feet touch the floor
|
|
146
172
|
physics.registerBody(character.body, character.root); // root now follows the body, interpolated
|
|
147
173
|
|
|
148
|
-
|
|
174
|
+
// locomotionProfile is present in the generated lane and undefined in the
|
|
175
|
+
// fallback — pass it either way; CharacterAnimations handles both.
|
|
176
|
+
const anims = new CharacterAnimations(player.scene, player.clips, {
|
|
177
|
+
locomotionProfile: player.locomotionProfile,
|
|
178
|
+
});
|
|
149
179
|
addEventListener("pointerdown", () => anims.playOneShot("Punch_Jab")); // punch on click
|
|
150
180
|
|
|
151
181
|
const kb = new KeyboardInput();
|
|
@@ -184,7 +214,10 @@ renderer.setAnimationLoop(() => {
|
|
|
184
214
|
followCam.update(delta);
|
|
185
215
|
|
|
186
216
|
anims.update(character, delta); // see the animations reference
|
|
187
|
-
|
|
217
|
+
player.update(delta); // REQUIRED — ticks the VRM humanoid + spring bones
|
|
218
|
+
// in the fallback lane, a no-op for native rigs.
|
|
219
|
+
// Always call it: that is why the lane can change
|
|
220
|
+
// under you without this loop changing.
|
|
188
221
|
renderer.render(scene, camera);
|
|
189
222
|
});
|
|
190
223
|
```
|
|
@@ -312,20 +345,39 @@ and the style-matching rules live in `$genex-threejs-touch-controls`.
|
|
|
312
345
|
|
|
313
346
|
**The local player is physics-authoritative; remote players are interpolated
|
|
314
347
|
visuals only.** Exactly one `CharacterController` exists — yours. For every
|
|
315
|
-
remote player:
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
348
|
+
remote player:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
import { loadRemotePlayerCharacter } from "./controllers/character/player-character.ts";
|
|
352
|
+
|
|
353
|
+
const remote = await loadRemotePlayerCharacter({ avatarUrl: p.avatarUrl });
|
|
354
|
+
scene.add(remote.scene);
|
|
355
|
+
const remoteAnims = new CharacterAnimations(remote.scene, remote.clips, {
|
|
356
|
+
locomotionProfile: remote.locomotionProfile,
|
|
357
|
+
});
|
|
358
|
+
// on 'leave': remote.dispose()
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Same rule as the local player, and the same one call: **in a game with its own
|
|
362
|
+
generated character, every remote wears it** — the game should look like the
|
|
363
|
+
game that was designed, and a mixed roster (one themed knight plus three stock
|
|
364
|
+
avatars) is the same incoherence as capsule-and-cone remotes. Only when this
|
|
365
|
+
game has NO generated character does each remote wear their own
|
|
366
|
+
`p.avatarUrl` VRM, which is then the right look. Several generated characters
|
|
367
|
+
(per class, per team, a picker) are fine when the player asks for them — the
|
|
368
|
+
rule is a coherent themed cast, not exactly one model. Name tags distinguish
|
|
369
|
+
players; their bodies don't have to.
|
|
370
|
+
|
|
371
|
+
The loader shares one parsed base across every remote (N remotes ≈ 1 body of
|
|
372
|
+
GPU memory) and `remote.dispose()` detaches the clone without touching those
|
|
373
|
+
shared resources. Move it with the interpolator from
|
|
374
|
+
`$genex-threejs-multiplayer`, and **never** create a rigid body, a
|
|
375
|
+
`CharacterController`, or any physics for it. Simulating remote players'
|
|
376
|
+
physics locally guarantees divergence — every client would compute a different
|
|
377
|
+
world. And a remote player's body is **never hand-built primitives** (no
|
|
378
|
+
capsule-plus-cone "person"). First-person games are not exempt — the local
|
|
379
|
+
player may be invisible to themselves (`setFirstPersonBody`), but every remote
|
|
380
|
+
is a full character on screen.
|
|
329
381
|
|
|
330
382
|
- **Publish `character.netState()`** on the fixed 10–20 Hz tick (never per frame):
|
|
331
383
|
`room.me.set(character.netState())`. It bundles the network-safe position, a four-number
|
|
@@ -8,12 +8,16 @@ clips only on the exact matching generated rig revision.
|
|
|
8
8
|
|
|
9
9
|
## The bundled assets + animation packs
|
|
10
10
|
|
|
11
|
-
`npx genex controller character`
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
`npx genex controller character` installs both lanes of the player's body — the
|
|
12
|
+
game's own generated character (the default) and the VRM avatar fallback:
|
|
13
|
+
|
|
14
|
+
- `public/assets/meshy-character.json` — written by
|
|
15
|
+
`npx genex controller character --character <id>` once the game's character
|
|
16
|
+
has been generated. Its presence is what `loadPlayerCharacter` routes on.
|
|
17
|
+
- `public/assets/avatar.vrm` — the FALLBACK body (the bundled CC0 default).
|
|
18
|
+
Always present; always one path. At runtime the fallback lane loads the
|
|
19
|
+
visiting player's own picked avatar instead (`user.avatarUrl` from the embed
|
|
20
|
+
identity) — this file covers local dev and load failures.
|
|
17
21
|
- `public/assets/animation-library.glb` (~1.3 MB) — the 12-clip core
|
|
18
22
|
(idle/walk/jog/sprint, the jump trio, crouch idle+move, hit, death, interact)
|
|
19
23
|
on a shared Quaternius rig (provenance in `src/controllers/NOTICE.md`).
|
|
@@ -26,38 +30,39 @@ clips only on the exact matching generated rig revision.
|
|
|
26
30
|
VRM helpers live in `src/controllers/character/vrm/`. Install three-vrm once:
|
|
27
31
|
`npm i @pixiv/three-vrm`.
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
`loadPlayerCharacter` loads the body and everything the game can play on it in
|
|
34
|
+
one call — the generated character's exact-rig clips, or the core library plus
|
|
35
|
+
installed packs retargeted onto the avatar's humanoid rig. All with
|
|
36
|
+
**relative** paths so the published game works under its subpath:
|
|
32
37
|
|
|
33
38
|
```ts
|
|
34
|
-
import {
|
|
35
|
-
import { loadCharacterClips } from "./controllers/character/animation-packs.ts";
|
|
39
|
+
import { loadPlayerCharacter } from "./controllers/character/player-character.ts";
|
|
36
40
|
import { capsuleFromModel } from "./controllers/character/vrm/capsule-fit.ts";
|
|
37
41
|
import { CharacterController } from "./controllers/character/character-controller.ts";
|
|
38
42
|
import { CharacterAnimations } from "./controllers/character/character-animations.ts";
|
|
39
43
|
import { characterPresets } from "./controllers/character/presets.ts";
|
|
40
44
|
import { waitForPlayer } from "@genex-ai/embed-sdk";
|
|
41
45
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
46
|
+
// This game's own generated character when it has one; otherwise the playing
|
|
47
|
+
// user's own avatar (their profile pick; per-session for guests), with the
|
|
48
|
+
// baked file as the local-dev / failure fallback.
|
|
44
49
|
const { user } = await waitForPlayer();
|
|
45
|
-
const
|
|
46
|
-
.catch(() => loadVrm("./assets/avatar.vrm"));
|
|
47
|
-
const clips = await loadCharacterClips(vrm); // core + every installed pack, retargeted
|
|
50
|
+
const player = await loadPlayerCharacter({ avatarUrl: user.avatarUrl });
|
|
48
51
|
|
|
49
|
-
// capsuleFromModel derives the collider from the
|
|
50
|
-
// per-
|
|
51
|
-
const fit = capsuleFromModel(scene);
|
|
52
|
+
// capsuleFromModel derives the collider from the body's bounds — no manual
|
|
53
|
+
// per-model tuning even as heights and proportions vary between lanes.
|
|
54
|
+
const fit = capsuleFromModel(player.scene);
|
|
52
55
|
const character = new CharacterController(physics.world, camera, {
|
|
53
56
|
...characterPresets["default"].options,
|
|
54
57
|
...fit,
|
|
55
58
|
position: { x: 0, y: 2, z: 0 },
|
|
56
59
|
});
|
|
57
|
-
character.root.add(scene);
|
|
58
|
-
scene.position.y = fit.modelOffsetY; // root = capsule CENTER; drop the model so feet touch the floor
|
|
60
|
+
character.root.add(player.scene);
|
|
61
|
+
player.scene.position.y = fit.modelOffsetY; // root = capsule CENTER; drop the model so feet touch the floor
|
|
59
62
|
|
|
60
|
-
const anims = new CharacterAnimations(scene, clips
|
|
63
|
+
const anims = new CharacterAnimations(player.scene, player.clips, {
|
|
64
|
+
locomotionProfile: player.locomotionProfile, // undefined in the fallback lane — fine
|
|
65
|
+
});
|
|
61
66
|
```
|
|
62
67
|
|
|
63
68
|
(Advanced: to load a single GLB by hand, `retargetClips(vrm, gltf.scene,
|
|
@@ -68,12 +73,15 @@ Per render frame, **after** `physics.step(delta)`:
|
|
|
68
73
|
|
|
69
74
|
```ts
|
|
70
75
|
anims.update(character, delta); // the controller itself satisfies the snapshot type
|
|
71
|
-
|
|
76
|
+
player.update(delta); // REQUIRED — ticks the humanoid rig + spring bones
|
|
72
77
|
```
|
|
73
78
|
|
|
74
|
-
`
|
|
75
|
-
animated normalized pose onto the render
|
|
76
|
-
|
|
79
|
+
`player.update(delta)` MUST run every frame, AFTER `anims.update`. In the
|
|
80
|
+
fallback (VRM) lane it applies the animated normalized pose onto the render
|
|
81
|
+
mesh and advances spring bones (hair, cloth); native generated rigs need no
|
|
82
|
+
such pass, so it costs nothing there. Call it unconditionally — that is what
|
|
83
|
+
lets the body change lanes without this loop changing.
|
|
84
|
+
`anims.update` takes the RAW render delta — pause/slow-motion go through
|
|
77
85
|
`anims.setPaused(true)` / `anims.setTimeScale(0.5)` (fade durations stretch with
|
|
78
86
|
the time scale so slow motion doesn't pop).
|
|
79
87
|
|
|
@@ -107,26 +115,25 @@ estimated cost. Use a returned ID; do not guess from a name. Search is free and
|
|
|
107
115
|
local. Character and animation generation print a Genex-credit quote before
|
|
108
116
|
enqueueing, and successful outputs are copied to permanent R2 URLs.
|
|
109
117
|
|
|
110
|
-
The install command writes `public/assets/meshy-character.json`.
|
|
111
|
-
|
|
118
|
+
The install command writes `public/assets/meshy-character.json`. **There is
|
|
119
|
+
nothing to wire** — the boot block above already routes on that file, so the
|
|
120
|
+
next reload plays the generated character with its exact-rig clips and
|
|
121
|
+
locomotion profile, through the same physics controller and camera.
|
|
122
|
+
|
|
123
|
+
`loadMeshyCharacter` is the direct handle underneath, for the rare case that
|
|
124
|
+
needs the manifest itself (reading `rigSignature`, a second character in the
|
|
125
|
+
scene):
|
|
112
126
|
|
|
113
127
|
```ts
|
|
114
128
|
import { loadMeshyCharacter } from "./controllers/character/meshy/meshy-loader.ts";
|
|
115
129
|
|
|
116
130
|
const native = await loadMeshyCharacter("./assets/meshy-character.json");
|
|
117
|
-
|
|
118
|
-
const character = new CharacterController(physics.world, camera, {
|
|
119
|
-
...characterPresets["default"].options,
|
|
120
|
-
...fit,
|
|
121
|
-
position: { x: 0, y: 2, z: 0 },
|
|
122
|
-
});
|
|
123
|
-
character.root.add(native.scene);
|
|
124
|
-
native.scene.position.y = fit.modelOffsetY;
|
|
125
|
-
const anims = new CharacterAnimations(native.scene, native.clips, {
|
|
126
|
-
locomotionProfile: native.locomotionProfile,
|
|
127
|
-
});
|
|
131
|
+
// native.scene / native.clips / native.locomotionProfile / native.manifest
|
|
128
132
|
```
|
|
129
133
|
|
|
134
|
+
Never make it the player's boot path: that is the two-lane split
|
|
135
|
+
`loadPlayerCharacter` exists to remove.
|
|
136
|
+
|
|
130
137
|
During Meshy validation, record the action ID actually bound to every slot.
|
|
131
138
|
A public preview is not evidence when the game is playing a different clip or
|
|
132
139
|
a rig-basic fallback. Meshy limb rotations play unchanged. Never freeze hand
|
|
@@ -210,15 +217,19 @@ const footIK = new FootIK(vrm, (foot) => {
|
|
|
210
217
|
});
|
|
211
218
|
```
|
|
212
219
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
`
|
|
220
|
+
Foot IK is a VRM-lane feature (it poses a VRM's normalized bones), so it
|
|
221
|
+
applies only when the player is on the fallback body — check
|
|
222
|
+
`player.kind === "avatar"` before creating it.
|
|
223
|
+
|
|
224
|
+
ORDER IS LOAD-BEARING — foot IK poses the VRM's *normalized* bones, which the
|
|
225
|
+
VRM's own update then copies onto the render mesh. Run it BETWEEN the two
|
|
226
|
+
(after that copy it has no visible effect at all — the next frame's mixer tick
|
|
216
227
|
overwrites it before the copy):
|
|
217
228
|
|
|
218
229
|
```ts
|
|
219
230
|
anims.update(character, delta); // 1. mixer poses the normalized rig
|
|
220
|
-
footIK
|
|
221
|
-
|
|
231
|
+
footIK?.update(delta); // 2. plant the feet on that pose (fallback lane only)
|
|
232
|
+
player.update(delta); // 3. copy normalized -> render mesh + spring bones
|
|
222
233
|
```
|
|
223
234
|
|
|
224
235
|
## States and default clips
|
|
@@ -409,12 +420,13 @@ remoteAnims.update(remoteState.flags, delta);
|
|
|
409
420
|
resolves correctly (a missing flag reads as not-crouched).
|
|
410
421
|
|
|
411
422
|
The mixer crossfades exactly as it does locally, so remote players animate
|
|
412
|
-
correctly without simulating anything.
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
423
|
+
correctly without simulating anything. Build every remote's body with
|
|
424
|
+
`loadRemotePlayerCharacter({ avatarUrl: p.avatarUrl })` — the same routing the
|
|
425
|
+
local player gets, so in a game with a generated character everyone wears it,
|
|
426
|
+
and only a game without one falls back to per-player avatars (`p.avatarUrl` is
|
|
427
|
+
the multiplayer SDK's verified per-player field; empty means fall back). It
|
|
428
|
+
clones a shared parsed base, so N remotes cost about one body of GPU memory;
|
|
429
|
+
call `remote.dispose()` on leave. Relay one-shot events (punch,
|
|
418
430
|
hit, validated planar-action start) alongside the flags and call the matching
|
|
419
431
|
`remoteAnims.playOneShot(...)` on receipt. Never run `MotionActionDriver` for a
|
|
420
432
|
remote: its smoothed owner-authored transform is the sole movement authority.
|
|
@@ -72,9 +72,11 @@ import { waitForPlayer, waitForAuth, getColyseusAuth, getEmbedToken } from "@gen
|
|
|
72
72
|
const { user, guest } = await waitForPlayer();
|
|
73
73
|
// user.id / user.name — real account identity, or guest:<id> / "Guest-1234"
|
|
74
74
|
// user.avatarUrl — the player's OWN VRM avatar (profile pick; per-session for
|
|
75
|
-
// guests).
|
|
76
|
-
//
|
|
77
|
-
//
|
|
75
|
+
// guests). It is the FALLBACK body, not the game's character: pass it to
|
|
76
|
+
// loadPlayerCharacter({ avatarUrl: user.avatarUrl }) and it is used only when
|
|
77
|
+
// this game has no generated character of its own, with the baked
|
|
78
|
+
// ./assets/avatar.vrm behind it (absent on old APIs / local test mode).
|
|
79
|
+
// Peers resolve the same way via the multiplayer SDK's player.avatarUrl.
|
|
78
80
|
|
|
79
81
|
// ACCOUNT gate — resolves ONLY for signed-in players (stays pending for
|
|
80
82
|
// guests; resolves later if they sign in mid-game). Use ONLY for /state
|
|
@@ -537,14 +537,16 @@ fights (many writers). A ball on `objects` glides and has one owner. That's the
|
|
|
537
537
|
`{ id, name, avatarUrl, connected, state, stateRaw }`: `state` is auto-smoothed (remotes) / live (you);
|
|
538
538
|
`stateRaw` is the raw latest (hit-tests, discrete values). A reconnect-grace seat remains in this
|
|
539
539
|
map with `connected: false`. `avatarUrl` is that player's verified VRM pick (server-set, `''`
|
|
540
|
-
when unknown) —
|
|
541
|
-
`
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
540
|
+
when unknown) — render each remote with
|
|
541
|
+
`loadRemotePlayerCharacter({ avatarUrl: p.avatarUrl })` (the vendored controller kit's
|
|
542
|
+
loader) and `remote.dispose()` on leave; never publish avatar URLs through `me.set`.
|
|
543
|
+
That one call carries the whole rule: **in a game with its own generated character,
|
|
544
|
+
every remote wears it** — `p.avatarUrl` is used only when this game has no generated
|
|
545
|
+
character, which is exactly when per-player looks are the right answer. It shares one
|
|
546
|
+
parsed base across remotes (N remotes ≈ 1 body of geometry/textures instead of a parse
|
|
547
|
+
per player) and clips are retargeted once on that base. **A remote player's body is
|
|
548
|
+
NEVER hand-built primitives** (no capsule + cone "person" — the single most common
|
|
549
|
+
shipped co-op defect). This applies with full force to
|
|
548
550
|
first-person games — the local player may be invisible to themselves, but every
|
|
549
551
|
remote hunter/partner is a full character on screen. On phone tiers, animate and fully draw only the
|
|
550
552
|
nearest `tier.remoteAvatarCap` remotes (`$genex-threejs-adaptive-quality`) — freeze the
|
|
@@ -273,13 +273,14 @@ room.me.set({
|
|
|
273
273
|
r: character.runActive, j: character.jumpActive, c: character.crouchActive,
|
|
274
274
|
});
|
|
275
275
|
|
|
276
|
-
// Remote players: a VISUAL-ONLY
|
|
277
|
-
// Build each remote
|
|
278
|
-
//
|
|
279
|
-
// relay replicates
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
276
|
+
// Remote players: a VISUAL-ONLY body — NO Rapier body, NO controller instance for remotes.
|
|
277
|
+
// Build each remote with loadRemotePlayerCharacter({ avatarUrl: pl.avatarUrl }) — in a game
|
|
278
|
+
// with its own generated character everyone wears it, and pl.avatarUrl (the verified
|
|
279
|
+
// per-player pick the relay replicates; '' = unknown → fall back) is used only when this
|
|
280
|
+
// game has no generated character. It clones a shared parsed base, so remotes share GPU
|
|
281
|
+
// geometry/textures instead of re-parsing per player, and retargets clips once on that
|
|
282
|
+
// base; drive each clone's own AnimationMixer and call remote.dispose() on 'leave'.
|
|
283
|
+
// Never reuse your own body OBJECT for a remote (clones, not references).
|
|
283
284
|
// Phone tiers animate only the nearest tier.remoteAvatarCap remotes (adaptive-quality skill). Position/rotation from smoothed state; animation from the
|
|
284
285
|
// synced flags via the avatar's own update(flags, dt). The character-controller skill's
|
|
285
286
|
// animations reference owns the flag set.
|