Skip to content

Commit 7df9c30

Browse files
authored
chore: tween sequence explorer migration + mini refactor (#1226)
* Disables the TweenSequence component handling logic from the SDK Runtime, as it's being migrated into the Explorer for better performance. * Alternative client Explorers (and web explorer) will still have the system logic running in the SDK handling the TweenSequence to avoid breaking the functionality in for them. * Separated the isCompleted() handling with the internal cache into a separate mini system to have it working even when the TweenSequence logic is disabled. (commit 4feb858) * Removed 2-frames waiting logic since we no longer support networked tweens (commit b682c1d) * Added test coverage for making sure TweenSequence only runs when the detected platform is not 'desktop' (the value that E@ returns)
1 parent 9706d67 commit 7df9c30

17 files changed

Lines changed: 307 additions & 130 deletions

File tree

packages/@dcl/ecs/src/runtime/initialization/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export { VideoEventsSystem }
6161

6262
/**
6363
* @public
64-
* Register callback functions to a particular entity on video events.
64+
* Register callback functions to a particular entity on tween events.
6565
*/
6666
export const tweenSystem: TweenSystem = createTweenSystem(engine)
6767
export { TweenSystem }

packages/@dcl/ecs/src/systems/tween.ts

Lines changed: 84 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,13 @@ import { PBTween, TweenLoop, TweenStateStatus } from '../components'
33
import { Entity, IEngine } from '../engine'
44
import { ReadWriteByteBuffer } from '../serialization/ByteBuffer'
55
import { dataCompare } from './crdt/utils'
6-
76
export type TweenSystem = {
87
tweenCompleted(entity: Entity): boolean
98
}
10-
119
/**
1210
* Avoid creating multiple tween systems
1311
*/
1412
const cacheTween: Map<number, TweenSystem> = new Map()
15-
1613
/**
1714
* @public
1815
* @returns tween helper to be used on the scene
@@ -24,140 +21,151 @@ export function createTweenSystem(engine: IEngine): TweenSystem {
2421
const Tween = components.Tween(engine)
2522
const TweenState = components.TweenState(engine)
2623
const TweenSequence = components.TweenSequence(engine)
27-
2824
const cache = new Map<
2925
Entity,
3026
{
3127
// Used to detect new tweens for the same entity
3228
tween: Uint8Array
33-
// Avoid updaing again the tween in the case we receieve a network tween from other client
34-
frames: number
3529
// Trigger the isCompleted only once per tween
3630
completed: boolean
3731
// Tween has changed on this frame
3832
changed: boolean
3933
}
4034
>()
41-
4235
function isCompleted(entity: Entity) {
4336
const tweenState = TweenState.getOrNull(entity)
4437
const tween = Tween.getOrNull(entity)
4538
const tweenCache = cache.get(entity)
46-
if (!tweenState || !tween) return false
39+
if (!tweenState || !tween || !tweenCache) return false
4740
/* istanbul ignore next */
4841
if (
4942
// Renderer notified that the tween is completed
50-
(tweenChanged(entity) || tweenState.state === TweenStateStatus.TS_COMPLETED) &&
43+
// Only consider it completed if the tween hasn't changed this frame (to avoid false positives after YOYO/sequence processing)
44+
((tweenState.state === TweenStateStatus.TS_COMPLETED && !tweenCache.changed) ||
45+
(tweenChanged(entity) && !tweenCache.changed)) &&
5146
// Avoid sending isCompleted multiple times
52-
!tweenCache?.completed &&
53-
// Amount of frames needed to consider a tween completed
54-
(tweenCache?.frames ?? 0) > 2
47+
!tweenCache.completed
5548
) {
5649
return true
5750
}
58-
5951
return false
6052
}
61-
6253
function tweenChanged(entity: Entity) {
6354
const currentTween = Tween.getOrNull(entity)
6455
const prevTween = cache.get(entity)?.tween
65-
6656
/* istanbul ignore next */
6757
if ((currentTween && !prevTween) || (!currentTween && prevTween)) {
6858
return true
6959
}
70-
60+
if (!currentTween || !prevTween) return false
7161
const currentBuff = new ReadWriteByteBuffer()
72-
Tween.schema.serialize(currentTween!, currentBuff)
73-
const equal = dataCompare(currentBuff.toBinary(), prevTween)
74-
75-
return equal
62+
Tween.schema.serialize(currentTween, currentBuff)
63+
const compareResult = dataCompare(currentBuff.toBinary(), prevTween)
64+
return compareResult !== 0
7665
}
7766

