create-airjam 0.1.0 → 0.1.2
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/dist/index.js +11 -3
- package/package.json +6 -3
- package/templates/pong/.env.example +11 -0
- package/templates/pong/.env.local +10 -0
- package/templates/pong/AI_INSTRUCTIONS.md +44 -0
- package/templates/pong/README.md +111 -0
- package/templates/pong/airjam-docs/getting-started/architecture/page.md +165 -0
- package/templates/pong/airjam-docs/getting-started/game-ideas/page.md +114 -0
- package/templates/pong/airjam-docs/getting-started/introduction/page.md +122 -0
- package/templates/pong/airjam-docs/how-it-works/host-system/page.md +241 -0
- package/templates/pong/airjam-docs/sdk/hooks/page.md +403 -0
- package/templates/pong/airjam-docs/sdk/input-system/page.md +336 -0
- package/templates/pong/airjam-docs/sdk/networked-state/page.md +575 -0
- package/templates/pong/dist/assets/index-B9l0NKly.js +269 -0
- package/templates/pong/dist/assets/index-CHKqdIQG.css +1 -0
- package/templates/pong/dist/index.html +14 -0
- package/templates/pong/eslint.config.js +33 -0
- package/templates/pong/index.html +6 -1
- package/templates/pong/node_modules/.bin/air-jam-server +17 -0
- package/templates/pong/node_modules/.bin/eslint +17 -0
- package/templates/pong/node_modules/.bin/eslint-config-prettier +17 -0
- package/templates/pong/node_modules/.bin/jiti +17 -0
- package/templates/pong/node_modules/.bin/tsc +17 -0
- package/templates/pong/node_modules/.bin/tsserver +17 -0
- package/templates/pong/node_modules/.bin/tsx +17 -0
- package/templates/pong/node_modules/.bin/vite +17 -0
- package/templates/pong/node_modules/.vite/deps/@air-jam_sdk.js +66143 -0
- package/templates/pong/node_modules/.vite/deps/@air-jam_sdk.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/_metadata.json +73 -0
- package/templates/pong/node_modules/.vite/deps/chunk-3TUQC5ZT.js +292 -0
- package/templates/pong/node_modules/.vite/deps/chunk-3TUQC5ZT.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/chunk-DC5AMYBS.js +38 -0
- package/templates/pong/node_modules/.vite/deps/chunk-DC5AMYBS.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/chunk-QUPSG5AV.js +280 -0
- package/templates/pong/node_modules/.vite/deps/chunk-QUPSG5AV.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/chunk-TYOCAO5S.js +13810 -0
- package/templates/pong/node_modules/.vite/deps/chunk-TYOCAO5S.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/chunk-YG4BJP3V.js +1004 -0
- package/templates/pong/node_modules/.vite/deps/chunk-YG4BJP3V.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/package.json +3 -0
- package/templates/pong/node_modules/.vite/deps/react-dom.js +6 -0
- package/templates/pong/node_modules/.vite/deps/react-dom.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/react-dom_client.js +20217 -0
- package/templates/pong/node_modules/.vite/deps/react-dom_client.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/react-router-dom.js +13900 -0
- package/templates/pong/node_modules/.vite/deps/react-router-dom.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/react.js +5 -0
- package/templates/pong/node_modules/.vite/deps/react.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/react_jsx-dev-runtime.js +278 -0
- package/templates/pong/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/react_jsx-runtime.js +6 -0
- package/templates/pong/node_modules/.vite/deps/react_jsx-runtime.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/zod.js +476 -0
- package/templates/pong/node_modules/.vite/deps/zod.js.map +7 -0
- package/templates/pong/package.json +12 -1
- package/templates/pong/src/App.tsx +2 -2
- package/templates/pong/src/controller-view.tsx +143 -0
- package/templates/pong/src/host-view.tsx +401 -0
- package/templates/pong/src/main.tsx +2 -1
- package/templates/pong/src/store.ts +80 -0
- package/templates/pong/tsconfig.json +3 -2
- package/templates/pong/vite.config.ts +3 -0
- package/templates/pong/src/ControllerView.tsx +0 -64
- package/templates/pong/src/HostView.tsx +0 -148
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# Input System
|
|
2
|
+
|
|
3
|
+
The Air Jam input system provides type-safe, validated, and latched input handling. This page explains how input flows from controllers to your game and how to configure it for optimal performance.
|
|
4
|
+
|
|
5
|
+
## Input Flow
|
|
6
|
+
|
|
7
|
+
## Configuration
|
|
8
|
+
|
|
9
|
+
Configure input handling in the `AirJamProvider`:
|
|
10
|
+
|
|
11
|
+
```tsx filename="src/App.tsx"
|
|
12
|
+
import { AirJamProvider } from "@air-jam/sdk";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
// Define your input schema with Zod
|
|
16
|
+
const gameInputSchema = z.object({
|
|
17
|
+
vector: z.object({
|
|
18
|
+
x: z.number(),
|
|
19
|
+
y: z.number(),
|
|
20
|
+
}),
|
|
21
|
+
action: z.boolean(),
|
|
22
|
+
ability: z.boolean(),
|
|
23
|
+
timestamp: z.number(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Type is automatically inferred
|
|
27
|
+
type GameInput = z.infer<typeof gameInputSchema>;
|
|
28
|
+
|
|
29
|
+
<AirJamProvider
|
|
30
|
+
input={{
|
|
31
|
+
// Schema for validation and type inference
|
|
32
|
+
schema: gameInputSchema,
|
|
33
|
+
|
|
34
|
+
// Latching configuration
|
|
35
|
+
latch: {
|
|
36
|
+
booleanFields: ["action", "ability"],
|
|
37
|
+
vectorFields: ["vector"],
|
|
38
|
+
},
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<App />
|
|
42
|
+
</AirJamProvider>;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Schema Validation
|
|
46
|
+
|
|
47
|
+
When you provide a Zod schema, all incoming input is validated:
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
// ✅ Valid input - passes through
|
|
51
|
+
{
|
|
52
|
+
vector: { x: 0.5, y: -0.3 },
|
|
53
|
+
action: true,
|
|
54
|
+
ability: false,
|
|
55
|
+
timestamp: 1703123456789
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ❌ Invalid input - returns undefined, logs warning
|
|
59
|
+
{
|
|
60
|
+
vector: { x: "not a number", y: 0 }, // Type mismatch
|
|
61
|
+
action: true,
|
|
62
|
+
// missing required fields
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Benefits of Schema Validation:**
|
|
67
|
+
|
|
68
|
+
- Catch malformed input early
|
|
69
|
+
- TypeScript knows exact input shape
|
|
70
|
+
- Protect game logic from invalid data
|
|
71
|
+
- Helpful development warnings
|
|
72
|
+
|
|
73
|
+
## Input Latching
|
|
74
|
+
|
|
75
|
+
### The Problem
|
|
76
|
+
|
|
77
|
+
Game loops typically run at 60fps, but network events arrive asynchronously. Without latching, quick button taps can be missed:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
Frame 1: getInput() → action: false
|
|
81
|
+
[Network: action: true arrives]
|
|
82
|
+
[Network: action: false arrives]
|
|
83
|
+
Frame 2: getInput() → action: false ← Tap missed!
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### The Solution
|
|
87
|
+
|
|
88
|
+
Latching "holds" true values until they're consumed:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
Frame 1: getInput() → action: false
|
|
92
|
+
[Network: action: true arrives]
|
|
93
|
+
[Network: action: false arrives]
|
|
94
|
+
Frame 2: getInput() → action: true ← Tap captured!
|
|
95
|
+
Frame 3: getInput() → action: false ← Auto-reset
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Boolean Latching
|
|
99
|
+
|
|
100
|
+
For buttons and triggers:
|
|
101
|
+
|
|
102
|
+
```tsx filename="src/App.tsx"
|
|
103
|
+
latch: {
|
|
104
|
+
booleanFields: ["action", "ability", "jump", "fire"],
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Behavior:**
|
|
109
|
+
|
|
110
|
+
- When field becomes `true`, it stays `true` until consumed
|
|
111
|
+
- After `getInput()` reads it, it resets to actual current value
|
|
112
|
+
- Rapid taps (even between frames) are never missed
|
|
113
|
+
|
|
114
|
+
### Vector Latching
|
|
115
|
+
|
|
116
|
+
For joysticks and directional inputs:
|
|
117
|
+
|
|
118
|
+
```tsx filename="src/App.tsx"
|
|
119
|
+
latch: {
|
|
120
|
+
vectorFields: ["vector", "aim", "movement"],
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Behavior:**
|
|
125
|
+
|
|
126
|
+
- Non-zero vectors are kept for one frame after release
|
|
127
|
+
- Quick stick flicks register in the game loop
|
|
128
|
+
- Prevents "dead zone" issues with fast movements
|
|
129
|
+
|
|
130
|
+
### Example: Without vs With Latching
|
|
131
|
+
|
|
132
|
+
**Without latching (missed inputs):**
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
Player rapidly taps fire button
|
|
136
|
+
Frame 1: action: false (tap happened between frames)
|
|
137
|
+
Frame 2: action: false (another tap missed)
|
|
138
|
+
Frame 3: action: true (finally caught one)
|
|
139
|
+
Result: Player fires once instead of 3 times
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**With latching (all inputs captured):**
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
Player rapidly taps fire button
|
|
146
|
+
Frame 1: action: true (first tap latched)
|
|
147
|
+
Frame 2: action: true (second tap latched)
|
|
148
|
+
Frame 3: action: true (third tap latched)
|
|
149
|
+
Result: Player fires 3 times as expected
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Reading Input
|
|
153
|
+
|
|
154
|
+
### In the Main Host Component
|
|
155
|
+
|
|
156
|
+
```tsx filename="src/components/HostView.tsx"
|
|
157
|
+
const host = useAirJamHost();
|
|
158
|
+
|
|
159
|
+
useFrame(() => {
|
|
160
|
+
host.players.forEach((player) => {
|
|
161
|
+
const input = host.getInput(player.id);
|
|
162
|
+
if (!input) return;
|
|
163
|
+
|
|
164
|
+
// Input is fully typed!
|
|
165
|
+
movePlayer(player.id, input.vector);
|
|
166
|
+
|
|
167
|
+
if (input.action) {
|
|
168
|
+
playerShoot(player.id);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### In Performance-Critical Components
|
|
175
|
+
|
|
176
|
+
Use `useGetInput()` to avoid re-renders:
|
|
177
|
+
|
|
178
|
+
```tsx filename="src/components/Ship.tsx"
|
|
179
|
+
import { useGetInput } from "@air-jam/sdk";
|
|
180
|
+
|
|
181
|
+
const Ship = ({ playerId }: { playerId: string }) => {
|
|
182
|
+
// No store subscription = no re-renders
|
|
183
|
+
const getInput = useGetInput<typeof gameInputSchema>();
|
|
184
|
+
|
|
185
|
+
useFrame(() => {
|
|
186
|
+
const input = getInput(playerId);
|
|
187
|
+
// Update ship...
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return <mesh>...</mesh>;
|
|
191
|
+
};
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Best Practices
|
|
195
|
+
|
|
196
|
+
### 1. Define Schema Once
|
|
197
|
+
|
|
198
|
+
Create your schema in a shared file:
|
|
199
|
+
|
|
200
|
+
```tsx filename="src/game/types.ts"
|
|
201
|
+
import { z } from "zod";
|
|
202
|
+
|
|
203
|
+
export const gameInputSchema = z.object({
|
|
204
|
+
vector: z.object({ x: z.number(), y: z.number() }),
|
|
205
|
+
action: z.boolean(),
|
|
206
|
+
ability: z.boolean(),
|
|
207
|
+
timestamp: z.number(),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
export type GameInput = z.infer<typeof gameInputSchema>;
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### 2. Latch All Interactive Fields
|
|
214
|
+
|
|
215
|
+
Always latch buttons and sticks:
|
|
216
|
+
|
|
217
|
+
```tsx filename="src/App.tsx"
|
|
218
|
+
latch: {
|
|
219
|
+
booleanFields: ["action", "ability", "jump", "fire", "menu"],
|
|
220
|
+
vectorFields: ["vector", "aim"],
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### 3. Use Lightweight Hooks in Game Objects
|
|
225
|
+
|
|
226
|
+
```tsx filename="src/components/Ship.tsx"
|
|
227
|
+
// ❌ Don't do this (causes re-renders)
|
|
228
|
+
const Ship = () => {
|
|
229
|
+
const host = useAirJamHost(); // Re-renders on connection changes
|
|
230
|
+
// ...
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// ✅ Do this instead
|
|
234
|
+
const Ship = () => {
|
|
235
|
+
const getInput = useGetInput(); // Stable, no re-renders
|
|
236
|
+
// ...
|
|
237
|
+
};
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### 4. Handle Missing Input Gracefully
|
|
241
|
+
|
|
242
|
+
```tsx filename="src/components/Player.tsx"
|
|
243
|
+
const input = getInput(playerId);
|
|
244
|
+
|
|
245
|
+
// Always check if input exists
|
|
246
|
+
if (!input) {
|
|
247
|
+
// Player just connected, input not yet received
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Now safely use input
|
|
252
|
+
movePlayer(input.vector);
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### 5. Include Timestamps for Advanced Physics
|
|
256
|
+
|
|
257
|
+
```tsx filename="src/game/types.ts"
|
|
258
|
+
const inputSchema = z.object({
|
|
259
|
+
// ...
|
|
260
|
+
timestamp: z.number(), // Client-side timestamp
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Use for input prediction, lag compensation, etc.
|
|
264
|
+
const inputAge = Date.now() - input.timestamp;
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Debugging Input
|
|
268
|
+
|
|
269
|
+
### Console Logging
|
|
270
|
+
|
|
271
|
+
```tsx filename="src/components/DebugView.tsx"
|
|
272
|
+
useFrame(() => {
|
|
273
|
+
const input = getInput(playerId);
|
|
274
|
+
console.log(`Player ${playerId}:`, input);
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Visual Debug
|
|
279
|
+
|
|
280
|
+
```tsx filename="src/components/InputDebugger.tsx"
|
|
281
|
+
const InputDebugger = ({ playerId }: { playerId: string }) => {
|
|
282
|
+
const getInput = useGetInput();
|
|
283
|
+
const [display, setDisplay] = useState("");
|
|
284
|
+
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
const interval = setInterval(() => {
|
|
287
|
+
const input = getInput(playerId);
|
|
288
|
+
setDisplay(JSON.stringify(input, null, 2));
|
|
289
|
+
}, 100);
|
|
290
|
+
return () => clearInterval(interval);
|
|
291
|
+
}, [playerId, getInput]);
|
|
292
|
+
|
|
293
|
+
return <pre style={{ position: "fixed", top: 0, left: 0 }}>{display}</pre>;
|
|
294
|
+
};
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Common Issues
|
|
298
|
+
|
|
299
|
+
### Input Not Received
|
|
300
|
+
|
|
301
|
+
1. **Check connection status:**
|
|
302
|
+
|
|
303
|
+
```tsx filename="src/components/HostView.tsx"
|
|
304
|
+
console.log(host.connectionStatus); // Should be "connected"
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
2. **Verify room code matches:**
|
|
308
|
+
|
|
309
|
+
```tsx filename="src/components/HostView.tsx"
|
|
310
|
+
console.log("Host room:", host.roomId);
|
|
311
|
+
// Should match controller's room
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
3. **Check schema validation:**
|
|
315
|
+
```tsx filename="src/components/HostView.tsx"
|
|
316
|
+
// Look for warnings in console:
|
|
317
|
+
// [InputManager] Invalid input for controller XXX: [errors]
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Input Feels Laggy
|
|
321
|
+
|
|
322
|
+
1. **Use `useGetInput()` in game objects** (not `useAirJamHost`)
|
|
323
|
+
2. **Check network latency** (input includes timestamp)
|
|
324
|
+
3. **Verify latching is configured** for buttons/sticks
|
|
325
|
+
|
|
326
|
+
### Buttons Missed
|
|
327
|
+
|
|
328
|
+
1. **Enable boolean latching:**
|
|
329
|
+
|
|
330
|
+
```tsx filename="src/App.tsx"
|
|
331
|
+
latch: {
|
|
332
|
+
booleanFields: ["action", "ability"],
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
2. **Ensure game loop runs consistently** (60fps target)
|