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,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SpriteBatch` — the Atlas-backed shared-texture batch (T7.7).
|
|
3
|
+
*
|
|
4
|
+
* Draws many instances of one decoded sprite-sheet image through Skia's
|
|
5
|
+
* `Atlas` with fixed-capacity, UI-owned buffers. The normal path owns all
|
|
6
|
+
* derived-value plumbing: `select` maps the committed snapshot to the item
|
|
7
|
+
* array and `write` is a UI-runtime setter that writes transforms in place —
|
|
8
|
+
* no per-frame allocation, no per-frame React, no author-written
|
|
9
|
+
* `useDerivedValue` side effects.
|
|
10
|
+
*
|
|
11
|
+
* Contract:
|
|
12
|
+
* - `capacity` is fixed for the mounted batch; active count is explicit.
|
|
13
|
+
* - Writes past capacity are rejected in development; inactive slots are
|
|
14
|
+
* hidden (zero-size) and never remount topology.
|
|
15
|
+
* - Item order is the drawing order; the anchor is uniform per batch.
|
|
16
|
+
* - The batch never disposes the image (the asset lease owns it).
|
|
17
|
+
*/
|
|
18
|
+
import { useMemo } from 'react';
|
|
19
|
+
import { Atlas, type SkImage } from '@shopify/react-native-skia';
|
|
20
|
+
import { useDerivedValue, type SharedValue } from 'react-native-reanimated';
|
|
21
|
+
import { useRectBuffer, useRSXformBuffer } from '@shopify/react-native-skia';
|
|
22
|
+
|
|
23
|
+
import type { CommitFrame } from '../../core/session/types';
|
|
24
|
+
import type { SceneSnapshot } from '../../scene/types';
|
|
25
|
+
import type { SceneMap } from '../../definition/types';
|
|
26
|
+
import type { LoadedImage, LoadedSpriteSheet } from '../../assets/types';
|
|
27
|
+
import { computeSpriteRsxform } from './spriteTransform';
|
|
28
|
+
import { batchUpdatePolicy } from './spriteBatchPolicy';
|
|
29
|
+
|
|
30
|
+
/** The per-item write surface handed to the author's `write` mapper. */
|
|
31
|
+
export interface SpriteBatchWrite {
|
|
32
|
+
/**
|
|
33
|
+
* Write one batch slot. `frame` names the sheet frame; `x`/`y` are world
|
|
34
|
+
* logical units; `rotation` is radians around the batch anchor; `scale`
|
|
35
|
+
* is uniform; `visible: false` hides the slot (zero-size rect). Writes
|
|
36
|
+
* past capacity fail in development.
|
|
37
|
+
*/
|
|
38
|
+
readonly set: (
|
|
39
|
+
index: number,
|
|
40
|
+
frame: string,
|
|
41
|
+
x: number,
|
|
42
|
+
y: number,
|
|
43
|
+
rotation: number,
|
|
44
|
+
scale: number,
|
|
45
|
+
visible?: boolean,
|
|
46
|
+
) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SpriteBatchProps<
|
|
50
|
+
TScenes extends SceneMap,
|
|
51
|
+
TSceneName extends Extract<keyof TScenes, string>,
|
|
52
|
+
TItem,
|
|
53
|
+
> {
|
|
54
|
+
/** The scene whose snapshot `select` reads. */
|
|
55
|
+
readonly scene: TSceneName;
|
|
56
|
+
/** The renderer's latest commit. */
|
|
57
|
+
readonly commit: SharedValue<CommitFrame<TScenes>>;
|
|
58
|
+
/** The renderer's presentation alpha. */
|
|
59
|
+
readonly alpha: SharedValue<number>;
|
|
60
|
+
/** One decoded sheet shared by every instance. */
|
|
61
|
+
readonly source: LoadedImage | LoadedSpriteSheet;
|
|
62
|
+
/** Fixed slot capacity for the mounted batch. */
|
|
63
|
+
readonly capacity: number;
|
|
64
|
+
/** Map the committed snapshot to the item array. */
|
|
65
|
+
readonly select: (context: {
|
|
66
|
+
readonly current: SceneSnapshot<TScenes[TSceneName]>;
|
|
67
|
+
readonly alpha: number;
|
|
68
|
+
}) => readonly TItem[];
|
|
69
|
+
/** Write each item into its batch slot (UI runtime). */
|
|
70
|
+
readonly write: (write: SpriteBatchWrite, item: TItem, index: number) => void;
|
|
71
|
+
/** Uniform anchor in [0, 1] relative to the selected frame. */
|
|
72
|
+
readonly anchor?: { readonly x: number; readonly y: number };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function frameRectOf(
|
|
76
|
+
source: LoadedImage | LoadedSpriteSheet,
|
|
77
|
+
frame: string,
|
|
78
|
+
): { x: number; y: number; width: number; height: number } | undefined {
|
|
79
|
+
'worklet';
|
|
80
|
+
if (source.descriptor.kind === 'image') {
|
|
81
|
+
const image = source as LoadedImage;
|
|
82
|
+
return { x: 0, y: 0, width: image.width, height: image.height };
|
|
83
|
+
}
|
|
84
|
+
return (source as LoadedSpriteSheet).frames[frame];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function SpriteBatch<
|
|
88
|
+
TScenes extends SceneMap,
|
|
89
|
+
TSceneName extends Extract<keyof TScenes, string>,
|
|
90
|
+
TItem,
|
|
91
|
+
>({
|
|
92
|
+
scene,
|
|
93
|
+
commit,
|
|
94
|
+
alpha,
|
|
95
|
+
source,
|
|
96
|
+
capacity,
|
|
97
|
+
select,
|
|
98
|
+
write,
|
|
99
|
+
anchor = { x: 0, y: 0 },
|
|
100
|
+
}: SpriteBatchProps<TScenes, TSceneName, TItem>) {
|
|
101
|
+
const image = source.image as SkImage;
|
|
102
|
+
if (!Number.isInteger(capacity) || capacity <= 0) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`SpriteBatch capacity must be a positive integer, got ${String(capacity)}`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const rects = useRectBuffer(capacity, (rect) => {
|
|
108
|
+
'worklet';
|
|
109
|
+
rect.setXYWH(0, 0, 0, 0);
|
|
110
|
+
});
|
|
111
|
+
const xforms = useRSXformBuffer(capacity, (xform) => {
|
|
112
|
+
'worklet';
|
|
113
|
+
xform.set(1, 0, 0, 0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// R7: one stable write coordinator for the batch lifetime; the per-update
|
|
117
|
+
// worklet only calls it (no per-frame coordinator allocation).
|
|
118
|
+
const writeApi: SpriteBatchWrite = useMemo(
|
|
119
|
+
() => ({
|
|
120
|
+
set: (index, frame, x, y, rotation, scale, visible = true) => {
|
|
121
|
+
'worklet';
|
|
122
|
+
if (index < 0 || index >= capacity) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`SpriteBatch write index ${index} is outside the capacity ${capacity}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
const rectSlot = rects.value[index];
|
|
128
|
+
const xformSlot = xforms.value[index];
|
|
129
|
+
if (rectSlot === undefined || xformSlot === undefined) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!visible) {
|
|
133
|
+
rectSlot.setXYWH(0, 0, 0, 0);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const rect = frameRectOf(source, frame);
|
|
137
|
+
if (rect === undefined) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`frame ${JSON.stringify(frame)} does not belong to this sprite sheet`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
rectSlot.setXYWH(rect.x, rect.y, rect.width, rect.height);
|
|
143
|
+
const rsxform = computeSpriteRsxform({
|
|
144
|
+
x,
|
|
145
|
+
y,
|
|
146
|
+
rotation,
|
|
147
|
+
scale,
|
|
148
|
+
flipX: false,
|
|
149
|
+
flipY: false,
|
|
150
|
+
anchorX: anchor.x,
|
|
151
|
+
anchorY: anchor.y,
|
|
152
|
+
frameWidth: rect.width,
|
|
153
|
+
frameHeight: rect.height,
|
|
154
|
+
});
|
|
155
|
+
xformSlot.set(rsxform.scos, rsxform.ssin, rsxform.tx, rsxform.ty);
|
|
156
|
+
},
|
|
157
|
+
}),
|
|
158
|
+
// The buffers, source, and anchor are stable for the mounted batch.
|
|
159
|
+
[capacity, rects, xforms, source, anchor.x, anchor.y],
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// The batch's own UI mapper: one derived value reads the committed
|
|
163
|
+
// snapshot, runs the author's select + write per item, and reports the
|
|
164
|
+
// active count. Buffers are mutated in place; no objects are allocated
|
|
165
|
+
// per item per frame beyond the item array the select returns.
|
|
166
|
+
const activeCount = useDerivedValue(() => {
|
|
167
|
+
'worklet';
|
|
168
|
+
const envelope = commit.value;
|
|
169
|
+
if (envelope.scene !== scene) {
|
|
170
|
+
// RF8: leaving the scene clears every previously active slot so no
|
|
171
|
+
// stale sprite from the old scene stays visible.
|
|
172
|
+
for (let index = 0; index < capacity; index += 1) {
|
|
173
|
+
const slot = rects.value[index];
|
|
174
|
+
if (slot !== undefined) {
|
|
175
|
+
slot.setXYWH(0, 0, 0, 0);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
const items = select({
|
|
181
|
+
current: envelope.current as never as SceneSnapshot<TScenes[TSceneName]>,
|
|
182
|
+
alpha: alpha.value,
|
|
183
|
+
});
|
|
184
|
+
const policy = batchUpdatePolicy(items.length, capacity, __DEV__);
|
|
185
|
+
if (policy.overflow) {
|
|
186
|
+
// RF8: production never crashes the UI runtime for ordinary data
|
|
187
|
+
// growth: hide the overflowing items.
|
|
188
|
+
for (let index = capacity; index < items.length; index += 1) {
|
|
189
|
+
const slot = rects.value[index];
|
|
190
|
+
if (slot !== undefined) {
|
|
191
|
+
slot.setXYWH(0, 0, 0, 0);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const count = policy.activeCount;
|
|
196
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
197
|
+
const item = items[index];
|
|
198
|
+
if (item !== undefined) {
|
|
199
|
+
write(writeApi, item, index);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Hide every inactive slot.
|
|
203
|
+
for (let index = count; index < capacity; index += 1) {
|
|
204
|
+
const slot = rects.value[index];
|
|
205
|
+
if (slot !== undefined) {
|
|
206
|
+
slot.setXYWH(0, 0, 0, 0);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return count;
|
|
210
|
+
}, [commit, alpha, scene, select, write, writeApi]);
|
|
211
|
+
|
|
212
|
+
void activeCount;
|
|
213
|
+
|
|
214
|
+
// The atlas renders the full buffer; the UI mapper keeps slots aligned.
|
|
215
|
+
const sampling = useMemo(() => 'nearest' as const, []);
|
|
216
|
+
return (
|
|
217
|
+
<Atlas image={image} sprites={rects} transforms={xforms} sampling={sampling as never} />
|
|
218
|
+
);
|
|
219
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Pure SpriteBatch update policy (RF8): the overflow decision and the
|
|
2
|
+
* active count. Development reports a structured error; production clamps
|
|
3
|
+
* and hides the overflow so the UI runtime never crashes for ordinary data
|
|
4
|
+
* growth. Runs inside the batch's update worklet, so it carries the worklet
|
|
5
|
+
* directive. */
|
|
6
|
+
export function batchUpdatePolicy(
|
|
7
|
+
itemsCount: number,
|
|
8
|
+
capacity: number,
|
|
9
|
+
dev: boolean,
|
|
10
|
+
): { readonly overflow: boolean; readonly activeCount: number } {
|
|
11
|
+
'worklet';
|
|
12
|
+
if (itemsCount <= capacity) {
|
|
13
|
+
return { overflow: false, activeCount: itemsCount };
|
|
14
|
+
}
|
|
15
|
+
if (dev) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`SpriteBatch overflow: ${itemsCount} items selected with capacity ${capacity}`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return { overflow: true, activeCount: capacity };
|
|
21
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { LoadedImage, LoadedSpriteSheet } from '../../assets/types';
|
|
2
|
+
|
|
3
|
+
/** The resolved source rectangle for one frame selection. */
|
|
4
|
+
export interface SpriteFrameRect {
|
|
5
|
+
readonly x: number;
|
|
6
|
+
readonly y: number;
|
|
7
|
+
readonly width: number;
|
|
8
|
+
readonly height: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the source rectangle for a static frame selection.
|
|
13
|
+
*
|
|
14
|
+
* RF4: an absent selection (the dynamic clip/elapsed mode before its first
|
|
15
|
+
* value) presents nothing instead of throwing. The static baseline uses the
|
|
16
|
+
* sheet's first frame so the anchor/scale correction has sane dimensions
|
|
17
|
+
* while the per-frame worklet resolves the real selection.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveSpriteFrameRect(
|
|
20
|
+
source: LoadedImage | LoadedSpriteSheet,
|
|
21
|
+
frame: string | undefined,
|
|
22
|
+
): SpriteFrameRect {
|
|
23
|
+
if (source.descriptor.kind === 'image') {
|
|
24
|
+
const image = source as LoadedImage;
|
|
25
|
+
return { x: 0, y: 0, width: image.width, height: image.height };
|
|
26
|
+
}
|
|
27
|
+
const sheet = source as LoadedSpriteSheet;
|
|
28
|
+
if (frame === undefined) {
|
|
29
|
+
const firstAnimationName = Object.keys(sheet.descriptor.animations)[0];
|
|
30
|
+
const firstAnimation =
|
|
31
|
+
firstAnimationName === undefined
|
|
32
|
+
? undefined
|
|
33
|
+
: sheet.descriptor.animations[firstAnimationName];
|
|
34
|
+
const firstFrameName = firstAnimation?.frames[0];
|
|
35
|
+
const baseline =
|
|
36
|
+
firstFrameName === undefined ? undefined : sheet.frames[firstFrameName];
|
|
37
|
+
return baseline ?? { x: 0, y: 0, width: 0, height: 0 };
|
|
38
|
+
}
|
|
39
|
+
const rect = sheet.frames[frame];
|
|
40
|
+
if (rect === undefined) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`frame ${JSON.stringify(frame)} does not belong to this sprite sheet (loaded frames: ${Object.keys(sheet.frames).join(', ')})`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return rect;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Sprite transform math (T7.6).
|
|
50
|
+
*
|
|
51
|
+
* Pure, worklet-compatible. A sprite's frame occupies
|
|
52
|
+
* `[0, 0, frameWidth, frameHeight]` in local space and the documented pivot
|
|
53
|
+
* order is:
|
|
54
|
+
*
|
|
55
|
+
* M = T(x, y) · R(rotation) · T(anchor) · S(scaleX, scaleY) · T(-anchor)
|
|
56
|
+
*
|
|
57
|
+
* i.e. scale and explicit flips happen around the anchor, then the frame
|
|
58
|
+
* rotates around the anchor at the world position. The RSXform carries the
|
|
59
|
+
* rotation + position (its 2x2 cannot express flips or non-uniform scale);
|
|
60
|
+
* the wrapping Group applies the scale/flip part around the anchor.
|
|
61
|
+
*/
|
|
62
|
+
import type { SharedValue } from 'react-native-reanimated';
|
|
63
|
+
|
|
64
|
+
/** A sprite's presentation input; numbers or UI-runtime animated values. */
|
|
65
|
+
export interface SpriteTransformInput {
|
|
66
|
+
/** World position in logical units. */
|
|
67
|
+
readonly x: number | SharedValue<number>;
|
|
68
|
+
readonly y: number | SharedValue<number>;
|
|
69
|
+
/** Rotation in radians around the anchor. */
|
|
70
|
+
readonly rotation: number | SharedValue<number>;
|
|
71
|
+
/** Uniform scale around the anchor. */
|
|
72
|
+
readonly scale: number | SharedValue<number>;
|
|
73
|
+
/** Explicit flips preserve the anchor. */
|
|
74
|
+
readonly flipX: boolean | SharedValue<boolean>;
|
|
75
|
+
readonly flipY: boolean | SharedValue<boolean>;
|
|
76
|
+
/** Normalized anchor in [0, 1] relative to the selected frame. */
|
|
77
|
+
readonly anchorX: number;
|
|
78
|
+
readonly anchorY: number;
|
|
79
|
+
/** Selected frame size in source pixels (logical units for placement). */
|
|
80
|
+
readonly frameWidth: number;
|
|
81
|
+
readonly frameHeight: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The computed RSXform (rotation + position, pivot-compensated). */
|
|
85
|
+
export interface SpriteRsxform {
|
|
86
|
+
readonly scos: number;
|
|
87
|
+
readonly ssin: number;
|
|
88
|
+
readonly tx: number;
|
|
89
|
+
readonly ty: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A Skia-compatible 3x3 transform element list. */
|
|
93
|
+
export type SkiaTransformElement =
|
|
94
|
+
| { readonly translateX: number }
|
|
95
|
+
| { readonly translateY: number }
|
|
96
|
+
| { readonly scaleX: number }
|
|
97
|
+
| { readonly scaleY: number };
|
|
98
|
+
|
|
99
|
+
function readNumber(value: number | SharedValue<number>): number {
|
|
100
|
+
'worklet';
|
|
101
|
+
return typeof value === 'number' ? value : value.value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readBoolean(value: boolean | SharedValue<boolean>): boolean {
|
|
105
|
+
'worklet';
|
|
106
|
+
return typeof value === 'boolean' ? value : value.value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The RSXform for the atlas path: rotation + position with the pivot
|
|
111
|
+
* compensated (`tx = x - pivotX·cos + pivotY·sin`). Scale and flips are
|
|
112
|
+
* applied by the wrapping Group (`spriteGroupCorrection`).
|
|
113
|
+
*/
|
|
114
|
+
export function computeSpriteRsxform(input: SpriteTransformInput): SpriteRsxform {
|
|
115
|
+
'worklet';
|
|
116
|
+
const x = readNumber(input.x);
|
|
117
|
+
const y = readNumber(input.y);
|
|
118
|
+
const rotation = readNumber(input.rotation);
|
|
119
|
+
const pivotX = input.anchorX * input.frameWidth;
|
|
120
|
+
const pivotY = input.anchorY * input.frameHeight;
|
|
121
|
+
const cos = Math.cos(rotation);
|
|
122
|
+
const sin = Math.sin(rotation);
|
|
123
|
+
return {
|
|
124
|
+
scos: cos,
|
|
125
|
+
ssin: sin,
|
|
126
|
+
tx: x - pivotX * cos + pivotY * sin,
|
|
127
|
+
ty: y - pivotX * sin - pivotY * cos,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The scale/flip part around the anchor as a Skia transform element list:
|
|
133
|
+
* `T(anchor) · S(scaleX, scaleY) · T(-anchor)` (the first element is the
|
|
134
|
+
* outermost).
|
|
135
|
+
*/
|
|
136
|
+
export function spriteGroupCorrection(input: SpriteTransformInput): readonly SkiaTransformElement[] {
|
|
137
|
+
'worklet';
|
|
138
|
+
const scale = readNumber(input.scale);
|
|
139
|
+
const flipX = readBoolean(input.flipX);
|
|
140
|
+
const flipY = readBoolean(input.flipY);
|
|
141
|
+
const pivotX = input.anchorX * input.frameWidth;
|
|
142
|
+
const pivotY = input.anchorY * input.frameHeight;
|
|
143
|
+
return [
|
|
144
|
+
{ translateX: pivotX },
|
|
145
|
+
{ translateY: pivotY },
|
|
146
|
+
{ scaleX: flipX ? -scale : scale },
|
|
147
|
+
{ scaleY: flipY ? -scale : scale },
|
|
148
|
+
{ translateX: -pivotX },
|
|
149
|
+
{ translateY: -pivotY },
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** World position of the anchor point (for debugging and overlays). */
|
|
154
|
+
export function spriteAnchorWorld(input: SpriteTransformInput): {
|
|
155
|
+
readonly x: number;
|
|
156
|
+
readonly y: number;
|
|
157
|
+
} {
|
|
158
|
+
'worklet';
|
|
159
|
+
return {
|
|
160
|
+
x: readNumber(input.x),
|
|
161
|
+
y: readNumber(input.y),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { resolveViewport2D, type ResolvedViewport2D, type SurfaceSize, type Viewport } from '../viewport2d';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Platform-neutral holder for the resolved viewport of a mounted game
|
|
5
|
+
* surface.
|
|
6
|
+
*
|
|
7
|
+
* It re-resolves whenever the surface size changes, notifies subscribers on
|
|
8
|
+
* each layout revision (so pointer input can cancel an active gesture on
|
|
9
|
+
* layout invalidation), and exposes the latest immutable resolved viewport to
|
|
10
|
+
* both the renderer and the input adapter. It never touches React, Skia, or
|
|
11
|
+
* platform dimensions.
|
|
12
|
+
*/
|
|
13
|
+
export class ViewportBinding {
|
|
14
|
+
readonly #config: Viewport;
|
|
15
|
+
#resolved: ResolvedViewport2D | undefined;
|
|
16
|
+
#lastSurfaceSize: SurfaceSize | undefined;
|
|
17
|
+
#revision = 0;
|
|
18
|
+
#listeners = new Set<() => void>();
|
|
19
|
+
|
|
20
|
+
constructor(config: Viewport) {
|
|
21
|
+
this.#config = config;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The authored viewport configuration this binding resolves. */
|
|
25
|
+
get config(): Viewport {
|
|
26
|
+
return this.#config;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The latest resolved viewport, or `undefined` while the layout is invalid. */
|
|
30
|
+
get resolved(): ResolvedViewport2D | undefined {
|
|
31
|
+
return this.#resolved;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The latest native surface measurement, retained across viewport swaps. */
|
|
35
|
+
get surfaceSize(): SurfaceSize | undefined {
|
|
36
|
+
return this.#lastSurfaceSize;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Monotonic counter incremented on every layout revision. */
|
|
40
|
+
get revision(): number {
|
|
41
|
+
return this.#revision;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the configured viewport against a new surface size.
|
|
46
|
+
*
|
|
47
|
+
* No-op for unchanged sizes. Subscribers are notified only when the layout
|
|
48
|
+
* actually changes.
|
|
49
|
+
*/
|
|
50
|
+
setSurfaceSize(size: SurfaceSize): void {
|
|
51
|
+
if (
|
|
52
|
+
this.#lastSurfaceSize !== undefined &&
|
|
53
|
+
this.#lastSurfaceSize.width === size.width &&
|
|
54
|
+
this.#lastSurfaceSize.height === size.height
|
|
55
|
+
) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this.#lastSurfaceSize = Object.freeze({ width: size.width, height: size.height });
|
|
59
|
+
this.#resolved = resolveViewport2D(this.#config, size);
|
|
60
|
+
this.#revision += 1;
|
|
61
|
+
for (const listener of [...this.#listeners]) {
|
|
62
|
+
listener();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Observe layout revisions. Returns an idempotent unsubscribe. */
|
|
67
|
+
subscribe(listener: () => void): () => void {
|
|
68
|
+
this.#listeners.add(listener);
|
|
69
|
+
let removed = false;
|
|
70
|
+
return () => {
|
|
71
|
+
if (removed) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
removed = true;
|
|
75
|
+
this.#listeners.delete(listener);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Release subscribers. The binding becomes inert. */
|
|
80
|
+
dispose(): void {
|
|
81
|
+
this.#listeners.clear();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Return the existing binding when its authored viewport is unchanged, or
|
|
87
|
+
* create a replacement that immediately resolves against the already-mounted
|
|
88
|
+
* native surface. React Native does not guarantee another `onLayout` event
|
|
89
|
+
* when only the game/viewport changes and the View's dimensions stay equal.
|
|
90
|
+
*/
|
|
91
|
+
export function bindingForViewport(
|
|
92
|
+
config: Viewport,
|
|
93
|
+
previous: ViewportBinding | null,
|
|
94
|
+
): ViewportBinding {
|
|
95
|
+
if (previous?.config === config) {
|
|
96
|
+
return previous;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const replacement = new ViewportBinding(config);
|
|
100
|
+
if (previous?.surfaceSize !== undefined) {
|
|
101
|
+
replacement.setSurfaceSize(previous.surfaceSize);
|
|
102
|
+
}
|
|
103
|
+
return replacement;
|
|
104
|
+
}
|
package/src/react.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export { GameView } from './react/GameView';
|
|
2
|
+
export { createGameAssetStore } from './react/assets/decodeSkiaImage';
|
|
3
|
+
export { useGameAssets, stableGroupsKey } from './react/assets/useGameAssets';
|
|
4
|
+
export type { GameAssetsState } from './react/assets/useGameAssets';
|
|
5
|
+
export { GameWorld2D, viewportTransform } from './react/sprites/GameWorld2D';
|
|
6
|
+
export { Sprite, resolveSpriteFrameRect } from './react/sprites/Sprite';
|
|
7
|
+
export type { SpriteAnimatable, SpriteAnimatableBoolean, SpriteProps } from './react/sprites/Sprite';
|
|
8
|
+
export { GameSprite, spriteFrameNameForClip } from './react/sprites/GameSprite';
|
|
9
|
+
export type {
|
|
10
|
+
GameSpriteProps,
|
|
11
|
+
GameSpriteSelectContext,
|
|
12
|
+
GameSpriteSelection,
|
|
13
|
+
} from './react/sprites/GameSprite';
|
|
14
|
+
export { SpriteBatch } from './react/sprites/SpriteBatch';
|
|
15
|
+
export type { SpriteBatchProps, SpriteBatchWrite } from './react/sprites/SpriteBatch';
|
|
16
|
+
export {
|
|
17
|
+
computeSpriteRsxform,
|
|
18
|
+
spriteAnchorWorld,
|
|
19
|
+
spriteGroupCorrection,
|
|
20
|
+
} from './react/sprites/spriteTransform';
|
|
21
|
+
export type {
|
|
22
|
+
SkiaTransformElement,
|
|
23
|
+
SpriteRsxform,
|
|
24
|
+
SpriteTransformInput,
|
|
25
|
+
} from './react/sprites/spriteTransform';
|
|
26
|
+
export { createGameAssetStoreCore } from './react/assets/createGameAssetStore';
|
|
27
|
+
export type {
|
|
28
|
+
AcquireOptions,
|
|
29
|
+
AssetPipelines,
|
|
30
|
+
GameAssetStore,
|
|
31
|
+
NativeImageHandle,
|
|
32
|
+
} from './react/assets/createGameAssetStore';
|
|
33
|
+
|
|
34
|
+
export type { GameRendererProps, GameViewProps } from './react/GameView';
|
|
35
|
+
export { GamePointerInput } from './react/GamePointerInput';
|
|
36
|
+
export type { GamePointerInputProps } from './react/GamePointerInput';
|
|
37
|
+
export type { GamePointerInstrumentation, GameViewInstrumentation } from './react/instrumentation';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { SceneDefinition } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Define a synchronous functional scene while inferring its state, snapshot,
|
|
5
|
+
* action, and transition types.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const ready = defineScene({
|
|
10
|
+
* actions: ['primary'],
|
|
11
|
+
* transitions: ['play'],
|
|
12
|
+
* create: () => ({ ready: true }),
|
|
13
|
+
* update: ({ state, input, transition }) => {
|
|
14
|
+
* if (input.pointer('primary').pressed) {
|
|
15
|
+
* transition.setScene('play');
|
|
16
|
+
* }
|
|
17
|
+
* return state;
|
|
18
|
+
* },
|
|
19
|
+
* snapshot: ({ state }) => state,
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function defineScene<
|
|
24
|
+
const TActions extends readonly string[],
|
|
25
|
+
TState,
|
|
26
|
+
TSnapshot,
|
|
27
|
+
const TTransitions extends readonly string[] = [],
|
|
28
|
+
>(
|
|
29
|
+
definition: Omit<
|
|
30
|
+
SceneDefinition<TState, TSnapshot, TActions[number], TTransitions[number]>,
|
|
31
|
+
'kind' | '__actionType' | '__transitionType' | '__snapshotType' | 'actions' | 'transitions'
|
|
32
|
+
> & { readonly actions: TActions; readonly transitions?: TTransitions },
|
|
33
|
+
): SceneDefinition<TState, TSnapshot, TActions[number], TTransitions[number]> {
|
|
34
|
+
return {
|
|
35
|
+
kind: 'gamekit.scene',
|
|
36
|
+
...definition,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { InputFrame } from '../core/input/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Update-scoped scene transition controls.
|
|
5
|
+
*
|
|
6
|
+
* The controller is valid only for the duration of the `update` call that
|
|
7
|
+
* received it. Retaining it and calling either method afterwards throws a
|
|
8
|
+
* lifecycle error. Conflicting requests within one update fail clearly.
|
|
9
|
+
*/
|
|
10
|
+
export interface SceneTransitionController<TTransitionName extends string> {
|
|
11
|
+
/**
|
|
12
|
+
* Request a transition to a declared target scene.
|
|
13
|
+
*
|
|
14
|
+
* Requests to the current scene are idempotent no-ops. The transition
|
|
15
|
+
* commits after the current update completes successfully.
|
|
16
|
+
*/
|
|
17
|
+
setScene(name: TTransitionName): void;
|
|
18
|
+
/** Recreate the current scene with fresh state. */
|
|
19
|
+
restartScene(): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Values supplied to a scene for one deterministic simulation tick. */
|
|
23
|
+
export interface SceneUpdate<TState, TActionName extends string, TTransitionName extends string> {
|
|
24
|
+
/** The authoritative state produced by the previous tick. */
|
|
25
|
+
readonly state: Readonly<TState>;
|
|
26
|
+
/** One immutable semantic input snapshot for this tick. */
|
|
27
|
+
readonly input: InputFrame<TActionName>;
|
|
28
|
+
/** Update-scoped transition controls restricted to declared targets. */
|
|
29
|
+
readonly transition: SceneTransitionController<TTransitionName>;
|
|
30
|
+
/** One-based global simulation tick number. */
|
|
31
|
+
readonly tick: number;
|
|
32
|
+
/** One-based tick number within the current scene instance. */
|
|
33
|
+
readonly sceneTick: number;
|
|
34
|
+
/** Constant fixed-step duration in seconds. */
|
|
35
|
+
readonly deltaSeconds: number;
|
|
36
|
+
/** Deterministic global simulation time after this tick. */
|
|
37
|
+
readonly elapsedSeconds: number;
|
|
38
|
+
/** Deterministic time within the current scene instance. */
|
|
39
|
+
readonly sceneElapsedSeconds: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Values supplied when extracting a renderer-specific snapshot. */
|
|
43
|
+
export interface SceneSnapshotContext<TState> {
|
|
44
|
+
/** Current authoritative scene state. */
|
|
45
|
+
readonly state: Readonly<TState>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A synchronous functional scene used by the runtime. */
|
|
49
|
+
export interface SceneDefinition<
|
|
50
|
+
TState,
|
|
51
|
+
TSnapshot,
|
|
52
|
+
TActionName extends string = never,
|
|
53
|
+
TTransitionName extends string = never,
|
|
54
|
+
> {
|
|
55
|
+
/** Internal scene-definition discriminator. */
|
|
56
|
+
readonly kind: 'gamekit.scene';
|
|
57
|
+
/** @internal Covariant type witness used to infer presentation snapshots. */
|
|
58
|
+
readonly __snapshotType?: TSnapshot;
|
|
59
|
+
/** @internal Type witness used to validate scene actions against the game. */
|
|
60
|
+
readonly __actionType?: TActionName;
|
|
61
|
+
/** @internal Type witness used to validate scene transition targets. */
|
|
62
|
+
readonly __transitionType?: TTransitionName;
|
|
63
|
+
/** Semantic input actions this scene may read. */
|
|
64
|
+
readonly actions: readonly TActionName[];
|
|
65
|
+
/** Declared scene names this scene may transition to. */
|
|
66
|
+
readonly transitions?: readonly TTransitionName[];
|
|
67
|
+
/** Create the scene's initial authoritative state. */
|
|
68
|
+
readonly create: () => TState;
|
|
69
|
+
/** Produce the next state for one fixed simulation tick. */
|
|
70
|
+
readonly update: (frame: SceneUpdate<TState, TActionName, TTransitionName>) => TState;
|
|
71
|
+
/** Extract a compact renderer-specific snapshot from current state. */
|
|
72
|
+
readonly snapshot: (context: SceneSnapshotContext<TState>) => TSnapshot;
|
|
73
|
+
/** Release scene-owned resources exactly once. */
|
|
74
|
+
readonly dispose?: (state: Readonly<TState>) => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Infer the presentation snapshot produced by a scene definition. */
|
|
78
|
+
export type SceneSnapshot<TScene> =
|
|
79
|
+
TScene extends { readonly __snapshotType?: infer TSnapshot } ? TSnapshot : never;
|