78-
const restartTweens: (() => void)[] = []
79-
// Logic for sequence tweens
67+
// System to manage cache (needed for tweenSystem.tweenCompleted() to work)
8068
engine.addSystem(() => {
81-
for (const restart of restartTweens) {
82-
restart()
83-
}
84-
restartTweens.length = 0
8569
for (const [entity, tween] of engine.getEntitiesWith(Tween)) {
8670
if (tweenChanged(entity)) {
8771
const buffer = new ReadWriteByteBuffer()
8872
Tween.schema.serialize(tween, buffer)
8973
cache.set(entity, {
9074
tween: buffer.toBinary(),
91-
frames: 0,
9275
completed: false,
9376
changed: true
9477
})
9578
continue
9679
}
97-
const tweenCache = cache.get(entity)!
98-
tweenCache.frames += 1
99-
tweenCache.changed = false
100-
if (isCompleted(entity)) {
101-
// Reset tween frames.
102-
tweenCache.frames = 0
103-
// set the tween completed to avoid calling this again for the same tween
104-
tweenCache.completed = true
105-
106-
const tweenSequence = TweenSequence.getOrNull(entity)
107-
if (!tweenSequence) continue
108-
const { sequence } = tweenSequence
109-
110-
if (sequence && sequence.length) {
111-
const [nextTweenSequence, ...otherTweens] = sequence
112-
Tween.createOrReplace(entity, nextTweenSequence)
113-
const mutableTweenHelper = TweenSequence.getMutable(entity)
114-
mutableTweenHelper.sequence = otherTweens
115-
if (tweenSequence.loop === TweenLoop.TL_RESTART) {
116-
mutableTweenHelper.sequence.push(tween)
117-
}
118-
} else if (tweenSequence.loop === TweenLoop.TL_YOYO) {
119-
Tween.createOrReplace(entity, backwardsTween(tween))
120-
} else if (tweenSequence.loop === TweenLoop.TL_RESTART) {
121-
Tween.deleteFrom(entity)
122-
cache.delete(entity)
123-
124-
restartTweens.push(() => {
125-
Tween.createOrReplace(entity, tween)
126-
})
80+
const tweenCache = cache.get(entity)
81+
if (tweenCache) {
82+
tweenCache.changed = false
83+
if (isCompleted(entity)) {
84+
// set the tween completed to avoid calling this again for the same tween
85+
tweenCache.completed = true
12786
}
12887
}
12988
}
13089
}, Number.NEGATIVE_INFINITY)
13190

132-
function backwardsTween(tween: PBTween): PBTween {
133-
if (tween.mode?.$case === 'move' && tween.mode.move) {
134-
return { ...tween, mode: { ...tween.mode, move: { start: tween.mode.move.end, end: tween.mode.move.start } } }
135-
}
136-
if (tween.mode?.$case === 'rotate' && tween.mode.rotate) {
137-
return {
138-
...tween,
139-
mode: { ...tween.mode, rotate: { start: tween.mode.rotate.end, end: tween.mode.rotate.start } }
91+
function initializeTweenSequenceSystem() {
92+
const restartTweens: (() => void)[] = []
93+
function backwardsTween(tween: PBTween): PBTween {
94+
if (tween.mode?.$case === 'move' && tween.mode.move) {
95+
return { ...tween, mode: { ...tween.mode, move: { start: tween.mode.move.end, end: tween.mode.move.start } } }
14096
}
141-
}
142-
if (tween.mode?.$case === 'scale' && tween.mode.scale) {
143-
return { ...tween, mode: { ...tween.mode, scale: { start: tween.mode.scale.end, end: tween.mode.scale.start } } }
144-
}
145-
if (tween.mode?.$case === 'textureMove' && tween.mode.textureMove) {
146-
return {
147-
...tween,
148-
mode: { ...tween.mode, textureMove: { start: tween.mode.textureMove.end, end: tween.mode.textureMove.start } }
97+
if (tween.mode?.$case === 'rotate' && tween.mode.rotate) {
98+
return {
99+
...tween,
100+
mode: { ...tween.mode, rotate: { start: tween.mode.rotate.end, end: tween.mode.rotate.start } }
101+
}
102+
}
103+
if (tween.mode?.$case === 'scale' && tween.mode.scale) {
104+
return { ...tween, mode: { ...tween.mode, scale: { start: tween.mode.scale.end, end: tween.mode.scale.start } } }
105+
}
106+
if (tween.mode?.$case === 'textureMove' && tween.mode.textureMove) {
107+
return {
108+
...tween,
109+
mode: { ...tween.mode, textureMove: { start: tween.mode.textureMove.end, end: tween.mode.textureMove.start } }
110+
}
149111
}
112+
/* istanbul ignore next */
113+
throw new Error('Invalid tween')
150114
}
151115

152-
/* istanbul ignore next */
153-
throw new Error('Invalid tween')
116+
// Logic for sequence tweens
117+
engine.addSystem(() => {
118+
for (const restart of restartTweens) {
119+
restart()
120+
}
121+
restartTweens.length = 0
122+
for (const [entity, tween] of engine.getEntitiesWith(Tween)) {
123+
const tweenCache = cache.get(entity)
124+
if (!tweenCache) continue
125+
126+
// Only process tween sequences if the tween is completed
127+
if (tweenCache.completed) {
128+
const tweenSequence = TweenSequence.getOrNull(entity)
129+
if (!tweenSequence) continue
130+
const { sequence } = tweenSequence
131+
if (sequence && sequence.length) {
132+
const [nextTweenSequence, ...otherTweens] = sequence
133+
Tween.createOrReplace(entity, nextTweenSequence)
134+
const mutableTweenHelper = TweenSequence.getMutable(entity)
135+
mutableTweenHelper.sequence = otherTweens
136+
if (tweenSequence.loop === TweenLoop.TL_RESTART) {
137+
mutableTweenHelper.sequence.push(tween)
138+
}
139+
// Reset completed flag for the next tween in sequence
140+
// Mark as changed so the cache system will detect the change and reset the cache properly
141+
tweenCache.completed = false
142+
tweenCache.changed = true
143+
} else if (tweenSequence.loop === TweenLoop.TL_YOYO) {
144+
Tween.createOrReplace(entity, backwardsTween(tween))
145+
// Reset completed flag for the backwards tween
146+
// Mark as changed so the cache system will detect the change and reset the cache properly
147+
tweenCache.completed = false
148+
tweenCache.changed = true
149+
} else if (tweenSequence.loop === TweenLoop.TL_RESTART) {
150+
Tween.deleteFrom(entity)
151+
cache.delete(entity)
152+
restartTweens.push(() => {
153+
Tween.createOrReplace(entity, tween)
154+
})
155+
}
156+
}
157+
}
158+
}, Number.NEGATIVE_INFINITY)
154159
}
155160

161+
// Some Explorers may not inject the flag and TweenSequence logic must be enabled in that case
162+
const enableTweenSequenceLogic = (globalThis as any).ENABLE_SDK_TWEEN_SEQUENCE
163+
if (enableTweenSequenceLogic !== false) initializeTweenSequenceSystem()
164+
156165
const tweenSystem: TweenSystem = {
157166
// This event is fired only once per tween
158167
tweenCompleted: isCompleted
159168
}
160-
161169
cacheTween.set(engine._id, tweenSystem)
162170
return tweenSystem
163171
}

test/__mocks__/~system/Runtime.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Mock for ~system/Runtime that can be controlled per test
2+
let mockPlatform = 'web' // Default to non-desktop
3+
let shouldThrowError = false // Control if platform check should fail
4+
5+
const Runtime = {
6+
getExplorerInformation: jest.fn().mockImplementation(async () => {
7+
if (shouldThrowError) {
8+
throw new Error('Platform detection unavailable')
9+
}
10+
return {
11+
platform: mockPlatform,
12+
agent: 'test-agent',
13+
configurations: {}
14+
}
15+
}),
16+
getRealm: jest.fn().mockResolvedValue({
17+
realmInfo: {
18+
baseUrl: 'http://localhost',
19+
realmName: 'localhost',
20+
networkId: 1,
21+
commsAdapter: 'offline',
22+
isPreview: true
23+
}
24+
}),
25+
getWorldTime: jest.fn().mockResolvedValue({ seconds: Date.now() / 1000 }),
26+
readFile: jest.fn().mockResolvedValue({ content: new Uint8Array(), hash: '' }),
27+
getSceneInformation: jest.fn().mockResolvedValue({
28+
urn: '',
29+
content: [],
30+
metadataJson: '{}',
31+
baseUrl: ''
32+
})
33+
}
34+
35+
// Helper to set platform for tests
36+
Runtime.setPlatform = (platform: string) => {
37+
mockPlatform = platform
38+
Runtime.getExplorerInformation.mockResolvedValue({
39+
platform: mockPlatform,
40+
agent: 'test-agent',
41+
configurations: {}
42+
})
43+
}
44+
45+
// Helper to reset to default
46+
Runtime.resetPlatform = () => {
47+
Runtime.setPlatform('web')
48+
shouldThrowError = false
49+
}
50+
51+
// Helper to make platform check fail (for testing catch block)
52+
Runtime.setShouldThrowError = (throwError: boolean) => {
53+
shouldThrowError = throwError
54+
}
55+
56+
module.exports = Runtime
57+

0 commit comments

Comments
 (0)