rn-gamekit 0.1.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/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/lib/module/assets/defineAssets.js +48 -0
- package/lib/module/assets/defineAssets.js.map +1 -0
- package/lib/module/assets/errors.js +38 -0
- package/lib/module/assets/errors.js.map +1 -0
- package/lib/module/assets/types.js +2 -0
- package/lib/module/assets/types.js.map +1 -0
- package/lib/module/assets/validation.js +99 -0
- package/lib/module/assets/validation.js.map +1 -0
- package/lib/module/core/frameDriver.js +20 -0
- package/lib/module/core/frameDriver.js.map +1 -0
- package/lib/module/core/input/createInputBuffer.js +296 -0
- package/lib/module/core/input/createInputBuffer.js.map +1 -0
- package/lib/module/core/input/types.js +4 -0
- package/lib/module/core/input/types.js.map +1 -0
- package/lib/module/core/session/createGameSession.js +554 -0
- package/lib/module/core/session/createGameSession.js.map +1 -0
- package/lib/module/core/session/deepFreeze.js +155 -0
- package/lib/module/core/session/deepFreeze.js.map +1 -0
- package/lib/module/core/session/diagnostics.js +2 -0
- package/lib/module/core/session/diagnostics.js.map +1 -0
- package/lib/module/core/session/types.js +43 -0
- package/lib/module/core/session/types.js.map +1 -0
- package/lib/module/definition/defineGame.js +57 -0
- package/lib/module/definition/defineGame.js.map +1 -0
- package/lib/module/definition/types.js +4 -0
- package/lib/module/definition/types.js.map +1 -0
- package/lib/module/geometry/types.js +2 -0
- package/lib/module/geometry/types.js.map +1 -0
- package/lib/module/index.js +12 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/react/GamePointerInput.js +385 -0
- package/lib/module/react/GamePointerInput.js.map +1 -0
- package/lib/module/react/GameView.js +223 -0
- package/lib/module/react/GameView.js.map +1 -0
- package/lib/module/react/alphaClock.js +42 -0
- package/lib/module/react/alphaClock.js.map +1 -0
- package/lib/module/react/assets/createGameAssetStore.js +358 -0
- package/lib/module/react/assets/createGameAssetStore.js.map +1 -0
- package/lib/module/react/assets/decodeSkiaImage.js +54 -0
- package/lib/module/react/assets/decodeSkiaImage.js.map +1 -0
- package/lib/module/react/assets/errors.js +10 -0
- package/lib/module/react/assets/errors.js.map +1 -0
- package/lib/module/react/assets/useGameAssets.js +160 -0
- package/lib/module/react/assets/useGameAssets.js.map +1 -0
- package/lib/module/react/bindAppLifecycle.js +58 -0
- package/lib/module/react/bindAppLifecycle.js.map +1 -0
- package/lib/module/react/bindGameSession.js +37 -0
- package/lib/module/react/bindGameSession.js.map +1 -0
- package/lib/module/react/gestureLifecycle.js +68 -0
- package/lib/module/react/gestureLifecycle.js.map +1 -0
- package/lib/module/react/instrumentation.js +4 -0
- package/lib/module/react/instrumentation.js.map +1 -0
- package/lib/module/react/pointerBinding.js +196 -0
- package/lib/module/react/pointerBinding.js.map +1 -0
- package/lib/module/react/pointerCoalescer.js +224 -0
- package/lib/module/react/pointerCoalescer.js.map +1 -0
- package/lib/module/react/pointerContainment.js +27 -0
- package/lib/module/react/pointerContainment.js.map +1 -0
- package/lib/module/react/sprites/GameSprite.js +116 -0
- package/lib/module/react/sprites/GameSprite.js.map +1 -0
- package/lib/module/react/sprites/GameWorld2D.js +44 -0
- package/lib/module/react/sprites/GameWorld2D.js.map +1 -0
- package/lib/module/react/sprites/Sprite.js +147 -0
- package/lib/module/react/sprites/Sprite.js.map +1 -0
- package/lib/module/react/sprites/SpriteBatch.js +173 -0
- package/lib/module/react/sprites/SpriteBatch.js.map +1 -0
- package/lib/module/react/sprites/spriteBatchPolicy.js +25 -0
- package/lib/module/react/sprites/spriteBatchPolicy.js.map +1 -0
- package/lib/module/react/sprites/spriteTransform.js +135 -0
- package/lib/module/react/sprites/spriteTransform.js.map +1 -0
- package/lib/module/react/viewportBinding.js +101 -0
- package/lib/module/react/viewportBinding.js.map +1 -0
- package/lib/module/react.js +13 -0
- package/lib/module/react.js.map +1 -0
- package/lib/module/scene/defineScene.js +29 -0
- package/lib/module/scene/defineScene.js.map +1 -0
- package/lib/module/scene/types.js +4 -0
- package/lib/module/scene/types.js.map +1 -0
- package/lib/module/sprites/sampleSpriteClip.js +50 -0
- package/lib/module/sprites/sampleSpriteClip.js.map +1 -0
- package/lib/module/sprites/spriteAnimationState.js +165 -0
- package/lib/module/sprites/spriteAnimationState.js.map +1 -0
- package/lib/module/testing.js +57 -0
- package/lib/module/testing.js.map +1 -0
- package/lib/module/viewport2d/index.js +4 -0
- package/lib/module/viewport2d/index.js.map +1 -0
- package/lib/module/viewport2d/math.js +129 -0
- package/lib/module/viewport2d/math.js.map +1 -0
- package/lib/module/viewport2d/types.js +2 -0
- package/lib/module/viewport2d/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/assets/defineAssets.d.ts +20 -0
- package/lib/typescript/src/assets/defineAssets.d.ts.map +1 -0
- package/lib/typescript/src/assets/errors.d.ts +20 -0
- package/lib/typescript/src/assets/errors.d.ts.map +1 -0
- package/lib/typescript/src/assets/types.d.ts +138 -0
- package/lib/typescript/src/assets/types.d.ts.map +1 -0
- package/lib/typescript/src/assets/validation.d.ts +15 -0
- package/lib/typescript/src/assets/validation.d.ts.map +1 -0
- package/lib/typescript/src/core/frameDriver.d.ts +12 -0
- package/lib/typescript/src/core/frameDriver.d.ts.map +1 -0
- package/lib/typescript/src/core/input/createInputBuffer.d.ts +12 -0
- package/lib/typescript/src/core/input/createInputBuffer.d.ts.map +1 -0
- package/lib/typescript/src/core/input/types.d.ts +64 -0
- package/lib/typescript/src/core/input/types.d.ts.map +1 -0
- package/lib/typescript/src/core/session/createGameSession.d.ts +18 -0
- package/lib/typescript/src/core/session/createGameSession.d.ts.map +1 -0
- package/lib/typescript/src/core/session/deepFreeze.d.ts +27 -0
- package/lib/typescript/src/core/session/deepFreeze.d.ts.map +1 -0
- package/lib/typescript/src/core/session/diagnostics.d.ts +36 -0
- package/lib/typescript/src/core/session/diagnostics.d.ts.map +1 -0
- package/lib/typescript/src/core/session/types.d.ts +104 -0
- package/lib/typescript/src/core/session/types.d.ts.map +1 -0
- package/lib/typescript/src/definition/defineGame.d.ts +66 -0
- package/lib/typescript/src/definition/defineGame.d.ts.map +1 -0
- package/lib/typescript/src/definition/types.d.ts +56 -0
- package/lib/typescript/src/definition/types.d.ts.map +1 -0
- package/lib/typescript/src/geometry/types.d.ts +8 -0
- package/lib/typescript/src/geometry/types.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +18 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/react/GamePointerInput.d.ts +37 -0
- package/lib/typescript/src/react/GamePointerInput.d.ts.map +1 -0
- package/lib/typescript/src/react/GameView.d.ts +61 -0
- package/lib/typescript/src/react/GameView.d.ts.map +1 -0
- package/lib/typescript/src/react/alphaClock.d.ts +29 -0
- package/lib/typescript/src/react/alphaClock.d.ts.map +1 -0
- package/lib/typescript/src/react/assets/createGameAssetStore.d.ts +33 -0
- package/lib/typescript/src/react/assets/createGameAssetStore.d.ts.map +1 -0
- package/lib/typescript/src/react/assets/decodeSkiaImage.d.ts +12 -0
- package/lib/typescript/src/react/assets/decodeSkiaImage.d.ts.map +1 -0
- package/lib/typescript/src/react/assets/errors.d.ts +8 -0
- package/lib/typescript/src/react/assets/errors.d.ts.map +1 -0
- package/lib/typescript/src/react/assets/useGameAssets.d.ts +35 -0
- package/lib/typescript/src/react/assets/useGameAssets.d.ts.map +1 -0
- package/lib/typescript/src/react/bindAppLifecycle.d.ts +32 -0
- package/lib/typescript/src/react/bindAppLifecycle.d.ts.map +1 -0
- package/lib/typescript/src/react/bindGameSession.d.ts +14 -0
- package/lib/typescript/src/react/bindGameSession.d.ts.map +1 -0
- package/lib/typescript/src/react/gestureLifecycle.d.ts +37 -0
- package/lib/typescript/src/react/gestureLifecycle.d.ts.map +1 -0
- package/lib/typescript/src/react/instrumentation.d.ts +47 -0
- package/lib/typescript/src/react/instrumentation.d.ts.map +1 -0
- package/lib/typescript/src/react/pointerBinding.d.ts +104 -0
- package/lib/typescript/src/react/pointerBinding.d.ts.map +1 -0
- package/lib/typescript/src/react/pointerCoalescer.d.ts +106 -0
- package/lib/typescript/src/react/pointerCoalescer.d.ts.map +1 -0
- package/lib/typescript/src/react/pointerContainment.d.ts +12 -0
- package/lib/typescript/src/react/pointerContainment.d.ts.map +1 -0
- package/lib/typescript/src/react/sprites/GameSprite.d.ts +75 -0
- package/lib/typescript/src/react/sprites/GameSprite.d.ts.map +1 -0
- package/lib/typescript/src/react/sprites/GameWorld2D.d.ts +29 -0
- package/lib/typescript/src/react/sprites/GameWorld2D.d.ts.map +1 -0
- package/lib/typescript/src/react/sprites/Sprite.d.ts +43 -0
- package/lib/typescript/src/react/sprites/Sprite.d.ts.map +1 -0
- package/lib/typescript/src/react/sprites/SpriteBatch.d.ts +41 -0
- package/lib/typescript/src/react/sprites/SpriteBatch.d.ts.map +1 -0
- package/lib/typescript/src/react/sprites/spriteBatchPolicy.d.ts +10 -0
- package/lib/typescript/src/react/sprites/spriteBatchPolicy.d.ts.map +1 -0
- package/lib/typescript/src/react/sprites/spriteTransform.d.ts +86 -0
- package/lib/typescript/src/react/sprites/spriteTransform.d.ts.map +1 -0
- package/lib/typescript/src/react/viewportBinding.d.ts +42 -0
- package/lib/typescript/src/react/viewportBinding.d.ts.map +1 -0
- package/lib/typescript/src/react.d.ts +20 -0
- package/lib/typescript/src/react.d.ts.map +1 -0
- package/lib/typescript/src/scene/defineScene.d.ts +26 -0
- package/lib/typescript/src/scene/defineScene.d.ts.map +1 -0
- package/lib/typescript/src/scene/types.d.ts +71 -0
- package/lib/typescript/src/scene/types.d.ts.map +1 -0
- package/lib/typescript/src/sprites/sampleSpriteClip.d.ts +24 -0
- package/lib/typescript/src/sprites/sampleSpriteClip.d.ts.map +1 -0
- package/lib/typescript/src/sprites/spriteAnimationState.d.ts +45 -0
- package/lib/typescript/src/sprites/spriteAnimationState.d.ts.map +1 -0
- package/lib/typescript/src/testing.d.ts +28 -0
- package/lib/typescript/src/testing.d.ts.map +1 -0
- package/lib/typescript/src/viewport2d/index.d.ts +3 -0
- package/lib/typescript/src/viewport2d/index.d.ts.map +1 -0
- package/lib/typescript/src/viewport2d/math.d.ts +25 -0
- package/lib/typescript/src/viewport2d/math.d.ts.map +1 -0
- package/lib/typescript/src/viewport2d/types.d.ts +59 -0
- package/lib/typescript/src/viewport2d/types.d.ts.map +1 -0
- package/package.json +120 -0
- package/src/assets/defineAssets.ts +62 -0
- package/src/assets/errors.ts +64 -0
- package/src/assets/types.ts +164 -0
- package/src/assets/validation.ts +158 -0
- package/src/core/frameDriver.ts +31 -0
- package/src/core/input/createInputBuffer.ts +337 -0
- package/src/core/input/types.ts +67 -0
- package/src/core/session/createGameSession.ts +652 -0
- package/src/core/session/deepFreeze.ts +158 -0
- package/src/core/session/diagnostics.ts +35 -0
- package/src/core/session/types.ts +122 -0
- package/src/definition/defineGame.ts +94 -0
- package/src/definition/types.ts +67 -0
- package/src/geometry/types.ts +7 -0
- package/src/index.ts +84 -0
- package/src/react/GamePointerInput.tsx +478 -0
- package/src/react/GameView.tsx +317 -0
- package/src/react/alphaClock.ts +59 -0
- package/src/react/assets/createGameAssetStore.ts +459 -0
- package/src/react/assets/decodeSkiaImage.ts +64 -0
- package/src/react/assets/errors.ts +7 -0
- package/src/react/assets/useGameAssets.ts +171 -0
- package/src/react/bindAppLifecycle.ts +79 -0
- package/src/react/bindGameSession.ts +41 -0
- package/src/react/gestureLifecycle.ts +73 -0
- package/src/react/instrumentation.ts +54 -0
- package/src/react/pointerBinding.ts +226 -0
- package/src/react/pointerCoalescer.ts +234 -0
- package/src/react/pointerContainment.ts +23 -0
- package/src/react/sprites/GameSprite.tsx +192 -0
- package/src/react/sprites/GameWorld2D.tsx +46 -0
- package/src/react/sprites/Sprite.tsx +194 -0
- package/src/react/sprites/SpriteBatch.tsx +219 -0
- package/src/react/sprites/spriteBatchPolicy.ts +21 -0
- package/src/react/sprites/spriteTransform.ts +163 -0
- package/src/react/viewportBinding.ts +104 -0
- package/src/react.ts +37 -0
- package/src/scene/defineScene.ts +38 -0
- package/src/scene/types.ts +79 -0
- package/src/sprites/sampleSpriteClip.ts +51 -0
- package/src/sprites/spriteAnimationState.ts +176 -0
- package/src/testing.ts +61 -0
- package/src/viewport2d/index.ts +7 -0
- package/src/viewport2d/math.ts +134 -0
- package/src/viewport2d/types.ts +63 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { Canvas } from '@shopify/react-native-skia';
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
type ComponentType,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from 'react';
|
|
11
|
+
import {
|
|
12
|
+
AppState,
|
|
13
|
+
StyleSheet,
|
|
14
|
+
View,
|
|
15
|
+
type StyleProp,
|
|
16
|
+
type ViewStyle,
|
|
17
|
+
} from 'react-native';
|
|
18
|
+
import { useFrameCallback, useSharedValue, type SharedValue } from 'react-native-reanimated';
|
|
19
|
+
import { scheduleOnRN } from 'react-native-worklets';
|
|
20
|
+
|
|
21
|
+
import type { InputMap, SceneMap } from '../definition/types';
|
|
22
|
+
import type { AssetGroupMap, LoadedAssets } from '../assets/types';
|
|
23
|
+
import type { CommitFrame, GameSession } from '../core/session/types';
|
|
24
|
+
import type { ResolvedViewport2D } from '../viewport2d';
|
|
25
|
+
import { advanceAlpha } from './alphaClock';
|
|
26
|
+
import { bindAppLifecycle } from './bindAppLifecycle';
|
|
27
|
+
import { bindGameSession } from './bindGameSession';
|
|
28
|
+
import type { GameViewInstrumentation } from './instrumentation';
|
|
29
|
+
import { bindingForViewport, type ViewportBinding } from './viewportBinding';
|
|
30
|
+
|
|
31
|
+
/** Stable imperative values supplied to a Skia renderer component. */
|
|
32
|
+
export interface GameRendererProps<
|
|
33
|
+
TScenes extends SceneMap,
|
|
34
|
+
TAssets extends AssetGroupMap = AssetGroupMap,
|
|
35
|
+
> {
|
|
36
|
+
/** Latest simulation commit, updated at commit frequency only. */
|
|
37
|
+
readonly frame: SharedValue<CommitFrame<TScenes>>;
|
|
38
|
+
/**
|
|
39
|
+
* UI-owned presentation fraction: advances on the UI runtime and resets
|
|
40
|
+
* to zero on every new commit. Never extrapolates past 1.
|
|
41
|
+
*/
|
|
42
|
+
readonly alpha: SharedValue<number>;
|
|
43
|
+
/** Latest resolved viewport shared with the input adapter. */
|
|
44
|
+
readonly viewport: SharedValue<ResolvedViewport2D | undefined>;
|
|
45
|
+
/** The stable loaded asset lease (T7.5); shape-only renderers omit it. */
|
|
46
|
+
readonly assets?: LoadedAssets<TAssets>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Props for the Skia-backed GameKit view. */
|
|
50
|
+
export interface GameViewProps<
|
|
51
|
+
TScenes extends SceneMap,
|
|
52
|
+
TInput extends InputMap,
|
|
53
|
+
TAssets extends AssetGroupMap = AssetGroupMap,
|
|
54
|
+
> {
|
|
55
|
+
/** Externally owned headless game session. */
|
|
56
|
+
readonly game: GameSession<TScenes, TInput>;
|
|
57
|
+
/**
|
|
58
|
+
* Explicit presentation key (RF6): remounts the per-session presentation
|
|
59
|
+
* when the session changes. The fallback is a stable per-session identity
|
|
60
|
+
* allocated from a WeakMap (T8.6) — never object stringification, which
|
|
61
|
+
* maps every session to the same value.
|
|
62
|
+
*/
|
|
63
|
+
readonly presentationKey?: string | number;
|
|
64
|
+
/** The stable loaded asset lease; shape-only games omit it. */
|
|
65
|
+
readonly assets?: LoadedAssets<TAssets>;
|
|
66
|
+
/** Stable Skia renderer component for the session's scene snapshots. */
|
|
67
|
+
readonly renderer: ComponentType<GameRendererProps<TScenes, TAssets>>;
|
|
68
|
+
/** Optional measurement callbacks for the Performance Lab (F1). */
|
|
69
|
+
readonly instrumentation?: GameViewInstrumentation;
|
|
70
|
+
/** Static React HUD and controls mounted above the canvas. */
|
|
71
|
+
readonly children?: ReactNode;
|
|
72
|
+
/** Optional style for the mounted surface. */
|
|
73
|
+
readonly style?: StyleProp<ViewStyle>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Context supplying the resolved viewport to pointer input children.
|
|
78
|
+
*
|
|
79
|
+
* `GamePointerInput` consumes the binding for JS-side conversion and the
|
|
80
|
+
* shared value for the UI-side containment mirror, so drawing and hit
|
|
81
|
+
* testing always use the same resolved viewport instance for a layout
|
|
82
|
+
* revision.
|
|
83
|
+
*/
|
|
84
|
+
export interface GameViewport {
|
|
85
|
+
readonly binding: ViewportBinding;
|
|
86
|
+
/** Resolved viewport shared value (layout-only updates). */
|
|
87
|
+
readonly viewport: SharedValue<ResolvedViewport2D | undefined>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const GameViewportContext = createContext<GameViewport | null>(null);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Mount a headless GameSession into a Skia canvas.
|
|
94
|
+
*
|
|
95
|
+
* Simulation commits update the frame shared value at commit frequency (one
|
|
96
|
+
* per fixed step, never per display frame); the presentation fraction lives
|
|
97
|
+
* on the UI runtime and is advanced by a frame callback that resets on every
|
|
98
|
+
* new commit, clamps at 1, and never extrapolates. The clock is gated by a
|
|
99
|
+
* `running` shared value so pause, background, unmount, and dispose hold the
|
|
100
|
+
* presentation. React owns the surface and static overlay tree; it is never
|
|
101
|
+
* the frame store. The session is started while mounted, paused on cleanup
|
|
102
|
+
* and app backgrounding, and is never disposed by this component — the
|
|
103
|
+
* creator owns disposal.
|
|
104
|
+
*/
|
|
105
|
+
/**
|
|
106
|
+
* Per-session presentation binding (RF6).
|
|
107
|
+
*
|
|
108
|
+
* Keyed by the session so the frame, alpha, epoch, and clock initialize from
|
|
109
|
+
* the new session BEFORE its renderer can execute — a replacement renderer
|
|
110
|
+
* never observes the previous session's frame, even when both scenes are
|
|
111
|
+
* named the same.
|
|
112
|
+
*/
|
|
113
|
+
function GamePresentation<
|
|
114
|
+
TScenes extends SceneMap,
|
|
115
|
+
TInput extends InputMap,
|
|
116
|
+
TAssets extends AssetGroupMap,
|
|
117
|
+
>({
|
|
118
|
+
game,
|
|
119
|
+
assets,
|
|
120
|
+
renderer,
|
|
121
|
+
viewportValue,
|
|
122
|
+
instrumentationRef,
|
|
123
|
+
}: {
|
|
124
|
+
readonly game: GameSession<TScenes, TInput>;
|
|
125
|
+
readonly assets: LoadedAssets<TAssets> | undefined;
|
|
126
|
+
readonly renderer: ComponentType<GameRendererProps<TScenes, TAssets>>;
|
|
127
|
+
readonly viewportValue: SharedValue<ResolvedViewport2D | undefined>;
|
|
128
|
+
readonly instrumentationRef: { readonly current: GameViewInstrumentation | undefined };
|
|
129
|
+
}) {
|
|
130
|
+
const Renderer = renderer;
|
|
131
|
+
const frame = useSharedValue<CommitFrame<TScenes>>(() => game.getRenderFrame());
|
|
132
|
+
const alpha = useSharedValue(0);
|
|
133
|
+
const running = useSharedValue(true);
|
|
134
|
+
// Binding epoch: bumped per (re)subscription so a replacement session whose
|
|
135
|
+
// revision restarts at zero is still accepted by the UI clock.
|
|
136
|
+
const epoch = useSharedValue(0);
|
|
137
|
+
const clockEpoch = useSharedValue(0);
|
|
138
|
+
const clockRevision = useSharedValue(-1);
|
|
139
|
+
// F1: last revision/epoch the UI clock observed, so the instrumentation can
|
|
140
|
+
// report the FIRST UI frame that saw a new commit.
|
|
141
|
+
const observedRevision = useSharedValue(-1);
|
|
142
|
+
const observedEpoch = useSharedValue(-1);
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
epoch.value += 1;
|
|
146
|
+
// RF6: the frame is seeded at mount from this session; the keyed remount
|
|
147
|
+
// guarantees the renderer below never reads the previous session's frame.
|
|
148
|
+
frame.value = game.getRenderFrame();
|
|
149
|
+
const cleanupBinding = bindGameSession(game, (nextFrame) => {
|
|
150
|
+
frame.value = nextFrame;
|
|
151
|
+
instrumentationRef.current?.onPresentCommit?.(nextFrame.revision, Date.now());
|
|
152
|
+
});
|
|
153
|
+
const cleanupLifecycle = bindAppLifecycle(AppState, {
|
|
154
|
+
getStatus: () => game.status,
|
|
155
|
+
pause: () => {
|
|
156
|
+
running.value = false;
|
|
157
|
+
if (game.status !== 'disposed') {
|
|
158
|
+
game.pause();
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
resume: () => {
|
|
162
|
+
if (game.status !== 'disposed') {
|
|
163
|
+
game.start();
|
|
164
|
+
running.value = true;
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
return () => {
|
|
169
|
+
cleanupLifecycle();
|
|
170
|
+
cleanupBinding();
|
|
171
|
+
};
|
|
172
|
+
}, [epoch, frame, game, running]);
|
|
173
|
+
|
|
174
|
+
// F1: the first UI frame that sees a new commit revision is reported back
|
|
175
|
+
// to the RN runtime through scheduleOnRN — never by calling the JS hook
|
|
176
|
+
// directly from the UI worklet (cross-runtime calls crash the app).
|
|
177
|
+
const reportUiObserved = useCallback((revision: number, atMs: number) => {
|
|
178
|
+
instrumentationRef.current?.onUiRevisionObserved?.(revision, atMs);
|
|
179
|
+
}, []);
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
// UI-owned alpha clock: advances only while the session is running,
|
|
183
|
+
// resets on every new commit, clamps at 1 and holds (no extrapolation).
|
|
184
|
+
useFrameCallback((frameInfo) => {
|
|
185
|
+
'worklet';
|
|
186
|
+
if (!running.value) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const envelope = frame.value;
|
|
190
|
+
if (observedEpoch.value !== epoch.value || observedRevision.value !== envelope.revision) {
|
|
191
|
+
observedEpoch.value = epoch.value;
|
|
192
|
+
observedRevision.value = envelope.revision;
|
|
193
|
+
scheduleOnRN(reportUiObserved, envelope.revision, Date.now());
|
|
194
|
+
}
|
|
195
|
+
const previousState = {
|
|
196
|
+
epoch: clockEpoch.value,
|
|
197
|
+
revision: clockRevision.value,
|
|
198
|
+
alpha: alpha.value,
|
|
199
|
+
};
|
|
200
|
+
const nextState = advanceAlpha(
|
|
201
|
+
previousState,
|
|
202
|
+
{
|
|
203
|
+
epoch: epoch.value,
|
|
204
|
+
revision: envelope.revision,
|
|
205
|
+
stepMs: envelope.stepMs,
|
|
206
|
+
},
|
|
207
|
+
frameInfo.timeSincePreviousFrame ?? 1000 / 60,
|
|
208
|
+
);
|
|
209
|
+
clockEpoch.value = nextState.epoch;
|
|
210
|
+
clockRevision.value = nextState.revision;
|
|
211
|
+
alpha.value = nextState.alpha;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<Renderer
|
|
216
|
+
frame={frame}
|
|
217
|
+
alpha={alpha}
|
|
218
|
+
viewport={viewportValue}
|
|
219
|
+
{...(assets === undefined ? {} : { assets })}
|
|
220
|
+
/>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Stable per-session fallback identity (T8.6): one incrementing id per
|
|
225
|
+
// session object for the life of the module. A replaced session is a new
|
|
226
|
+
// object and therefore a new id; the same session keeps its id across
|
|
227
|
+
// renders so the presentation never remounts spuriously.
|
|
228
|
+
const sessionPresentationIds = new WeakMap<object, number>();
|
|
229
|
+
let nextSessionPresentationId = 1;
|
|
230
|
+
function sessionPresentationId(game: object): number {
|
|
231
|
+
let id = sessionPresentationIds.get(game);
|
|
232
|
+
if (id === undefined) {
|
|
233
|
+
id = nextSessionPresentationId;
|
|
234
|
+
nextSessionPresentationId += 1;
|
|
235
|
+
sessionPresentationIds.set(game, id);
|
|
236
|
+
}
|
|
237
|
+
return id;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function GameView<
|
|
241
|
+
TScenes extends SceneMap,
|
|
242
|
+
TInput extends InputMap,
|
|
243
|
+
TAssets extends AssetGroupMap = AssetGroupMap,
|
|
244
|
+
>({
|
|
245
|
+
game,
|
|
246
|
+
presentationKey,
|
|
247
|
+
assets,
|
|
248
|
+
renderer: Renderer,
|
|
249
|
+
children,
|
|
250
|
+
style,
|
|
251
|
+
instrumentation,
|
|
252
|
+
}: GameViewProps<TScenes, TInput, TAssets>) {
|
|
253
|
+
const instrumentationRef = useRef(instrumentation);
|
|
254
|
+
|
|
255
|
+
const bindingRef = useRef<ViewportBinding | null>(null);
|
|
256
|
+
bindingRef.current = bindingForViewport(game.viewport, bindingRef.current);
|
|
257
|
+
const binding = bindingRef.current;
|
|
258
|
+
const viewportValue = useSharedValue<ResolvedViewport2D | undefined>(undefined);
|
|
259
|
+
const viewportContext = useMemo<GameViewport>(
|
|
260
|
+
() => ({ binding, viewport: viewportValue }),
|
|
261
|
+
[binding, viewportValue],
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
instrumentationRef.current = instrumentation;
|
|
266
|
+
}, [instrumentation]);
|
|
267
|
+
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
viewportValue.value = binding.resolved;
|
|
270
|
+
}, [binding, viewportValue]);
|
|
271
|
+
|
|
272
|
+
useEffect(
|
|
273
|
+
() => () => {
|
|
274
|
+
binding.dispose();
|
|
275
|
+
},
|
|
276
|
+
[binding],
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<GameViewportContext.Provider value={viewportContext}>
|
|
281
|
+
<View
|
|
282
|
+
style={[styles.surface, style]}
|
|
283
|
+
onLayout={(event) => {
|
|
284
|
+
const { width, height } = event.nativeEvent.layout;
|
|
285
|
+
binding.setSurfaceSize({ width, height });
|
|
286
|
+
viewportValue.value = binding.resolved;
|
|
287
|
+
}}
|
|
288
|
+
>
|
|
289
|
+
<Canvas style={StyleSheet.absoluteFill}>
|
|
290
|
+
{/* RF6/T8.6: the per-session presentation is keyed by an explicit
|
|
291
|
+
presentation key when provided, otherwise by a stable
|
|
292
|
+
per-session identity — never object stringification — so the
|
|
293
|
+
frame/alpha/epoch initialize before the renderer can run. */}
|
|
294
|
+
<GamePresentation
|
|
295
|
+
key={presentationKey ?? sessionPresentationId(game)}
|
|
296
|
+
game={game}
|
|
297
|
+
assets={assets}
|
|
298
|
+
renderer={Renderer}
|
|
299
|
+
viewportValue={viewportValue}
|
|
300
|
+
instrumentationRef={instrumentationRef}
|
|
301
|
+
/>
|
|
302
|
+
</Canvas>
|
|
303
|
+
{children === undefined ? null : (
|
|
304
|
+
<View pointerEvents="box-none" style={StyleSheet.absoluteFill}>
|
|
305
|
+
{children}
|
|
306
|
+
</View>
|
|
307
|
+
)}
|
|
308
|
+
</View>
|
|
309
|
+
</GameViewportContext.Provider>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const styles = StyleSheet.create({
|
|
314
|
+
surface: {
|
|
315
|
+
overflow: 'hidden',
|
|
316
|
+
},
|
|
317
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI-owned alpha clock (T5).
|
|
3
|
+
*
|
|
4
|
+
* Pure, worklet-safe presentation clock: `alpha` advances from UI frame
|
|
5
|
+
* deltas against the simulation step duration and resets to zero whenever a
|
|
6
|
+
* newer commit arrives. It never extrapolates past the current snapshot
|
|
7
|
+
* (clamped at 1, then held) and ignores stale or duplicate commits so a
|
|
8
|
+
* delayed write cannot reset presentation backward.
|
|
9
|
+
*
|
|
10
|
+
* The `epoch` guards session replacement: a new GameView binding carries a
|
|
11
|
+
* fresh epoch, so a replacement session whose revision restarts at zero is
|
|
12
|
+
* still accepted, while a delayed write from an old epoch is ignored.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface AlphaClockCommit {
|
|
16
|
+
/** Monotonic binding epoch; new epoch accepts any revision. */
|
|
17
|
+
readonly epoch: number;
|
|
18
|
+
/** Session commit revision; new revisions reset the clock. */
|
|
19
|
+
readonly revision: number;
|
|
20
|
+
/** Fixed simulation step duration in milliseconds. */
|
|
21
|
+
readonly stepMs: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AlphaClockState {
|
|
25
|
+
readonly epoch: number;
|
|
26
|
+
readonly revision: number;
|
|
27
|
+
readonly alpha: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Advance the clock by one UI frame; returns the next alpha state. */
|
|
31
|
+
export function advanceAlpha(
|
|
32
|
+
previous: AlphaClockState,
|
|
33
|
+
commit: AlphaClockCommit,
|
|
34
|
+
frameDeltaMs: number,
|
|
35
|
+
): AlphaClockState {
|
|
36
|
+
'worklet';
|
|
37
|
+
if (
|
|
38
|
+
commit.epoch > previous.epoch ||
|
|
39
|
+
(commit.epoch === previous.epoch && commit.revision > previous.revision)
|
|
40
|
+
) {
|
|
41
|
+
return { epoch: commit.epoch, revision: commit.revision, alpha: 0 };
|
|
42
|
+
}
|
|
43
|
+
if (
|
|
44
|
+
commit.epoch < previous.epoch ||
|
|
45
|
+
(commit.epoch === previous.epoch && commit.revision < previous.revision)
|
|
46
|
+
) {
|
|
47
|
+
return previous; // A stale write is ignored entirely, alpha preserved.
|
|
48
|
+
}
|
|
49
|
+
if (previous.alpha >= 1) {
|
|
50
|
+
return previous; // Clamp at 1: hold, never extrapolate.
|
|
51
|
+
}
|
|
52
|
+
const delta = Math.max(0, frameDeltaMs) / commit.stepMs;
|
|
53
|
+
const next = previous.alpha + delta;
|
|
54
|
+
return {
|
|
55
|
+
epoch: previous.epoch,
|
|
56
|
+
revision: previous.revision,
|
|
57
|
+
alpha: next >= 1 ? 1 : next,
|
|
58
|
+
};
|
|
59
|
+
}
|