You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: animations-tweens/SKILL.md
+23Lines changed: 23 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -156,6 +156,27 @@ The short-circuit avoids calling `getMutableOrNull()` on unchanged frames, elimi
156
156
157
157
**When to prefer `Animator.playSingleAnimation`.** It's the canonical write: pauses all other states in one pass and accepts a `resetCursor` argument (defaults to `true`). It's not idempotent — but it's the right call site when the trigger is a one-shot event. The custom-helper pattern only exists when porting SDK6 code that needs lazy clip registration (see the previous PITFALL) or `noLoop + revertToIdle` semantics.
158
158
159
+
## Detecting Animation Completion
160
+
161
+
When a non-looping clip finishes, the engine flips that state's `playing` back to `false` — no `setTimeout(clipLength)` guessing needed. Poll with the READ-ONLY `Animator.get()` and edge-detect the `true → false` transition:
162
+
163
+
```typescript
164
+
let wasPlaying =false
165
+
engine.addSystem(() => {
166
+
const state =Animator.get(entity).states.find((s) =>s.clip==='Bite')
167
+
const isPlaying =state?.playing??false
168
+
if (wasPlaying&&!isPlaying) {
169
+
console.log('Bite finished')
170
+
Animator.playSingleAnimation(entity, 'Walk') // chain the next clip
171
+
}
172
+
wasPlaying=isPlaying
173
+
})
174
+
```
175
+
176
+
-**Never poll with `Animator.getClip()` or `getMutable()`** — both return a mutable ref and mark the component dirty every frame (same serialization overhead as the PITFALL above). `Animator.get()` is the only safe per-frame read.
177
+
- The engine only flips the flag when the clip ends by itself. Looping clips never flip it, `speed: 0` states never finish, and a scene-side `stopAllAnimations()` is your own write (track it yourself if you need to tell them apart).
178
+
- Requires a DCL 2.0 desktop client with playback-completion support; on older clients the flag never flips — keep a `timers.setTimeout` fallback only if you must support them.
179
+
159
180
## Tweens (Code-Based Animation)
160
181
161
182
Animate entity properties smoothly over time. Create with `Tween.create(entity, { mode: Tween.Mode.Move/Rotate/Scale({start, end}), duration, easingFunction })`. Duration is in **milliseconds** (1000 = 1 second). An entity can only have one Tween component at a time.
@@ -223,6 +244,7 @@ For complex animations, create a system with `engine.addSystem((dt) => { ... })`
223
244
| Door/grave/lid spawns OPEN instead of closed | The renderer auto-plays the clip and holds its final (open) frame; the model's closed pose is frame 0 of that clip | Hold frame 0 with a `playing: true, speed: 0, loop: false` state — see "Resting an animated model at its FIRST frame" above |
224
245
| Unnecessary per-frame serialization overhead | Clip-switch helper calls `getMutableOrNull` every tick with identical values (custom helper or `playSingleAnimation` called from a system / per-frame input callback) | Track the last-applied clip and short-circuit when unchanged, or only call the helper on state transitions — see "Short-circuit clip-switch helpers" best practice above |
225
246
| Animator has no effect | Missing `GltfContainer`|`Animator` only works on entities with a loaded GLTF model |
247
+
| Need to know when a non-looping clip ends | Guessing with `timers.setTimeout(clipLength)`| Edge-detect `playing` flipping to `false` via read-only `Animator.get()` — see "Detecting Animation Completion" above |
226
248
| Tween doesn't move | Same start and end | Verify values differ in `Tween.Mode.Move()`|
227
249
| Entity teleports when a Move tween starts |`start` doesn't match the entity's current position — omitted `start` = `(0,0,0)` (scene origin), hardcoded/stale `start` = wherever you wrote | Pass `Transform.get(entity).position` as `start` — it's the live position even while a previous tween is mid-flight |
228
250
| Tween plays once then stops | No loop | Add `TweenSequence` with `loop: TweenLoop.TL_YOYO`|
@@ -235,6 +257,7 @@ For full code examples (Animator setup, all tween types, sequences, helpers, tex
235
257
Engine-team test scenes (ground-truth API usage):
236
258
237
259
-`https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/73,-8-animator-tests-shark` — `Animator` with two states + `weight`; clicking cycles `playSingleAnimation('swim')` / `('bite')` and demonstrates concurrent blend by also setting `getClip('bite').playing = true`.
260
+
-`https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/73,-9-animation-finish` — animation-completion detection: non-looping `bite` finish flips `playing` to `false` (observed via read-only `Animator.get()`), chains into `swim`; proves looping clips never report finish.
238
261
-`https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/78,-4-tweens` — every finite mode (`setMove`/`setRotate`/`setScale`/`setTextureMove`/`setMoveRotateScale`), continuous modes, `createOrReplace`, `deleteFrom`, pause-toggle via `getMutableOrNull().playing`, and mixed-mode `TweenSequence`.
239
262
-`https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/77,-5-tweens-moving-platforms` — `Tween.setMove` + empty-sequence `TL_YOYO` for bobbing platforms, multi-waypoint path via `sequence[]`, and `createOrReplace` retrigger from a `TriggerArea`.
240
263
-`https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/79,-4-tween-following-cube` — a cube that chases the player, with a pad that switches live between `setMoveContinuous` (smooth, the default) and re-creating a `setMove` tween from `Transform.get(entity).position` on every re-aim (visibly jittery). Both modes share the same re-aim trigger and speed, so the difference is attributable to the tween mode alone. Also shows `Billboard` with `BM_Y` on floating labels.
Both helpers return `false` if the entity has no `AudioSource`, so create the component first (e.g. `AudioSource.create(entity, { audioClipUrl, playing: false })` at init).
55
55
56
+
**Detecting when a sound finishes:** when a non-looping clip ends on its own, the engine flips `AudioSource.playing` back to `false`. Poll it with the READ-ONLY getter and edge-detect the `true → false` transition — never poll with `getMutable` (dirties the component every frame):
if (wasPlaying&&!isPlaying) console.log('sound finished') // chain next sound/action here
63
+
wasPlaying=isPlaying
64
+
})
65
+
```
66
+
67
+
The engine only flips the flag on natural completion — a scene-initiated `stopSound()` is your own write, and looping clips never flip it. Alternatively use `audioEventsSystem` (callback per `MediaState` change, works for `AudioSource` AND `AudioStream` entities; `MS_PLAYING → MS_READY` = stopped, `MS_ERROR` = file failed to load): `audioEventsSystem.registerAudioEventsEntity(entity, (e) => {...})`, plus `getAudioState(entity)` / `removeAudioEventsEntity(entity)` — same shape as `videoEventsSystem`. Both features require a DCL 2.0 desktop client with playback-completion support; on older clients the flag never flips and no finish signal arrives — don't build logic that hard-blocks on it without a timeout fallback.
68
+
56
69
Players must interact with the scene (click) before audio can play (browser autoplay policy). If an audio file needs to be ready to play the instant the player interacts, use the `AssetLoad` component to pre-load the asset.
57
70
58
71
> **Before adding audio**: Confirm with the user before fetching audio from external sources.
@@ -61,7 +74,7 @@ Players must interact with the scene (click) before audio can play (browser auto
61
74
62
75
Stream audio from a URL (radio, live streams). Key fields: `url` (streaming URL), `playing`, `volume`. Non-spatial by default — plays at same volume everywhere. Set `spatial: true` with `spatialMinDistance`/`spatialMaxDistance` for distance-based volume.
63
76
64
-
Query state with `AudioStream.getAudioState(entity)` which returns a `PBAudioEvent | undefined` — an object with a `state` field (a `MediaState` enum: `MS_PLAYING`, `MS_ERROR`, etc.) and a `timestamp` field, not a bare enum. Read the state as `AudioStream.getAudioState(entity)?.state`.
77
+
Query state with `AudioStream.getAudioState(entity)` which returns a `PBAudioEvent | undefined` — an object with a `state` field (a `MediaState` enum: `MS_PLAYING`, `MS_ERROR`, etc.) and a `timestamp` field, not a bare enum. Read the state as `AudioStream.getAudioState(entity)?.state`. For callback-style state changes instead of polling, `audioEventsSystem.registerAudioEventsEntity` works on AudioStream entities too (see the AudioSource finish-detection note above).
65
78
66
79
> **Before adding a streaming URL**: If not provided by the user, confirm the source first.
67
80
@@ -115,5 +128,6 @@ Engine-team test scenes exercised against the real explorer:
115
128
116
129
-[audio-source-retrigger-test](https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/89,-10-audio-source-retrigger-test) — `AudioSource.playSound`/`stopSound`, same-URL retrigger, URL-swap on one entity, `resetCursor` semantics, volume/pitch/loop variations, and why `playSound` beats hand-mutating `getMutable` (LWW dedup).
117
130
-[audio-visualization](https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/88,-10-audio-visualization) — `AudioAnalysis` music visualizer (see the `audio-analysis` skill).
131
+
-[audio-finish](https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/89,-11-audio-finish) — natural-finish detection via the `playing` flip + `audioEventsSystem` callback, and how a scene-initiated stop is distinguished from a natural finish.
118
132
119
133
For full code examples and implementation patterns, see `{baseDir}/references/media-patterns.md`. For component field details, see `{baseDir}/references/media-reference.md`.
Copy file name to clipboardExpand all lines: audio-video/references/media-reference.md
+20Lines changed: 20 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -56,6 +56,26 @@ audio.playing = true
56
56
audio.currentTime=0// if playing/currentTime already had these values, LWW may dedup the PUT
57
57
```
58
58
59
+
### Audio Events (playback state changes, incl. finish detection)
60
+
61
+
Mirrors `videoEventsSystem`, but for `AudioSource` and `AudioStream` entities. The event's `state` is a `MediaState` enum value (`MS_NONE`, `MS_ERROR`, `MS_LOADING`, `MS_READY`, `MS_PLAYING`, `MS_BUFFERING`, `MS_SEEKING`, `MS_PAUSED`).
For AudioSource clips the engine also flips the component's `playing` field back to `false` on natural finish — pollable with the read-only `AudioSource.get(entity).playing` (see SKILL.md). Requires a DCL 2.0 desktop client with playback-completion support.
Copy file name to clipboardExpand all lines: player-avatar/SKILL.md
+24Lines changed: 24 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -302,6 +302,30 @@ import { stopEmote } from '~system/RestrictedActions'
302
302
stopEmote({})
303
303
```
304
304
305
+
### Detecting when an emote finishes
306
+
307
+
Every emote lifecycle event is appended to the `AvatarEmoteCommand` grow-only set on the player entity, with a `state` field (`EmoteState` enum). Works for scene-triggered emotes (`triggerEmote`/`triggerSceneEmote`), emotes the player plays via the emote wheel, AND other players' emotes (pass their entity instead of `engine.PlayerEntity`).
caseEmoteState.ES_STARTED: // emote started (also the value when `state` is absent — older clients)
316
+
break
317
+
caseEmoteState.ES_FINISHED: // non-looping emote played to its natural end
318
+
break
319
+
caseEmoteState.ES_INTERRUPTED: // cut short: movement/jump, teleport, another emote, stopEmote(), or scene exit
320
+
break
321
+
}
322
+
})
323
+
```
324
+
325
+
- Always default absent `state` to `ES_STARTED` (`cmd.state ?? EmoteState.ES_STARTED`) — entries from older clients omit the field, and older clients never send FINISHED/INTERRUPTED at all, so don't hard-block gameplay on a finish signal without a fallback.
326
+
-**Masked (partial-body) emotes on the local player report no lifecycle events** — known limitation.
327
+
- Requires a DCL 2.0 desktop client with playback-completion support.
328
+
305
329
### Emote masks (upper-body only)
306
330
307
331
`triggerEmote` and `triggerSceneEmote` both accept an optional `mask` (enum `AvatarMask`, imported from `@dcl/sdk/ecs`) that limits which bones the animation drives. Use it to restrict a looping emote to the upper body so the player can keep walking around while the upper body animates (e.g. carrying/juggling an object).
0 commit comments