@maccesar/titools 4.6.1 → 4.7.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.
@@ -0,0 +1,333 @@
1
+ # ti.game API reference
2
+
3
+ Complete JS surface of `ti.game` 0.3.0, verified against the module source (`android/src/ti/game/*.java` and `engine/*.java`, mirrored by `ios/Classes/TG*`). Defaults come from the engine fields, not from prose.
4
+
5
+ Every property listed is **live**: reading returns the current native value, even mid-drag or mid-tween. Every property can also be passed to its `create*` factory. All durations crossing the JS boundary are **milliseconds** (the engine converts to seconds internally).
6
+
7
+ ## Contents
8
+
9
+ - [Module](#module)
10
+ - [GameView](#gameview)
11
+ - [SpriteSheet](#spritesheet)
12
+ - [Sprite](#sprite)
13
+ - [Sound](#sound)
14
+ - [Emitter](#emitter)
15
+ - [Rope](#rope)
16
+ - [Events at a glance](#events-at-a-glance)
17
+ - [Gotchas the property tables do not show](#gotchas-the-property-tables-do-not-show)
18
+
19
+ ## Module
20
+
21
+ ```javascript
22
+ const Game = require('ti.game');
23
+ ```
24
+
25
+ | Factory | Returns |
26
+ | --- | --- |
27
+ | `Game.createGameView(options)` | GameView — the canvas, a Titanium view |
28
+ | `Game.createSpriteSheet(options)` | SpriteSheet — a texture cut into frames |
29
+ | `Game.createSprite(options)` | Sprite |
30
+ | `Game.createSound(options)` | Sound |
31
+ | `Game.createEmitter(options)` | Emitter — particle system |
32
+ | `Game.createRope(options)` | Rope — Verlet chain |
33
+
34
+ Easing constants (for `sprite.animate({ easing })`): `Game.EASE_LINEAR`, `Game.EASE_IN`, `Game.EASE_OUT`, `Game.EASE_IN_OUT`, `Game.EASE_BOUNCE`, `Game.EASE_ELASTIC`.
35
+
36
+ ## GameView
37
+
38
+ A normal Titanium view — add it to a window or any container, size it with the usual Titanium properties.
39
+
40
+ | Member | Type | Default | Notes |
41
+ | --- | --- | --- | --- |
42
+ | `add(object)` / `add([objects])` | method | — | Accepts Sprite, Emitter or Rope. An array crosses the bridge once and is committed under a single native scene lock — build a level into an array and add it in one call |
43
+ | `remove(object)` | method | — | Remove one object from the scene |
44
+ | `removeAllSprites()` | method | — | Clear the whole scene |
45
+ | `pause()` / `resume()` | method | — | Render loop control. The activity/app lifecycle already pauses and resumes automatically |
46
+ | `backgroundColor` | string | — | GL clear color |
47
+ | `maxFps` | int | `0` | Frame cap. `60` stops 120 Hz ProMotion displays from doubling render work; `0` = display refresh rate |
48
+ | `timeScale` | float | `1` | Global multiplier for everything the engine ticks: physics, sheet animations, tweens, particles, camera. `0.5` = slow motion, `0` freezes the scene while rendering and touch keep running (pause menus, hit-stop) |
49
+ | `surfaceWidth` / `surfaceHeight` | int | — | Read-only surface size in pixels |
50
+ | `cameraX` / `cameraY` | float | `0` | World-space offset of the view |
51
+ | `cameraScale` | float | `1` | Zoom, anchored on the view center |
52
+ | `cameraBounds` | dict / null | `null` | `{ minX, minY, maxX, maxY }` world rect the visible area is clamped into |
53
+ | `follow(sprite, options)` | method | — | Native dead-zone follow, see below |
54
+ | `stopFollow()` | method | — | Stop following; the camera stays where it is |
55
+ | `shake({ strength, duration })` | method | `12` px, `400` ms | Detuned-sine rumble on the projection only — follow, bounds and touch mapping are unaffected |
56
+ | `cameraEffect` | string | `'none'` | Fullscreen shader pass: `'none'`, `'tint'`, `'glitch'`. With `'none'` the extra pass is skipped entirely |
57
+ | `cameraTint` | string | — | Color for the `'tint'` effect, e.g. `'#4f8'` (night vision, poison, flashback) |
58
+ | `cameraEffectIntensity` | float | `1` | 0..1 — tint mix or glitch amount |
59
+ | `debug` | bool | `false` | Draw collision shapes for every sprite in the scene |
60
+
61
+ Events: `press`, `tap`, `release` — fired for **every** touch anywhere on the view, with payload `x`, `y` in scene coordinates (tap-anywhere controls, flappy-style). And `resize` with payload `width`, `height` — the real surface size in pixels.
62
+
63
+ ### `follow(sprite, options)`
64
+
65
+ | Option | Default | Effect |
66
+ | --- | --- | --- |
67
+ | `topMargin` | `0.33` | Vertical dead-zone top edge, as a fraction of visible height |
68
+ | `bottomMargin` | `0.7` | Vertical dead-zone bottom edge |
69
+ | `leftMargin` | `0.35` when horizontal follow is enabled | Horizontal dead-zone left edge |
70
+ | `rightMargin` | `0.65` | Horizontal dead-zone right edge |
71
+ | `smoothing` | `0` | 0..1. `0` snaps; otherwise the camera eases by that fraction of the remaining distance per 1/60 s |
72
+ | `maxY` | `0` | The camera's `cameraY` never goes past this. Default `0` suits a platformer whose start view is the bottom of the level — for a free-roaming world pass the world height |
73
+
74
+ Vertical follow is always on. **Horizontal follow is off until you pass `leftMargin` or `rightMargin`.**
75
+
76
+ Each call **resets every option to its default** before applying what you passed. Calling `follow(sprite)` with no options after configuring it wipes the configuration.
77
+
78
+ ## SpriteSheet
79
+
80
+ Create one sheet per texture and share it across every sprite that uses it — sprites sharing a sheet batch into a single draw call.
81
+
82
+ | Option | Default | Notes |
83
+ | --- | --- | --- |
84
+ | `image` | — | Path to the texture, relative to the resources root (`'assets/hero.png'`) |
85
+ | `frameWidth` / `frameHeight` | — | Grid mode: cell size in source pixels. Frames are numbered left-to-right, top-to-bottom, starting at 0 |
86
+ | `atlas` | — | TexturePacker JSON (hash or array format) instead of a grid |
87
+ | `smoothing` | `true` | `false` = GL_NEAREST, the pixel-art setting |
88
+ | `repeat` | `false` | GL_REPEAT wrap, required by sprites using `tileRepeat`. Needs power-of-two texture dimensions |
89
+
90
+ | Member | Notes |
91
+ | --- | --- |
92
+ | `frameCount` | Number of frames. `0` until the texture has loaded, for grid sheets |
93
+ | `frameNames` | Sorted frame names — atlas sheets only |
94
+ | `frameIndex(name)` | Index for an atlas frame name, `-1` if unknown |
95
+
96
+ Textures upload to the GPU lazily on first use, from the render thread, and are re-created automatically after an EGL context loss.
97
+
98
+ ## Sprite
99
+
100
+ ### Transform
101
+
102
+ | Property | Default | Notes |
103
+ | --- | --- | --- |
104
+ | `x`, `y` | `0` | Position of the **anchor**, not the top-left corner |
105
+ | `width`, `height` | frame size | `0` means "use the sheet frame size" |
106
+ | `scale` | `1` | Write-only convenience that sets both axes |
107
+ | `scaleX`, `scaleY` | `1` | Negative values flip *and* affect physics/hit testing — prefer `flipX`/`flipY` for pure mirroring |
108
+ | `rotation` | `0` | Degrees, clockwise |
109
+ | `anchorX`, `anchorY` | `0.5` | `0/0` = top-left, `0.5/0.5` = center, `0.5/1` = bottom-center (useful for feet) |
110
+ | `opacity` | `1` | |
111
+ | `visible` | `true` | `false` = no render **and no collision** — the pooling idiom |
112
+ | `zIndex` | `0` | Draw order |
113
+ | `ySort` | `false` | Within the same `zIndex`, sort by the sprite's **bottom edge** — top-down depth (walk behind a tree, in front of it below) |
114
+ | `flipX`, `flipY` | `false` | Mirror the drawn frame only. Position, anchor, physics and hit testing are untouched |
115
+ | `pixelSnap` | `false` | Rounds only the rendered anchor to a framebuffer pixel after camera position and zoom. Physics and live `x`/`y` stay subpixel floats. Combine with `smoothing: false` when a moving pixel-art sprite must keep a stable texel phase |
116
+
117
+ ### Sheet and animation
118
+
119
+ | Property | Default | Notes |
120
+ | --- | --- | --- |
121
+ | `sheet` | — | The SpriteSheet |
122
+ | `frame` | `0` | Current frame index; writing it stops nothing, it just shows that frame |
123
+ | `animations` | — | Named definitions: `{ walk: { frames: [1, 2], fps: 6, loop: true } }` |
124
+ | `animation` | — | Read-only name of the current animation. **Keeps its value after `stop()`** — track "is walking" yourself if you need it |
125
+ | `tileRepeat` | `false` | `true` / `'x'` / `'y'` — tile the frame at native size instead of stretching. Needs `repeat: true` on the sheet and a frame spanning the whole texture |
126
+
127
+ An animation definition takes `frames` (array of indices), `fps`, `loop`, and an optional `frame` — the sheet frame to show once a non-looping animation finishes (default: hold the last animation frame).
128
+
129
+ Methods: `play(name)` starts an animation, `stop()` halts it on the current frame.
130
+
131
+ ### Touch
132
+
133
+ | Property | Default | Notes |
134
+ | --- | --- | --- |
135
+ | `draggable` | `false` | Native drag & drop — the sprite moves on the UI thread, JS only hears milestones |
136
+ | `pinchable` | `false` | Two-finger scale |
137
+ | `rotatable` | `false` | Two-finger rotate |
138
+ | `touchEnabled` | `true` | `false` = touches pass through to sprites underneath |
139
+
140
+ Hit-testing runs against the transformed shape (rotation and scale included), topmost first, and is multi-touch: each finger runs its own gesture and a sprite belongs to at most one finger. A second finger landing on empty space — or on the sprite already held — pinches/rotates that sprite instead, per its flags.
141
+
142
+ ### Physics
143
+
144
+ | Property | Default | Notes |
145
+ | --- | --- | --- |
146
+ | `velocityX`, `velocityY` | `0` | px/s |
147
+ | `gravity` | `0` | px/s², added to `velocityY`. Per sprite — there is no global gravity |
148
+ | `maxSpeed` | `500` | px/s cap |
149
+ | `angularVelocity` | `0` | deg/s |
150
+ | `thrust` | `0` | px/s² along the current heading (Newtonian flight) |
151
+ | `wrapAround` | `false` | Re-enter from the opposite screen edge (Asteroids) |
152
+ | `wrapX`, `wrapShift` | `0` | Scroll looping: at `x < wrapX`, add `wrapShift`. Two screen-wide copies with `{ wrapX: -W/2, wrapShift: 2*W }` and a negative `velocityX` make a seamless parallax layer with no JS in the loop |
153
+
154
+ ### Solids
155
+
156
+ | Property | Default | Notes |
157
+ | --- | --- | --- |
158
+ | `solidWith` | — | Array of group names that block this sprite. The engine pushes it out along the axis of least penetration |
159
+ | `onGround` | `false` | Read-only. Gate jumps on it |
160
+ | `restitution` | `0` | 0..1 bounce factor against solids |
161
+ | `oneWay` | `false` | On the **solid**: pass-through except for landings on its top edge — classic platformer floors |
162
+ | `carryRiders` | `true` | On the **solid**: a moving solid carries whoever stands on it (velocity, tweens, idle wobble — wrap teleports excluded). Set `false` for world-scroll terrain that moves while the player is meant to stay put |
163
+
164
+ ### Collision
165
+
166
+ | Property | Default | Notes |
167
+ | --- | --- | --- |
168
+ | `collisionGroup` | — | The tag *this* sprite carries |
169
+ | `collidesWith` | — | Array of groups this sprite reports overlaps with |
170
+ | `hitboxScale` | `1` | Shrinks the collision box around the anchor |
171
+ | `hitboxShape` | `'rect'` | `'circle'` — radius = half the smaller drawn side × `hitboxScale`. Circles resolve against solids along the contact normal (corner bounces) and get a round touch area |
172
+ | `debug` | `false` | Draw this sprite's shapes: green = collision AABB, blue = touch bounds, orange dot = anchor |
173
+
174
+ ### Car (`carMode`)
175
+
176
+ | Property | Default | Notes |
177
+ | --- | --- | --- |
178
+ | `carMode` | `false` | Enables the arcade car model |
179
+ | `throttle` | `0` | `-1` (brake/reverse) .. `1` (gas) |
180
+ | `steering` | `0` | `-1` (left) .. `1` (right) |
181
+ | `enginePower` | `600` | Forward acceleration, px/s² |
182
+ | `maxSpeed` | `500` | px/s; reverse caps at 40% |
183
+ | `turnRate` | `200` | deg/s at full steering and speed |
184
+ | `grip` | `4` | Lateral friction, 1/s. **Lower = more drift** |
185
+ | `drag` | `0.6` | Longitudinal friction, 1/s |
186
+ | `skidMarks` | `false` | Fading rubber trails while drifting |
187
+ | `skidThreshold` | `0` | Lateral px/s that counts as drifting |
188
+ | `drifting` | `false` | Read-only — handy for triggering tire sounds |
189
+
190
+ Drift is emergent, not a mode: lateral grip is finite, so hard cornering at speed keeps sideways momentum.
191
+
192
+ ### Idle wobble
193
+
194
+ | Property | Default | Notes |
195
+ | --- | --- | --- |
196
+ | `idleAnimation` | `false` | Gentle organic sway around the base transform |
197
+ | `idleRotation` | `3` | Degrees |
198
+ | `idleMovement` | `4` | px |
199
+ | `idleSpeed` | `1` | Frequency multiplier |
200
+
201
+ Every sprite gets its own phase, and the wobble unwinds exactly when disabled. Turn it **off before tweening a sprite to a position where alignment matters** — tweens write absolute values, so a wobbling sprite lands with a leftover offset.
202
+
203
+ ### Color
204
+
205
+ | Property | Default | Notes |
206
+ | --- | --- | --- |
207
+ | `tintColor` | — | Multiplies the frame's colors (team colors, day/night, damage states). `null` or `'#fff'` = unchanged. Multiplicative, so it can only darken |
208
+ | `glowColor` | — | Tinted, blurred silhouette drawn behind the sprite by a shader pass |
209
+ | `glowBlur` | `0` | Blur radius in px; `0` = off |
210
+ | `glowOpacity` | `1` | Halo strength 0..1 — tweenable via `animate`, so a glow can fade in without touching the blur |
211
+ | `blend` | `'normal'` | `'add'` brightens the backdrop instead of covering it (glows, fire, lasers). Costs one batch flush per mode change — group additive sprites by `zIndex` |
212
+
213
+ ### Methods
214
+
215
+ | Method | Notes |
216
+ | --- | --- |
217
+ | `play(name)` | Start a named animation |
218
+ | `stop()` | Stop the animation on the current frame |
219
+ | `animate(options)` | Native tween, fires `complete` |
220
+ | `clearTweens()` | Cancel tweens in progress — call before starting a replacement tween |
221
+ | `flash(color, duration)` | Fills the sprite's silhouette with `color` (default white) and fades it out over `duration` ms (default 150). The classic damage/invincibility flash that a multiplicative `tintColor` cannot do |
222
+
223
+ `animate()` accepts exactly these keys — anything else is silently ignored: `x`, `y`, `scale`, `scaleX`, `scaleY`, `rotation`, `opacity`, `glowOpacity`, `duration` (ms), `delay` (ms), `easing`, `frame` (the sheet frame to set once the tween finishes). Chain moves by re-calling `animate` from the `complete` handler — that is how patrols and ping-pong motions are built.
224
+
225
+ ### Sprite events
226
+
227
+ | Event | Payload | Fires |
228
+ | --- | --- | --- |
229
+ | `press` | `x`, `y`, `touchX`, `touchY` | Finger down on the sprite |
230
+ | `release` | `x`, `y` | Finger up or cancel after a press (after `tap`/`dragend`) |
231
+ | `tap` | `x`, `y`, `touchX`, `touchY` | Quick touch without movement |
232
+ | `dragstart` | `x`, `y` | Drag exceeded touch slop |
233
+ | `drag` | `x`, `y` | Throttled to ~10 Hz while dragging |
234
+ | `dragend` | `x`, `y` | Finger lifted; the sprite has already moved natively |
235
+ | `pinch` | `scaleX`, `scaleY` | While two-finger scaling |
236
+ | `rotate` | `rotation` | While two-finger rotating |
237
+ | `animationcomplete` | `animation` | A non-looping sheet animation finished |
238
+ | `complete` | final transform values | A tween finished |
239
+ | `collision` | `group`, `other`, `x`, `y` | Overlap with a `collidesWith` group began |
240
+ | `land` | `x`, `y`, `other`, `group` | Landed on top of a `solidWith` solid. `other` is the solid — read it to react to what you landed on (trampolines, hazards) |
241
+
242
+ ## Sound
243
+
244
+ ```javascript
245
+ const jump = Game.createSound({ url: 'assets/jump.wav', volume: 0.8 });
246
+ jump.play(); // fire-and-forget; rapid plays overlap
247
+
248
+ const music = Game.createSound({ url: 'assets/theme.mp3', music: true, loop: true });
249
+ music.play();
250
+ ```
251
+
252
+ | Option | Default | Notes |
253
+ | --- | --- | --- |
254
+ | `url` | required | App resource or file path |
255
+ | `volume` | `1` | 0..1, live |
256
+ | `loop` | `false` | Live |
257
+ | `music` | `false` | Chooses the backend — **set at creation, not changeable later** |
258
+
259
+ Effect mode (the default) is built for low latency: a shared `SoundPool` on Android, a small pool of preloaded players on iOS. Call `play()` from any handler and repeated plays overlap instead of cutting each other off. `music: true` picks the streaming backend (`MediaPlayer` / `AVAudioPlayer`) for longer tracks; music pauses when the app backgrounds and resumes with it.
260
+
261
+ Methods: `play()`, `pause()` (resumes where it stopped), `stop()` (rewinds to the beginning). Formats: WAV, MP3, OGG on Android; WAV, MP3, M4A on iOS.
262
+
263
+ ## Emitter
264
+
265
+ Add and remove like a sprite: `gameView.add(emitter)` / `remove(emitter)`. Spawning, integration, fading and drawing all run in the native loop — JS only writes configuration and calls `emit()`.
266
+
267
+ | Property | Default | Notes |
268
+ | --- | --- | --- |
269
+ | `sheet`, `frame` | — | All particles share one frame, so an emitter renders as a single batch run |
270
+ | `x`, `y` | `0` | Ignored while `target` is set |
271
+ | `target` | — | Sprite to follow. Set to `null` to detach |
272
+ | `offsetX`, `offsetY` | `0` | Offset from the target (rear wheels, exhaust) |
273
+ | `zIndex` | `0` | Emitters draw above sprites of the same z |
274
+ | `rate` | `0` | Particles per second while `emitting`. Leave at `0` for burst-only emitters |
275
+ | `emitting` | `true` | Toggle the continuous stream |
276
+ | `lifetime` | `800` | ms |
277
+ | `speed` | `100` | px/s, randomized between 50% and 100% so bursts do not form perfect rings |
278
+ | `angle` | `0` | Base direction: `0` = up, clockwise degrees |
279
+ | `spread` | `360` | Cone width in degrees |
280
+ | `gravity` | `0` | px/s² applied to particle velocity |
281
+ | `size` | `0` | Base particle width in px; `0` = frame size |
282
+ | `startScale` / `endScale` | `1` / `1` | Interpolated over `lifetime` |
283
+ | `startOpacity` / `endOpacity` | `1` / `0` | Interpolated over `lifetime` |
284
+ | `tint` | white | Tint white particle art at runtime — one tiny texture covers every color in the game |
285
+ | `blend` | `'normal'` | `'add'` for fire, sparks and magic |
286
+ | `maxParticles` | `200` | Pool size, hard cap 1000 |
287
+
288
+ Methods: `emit(n)` fires a one-shot burst on top of `rate`; `clear()` kills all live particles.
289
+
290
+ ## Rope
291
+
292
+ A native Verlet chain: integration and distance constraints run in the game loop, segments render as quads oriented along the rope (one sheet frame, one batch run).
293
+
294
+ | Property | Default | Notes |
295
+ | --- | --- | --- |
296
+ | `sheet`, `frame` | — | One frame, textured along each link |
297
+ | `segments` | `10` | Number of links |
298
+ | `segmentLength` | `30` | px |
299
+ | `thickness` | `10` | Drawn width in px |
300
+ | `gravity` | `1500` | px/s² |
301
+ | `damping` | `0.98` | Velocity kept per step |
302
+ | `iterations` | `3` | Constraint passes per frame — higher is stiffer and costlier |
303
+ | `head` | — | Sprite the head is pinned to. Without it, the head anchors at `x`/`y` |
304
+ | `tail` | — | Sprite pinned to the other end (hanging weights, bridges) |
305
+ | `x`, `y` | `0` | Fixed head anchor when no `head` sprite is set |
306
+ | `maxLength` | `0` | `0` = off. With a `tail`, turns the rope into a tether: when head→tail distance exceeds it, the sprites are pulled back onto the limit each frame and their outward velocity is cancelled — a falling weight snaps taut and swings like a pendulum instead of stretching |
307
+ | `zIndex` | `0` | |
308
+ | `visible` | `true` | |
309
+ | `endX`, `endY` | — | Read-only live position of the loose end (grappling-hook tips) |
310
+
311
+ The tether yields at the end no finger owns: with a fixed head anchor the tail sprite is simply leashed, but with sprites on both ends you can drag either one and the other is towed once the rope goes taut.
312
+
313
+ ## Events at a glance
314
+
315
+ Only these events exist. Nothing fires per frame, and events are only fired if a listener is registered.
316
+
317
+ - **GameView**: `press`, `tap`, `release`, `resize`
318
+ - **Sprite**: `press`, `release`, `tap`, `dragstart`, `drag`, `dragend`, `pinch`, `rotate`, `animationcomplete`, `complete`, `collision`, `land`
319
+ - **SpriteSheet, Sound, Emitter, Rope**: none
320
+
321
+ ## Gotchas the property tables do not show
322
+
323
+ - **`maxSpeed` defaults to 500 px/s and silently caps anything fast.** A bullet given `velocityX = 1400` travels at 500 unless you raise its own `maxSpeed`. The symptom is a projectile that looks sluggish on a large surface and fine on a small one, with nothing in the code hinting at a cap.
324
+ - **`sprite.animation` survives `stop()`.** Comparing it to decide whether to restart an animation gives a false negative. The top-down demo tracks a `walking` boolean itself.
325
+ - **`follow()` resets its options.** See the GameView section.
326
+ - **`animate()` ignores unknown keys.** Tweening `width`, `height` or `tintColor` silently does nothing — those are instant writes only.
327
+ - **`visible: false` removes a sprite from collision too.** That is why pooled bullets and rocks use it instead of removing them from the scene.
328
+ - **`scaleX: -1` versus `flipX: true`.** Negative scale flips the physics and hit-test shape with the art; `flipX` mirrors only the drawing. Use `flipX` unless you specifically want the former.
329
+ - **Tween positions are absolute.** A sprite with `idleAnimation` on will land off-target; disable the wobble before the tween and re-enable it on `complete`.
330
+ - **`music` is chosen at creation.** Creating an effect sound and flipping `music` later does not switch backends.
331
+ - **`collision` fires once per overlap-enter**, re-arming only after the shapes separate. A sprite resting inside a trigger does not re-fire.
332
+ - **`hitboxShape: 'circle'` also changes the touch area**, not just collisions.
333
+ - **`blend: 'add'` costs a batch flush per mode change.** Group additive sprites on their own `zIndex` band.
@@ -0,0 +1,280 @@
1
+ # Setting up a ti.game project
2
+
3
+ Installing the module, wiring it into Classic and Alloy projects, organizing art and sound, and the lifecycle work the engine does not do for you.
4
+
5
+ ## Contents
6
+
7
+ - [Install the module](#install-the-module)
8
+ - [Classic projects](#classic-projects)
9
+ - [Alloy projects](#alloy-projects)
10
+ - [Where art and sound live](#where-art-and-sound-live)
11
+ - [Sprite sheets and atlases](#sprite-sheets-and-atlases)
12
+ - [Lifecycle and cleanup](#lifecycle-and-cleanup)
13
+ - [Debugging and tuning](#debugging-and-tuning)
14
+ - [Platform differences](#platform-differences)
15
+
16
+ ## Install the module
17
+
18
+ There is no registry and no `ti module install` command — Titanium modules are zips you drop into a project. Grab the platform zips from the module's `dist/` folders (or build them yourself) and put them where the CLI looks:
19
+
20
+ **Project-local** (recommended — the version travels with the repo):
21
+
22
+ ```
23
+ modules/
24
+ ├── android/ti.game/0.3.0/
25
+ └── iphone/ti.game/0.3.0/
26
+ ```
27
+
28
+ Unzipping the module zip at the project root creates exactly that layout.
29
+
30
+ **Global** (shared across projects): `~/Library/Application Support/Titanium/modules/<platform>/ti.game/<version>/`.
31
+
32
+ Then declare it in `tiapp.xml`. The iOS platform key is `iphone`, not `ios`:
33
+
34
+ ```xml
35
+ <modules>
36
+ <module platform="android" version="0.3.0">ti.game</module>
37
+ <module platform="iphone" version="0.3.0">ti.game</module>
38
+ </modules>
39
+ ```
40
+
41
+ Pinning `version` is worth the extra attribute: with two module versions installed side by side, an unpinned entry silently picks the highest one.
42
+
43
+ Building the module from source:
44
+
45
+ ```bash
46
+ cd android
47
+ ti build -p android --build-only # → android/dist/ti.game-android-<version>.zip
48
+ ti build -p ios --build-only # macOS only → ios/dist/ti.game-iphone-<version>.zip
49
+ ```
50
+
51
+ ## Classic projects
52
+
53
+ `require('ti.game')` anywhere and add the GameView to a window. The 18 demos in the module's `example/` folder are Classic, each one a CommonJS module exporting a start function:
54
+
55
+ ```javascript
56
+ // Resources/flappy.js
57
+ const Game = require('ti.game');
58
+
59
+ module.exports = function () {
60
+ const win = Ti.UI.createWindow({ backgroundColor: '#000', theme: 'Theme.Titanium.DayNight.NoTitleBar' });
61
+ const gameView = Game.createGameView({ backgroundColor: '#8ed8f8' });
62
+ // ... build the game
63
+ win.add(gameView);
64
+ win.open();
65
+ };
66
+ ```
67
+
68
+ ```javascript
69
+ // Resources/app.js
70
+ require('/flappy')();
71
+ ```
72
+
73
+ One module per screen or per game mode keeps things separable, and a window per mode means closing it tears the scene down.
74
+
75
+ ## Alloy projects
76
+
77
+ Two ways to get a GameView on screen. Both work; pick by whether the scene is permanent or comes and goes.
78
+
79
+ ### A: declare it in XML (`<Module>`)
80
+
81
+ The standard Alloy element for a native module view. `method` names the factory to call and every attribute is passed as a creation property.
82
+
83
+ ```xml
84
+ <!-- app/views/index.xml -->
85
+ <Alloy>
86
+ <Window id="index" navBarHidden="true" exitOnClose="true" onOpen="onOpen" onClose="onClose">
87
+ <Module id="gameView" module="ti.game" method="createGameView" backgroundColor="#8ed8f8" />
88
+ <Label id="scoreLabel" />
89
+ <Button id="jumpButton" title="JUMP" onTouchstart="jump" />
90
+ </Window>
91
+ </Alloy>
92
+ ```
93
+
94
+ ```javascript
95
+ // app/controllers/index.js
96
+ const Game = require('ti.game');
97
+
98
+ const heroSheet = Game.createSpriteSheet({ image: 'hero.png', frameWidth: 64, frameHeight: 64 });
99
+
100
+ let initialized = false;
101
+
102
+ function onOpen() {
103
+ $.gameView.addEventListener('resize', onResize);
104
+ }
105
+
106
+ function onResize(e) {
107
+ if (initialized) {
108
+ return;
109
+ }
110
+ initialized = true;
111
+ buildLevel(e.width, e.height);
112
+ }
113
+ ```
114
+
115
+ Style the view in TSS like any other component (`'#gameView': { width: Ti.UI.FILL, height: Ti.UI.FILL }`). The `<Module>` element itself is documented Alloy behavior; the pattern below is the one verified in a shipped ti.game app.
116
+
117
+ ### B: an empty container in XML, the GameView created in JS
118
+
119
+ Preferred when the game is created and destroyed repeatedly — switching between a menu and a run, changing levels, or restarting cleanly. The XML holds a plain container and the game code owns everything inside it.
120
+
121
+ ```xml
122
+ <Alloy>
123
+ <Window id="index" navBarHidden="true" exitOnClose="true" onOpen="onOpen" onClose="onClose">
124
+ <View id="menuState" />
125
+ <View id="gameContainer" visible="false" />
126
+ </Window>
127
+ </Alloy>
128
+ ```
129
+
130
+ ```javascript
131
+ // app/controllers/index.js
132
+ const LanderGame = require('/game/LanderGame');
133
+
134
+ let game = null;
135
+
136
+ function startCampaign() {
137
+ destroyGame();
138
+ game = new LanderGame({ container: $.gameContainer, onExit: showMenu });
139
+ $.menuState.visible = false;
140
+ $.gameContainer.visible = true;
141
+ }
142
+
143
+ function destroyGame() {
144
+ if (!game) {
145
+ return;
146
+ }
147
+ game.destroy();
148
+ game = null;
149
+ }
150
+
151
+ function onClose() {
152
+ destroyGame();
153
+ $.destroy();
154
+ }
155
+
156
+ $.cleanup = onClose;
157
+ ```
158
+
159
+ ### Structure for anything bigger than a demo
160
+
161
+ Alloy Tier 2 applies unchanged: a slim controller that owns navigation and menu state, with the game itself in `app/lib/`. A shipped layout that works:
162
+
163
+ ```
164
+ app/lib/game/
165
+ ├── LanderGame.js session orchestration and the whole JS↔native boundary
166
+ ├── TerrainGenerator.js deterministic level geometry
167
+ ├── WorldCatalog.js immutable world/level data
168
+ ├── AudioManager.js owns every createSound handle
169
+ ├── GamePreferences.js Ti.App.Properties persistence
170
+ └── LeaderboardStore.js scores
171
+ ```
172
+
173
+ Catalogs, generators and stores are plain modules with no Titanium UI in them, so they are unit-testable in plain Node. The one class that touches sprites is the boundary — keep it explicit and keep the rest pure.
174
+
175
+ Alloy specifics worth remembering: `OS_IOS`/`OS_ANDROID` are compile-time constants (the dead branch is stripped, so an iOS-only API inside `if (OS_IOS)` never reaches an Android build), `Alloy.CFG` reads `config.json`, and `$.destroy()` releases Alloy's own bindings but knows nothing about your timers, sounds or sprites — see the cleanup section below.
176
+
177
+ ## Where art and sound live
178
+
179
+ **Alloy**: everything under `app/assets/` is copied to the resources root, so the path in code drops the `app/assets/` prefix.
180
+
181
+ | File | Referenced as |
182
+ | --- | --- |
183
+ | `app/assets/lander.png` | `'lander.png'` |
184
+ | `app/assets/game/stars_far.png` | `'game/stars_far.png'` |
185
+ | `app/assets/sounds/jump.wav` | `'sounds/jump.wav'` |
186
+
187
+ **Classic**: paths are relative to `Resources/`, so `Resources/assets/hero.png` is `'assets/hero.png'`.
188
+
189
+ Do not use `@2x`/`@3x` density suffixes for game art. The scene is measured in surface pixels and you size every sprite yourself from the `resize` dimensions — density variants would fight that. Ship one texture at a resolution high enough for the largest target and let the engine scale it.
190
+
191
+ ## Sprite sheets and atlases
192
+
193
+ **Grid** — frames numbered left-to-right, top-to-bottom from 0:
194
+
195
+ ```javascript
196
+ const sheet = Game.createSpriteSheet({
197
+ image: 'hero.png',
198
+ frameWidth: 64,
199
+ frameHeight: 64,
200
+ smoothing: false // pixel art
201
+ });
202
+ ```
203
+
204
+ **TexturePacker atlas** — hash or array JSON format:
205
+
206
+ ```javascript
207
+ const sheet = Game.createSpriteSheet({ image: 'hero.png', atlas: 'hero.json' });
208
+ const runFrame = sheet.frameIndex('run_01'); // -1 if unknown
209
+ ```
210
+
211
+ Atlases are the better default for hand-authored art with varied frame sizes; grids are simpler for uniform pixel art.
212
+
213
+ Practical rules:
214
+
215
+ - **One sheet per scene if you can manage it.** Sprites sharing a texture batch into a single draw call. The endless-runner demo even draws its crash burst from the *player's* sheet to keep the whole scene on one texture.
216
+ - **`smoothing: false` for pixel art.** Combine with `pixelSnap: true` on moving sprites when the texel phase must stay stable.
217
+ - **`repeat: true` for tiling.** Required by sprites using `tileRepeat`, and it needs power-of-two texture dimensions on ES 2.0 — a 512×64 street strip tiles, a 500×60 one does not.
218
+ - **White art plus `tint`** covers every color variant of a particle or effect with one small texture.
219
+
220
+ `frameCount` is `0` until a grid sheet's texture has loaded, so do not compute animation ranges from it at creation time.
221
+
222
+ ## Lifecycle and cleanup
223
+
224
+ The engine handles what it owns: the render loop pauses and resumes with the activity or app, music pauses and resumes with it, and textures survive an EGL context loss. **Everything you created in JS is yours to stop.**
225
+
226
+ A leaked `setInterval` keeps running after the window closes, keeps reading sprite properties from a dead scene and keeps the whole closure alive. A looping `createSound` keeps playing over the next screen.
227
+
228
+ ```javascript
229
+ function cleanup() {
230
+ clearInterval(aiTimer);
231
+ clearTimeout(spawnTimer);
232
+ clearHitStop(); // restore timeScale — a frozen scene must not persist
233
+ music.stop();
234
+ thrustSound.stop(); // looping effects too
235
+ listeners.forEach(({ target, eventName, handler }) => {
236
+ target.removeEventListener(eventName, handler);
237
+ });
238
+ gameView.pause();
239
+ gameView.removeAllSprites();
240
+ }
241
+
242
+ // Classic
243
+ win.addEventListener('close', cleanup);
244
+
245
+ // Alloy
246
+ $.cleanup = cleanup;
247
+ ```
248
+
249
+ Tracking listeners as you add them makes the teardown mechanical instead of a memory exercise:
250
+
251
+ ```javascript
252
+ const listeners = [];
253
+
254
+ function listen(target, eventName, handler) {
255
+ target.addEventListener(eventName, handler);
256
+ listeners.push({ target, eventName, handler });
257
+ }
258
+ ```
259
+
260
+ For restart-without-teardown (a retry button), `removeAllSprites()` plus rebuilding is simplest, but pooling and repositioning is cheaper: reset positions, zero velocities, `clearTweens()`, and re-`play()` the animations.
261
+
262
+ ## Debugging and tuning
263
+
264
+ - **`debug: true` on the GameView** draws collision shapes for the whole scene; on a single sprite, only that one. Green = collision AABB with `hitboxScale` applied, blue = sprite/touch bounds, orange dot = anchor. This is the fastest way to explain a hit that should not have registered.
265
+ - **Physics that feels wrong is usually a units problem.** Speeds are px/s and accelerations px/s², both in surface pixels — which is why the demos express them as fractions of `W` and `H`. A gravity of `20` is imperceptible; `H * 2.2` is a snappy platformer.
266
+ - **Sprite is invisible?** Check in order: was it added to the GameView, does it have a sheet, is `width`/`height` non-zero, is it inside the surface bounds, and is its `zIndex` below a background sprite.
267
+ - **Collision never fires?** `collisionGroup` goes on the *target*, `collidesWith` on the *mover*. Both need to be set, and `visible: false` disables collision entirely.
268
+ - **Never scrolls / never follows?** `follow()` only does horizontal tracking when `leftMargin` or `rightMargin` is passed, and `maxY` defaults to `0`.
269
+ - Testing: game logic that lives in plain modules (catalogs, generators, scoring, persistence) tests in Node with the built-in `node:test` runner. The scene layer needs a device — there is no headless renderer.
270
+
271
+ ## Platform differences
272
+
273
+ The JS API is identical on Android and iOS. What differs is around it:
274
+
275
+ - **`Ti.UI.createNotification` (Android Toast) does not exist on iOS.** The demos branch on `Ti.Platform.osname` and fall back to `Ti.UI.createAlertDialog`.
276
+ - **The iOS Simulator renders at a 1x logical drawable** (its translated OpenGL path is disproportionately expensive at 3x Retina) and is noticeably slower than any real device. Real iPhone and iPad builds keep the device's native screen scale. Judge performance on device.
277
+ - **`logicalDensityFactor` lies in the iOS Simulator** — it reports the device scale while the surface renders at 1x. Derive dp→scene units from the surface instead: `H / Ti.Platform.displayCaps.platformHeight`.
278
+ - **Android's `touchFeedback` ripple** cannot animate over the GL canvas and logs `RippleDrawable` errors; give on-screen buttons manual `backgroundColor` feedback on `touchstart`/`touchend`.
279
+ - **Sound formats**: WAV, MP3 and OGG on Android; WAV, MP3 and M4A on iOS. WAV for effects, MP3 for music covers both.
280
+ - **`theme: 'Theme.Titanium.DayNight.NoTitleBar'`** on the window is the Android idiom for a fullscreen game surface; on iOS use `navBarHidden`/`statusBarHidden` (or the Alloy XML equivalents).