@maccesar/titools 4.7.0 → 4.9.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/README.md +12 -5
- package/package.json +1 -1
- package/skills/ti-game/SKILL.md +39 -10
- package/skills/ti-game/references/api.md +111 -17
- package/skills/ti-game/references/project-setup.md +30 -2
- package/skills/ti-game/references/recipes.md +166 -23
- package/skills/ti-game/references/roadmap.md +18 -10
package/README.md
CHANGED
|
@@ -177,7 +177,7 @@ The knowledge index is based on the latest Titanium SDK documentation. If your p
|
|
|
177
177
|
| ti-expert | Architecture and implementation | Starting point for most tasks |
|
|
178
178
|
| purgetss | Utility-first styling | UI styling and animations |
|
|
179
179
|
| ti-ui | UI/UX patterns | Complex layouts, ListViews, platform UI |
|
|
180
|
-
| ti-game | 2D games with the `ti.game` module | Sprites, physics, collisions, particles, camera |
|
|
180
|
+
| ti-game | 2D games with the `ti.game` module | Sprites, physics, collisions, raycasts, paths, text HUDs, particles, camera |
|
|
181
181
|
| ti-api | Complete Titanium API reference | Looking up properties, methods, events |
|
|
182
182
|
| ti-guides | SDK fundamentals | tiapp.xml, Hyperloop, distribution, JDK/Xcode compatibility, release notes |
|
|
183
183
|
| ti-howtos | Native feature integration | Location, push, media, platform APIs |
|
|
@@ -186,7 +186,7 @@ The knowledge index is based on the latest Titanium SDK documentation. If your p
|
|
|
186
186
|
|
|
187
187
|
Notes:
|
|
188
188
|
- The first four (`ti-expert`, `purgetss`, `ti-ui`, `ti-game`) are opinionated workflow skills reflecting TiTools' Titanium conventions.
|
|
189
|
-
- `ti-game` documents the `ti.game` native module (2D sprite engine on OpenGL ES 2.0, Android and iOS). Its API reference was verified against the module source; it applies to Alloy and Classic projects alike and does not depend on PurgeTSS.
|
|
189
|
+
- `ti-game` documents the `ti.game` native module (2D sprite engine on OpenGL ES 2.0, Android and iOS). Its API reference was verified against the module source and tracks upstream `main` (the module ships features ahead of its manifest version); it applies to Alloy and Classic projects alike and does not depend on PurgeTSS.
|
|
190
190
|
- The latter five are documentation-mirror skills sourced from the official Titanium SDK docs (`tidev/titanium-docs`) and audited against them via the internal `titools-skill-auditor`.
|
|
191
191
|
- `purgetss` reference files are audited against the official PurgeTSS documentation, but its workflow conventions are opinionated.
|
|
192
192
|
|
|
@@ -435,8 +435,12 @@ When it activates:
|
|
|
435
435
|
- `tiapp.xml` declares `ti.game`, or any file calls `require('ti.game')`
|
|
436
436
|
- Sprites, sprite sheets, TexturePacker atlases, frame animations, native tweens
|
|
437
437
|
- Physics: gravity and velocity, solid platforms, one-way floors, bouncing, the arcade car model, Newtonian thrust
|
|
438
|
-
- Collision groups, invisible trigger zones, hitbox tuning
|
|
439
|
-
-
|
|
438
|
+
- Collision groups, invisible trigger zones, enter/exit events, swept AABB for fast bullets, hitbox tuning
|
|
439
|
+
- Raycasts for line of sight, ledge probes and hitscan shots
|
|
440
|
+
- Native patrol paths (`followPath`) and animation chaining (`play(name, { then })`)
|
|
441
|
+
- Bitmap-font text sprites and screen-fixed HUDs inside the scene
|
|
442
|
+
- Particle emitters, Verlet ropes, camera follow/zoom/shake/effects, `scrollFactor` parallax, low-latency sound
|
|
443
|
+
- Game-clock timers that obey pause and slow motion
|
|
440
444
|
- Drag & drop, pinch, rotate, multitouch controls
|
|
441
445
|
|
|
442
446
|
Example prompts:
|
|
@@ -446,6 +450,8 @@ Example prompts:
|
|
|
446
450
|
"Add a camera that follows the player with a dead zone."
|
|
447
451
|
"How do I make a top-down racer that drifts?"
|
|
448
452
|
"Why does my game stutter when I move sprites from setInterval?"
|
|
453
|
+
"Add a score HUD that stays put while the camera follows the player."
|
|
454
|
+
"Make the guard walk a patrol route and turn around before the ledge."
|
|
449
455
|
```
|
|
450
456
|
|
|
451
457
|
Key rules:
|
|
@@ -454,7 +460,8 @@ Key rules:
|
|
|
454
460
|
- `solidWith` blocks, `collidesWith` reports — they are independent
|
|
455
461
|
- A sprite with size but no sheet is an invisible trigger (scores, goals, walls)
|
|
456
462
|
- Add a whole level with one `gameView.add([...])` call
|
|
457
|
-
-
|
|
463
|
+
- HUDs are `Game.createText` sprites with `screenFixed: true`, not `Ti.UI.Label` overlays
|
|
464
|
+
- Timers that drive game logic belong on the game clock (`gameView.every`), so a paused game stops spawning
|
|
458
465
|
|
|
459
466
|
---
|
|
460
467
|
|
package/package.json
CHANGED
package/skills/ti-game/SKILL.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ti-game
|
|
3
|
-
description: "Use when building or reviewing 2D games with the ti.game module for Titanium SDK (Android and iOS) — sprites, sprite sheets,
|
|
3
|
+
description: "Use when building or reviewing 2D games with the ti.game module for Titanium SDK (Android and iOS) — sprites, sprite sheets, animations, tweens, physics (gravity, solid platforms, carMode drifting, thrust), collision groups, trigger zones, swept AABB, raycast, paths, bitmap-font text, particles, Verlet ropes, camera follow/zoom/shake, parallax, sound, drag & drop, multitouch. AUTO-DETECT: if tiapp.xml declares ti.game or any file calls require('ti.game'), invoke BEFORE writing ANY game code. Also trigger on: game, juego, sprite, spritesheet, platformer, endless runner, top-down, shooter, racer, flappy, breakout, rhythm, cards, puzzle, collision, hitbox, raycast, line of sight, patrol, parallax, particles, HUD, score, game loop, 60 fps, GameView. ti.game is NOT Phaser, Godot or Box2D: you never write a game loop, never move a sprite from setInterval, and several familiar features (tilemap layer, sprite parenting, joystick, A* pathfinding) do not exist yet — check references/roadmap.md before inventing an API."
|
|
4
4
|
argument-hint: "[genre or feature]"
|
|
5
5
|
allowed-tools: Read, Grep, Glob, Edit, Write, Bash(node *)
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# ti.game — 2D game engine for Titanium SDK
|
|
9
9
|
|
|
10
|
-
`ti.game` is a native OpenGL ES 2.0 sprite engine exposed to JavaScript, written and maintained by **Michael Gangolf (m1ga)** — upstream is https://github.com/m1ga/ti.game. This skill
|
|
10
|
+
`ti.game` is a native OpenGL ES 2.0 sprite engine exposed to JavaScript, written and maintained by **Michael Gangolf (m1ga)** — upstream is https://github.com/m1ga/ti.game. This skill tracks upstream `main` as of **2026-08-19** — the manifest still reads `0.3.0`, but the text engine, `screenFixed`, `swept` collisions, `collisionend`, `followPath`, animation chaining, `raycast`, game-clock timers, `scrollFactor` parallax and the `multiply`/`screen` blend modes all landed after it, so a version number alone will not tell you what a checkout has. When upstream and any fork disagree, upstream wins. Same JS API on Android and iOS — the iOS side is a class-per-class Obj-C twin of the Android engine.
|
|
11
11
|
|
|
12
12
|
Works in **both Classic and Alloy** projects. Nothing here depends on PurgeTSS.
|
|
13
13
|
|
|
@@ -29,8 +29,16 @@ The practical consequence, and the single biggest mistake to avoid:
|
|
|
29
29
|
| A car that drives and drifts | your own physics | `carMode: true` + `throttle`/`steering` |
|
|
30
30
|
| A ship with inertia | vector math per frame | `thrust` + `angularVelocity` |
|
|
31
31
|
| Drag a sprite with the finger | `touchmove` handler | `draggable: true` |
|
|
32
|
-
| Scrolling background | timer repositioning copies | `wrapX` / `wrapShift` + `velocityX` |
|
|
32
|
+
| Scrolling background under a fixed camera | timer repositioning copies | `wrapX` / `wrapShift` + `velocityX` |
|
|
33
|
+
| Parallax layers under a moving camera | hand-scrolled layers | `scrollFactor` on each layer |
|
|
33
34
|
| Camera that follows the player | timer writing `cameraX` | `gameView.follow(sprite, options)` |
|
|
35
|
+
| A score, a title, a label | `Ti.UI.Label` overlays | `Game.createText({ text, screenFixed: true })` |
|
|
36
|
+
| A bullet fast enough to skip past a thin wall | shrinking the time step | `swept: true` on the bullet |
|
|
37
|
+
| To know the player *left* a zone | polling on a timer | the `collisionend` event |
|
|
38
|
+
| An enemy on a fixed patrol route | chained `animate()` legs | `sprite.followPath(points, { loop })` |
|
|
39
|
+
| Play an attack, then go back to idle | an `animationcomplete` handler | `play('attack', { then: 'idle' })` |
|
|
40
|
+
| Line of sight, a ledge probe, a hitscan shot | an invisible probe sprite | `gameView.raycast(x0, y0, x1, y1, ['group'])` |
|
|
41
|
+
| A spawn wave or an AI tick that respects pause | `setInterval` | `gameView.every(ms, cb)` |
|
|
34
42
|
| Smoke, sparks, explosions | pooled sprites in JS | `Game.createEmitter(...)` |
|
|
35
43
|
| A swinging chain or cape | verlet solver in JS | `Game.createRope(...)` |
|
|
36
44
|
| Slow motion or a pause that still draws | pausing your own timers | `gameView.timeScale = 0.5` / `0` |
|
|
@@ -117,10 +125,16 @@ Gravity is per sprite, not global — set it on each body that should fall. A sp
|
|
|
117
125
|
## Block versus react: `solidWith` and `collidesWith` are independent
|
|
118
126
|
|
|
119
127
|
- `solidWith: ['ground']` **blocks** movement against sprites tagged `collisionGroup: 'ground'`. The engine pushes the sprite out along the axis of least penetration, sets read-only `onGround` and fires `land`.
|
|
120
|
-
- `collidesWith: ['coin', 'enemy']` **reports** overlap. Fires `collision` once per overlap-enter (
|
|
128
|
+
- `collidesWith: ['coin', 'enemy']` **reports** overlap. Fires `collision` once per overlap-enter and `collisionend` once on separation (both carry `group`, `other`, `x`, `y`). Nothing is blocked.
|
|
121
129
|
|
|
122
130
|
A sprite can use both — a player that stands on platforms (`solidWith`) and dies on spikes (`collidesWith`).
|
|
123
131
|
|
|
132
|
+
The enter/exit pair is the trigger lifecycle other engines call `OnTriggerEnter`/`Exit`: water that tints the hero while he is in it, a pressure plate holding a door open exactly while something rests on it. Removing or hiding the partner mid-contact also counts as separation, so a despawned object still closes its door. There is deliberately **no** per-frame "stay" event — keep that state in JS.
|
|
133
|
+
|
|
134
|
+
A fast mover can slip through a thin target between frames: it never overlaps on any single frame, so the discrete test misses. `swept: true` on the *mover* tests its movement as a path — `collision` fires for whatever the path crossed, and `solidWith` walls stop it at the impact point instead of letting it teleport through.
|
|
135
|
+
|
|
136
|
+
When the question is not "did we touch?" but "what is over there?", the answer is `gameView.raycast(x0, y0, x1, y1, ['group'])` — line of sight, a ledge probe ahead of a walker's feet, a hitscan shot. It returns `null` or the nearest hit (`{ x, y, distance, group, sprite, normal }`), needs nothing but a `collisionGroup` on the targets, and replaces the old trick of parking an invisible probe sprite where the ray would land. It is a discrete query: call it from a decision timer or a tap handler, never every frame.
|
|
137
|
+
|
|
124
138
|
**A sprite with `width`/`height` but no `sheet` renders nothing and works as an invisible trigger.** Score zones, goals, checkpoints, ceilings, kill floors and walls are all built this way in the official demos. This is the idiomatic way to add logic to a scene without art.
|
|
125
139
|
|
|
126
140
|
Tune fairness with `hitboxScale` (art rarely fills its frame; slightly small hitboxes feel better) and `hitboxShape: 'circle'` for balls and rocks — circles also resolve against solids along the contact normal, so they bounce off corners diagonally instead of like a box. Turn on `debug: true` per sprite, or on the GameView for everything, to see green collision AABBs, blue touch bounds and the orange anchor dot.
|
|
@@ -128,6 +142,7 @@ Tune fairness with `hitboxScale` (art rarely fills its frame; slightly small hit
|
|
|
128
142
|
## Performance rules that actually matter
|
|
129
143
|
|
|
130
144
|
- **One texture = one draw call.** The batcher accumulates up to 1000 quads and flushes on texture switch. A scene whose sprites all share one sheet renders in a single draw call regardless of sprite count. Pack art into as few sheets as possible; reuse one white particle texture and tint it at runtime.
|
|
145
|
+
- **Know what else breaks the batch**, since none of it is visible on screen: every blend-mode transition in draw order flushes — there are four (`'normal'`, `'add'`, `'multiply'`, `'screen'`), so group same-blend sprites into their own `zIndex` band and the frame switches a handful of times instead of constantly; each glowing sprite costs 2 extra draw calls per frame, plus 2 more while a `flash()` runs; ropes, skid marks and debug overlays always draw with normal blending, so interleaving them with non-normal content flushes too.
|
|
131
146
|
- **Bulk-add a level.** `gameView.add([spriteA, spriteB, emitter, rope])` crosses the bridge once and commits under one scene lock. Collect a level's objects in an array and add them in one call; `add(object)` still works for single objects.
|
|
132
147
|
- **Keep collision group lists targeted.** Cost is O(colliders × candidates) per frame. A bullet checking `['asteroid']` tests 5 sprites, not the whole scene.
|
|
133
148
|
- **Pool, don't create.** Bullets, notes, obstacles and enemies are created once (usually 3–10 of them), then recycled by setting `visible = false` and zeroing velocity. Creating sprites mid-game costs a bridge hop and a scene lock.
|
|
@@ -135,15 +150,27 @@ Tune fairness with `hitboxScale` (art rarely fills its frame; slightly small hit
|
|
|
135
150
|
- **`maxFps: 60`** on the GameView keeps 120 Hz ProMotion displays from doubling render work. `0` (default) follows the display refresh rate.
|
|
136
151
|
- **Judge performance on device.** The iOS Simulator's OpenGL translation layer is disproportionately slow and renders at a 1x drawable; real devices keep native screen scale and run far smoother.
|
|
137
152
|
|
|
138
|
-
##
|
|
153
|
+
## Text is a sprite
|
|
139
154
|
|
|
140
|
-
|
|
155
|
+
Text draws **inside** the scene: `Game.createText({ text: 'SCORE 0' })` returns a sprite made of glyph quads, so it z-sorts, tweens, tints, flashes, glows, wobbles on `idleAnimation` and takes `tap` events like any other sprite — a text button needs no overlay view. With no `font` it uses a pixel font embedded in the module (nothing to ship); `Game.createFont({...})` loads a BMFont/AngelCode atlas or a monospace grid image. There is no font size — scale the sprite, with integer values for crisp pixels.
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
const score = Game.createText({ text: 'SCORE 0', screenFixed: true, x: W / 2, y: H * 0.08, scale: 3, zIndex: 100 });
|
|
159
|
+
gameView.add(score);
|
|
160
|
+
score.text = 'SCORE 10'; // re-lays out natively
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`screenFixed: true` works on **any** sprite: `x`/`y` become surface coordinates and camera position, zoom and shake are ignored, while touch still maps back. That is the HUD pattern — the score stays put while the camera follows the player. Without it, text is world-space and scrolls past like scenery (signposts, floating damage numbers).
|
|
164
|
+
|
|
165
|
+
Overlaid `Ti.UI.Label` views still work and are still right for anything the engine cannot draw: native inputs, system fonts, accented text the built-in font lacks, scrollable menus. Just do not reach for them for a score any more.
|
|
141
166
|
|
|
142
167
|
Standard Titanium touch events do not reach the GameView, but ordinary Titanium buttons and views layered on top work normally, and sibling views receive simultaneous pointers — which is how the demos build multitouch d-pads (hold ▶ and press jump at the same time). One caveat from the demos: Android's `touchFeedback` ripple cannot animate over this canvas and spams `RippleDrawable` errors, so on-screen buttons set their own `backgroundColor` on `touchstart`/`touchend`.
|
|
143
168
|
|
|
144
169
|
## Clean up what you created
|
|
145
170
|
|
|
146
|
-
The render loop follows the activity lifecycle automatically, but **your timers and sounds do not**. Every demo that starts an interval or plays looping music stops it on window close. In Alloy, wire this into the controller's `cleanup()`.
|
|
171
|
+
The render loop follows the activity lifecycle automatically, but **your JS timers and sounds do not**. Every demo that starts an interval or plays looping music stops it on window close. In Alloy, wire this into the controller's `cleanup()`.
|
|
172
|
+
|
|
173
|
+
Game-clock timers (`gameView.after` / `every`) are the exception that proves the rule: they run on the engine's clock, so they freeze at `timeScale: 0` and pause with the render loop instead of firing into a dead scene. That is why spawn waves and AI ticks belong there. A `setInterval` keeps running through a pause, which is right for a real-time countdown and wrong for everything else.
|
|
147
174
|
|
|
148
175
|
```javascript
|
|
149
176
|
win.addEventListener('close', () => {
|
|
@@ -162,7 +189,7 @@ Read the one that matches the task instead of guessing — the API surface below
|
|
|
162
189
|
| Need | Reference |
|
|
163
190
|
| --- | --- |
|
|
164
191
|
| Exact properties, methods, events, defaults for every object | [api.md](references/api.md) |
|
|
165
|
-
| Working code per genre: flappy, platformer, top-down, racer, asteroids, endless runner, rhythm, cards, point & click, particles, ropes, camera | [recipes.md](references/recipes.md) |
|
|
192
|
+
| Working code per genre: flappy, platformer, top-down, racer, asteroids, endless runner, rhythm, cards, point & click, particles, ropes, camera — plus patrol paths, raycast probes, parallax and game-clock timers | [recipes.md](references/recipes.md) |
|
|
166
193
|
| Install the module, tiapp.xml, Alloy vs Classic wiring, assets, sprite sheets, atlases, lifecycle, testing | [project-setup.md](references/project-setup.md) |
|
|
167
194
|
| What does NOT exist yet, what is coming, and how to work around it today | [roadmap.md](references/roadmap.md) |
|
|
168
195
|
|
|
@@ -175,5 +202,7 @@ Read the one that matches the task instead of guessing — the API surface below
|
|
|
175
202
|
5. Is `hitboxScale` set on sprites whose art does not fill the frame?
|
|
176
203
|
6. Do triggers (scores, goals, kill zones) use sheet-less sprites instead of visible art?
|
|
177
204
|
7. Is every `createSound`, `setInterval` and `setTimeout` stopped when the window closes?
|
|
178
|
-
8. Is the HUD
|
|
179
|
-
9.
|
|
205
|
+
8. Is the HUD `Game.createText` with `screenFixed: true`, rather than a stack of overlaid labels?
|
|
206
|
+
9. Does anything travel far enough per frame to tunnel? Give it `swept: true`.
|
|
207
|
+
10. Does a timer drive game logic (spawn waves, AI ticks, respawns)? Move it to `gameView.every`/`after` so it obeys pause and slow motion.
|
|
208
|
+
11. Are you using an API that actually exists? Anything involving tilemaps, sprite parenting, joysticks, A* pathfinding or word-wrapped text — check [roadmap.md](references/roadmap.md) first.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ti.game API reference
|
|
2
2
|
|
|
3
|
-
Complete JS surface of `ti.game`
|
|
3
|
+
Complete JS surface of `ti.game` as of upstream `main` on 2026-08-19, 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. The manifest still reads `0.3.0`; the text engine (`createFont`/`createText`), `screenFixed`, `swept`, `collisionend`, `followPath`, animation chaining, `raycast`, the game-clock timers, `scrollFactor` and the `multiply`/`screen` blend modes all landed after it and are not in any numbered release yet.
|
|
4
4
|
|
|
5
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
6
|
|
|
@@ -10,6 +10,8 @@ Every property listed is **live**: reading returns the current native value, eve
|
|
|
10
10
|
- [GameView](#gameview)
|
|
11
11
|
- [SpriteSheet](#spritesheet)
|
|
12
12
|
- [Sprite](#sprite)
|
|
13
|
+
- [Font](#font)
|
|
14
|
+
- [Text](#text)
|
|
13
15
|
- [Sound](#sound)
|
|
14
16
|
- [Emitter](#emitter)
|
|
15
17
|
- [Rope](#rope)
|
|
@@ -27,6 +29,8 @@ const Game = require('ti.game');
|
|
|
27
29
|
| `Game.createGameView(options)` | GameView — the canvas, a Titanium view |
|
|
28
30
|
| `Game.createSpriteSheet(options)` | SpriteSheet — a texture cut into frames |
|
|
29
31
|
| `Game.createSprite(options)` | Sprite |
|
|
32
|
+
| `Game.createFont(options)` | Font — a bitmap font for text sprites |
|
|
33
|
+
| `Game.createText(options)` | Text — a sprite that draws a string |
|
|
30
34
|
| `Game.createSound(options)` | Sound |
|
|
31
35
|
| `Game.createEmitter(options)` | Emitter — particle system |
|
|
32
36
|
| `Game.createRope(options)` | Rope — Verlet chain |
|
|
@@ -53,6 +57,10 @@ A normal Titanium view — add it to a window or any container, size it with the
|
|
|
53
57
|
| `follow(sprite, options)` | method | — | Native dead-zone follow, see below |
|
|
54
58
|
| `stopFollow()` | method | — | Stop following; the camera stays where it is |
|
|
55
59
|
| `shake({ strength, duration })` | method | `12` px, `400` ms | Detuned-sine rumble on the projection only — follow, bounds and touch mapping are unaffected |
|
|
60
|
+
| `raycast(x0, y0, x1, y1, groups)` | method | — | One-shot nearest-hit query along the segment against **visible, non-`screenFixed`** sprites carrying a `collisionGroup` in `groups` (omit for any tagged sprite). Returns `null` for a clear ray, else `{ x, y, distance, group, sprite, normal: { x, y } }`. Rect hitboxes are tested as their AABB, circle hitboxes exactly; a ray starting inside a hitbox reports it at distance 0 |
|
|
61
|
+
| `after(ms, callback)` | method | — | Runs `callback` once after `ms` of **game time** — the delay stretches under `timeScale` slow motion, freezes at `0` and pauses with the render loop, unlike `setTimeout`. Returns an int id. The callback receives `{ id }` |
|
|
62
|
+
| `every(ms, callback)` | method | — | Like `after()`, repeating until cancelled. Fires at most once per frame, and restarts its interval after a stall instead of bursting to catch up |
|
|
63
|
+
| `cancelTimer(id)` | method | — | Cancels a timer from `after()` / `every()` |
|
|
56
64
|
| `cameraEffect` | string | `'none'` | Fullscreen shader pass: `'none'`, `'tint'`, `'glitch'`. With `'none'` the extra pass is skipped entirely |
|
|
57
65
|
| `cameraTint` | string | — | Color for the `'tint'` effect, e.g. `'#4f8'` (night vision, poison, flashback) |
|
|
58
66
|
| `cameraEffectIntensity` | float | `1` | 0..1 — tint mix or glitch amount |
|
|
@@ -60,6 +68,8 @@ A normal Titanium view — add it to a window or any container, size it with the
|
|
|
60
68
|
|
|
61
69
|
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
70
|
|
|
71
|
+
Plus `timer` with payload `id`, fired only for `after()` / `every()` calls made **without** a callback — pass a callback or listen for the event, not both.
|
|
72
|
+
|
|
63
73
|
### `follow(sprite, options)`
|
|
64
74
|
|
|
65
75
|
| Option | Default | Effect |
|
|
@@ -84,7 +94,7 @@ Create one sheet per texture and share it across every sprite that uses it — s
|
|
|
84
94
|
| `image` | — | Path to the texture, relative to the resources root (`'assets/hero.png'`) |
|
|
85
95
|
| `frameWidth` / `frameHeight` | — | Grid mode: cell size in source pixels. Frames are numbered left-to-right, top-to-bottom, starting at 0 |
|
|
86
96
|
| `atlas` | — | TexturePacker JSON (hash or array format) instead of a grid |
|
|
87
|
-
| `smoothing` | `true` | `false` = GL_NEAREST, the pixel-art setting |
|
|
97
|
+
| `smoothing` | `true` | `false` = GL_NEAREST, the pixel-art setting. On a **grid** sheet, `true` also insets the interior frame UVs by half a texel so magnified edges cannot sample the neighbouring frame; outer edges keep the exact 0..1 range so `tileRepeat` still wraps. Atlas frames come from the JSON untouched |
|
|
88
98
|
| `repeat` | `false` | GL_REPEAT wrap, required by sprites using `tileRepeat`. Needs power-of-two texture dimensions |
|
|
89
99
|
|
|
90
100
|
| Member | Notes |
|
|
@@ -112,6 +122,8 @@ Textures upload to the GPU lazily on first use, from the render thread, and are
|
|
|
112
122
|
| `zIndex` | `0` | Draw order |
|
|
113
123
|
| `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
124
|
| `flipX`, `flipY` | `false` | Mirror the drawn frame only. Position, anchor, physics and hit testing are untouched |
|
|
125
|
+
| `screenFixed` | `false` | `x`/`y` become **surface** coordinates and the sprite ignores camera position, zoom and shake — HUDs, on-screen buttons, overlays. Touch is mapped back automatically, so `tap` still works. Works on any sprite, not only text |
|
|
126
|
+
| `scrollFactor` | `1` | Parallax: how much camera travel (and shake) moves this sprite. `0.5` = a background layer at half speed, `1.5` = a foreground layer overtaking the camera, `0` = pinned to the view but **still zooming** with `cameraScale` (unlike `screenFixed`). Rendering and touch mapping only — `x`/`y`, physics and collisions stay in plain world coordinates. Ignored when `screenFixed` is true |
|
|
115
127
|
| `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
128
|
|
|
117
129
|
### Sheet and animation
|
|
@@ -145,7 +157,7 @@ Hit-testing runs against the transformed shape (rotation and scale included), to
|
|
|
145
157
|
| --- | --- | --- |
|
|
146
158
|
| `velocityX`, `velocityY` | `0` | px/s |
|
|
147
159
|
| `gravity` | `0` | px/s², added to `velocityY`. Per sprite — there is no global gravity |
|
|
148
|
-
| `maxSpeed` | `500` | px/s cap |
|
|
160
|
+
| `maxSpeed` | `500` | px/s cap applied **only** to `thrust` acceleration and the `carMode` model. Writing `velocityX`/`velocityY` directly is never clamped |
|
|
149
161
|
| `angularVelocity` | `0` | deg/s |
|
|
150
162
|
| `thrust` | `0` | px/s² along the current heading (Newtonian flight) |
|
|
151
163
|
| `wrapAround` | `false` | Re-enter from the opposite screen edge (Asteroids) |
|
|
@@ -169,6 +181,7 @@ Hit-testing runs against the transformed shape (rotation and scale included), to
|
|
|
169
181
|
| `collidesWith` | — | Array of groups this sprite reports overlaps with |
|
|
170
182
|
| `hitboxScale` | `1` | Shrinks the collision box around the anchor |
|
|
171
183
|
| `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 |
|
|
184
|
+
| `swept` | `false` | Test this sprite's movement as a **path** (swept AABB), not just at the end position, so a fast mover cannot tunnel between frames. Applies to both `collidesWith` events and `solidWith` blocking (the sprite is clamped to the impact point, then resolved by the normal static pass). Circle hitboxes sweep as their bounding box. Set it on the *mover* — the bullet, not the wall |
|
|
172
185
|
| `debug` | `false` | Draw this sprite's shapes: green = collision AABB, blue = touch bounds, orange dot = anchor |
|
|
173
186
|
|
|
174
187
|
### Car (`carMode`)
|
|
@@ -206,21 +219,22 @@ Every sprite gets its own phase, and the wobble unwinds exactly when disabled. T
|
|
|
206
219
|
| --- | --- | --- |
|
|
207
220
|
| `tintColor` | — | Multiplies the frame's colors (team colors, day/night, damage states). `null` or `'#fff'` = unchanged. Multiplicative, so it can only darken |
|
|
208
221
|
| `glowColor` | — | Tinted, blurred silhouette drawn behind the sprite by a shader pass |
|
|
209
|
-
| `glowBlur` | `0` | Blur radius in px; `0` = off |
|
|
222
|
+
| `glowBlur` | `0` | Blur radius in px; `0` = off. An active glow switches to the silhouette shader and back: **2 extra draw calls per glowing sprite per frame** (2 more while a `flash()` runs), even on a shared texture. Fine for a few highlights, not for every coin in the level |
|
|
210
223
|
| `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
|
|
224
|
+
| `blend` | `'normal'` | `'add'` brightens the backdrop (glows, fire, lasers), `'multiply'` darkens it (contact shadows, stains, grime), `'screen'` lightens softly without blowing out to white (fog, god rays, soft light). Costs one batch flush per mode change — group same-blend sprites by `zIndex`. Unknown strings fall back to `'normal'` silently, and the names are case-sensitive |
|
|
212
225
|
|
|
213
226
|
### Methods
|
|
214
227
|
|
|
215
228
|
| Method | Notes |
|
|
216
229
|
| --- | --- |
|
|
217
|
-
| `play(name)` | Start a named animation |
|
|
218
|
-
| `stop()` | Stop the animation on the current frame |
|
|
230
|
+
| `play(name, options)` | Start a named animation; returns `false` for an unknown name. `options.then` (a name or an array of names) chains natively — each queued animation starts as the previous non-looping one finishes, and `animationcomplete` still fires per step. A looping animation ends the chain |
|
|
231
|
+
| `stop()` | Stop the animation on the current frame; also drops any queued `then` chain |
|
|
232
|
+
| `followPath(points, options)` | Walk the sprite along `points` — `{ x, y }` objects or `[x, y]` pairs, **at least two**, fewer is ignored with a warning. `options`: `speed` px/s (default `100`), `loop` (closed circuit), `rotate` (face along the path, 0 = up), `smoothing` (corner radius in px, precomputed once). Fires `pathcomplete` at the end of a non-looping run and clears itself. `followPath(null)` stops in place |
|
|
219
233
|
| `animate(options)` | Native tween, fires `complete` |
|
|
220
234
|
| `clearTweens()` | Cancel tweens in progress — call before starting a replacement tween |
|
|
221
235
|
| `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
236
|
|
|
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).
|
|
237
|
+
`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). Re-calling `animate` from the `complete` handler is still how a ping-pong or a blink is built (there is no `repeat`/`yoyo` yet), but a fixed route is now `followPath` instead of a chain of tween legs.
|
|
224
238
|
|
|
225
239
|
### Sprite events
|
|
226
240
|
|
|
@@ -234,11 +248,73 @@ Every sprite gets its own phase, and the wobble unwinds exactly when disabled. T
|
|
|
234
248
|
| `dragend` | `x`, `y` | Finger lifted; the sprite has already moved natively |
|
|
235
249
|
| `pinch` | `scaleX`, `scaleY` | While two-finger scaling |
|
|
236
250
|
| `rotate` | `rotation` | While two-finger rotating |
|
|
237
|
-
| `animationcomplete` | `animation` | A non-looping sheet animation finished |
|
|
251
|
+
| `animationcomplete` | `animation` | A non-looping sheet animation finished — including each finished step of a `then` chain |
|
|
238
252
|
| `complete` | final transform values | A tween finished |
|
|
253
|
+
| `pathcomplete` | `x`, `y` | A non-looping `followPath` run reached the end |
|
|
239
254
|
| `collision` | `group`, `other`, `x`, `y` | Overlap with a `collidesWith` group began |
|
|
255
|
+
| `collisionend` | `group`, `other`, `x`, `y` | That overlap ended: the shapes separated, **or** the partner was removed from the scene, hidden (`visible = false`) or stopped matching the group filter |
|
|
240
256
|
| `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
257
|
|
|
258
|
+
## Font
|
|
259
|
+
|
|
260
|
+
A glyph atlas for text sprites. Create it once, outside `resize`, like a SpriteSheet — it holds a GL texture.
|
|
261
|
+
|
|
262
|
+
```javascript
|
|
263
|
+
// 1. Built-in: nothing to ship. A 9x15 pixel font embedded in the module.
|
|
264
|
+
// You do not even need a Font object — createText without `font` uses it.
|
|
265
|
+
const builtin = Game.createFont({});
|
|
266
|
+
|
|
267
|
+
// 2. BMFont / AngelCode: .fnt text format or its JSON export, kerning included.
|
|
268
|
+
// Produced by BMFont, Hiero, fontbm or the module's tools/genfont.py.
|
|
269
|
+
const hud = Game.createFont({ font: 'assets/hud.fnt' });
|
|
270
|
+
|
|
271
|
+
// 3. Monospace grid image: cells row-major, ASCII 32..126 by default.
|
|
272
|
+
const mono = Game.createFont({ image: 'assets/mono.png', charWidth: 9, charHeight: 15 });
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
| Option | Default | Notes |
|
|
276
|
+
| --- | --- | --- |
|
|
277
|
+
| `font` | — | Path to a `.fnt` descriptor (AngelCode text or JSON). The page image is loaded from **next to the descriptor** unless `image` overrides it |
|
|
278
|
+
| `image` | — | Grid mode: the glyph sheet. Also overrides a BMFont page path |
|
|
279
|
+
| `charWidth`, `charHeight` | — | **Required** in grid mode. Missing or ≤ 0 silently falls back to the built-in font |
|
|
280
|
+
| `characters` | ASCII 32..126 | Grid mode: which characters the cells map to, row-major |
|
|
281
|
+
| `smoothing` | `true` | Filters the glyph texture like a sheet. The built-in font is always crisp |
|
|
282
|
+
| `lineHeight` | — | Read-only: the font's natural line height in px |
|
|
283
|
+
|
|
284
|
+
A descriptor that fails to parse falls back to the built-in font and logs the error — the text still renders, in the wrong face, which is the symptom to recognize.
|
|
285
|
+
|
|
286
|
+
## Text
|
|
287
|
+
|
|
288
|
+
`Game.createText(options)` returns a **Sprite**. Everything in the Sprite section applies — `zIndex`, `ySort`, `animate()`, `flash()`, `tintColor`, glow, `idleAnimation`, `screenFixed`, touch events, `collidesWith`. Add it to the view like any sprite: `gameView.add(label)`.
|
|
289
|
+
|
|
290
|
+
```javascript
|
|
291
|
+
const score = Game.createText({
|
|
292
|
+
text: 'SCORE 0',
|
|
293
|
+
x: 16,
|
|
294
|
+
y: 40,
|
|
295
|
+
anchorX: 0, // left-aligned to a margin
|
|
296
|
+
anchorY: 0,
|
|
297
|
+
scale: 3, // bitmap fonts size by scale, not by fontSize
|
|
298
|
+
screenFixed: true, // ignore the camera
|
|
299
|
+
zIndex: 100
|
|
300
|
+
});
|
|
301
|
+
gameView.add(score);
|
|
302
|
+
score.text = 'SCORE 10'; // re-lays out natively on the next frame
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
| Property | Default | Notes |
|
|
306
|
+
| --- | --- | --- |
|
|
307
|
+
| `text` | `''` | The string. `\n` breaks lines. Writing it re-lays out natively |
|
|
308
|
+
| `font` | built-in | A Font object. Omitted or `null` = the embedded pixel font, which the scene assigns per GameView |
|
|
309
|
+
| `align` | `'left'` | `'center'` / `'right'` — how multiple lines align against each other. Unknown values fall back to `'left'` |
|
|
310
|
+
| `letterSpacing` | `0` | Extra px between glyphs; negative tightens |
|
|
311
|
+
| `lineSpacing` | `1` | Multiplier on the font's line height |
|
|
312
|
+
| `width`, `height` | laid-out size | **Derived, not settable** — the glyph layout drives the size, and with it the anchor, hit test, AABB and `ySort` bottom edge |
|
|
313
|
+
|
|
314
|
+
There is no font size: scale the sprite. With the built-in pixel font, use **integer** `scale` values (`Math.max(1, Math.round(W / 240))` is the demos' idiom) so texels stay square.
|
|
315
|
+
|
|
316
|
+
Every glyph is a quad in the same batch, so one label costs one draw call — a screen of labels sharing a font costs one too.
|
|
317
|
+
|
|
242
318
|
## Sound
|
|
243
319
|
|
|
244
320
|
```javascript
|
|
@@ -282,7 +358,7 @@ Add and remove like a sprite: `gameView.add(emitter)` / `remove(emitter)`. Spawn
|
|
|
282
358
|
| `startScale` / `endScale` | `1` / `1` | Interpolated over `lifetime` |
|
|
283
359
|
| `startOpacity` / `endOpacity` | `1` / `0` | Interpolated over `lifetime` |
|
|
284
360
|
| `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 |
|
|
361
|
+
| `blend` | `'normal'` | `'add'` for fire, sparks and magic; `'multiply'` for smoke and dust that darken what they cross; `'screen'` for soft haze |
|
|
286
362
|
| `maxParticles` | `200` | Pool size, hard cap 1000 |
|
|
287
363
|
|
|
288
364
|
Methods: `emit(n)` fires a one-shot burst on top of `rate`; `clear()` kills all live particles.
|
|
@@ -314,20 +390,38 @@ The tether yields at the end no finger owns: with a fixed head anchor the tail s
|
|
|
314
390
|
|
|
315
391
|
Only these events exist. Nothing fires per frame, and events are only fired if a listener is registered.
|
|
316
392
|
|
|
317
|
-
- **GameView**: `press`, `tap`, `release`, `resize`
|
|
318
|
-
- **Sprite
|
|
319
|
-
- **SpriteSheet, Sound, Emitter, Rope**: none
|
|
393
|
+
- **GameView**: `press`, `tap`, `release`, `resize`, `timer`
|
|
394
|
+
- **Sprite** (and therefore **Text**): `press`, `release`, `tap`, `dragstart`, `drag`, `dragend`, `pinch`, `rotate`, `animationcomplete`, `complete`, `pathcomplete`, `collision`, `collisionend`, `land`
|
|
395
|
+
- **SpriteSheet, Font, Sound, Emitter, Rope**: none
|
|
396
|
+
|
|
397
|
+
There is `collision` (enter) and `collisionend` (exit), but deliberately **no** per-frame "stay" event — that would be bridge traffic every frame. Hold the in-between state in JS: you heard the enter, you will hear the end.
|
|
320
398
|
|
|
321
399
|
## Gotchas the property tables do not show
|
|
322
400
|
|
|
323
|
-
- **`maxSpeed`
|
|
401
|
+
- **`maxSpeed` caps `thrust` and `carMode` only — not plain velocity.** The clamp runs inside the thrust integration and inside the car model, so a sprite given `velocityX = 3000` travels at 3000 no matter what `maxSpeed` says. A ship accelerating on `thrust`, on the other hand, stops gaining speed at 500 px/s unless you raise its own `maxSpeed`, which is the cap people actually hit.
|
|
324
402
|
- **`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
403
|
- **`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.
|
|
404
|
+
- **`animate()` ignores unknown keys.** Tweening `width`, `height` or `tintColor` silently does nothing — those are instant writes only. There is no `repeat` or `yoyo` either; re-launch from `complete`.
|
|
327
405
|
- **`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
406
|
- **`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
407
|
- **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
408
|
- **`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.
|
|
409
|
+
- **`collision` fires once per overlap-enter**, re-arming only after the shapes separate. A sprite resting inside a trigger does not re-fire — `collisionend` tells you when it left.
|
|
410
|
+
- **Pooling a sprite with `visible = false` fires `collisionend`** on whoever was touching it. That is usually what you want (a plate whose ball was despawned closes its door), but a handler that assumes separation means "the player walked away" will misfire.
|
|
411
|
+
- **The built-in pixel font only covers ASCII 32..126.** Em dashes, accents and `ñ` render as blanks or garbage — the module's own demos rewrote `—` as `-` for this reason. Ship a BMFont atlas (`tools/genfont.py` rasterizes any TTF) when the game needs accented Spanish text.
|
|
412
|
+
- **`swept` belongs on the fast mover, not on the target.** A stationary thin wall with `swept: true` changes nothing; the bullet is the sprite whose path needs testing.
|
|
413
|
+
- **A swept bullet can enter and leave in consecutive frames.** If it crosses a thin target entirely, you get `collision` and then `collisionend` almost immediately — react on the enter.
|
|
414
|
+
- **Text `width`/`height` are read-only in practice.** They report the laid-out block; to make text bigger use `scale`.
|
|
332
415
|
- **`hitboxShape: 'circle'` also changes the touch area**, not just collisions.
|
|
333
|
-
-
|
|
416
|
+
- **Every blend-mode change costs a batch flush.** Group same-blend sprites on their own `zIndex` band; alternating modes sprite by sprite degrades toward one draw call each.
|
|
417
|
+
- **`glowColor` alone draws nothing.** The glow pass only runs when `glowBlur > 0` *and* `glowOpacity > 0`; both default in a way that makes `glowColor` on its own a no-op (`glowBlur` is `0`). Always set the blur radius with the color.
|
|
418
|
+
- **`followPath` overrides position absolutely.** The path is applied after velocity and gravity have been integrated and writes `x`/`y` outright, so a path-driven sprite ignores its own physics for placement. Do not expect `velocityX` and a path to add up. A tween on `x`/`y` runs *after* the path in the same frame and wins outright — pick one.
|
|
419
|
+
- **A path needs at least two points.** One point (or a non-array) clears the path and logs a warning instead of parking the sprite there.
|
|
420
|
+
- **Path movement still carries riders.** It feeds the frame delta like a tweened or velocity-driven solid, so a platform on a `followPath` circuit moves whatever stands on it.
|
|
421
|
+
- **`raycast` groups: pass an array, not varargs.** Android accepts both `raycast(x0, y0, x1, y1, ['enemy'])` and loose arguments; iOS reads only the array. Write the array and it works on both.
|
|
422
|
+
- **`raycast` only sees `visible`, non-`screenFixed` sprites carrying a `collisionGroup`.** A hidden pooled sprite is invisible to the ray, which is usually what you want; an untagged wall is invisible too, which usually is not. HUD sprites are skipped by design, so a screen-fixed score never blocks a world-space line of sight.
|
|
423
|
+
- **`raycast` is a discrete query, not a sensor.** Calling it from a game-clock timer or a tap handler is the intended use. Calling it every frame from JS puts exactly the bridge traffic in the loop that the whole engine exists to avoid.
|
|
424
|
+
- **Game-clock timers are not `setTimeout`.** `after`/`every` freeze at `timeScale: 0` and pause with the render loop — which is why spawn waves belong there — but a pause menu that needs a real countdown still needs `setTimeout`.
|
|
425
|
+
- **`screenFixed` beats `scrollFactor`.** Setting both leaves the sprite screen-fixed; the parallax offset is skipped entirely.
|
|
426
|
+
- **Ghost lines along a frame's edge are a filtering artifact, not bad art.** A magnified `smoothing: true` sheet samples past the frame border — 1px seams, or the next row's heads at the bottom. Grid sheets have inset UVs upstream since 2026-08-19; an atlas needs padding and extrude at pack time, and an older module build needs either `smoothing: false` or a rebuild.
|
|
427
|
+
- **`scrollFactor` does not move the sprite.** It shifts where the sprite is *drawn* and maps touch back to match. `x`, `y`, physics and collisions stay in world coordinates, so a `scrollFactor: 0.5` platform still collides where its world `x` says, not where it looks.
|
|
@@ -9,6 +9,7 @@ Installing the module, wiring it into Classic and Alloy projects, organizing art
|
|
|
9
9
|
- [Alloy projects](#alloy-projects)
|
|
10
10
|
- [Where art and sound live](#where-art-and-sound-live)
|
|
11
11
|
- [Sprite sheets and atlases](#sprite-sheets-and-atlases)
|
|
12
|
+
- [Fonts](#fonts)
|
|
12
13
|
- [Lifecycle and cleanup](#lifecycle-and-cleanup)
|
|
13
14
|
- [Debugging and tuning](#debugging-and-tuning)
|
|
14
15
|
- [Platform differences](#platform-differences)
|
|
@@ -40,6 +41,8 @@ Then declare it in `tiapp.xml`. The iOS platform key is `iphone`, not `ios`:
|
|
|
40
41
|
|
|
41
42
|
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
|
|
|
44
|
+
The version number lies right now: the manifest has read `0.3.0` through several feature landings. A zip built before 2026-08-18 has no `createText`, no `createFont`, no `screenFixed`, no `swept` and no `collisionend`; one built before 2026-08-19 also lacks `followPath`, `play(name, { then })`, `raycast`, `after`/`every`, `scrollFactor` and the `multiply`/`screen` blend modes — all while still calling itself 0.3.0. A zip built before 2026-08-19 16:00 UTC+2 also bleeds at grid-frame edges on smoothed sheets. If a call that this skill documents is `undefined` at runtime, rebuild the module from upstream `main` rather than hunting for a typo.
|
|
45
|
+
|
|
43
46
|
Building the module from source:
|
|
44
47
|
|
|
45
48
|
```bash
|
|
@@ -50,7 +53,7 @@ ti build -p ios --build-only # macOS only → ios/dist/ti.game-iphone-<ve
|
|
|
50
53
|
|
|
51
54
|
## Classic projects
|
|
52
55
|
|
|
53
|
-
`require('ti.game')` anywhere and add the GameView to a window. The
|
|
56
|
+
`require('ti.game')` anywhere and add the GameView to a window. The 24 demos in the module's `example/` folder are Classic, each one a CommonJS module exporting a start function:
|
|
54
57
|
|
|
55
58
|
```javascript
|
|
56
59
|
// Resources/flappy.js
|
|
@@ -85,7 +88,6 @@ The standard Alloy element for a native module view. `method` names the factory
|
|
|
85
88
|
<Alloy>
|
|
86
89
|
<Window id="index" navBarHidden="true" exitOnClose="true" onOpen="onOpen" onClose="onClose">
|
|
87
90
|
<Module id="gameView" module="ti.game" method="createGameView" backgroundColor="#8ed8f8" />
|
|
88
|
-
<Label id="scoreLabel" />
|
|
89
91
|
<Button id="jumpButton" title="JUMP" onTouchstart="jump" />
|
|
90
92
|
</Window>
|
|
91
93
|
</Alloy>
|
|
@@ -112,6 +114,8 @@ function onResize(e) {
|
|
|
112
114
|
}
|
|
113
115
|
```
|
|
114
116
|
|
|
117
|
+
Only views that must be native — buttons, menus, dialogs — belong in the XML. The HUD does not: scores and labels are `Game.createText` sprites created in the controller and added to the game view, so they batch with the scene and can tween and flash.
|
|
118
|
+
|
|
115
119
|
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
120
|
|
|
117
121
|
### B: an empty container in XML, the GameView created in JS
|
|
@@ -214,11 +218,35 @@ Practical rules:
|
|
|
214
218
|
|
|
215
219
|
- **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
220
|
- **`smoothing: false` for pixel art.** Combine with `pixelSnap: true` on moving sprites when the texel phase must stay stable.
|
|
221
|
+
- **Ship sheets as PNG, not JPG.** JPEG has no alpha channel, so every frame comes back on an opaque block, and its compression smears colour across frame borders — exactly where a sheet can least afford it. The module's own `basic.js` demo was converted from `hero.jpg` to `hero.png` on 2026-08-19 for this reason.
|
|
222
|
+
- **Edge bleeding on a smoothed grid is fixed upstream, on grids only.** Linear filtering samples past a frame's edge when the sprite is magnified, which shows up as 1px ghost lines or the next row's heads peeking in at the bottom. Since 2026-08-19 a grid sheet with `smoothing: true` pulls its *interior* frame edges in by half a texel automatically; outer edges stay at the texture border, so full-texture `tileRepeat` frames still wrap seamlessly, and `smoothing: false` sheets skip the inset entirely (NEAREST cannot bleed, and pixel art at 1:1 needs exact UVs). **Atlas sheets are not covered** — their UVs come straight from the JSON, so prevent bleeding when packing instead: 2px padding and extrude in TexturePacker.
|
|
217
223
|
- **`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
224
|
- **White art plus `tint`** covers every color variant of a particle or effect with one small texture.
|
|
219
225
|
|
|
220
226
|
`frameCount` is `0` until a grid sheet's texture has loaded, so do not compute animation ranges from it at creation time.
|
|
221
227
|
|
|
228
|
+
## Fonts
|
|
229
|
+
|
|
230
|
+
Nothing to ship for the default look: `Game.createText({ text: 'SCORE 0' })` uses a 9×15 pixel font embedded in the module, on both platforms.
|
|
231
|
+
|
|
232
|
+
A custom face is an asset like any other, created once outside `resize`:
|
|
233
|
+
|
|
234
|
+
```javascript
|
|
235
|
+
// BMFont / AngelCode descriptor + its page image, side by side in assets/
|
|
236
|
+
const hud = Game.createFont({ font: 'fonts/hud.fnt' }); // hud.png resolved next to it
|
|
237
|
+
|
|
238
|
+
// or a monospace grid image
|
|
239
|
+
const mono = Game.createFont({ image: 'fonts/mono.png', charWidth: 9, charHeight: 15 });
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
| File | Referenced as (Alloy) |
|
|
243
|
+
| --- | --- |
|
|
244
|
+
| `app/assets/fonts/hud.fnt` + `app/assets/fonts/hud.png` | `'fonts/hud.fnt'` |
|
|
245
|
+
|
|
246
|
+
These are **bitmap** fonts — do not put them in `app/assets/fonts/` expecting Titanium's native font handling, and do not use a `.ttf` here. To go from a TTF to either format, the module ships `tools/genfont.py`, which rasterizes it into a grid or BMFont atlas.
|
|
247
|
+
|
|
248
|
+
The built-in font covers ASCII 32..126 only. Any game whose text includes `ñ`, accents or `¿` needs a generated font — or a `Ti.UI.Label` overlay for that specific string.
|
|
249
|
+
|
|
222
250
|
## Lifecycle and cleanup
|
|
223
251
|
|
|
224
252
|
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.**
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# ti.game recipes by genre
|
|
2
2
|
|
|
3
|
-
Working patterns distilled from the
|
|
3
|
+
Working patterns distilled from the 24 official demos in `example/` and from a shipped Alloy game (Titanium Lander). Each recipe shows the part that is specific to the genre; the shared scaffolding is in [The scaffolding](#the-scaffolding) and is not repeated.
|
|
4
4
|
|
|
5
|
-
Every snippet uses only API that exists in
|
|
5
|
+
Every snippet uses only API that exists in upstream `main` as of 2026-08-19. Check [roadmap.md](roadmap.md) before reaching for anything not shown here.
|
|
6
6
|
|
|
7
7
|
## Contents
|
|
8
8
|
|
|
9
9
|
- [The scaffolding](#the-scaffolding)
|
|
10
|
-
- [Cross-cutting patterns](#cross-cutting-patterns) — pooling, parallax, multitouch controls, HUD, invisible triggers, hit-stop
|
|
10
|
+
- [Cross-cutting patterns](#cross-cutting-patterns) — pooling, parallax, multitouch controls, HUD text, invisible triggers, enter/exit zones, patrol routes, line of sight, game-clock timers, hit-stop
|
|
11
11
|
- [Tap-to-flap](#tap-to-flap) (`flappy.js`)
|
|
12
12
|
- [Platformer](#platformer) (`platformer.js`)
|
|
13
13
|
- [Top-down / Zelda](#top-down--zelda) (`topdown.js`)
|
|
@@ -22,7 +22,7 @@ Every snippet uses only API that exists in 0.3.0. Check [roadmap.md](roadmap.md)
|
|
|
22
22
|
- [Particles](#particles) (`particles.js`)
|
|
23
23
|
- [Ropes and chains](#ropes-and-chains) (`rope.js`)
|
|
24
24
|
- [Camera work](#camera-work) (`camera.js`)
|
|
25
|
-
- [Visual effects](#visual-effects) (`blend.js`, `flip.js`, `timescale.js`)
|
|
25
|
+
- [Visual effects](#visual-effects) (`blend.js`, `flip.js`, `timescale.js`, `demoscene.js`)
|
|
26
26
|
|
|
27
27
|
## The scaffolding
|
|
28
28
|
|
|
@@ -91,7 +91,7 @@ for (let i = 0; i < 10; i++) {
|
|
|
91
91
|
height: 16,
|
|
92
92
|
visible: false,
|
|
93
93
|
wrapAround: true,
|
|
94
|
-
|
|
94
|
+
swept: true, // path-tested: a fast bolt cannot skip past a rock
|
|
95
95
|
collidesWith: ['asteroid']
|
|
96
96
|
});
|
|
97
97
|
const state = { sprite: sprite, active: false, generation: 0 };
|
|
@@ -135,9 +135,25 @@ function fire(x, y, vx, vy) {
|
|
|
135
135
|
|
|
136
136
|
The `generation` counter matters: without it, a timeout fired for an old shot deactivates the *new* bullet occupying that slot.
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
`swept: true` (compared side by side in `swept.js`) is what keeps a fast bolt from crossing a rock between two frames without ever overlapping it. The asteroids demo predates it and inflates the hitbox instead (`hitboxScale: 1.4`) — that works, at the price of counting near misses as hits. Prefer `swept`, and reserve `hitboxScale` for fairness tuning.
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
Recycling a sprite with `visible = false` fires `collisionend` on anything it was touching. Handlers that treat separation as "the player walked away" need to tolerate a despawn.
|
|
141
|
+
|
|
142
|
+
### Parallax
|
|
143
|
+
|
|
144
|
+
Two ways, and the choice is decided by whether the **camera** moves or the **world** does.
|
|
145
|
+
|
|
146
|
+
**A camera travelling through a static world** — platformer, top-down, anything using `follow()` — wants `scrollFactor`, one property per layer:
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
const clouds = Game.createSprite({ sheet: cloudSheet, x: W / 2, y: 120, zIndex: 0, scrollFactor: 0.25 });
|
|
150
|
+
const hills = Game.createSprite({ sheet: hillSheet, x: W / 2, y: 260, zIndex: 1, scrollFactor: 0.6 });
|
|
151
|
+
const sun = Game.createSprite({ sheet: sunSheet, x: 60, y: 60, zIndex: 0, scrollFactor: 0 });
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`0` pins the sun to the view while still zooming with `cameraScale` — that is the difference from `screenFixed`, which ignores zoom as well. Values above `1` overtake the camera for foreground grass. The sprite's `x`/`y` never change: `scrollFactor` shifts where it is *drawn* and maps touch back to match, so collisions still happen at the world coordinates you wrote.
|
|
155
|
+
|
|
156
|
+
**A world scrolling under a fixed camera** — endless runner, flappy — cannot use `scrollFactor`, because the camera never travels. Two screen-wide copies per layer, scrolled by native velocity; `wrapX`/`wrapShift` teleport a copy that left the screen back behind its sibling. No JS runs in the loop.
|
|
141
157
|
|
|
142
158
|
```javascript
|
|
143
159
|
function makeLayer(sheet, y, height, speed, z) {
|
|
@@ -168,7 +184,7 @@ const skyline = makeLayer(skylineSheet, groundTop - SKY_H / 2, SKY_H, SPEED * 0.
|
|
|
168
184
|
const street = makeLayer(streetSheet, (groundTop + H) / 2, H - groundTop, SPEED, 8);
|
|
169
185
|
```
|
|
170
186
|
|
|
171
|
-
Slower layers read as further away. Three layers (clouds at 0.15×, hills at 0.4×, ground at 1×) is the flappy demo's recipe.
|
|
187
|
+
Slower layers read as further away. Three layers (clouds at 0.15×, hills at 0.4×, ground at 1×) is the flappy demo's recipe. `camera.js` demos the other half: `scrollFactor` cloud shadows at 1.35× and a `scrollFactor: 0` sun.
|
|
172
188
|
|
|
173
189
|
### Multitouch on-screen controls
|
|
174
190
|
|
|
@@ -220,24 +236,37 @@ bindHold(leftButton, () => {
|
|
|
220
236
|
|
|
221
237
|
The guard in the release handler is what keeps two opposite buttons from fighting: releasing left only zeroes steering if left is still the active direction.
|
|
222
238
|
|
|
223
|
-
### HUD
|
|
239
|
+
### HUD — `text.js`, `flappy.js`
|
|
224
240
|
|
|
225
|
-
|
|
241
|
+
Text sprites with `screenFixed: true`. They live in the scene — one batch, no overlay views — but ignore the camera, so a score stays in the corner while the world scrolls under it.
|
|
226
242
|
|
|
227
243
|
```javascript
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
|
|
244
|
+
// Created outside resize (no size known yet), positioned once it is
|
|
245
|
+
const scoreLabel = Game.createText({ text: '0', screenFixed: true, zIndex: 100 });
|
|
246
|
+
const statusLabel = Game.createText({ text: 'Tap to start!', screenFixed: true, zIndex: 100 });
|
|
247
|
+
|
|
248
|
+
function buildLevel(W, H) {
|
|
249
|
+
const TEXT_SCALE = Math.max(1, Math.round(W / 240)); // integer: crisp pixels
|
|
250
|
+
Object.assign(scoreLabel, { scale: TEXT_SCALE, x: W / 2, y: H * 0.08 });
|
|
251
|
+
Object.assign(statusLabel, { scale: TEXT_SCALE, x: W / 2, y: H * 0.45 });
|
|
252
|
+
gameView.add([scoreLabel, statusLabel]);
|
|
253
|
+
// ...
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function score() {
|
|
257
|
+
points += 1;
|
|
258
|
+
scoreLabel.text = points + ' / ' + GATE_COUNT;
|
|
259
|
+
scoreLabel.flash('#4dff88', 250); // text flashes like any sprite
|
|
260
|
+
}
|
|
238
261
|
```
|
|
239
262
|
|
|
240
|
-
|
|
263
|
+
Scaling from `W` is what replaces `fontSize`: a bitmap font has one native size, and an integer multiplier is the only way to keep texels square. `Math.max(1, ...)` guards the smallest screens.
|
|
264
|
+
|
|
265
|
+
Because a label is a sprite, the HUD gets the whole toolbox for free — `animate()` to pop a score on change, `tintColor` per state, `glowColor` on a title, `idleAnimation` for a gentle wobble, and `tap` handlers so a `[ RESET ]` label works as a button with no Titanium view behind it.
|
|
266
|
+
|
|
267
|
+
Drop `screenFixed` and the same label becomes world furniture: signposts, floating damage numbers, a name over an NPC — it scrolls with everything else.
|
|
268
|
+
|
|
269
|
+
Overlaid `Ti.UI.Label` views remain the answer for text the engine's font cannot draw (accents, system fonts, right-to-left) and for real UI: menus, dialogs, text inputs.
|
|
241
270
|
|
|
242
271
|
### Invisible triggers
|
|
243
272
|
|
|
@@ -269,6 +298,111 @@ car.addEventListener('collision', (e) => {
|
|
|
269
298
|
});
|
|
270
299
|
```
|
|
271
300
|
|
|
301
|
+
### Zones you can be inside of (enter / exit) — `zones.js`
|
|
302
|
+
|
|
303
|
+
`collision` and `collisionend` are the two halves of a trigger: entering water, standing on a pressure plate, holding a capture point. Nothing fires while you stay inside — the state lives in JS between the two events, and that is deliberate, since a per-frame "stay" would be bridge traffic every frame.
|
|
304
|
+
|
|
305
|
+
```javascript
|
|
306
|
+
const pool = Game.createSprite({
|
|
307
|
+
x: W * 0.32, y: H * 0.3, width: W * 0.44, height: H * 0.18,
|
|
308
|
+
sheet: waterSheet, tintColor: '#3d6fd4', opacity: 0.35,
|
|
309
|
+
touchEnabled: false, // drags pass through to whatever is inside
|
|
310
|
+
collisionGroup: 'water'
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
hero.collidesWith = ['water'];
|
|
314
|
+
hero.addEventListener('collision', (e) => {
|
|
315
|
+
if (e.group === 'water') {
|
|
316
|
+
hero.tintColor = '#7fb2ff';
|
|
317
|
+
swimming = true;
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
hero.addEventListener('collisionend', (e) => {
|
|
321
|
+
if (e.group === 'water') {
|
|
322
|
+
hero.tintColor = null;
|
|
323
|
+
swimming = false;
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
The listener goes on the sprite that declares `collidesWith`. For a pressure plate holding a door open exactly while a crate rests on it, that is the **plate** (`collidesWith: ['crate']`), not the crate — then `collision` opens the door and `collisionend` closes it, with no polling anywhere.
|
|
329
|
+
|
|
330
|
+
Removing the partner from the scene, hiding it (`visible = false`) or changing its `collisionGroup` mid-contact all count as separation, so the exit still fires and the door cannot stick open when an object despawns.
|
|
331
|
+
|
|
332
|
+
### Patrol routes and animation chains — `path.js`
|
|
333
|
+
|
|
334
|
+
A fixed route is `followPath`, not a chain of tween legs. The points are walked natively at `speed` px/s; `rotate: true` turns the sprite to face along the path (heading `0` = up, same convention as `rotation`), and `smoothing` rounds the corners with a radius in px.
|
|
335
|
+
|
|
336
|
+
```javascript
|
|
337
|
+
guard.followPath([
|
|
338
|
+
{ x: W * 0.22, y: H * 0.46 },
|
|
339
|
+
{ x: W * 0.78, y: H * 0.46 },
|
|
340
|
+
{ x: W * 0.78, y: H * 0.60 },
|
|
341
|
+
{ x: W * 0.22, y: H * 0.60 }
|
|
342
|
+
], { speed: W * 0.16, loop: true }); // sharp rectangle, walks forever
|
|
343
|
+
|
|
344
|
+
ship.followPath(diamond, {
|
|
345
|
+
speed: W * 0.35,
|
|
346
|
+
loop: true,
|
|
347
|
+
rotate: true, // nose follows the heading
|
|
348
|
+
smoothing: W * 0.12 // rounds the diamond into an oval
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
A non-looping run fires `pathcomplete` and clears itself; `followPath(null)` stops in place. Two points is the minimum — one point is ignored with a warning, not treated as "go there".
|
|
353
|
+
|
|
354
|
+
The path writes `x`/`y` outright, after velocity and gravity have been integrated, so **a sprite on a path ignores its own physics for placement**. It still feeds the frame delta, though, which means a platform on a looping circuit carries whatever is standing on it — a moving platform is now a path, not a tween ping-pong.
|
|
355
|
+
|
|
356
|
+
Animation chains are native in the same spirit:
|
|
357
|
+
|
|
358
|
+
```javascript
|
|
359
|
+
guard.play('hop', { then: 'walk' }); // one call, no animationcomplete handler
|
|
360
|
+
bird.play('flap', { then: ['glide', 'idle'] });
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Each queued animation starts as the previous non-looping one finishes, and a looping animation ends the chain. `animationcomplete` still fires per step if you want to hook one. `stop()` drops the queue along with the current animation.
|
|
364
|
+
|
|
365
|
+
### Line of sight, ledge probes and hitscan — `raycast.js`
|
|
366
|
+
|
|
367
|
+
`gameView.raycast(x0, y0, x1, y1, groups)` answers "what is the first thing on this line?" with `null` or `{ x, y, distance, group, sprite, normal }`. The targets need nothing but a `collisionGroup` — no `collidesWith` on the asking side.
|
|
368
|
+
|
|
369
|
+
```javascript
|
|
370
|
+
const hit = gameView.raycast(eyeX, eyeY, dog.x, dog.y, ['blocker']);
|
|
371
|
+
if (hit) {
|
|
372
|
+
aimBeam(sight, eyeX, eyeY, hit.x, hit.y, hit.distance);
|
|
373
|
+
sight.tintColor = '#f44'; // view blocked at the impact point
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Pass the groups as an **array**. Android also accepts loose arguments, iOS does not, so the array is the portable form. Only `visible`, non-`screenFixed` sprites carrying a `collisionGroup` are candidates — a pooled sprite parked at `visible = false` is correctly invisible to the ray, an untagged wall is invisible too (the usual mistake), and HUD sprites are skipped by design so a screen-fixed score never blocks a world-space sight line.
|
|
378
|
+
|
|
379
|
+
A ledge probe is the same call pointed straight down, one step ahead of the feet:
|
|
380
|
+
|
|
381
|
+
```javascript
|
|
382
|
+
const dir = walker.velocityX >= 0 ? 1 : -1;
|
|
383
|
+
const probeX = walker.x + dir * W * 0.07;
|
|
384
|
+
if (!gameView.raycast(probeX, walker.y, probeX, walker.y + W * 0.12, ['ground'])) {
|
|
385
|
+
walker.velocityX = -walker.velocityX; // nothing underfoot: turn before falling
|
|
386
|
+
walker.flipX = walker.velocityX < 0;
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Run these from a coarse timer or a tap handler — the demo probes at 150 ms — never once per frame. A per-frame `raycast` from JS is exactly the bridge traffic the engine is built to avoid. Note that `raycast.js` drives its probes with `setInterval`: it was written two commits before game-clock timers existed, and `gameView.every` is the better home for an AI tick today.
|
|
391
|
+
|
|
392
|
+
### Timers on the game clock
|
|
393
|
+
|
|
394
|
+
`gameView.after(ms, cb)` and `every(ms, cb)` run on the engine's clock: they stretch under `timeScale` slow motion, freeze at `0`, and pause with the render loop. Spawn waves, AI decision ticks and respawn delays belong here — a paused game that keeps spawning is the bug this fixes.
|
|
395
|
+
|
|
396
|
+
```javascript
|
|
397
|
+
const wave = gameView.every(2000, () => spawnEnemy());
|
|
398
|
+
gameView.after(500, () => player.play('idle'));
|
|
399
|
+
gameView.cancelTimer(wave); // both return an int id
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
The callback receives `{ id }`. Called without a callback, the view fires a `timer` event carrying the same id instead — pick one, not both. A repeating timer fires at most once per frame and restarts its interval after a stall rather than bursting to catch up.
|
|
403
|
+
|
|
404
|
+
What still belongs in `setTimeout`: anything that must keep running while the game is frozen. A pause menu counting down in real seconds is not on the game clock.
|
|
405
|
+
|
|
272
406
|
### Hit-stop on impact
|
|
273
407
|
|
|
274
408
|
Freezing the scene for a few frames makes an impact read before the explosion starts. `timeScale = 0` stops the engine from advancing while it keeps drawing and accepting touches — no render loop to restart, no state to save. (Pattern from Titanium Lander: 140 ms on a crash, 90 ms when a shield absorbs a hit.)
|
|
@@ -1181,7 +1315,16 @@ For a big ground plane, one `tileRepeat` sprite covering the whole world beats a
|
|
|
1181
1315
|
|
|
1182
1316
|
## Visual effects
|
|
1183
1317
|
|
|
1184
|
-
|
|
1318
|
+
**Blend modes** — four, on sprites and emitters alike. `blend.js` shows all of them side by side over a bright meadow strip:
|
|
1319
|
+
|
|
1320
|
+
| Mode | Does | Use for |
|
|
1321
|
+
| --- | --- | --- |
|
|
1322
|
+
| `'normal'` | Covers what is behind it | Everything else |
|
|
1323
|
+
| `'add'` | Sums toward white | Glows, fire, lasers, light shafts, sparks |
|
|
1324
|
+
| `'multiply'` | Darkens the backdrop | Contact shadows under sprites, stains, grime, smoke that dims |
|
|
1325
|
+
| `'screen'` | Lightens without blowing out | Fog, haze, god rays, soft light |
|
|
1326
|
+
|
|
1327
|
+
Group same-blend sprites on their own `zIndex` band: every mode change in draw order costs a batch flush, so alternating them sprite by sprite degrades toward one draw call each. Unknown strings fall back to `'normal'` in silence and the names are case-sensitive — `blend: 'Add'` renders as normal with no warning.
|
|
1185
1328
|
|
|
1186
1329
|
**`flash(color, duration)`** — a solid-color silhouette that fades out natively. This is the damage flash a multiplicative `tintColor` cannot produce (white tint = no change). Retrigger it on a timer for invincibility blinking.
|
|
1187
1330
|
|
|
@@ -1215,6 +1358,6 @@ gameView.addEventListener('tap', () => {
|
|
|
1215
1358
|
});
|
|
1216
1359
|
```
|
|
1217
1360
|
|
|
1218
|
-
**`timeScale`** — one property slows or freezes physics, sheet animations, tweens, particles and the
|
|
1361
|
+
**`timeScale`** — one property slows or freezes physics, sheet animations, tweens, particles, the camera and the game-clock timers together, while rendering and touch keep running. `0.5` for slow motion, `0` for a pause that still draws, short bursts of `0` for hit-stop. `timescale.js` makes the timer half visible: a clock on `gameView.every(1000, ...)` freezes with the scene while a `setInterval` clock beside it keeps ticking.
|
|
1219
1362
|
|
|
1220
1363
|
**`idleAnimation`** — a per-sprite organic sway that composes with tweens and drags. Cheap life for cards, icons, floating pickups and menu art. Disable it before a tween that must land precisely.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# What does not exist yet — and what to do instead
|
|
2
2
|
|
|
3
|
-
The most expensive mistake with `ti.game` is writing code against a feature that sounds like it should be there because Phaser, Godot or Cocos has it. This file is the guard rail: everything below is **absent from
|
|
3
|
+
The most expensive mistake with `ti.game` is writing code against a feature that sounds like it should be there because Phaser, Godot or Cocos has it. This file is the guard rail: everything below is **absent from upstream `main` as of 2026-08-19**, with the workaround that is idiomatic today.
|
|
4
4
|
|
|
5
5
|
Planned items come from the module's `TODO.md`. Planned is not shipped — when a game needs one of these, use the workaround and keep the code isolated enough to swap later.
|
|
6
6
|
|
|
@@ -8,24 +8,23 @@ Planned items come from the module's `TODO.md`. Planned is not shipped — when
|
|
|
8
8
|
|
|
9
9
|
| You might reach for | Status | What to do now |
|
|
10
10
|
| --- | --- | --- |
|
|
11
|
-
| `Game.createText()`, bitmap fonts, text sprites | Planned (priority 5) | Overlay `Ti.UI.Label` views on the window, above the GameView. Costs: labels are screen-fixed, so they do not scroll with the camera, do not z-sort against sprites and cannot be tweened by the engine |
|
|
12
11
|
| A tilemap layer, Tiled JSON import, collision layer | Planned (priority 4) | One sprite per tile. Fine at 12×15, dead at 200×200. For large ground planes use a single `tileRepeat` sprite instead of a grid, and add sparse invisible solids for collision |
|
|
13
12
|
| `sprite.parent`, attaching a turret to a tank, a hat to a hero, multi-part bosses | Planned (priority 7), the biggest structural item | Move the parts together from JS on the same coarse tick, or bake the combination into sheet frames. There is no transform inheritance |
|
|
14
|
-
| `gameView.
|
|
13
|
+
| `gameView.panTo(x, y, { duration, easing })` for a cutscene camera move | Planned (priority 1) | `follow()` an invisible sprite and tween that sprite, then `follow()` the player again |
|
|
14
|
+
| `gameView.findPath(from, to)` A* over a collision grid | Planned (priority 4) | Hand-author the waypoint arrays and hand them to `followPath`. Straight-line walking is what the point-&-click and topdown demos do |
|
|
15
|
+
| `maxWidth` word wrap on text sprites | Planned (priority 5) | Break the lines yourself with `\n`. Measure with the font's `charWidth` on a monospace grid font; a proportional font needs a per-glyph tally |
|
|
16
|
+
| Ping-pong animation playback, a per-sprite animation speed multiplier, a random start offset | Planned (priority 9) | Author the ping-pong into the sheet as a longer animation. Desync a field of torches by giving each one its own `idleAnimation` timing, or start them from staggered timers |
|
|
15
17
|
| Native virtual joystick or d-pad bound to a sprite | Planned (priority 6) | Overlaid Titanium views, one per button, writing `velocityX`/`steering`/`throttle` on `touchstart`/`touchend`. Sibling views get simultaneous pointers, so multitouch works |
|
|
16
18
|
| Gamepad support | Planned (priority 6) | Nothing native. Touch controls only |
|
|
17
|
-
| `blend: 'multiply'` or `'screen'` | Planned (priority 2) | Only `'normal'` and `'add'` exist. For darkening, use `tintColor` (multiplicative) on the sprite itself |
|
|
18
|
-
| Swept AABB so fast bullets stop tunneling | Planned (priority 3) | Cap bullet speed, or enlarge the hitbox along the travel axis with `hitboxScale` — the asteroids demo uses `hitboxScale: 1.4` on its bolts for exactly this |
|
|
19
19
|
| Slopes in platformer terrain | Planned (priority 3), conditional | Build slopes as stepped rectangles, or keep terrain flat |
|
|
20
20
|
| Per-frame animation events (footstep on frame 3) | Planned (priority 9) | Use `animationcomplete` on a short non-looping animation, or a timer aligned to the animation's fps |
|
|
21
|
-
| `play('attack', { then: 'idle' })` chaining | Planned (priority 9) | Handle `animationcomplete` and call `play()` from it |
|
|
22
|
-
| `sprite.followPath([points], ...)` | Planned (priority 9) | Chain tweens: call `animate()` again from the `complete` handler, one leg per call |
|
|
23
21
|
| `playbackRate`, pitch jitter, stereo pan, `fadeTo()` on sounds | Planned (priority 8) | Only `volume`, `loop`, `play`, `pause`, `stop` exist. Ship a few pre-pitched variants of a sample and alternate between them |
|
|
24
22
|
| An fps / draw-call / sprite-count overlay | Planned (developer experience) | `debug: true` draws collision shapes only. There is no stats overlay and no `performance` event — measure with platform tooling |
|
|
25
23
|
| TypeScript definitions (`ti.game.d.ts`) | Planned (developer experience) | None yet |
|
|
26
24
|
| Aseprite JSON import | Planned (developer experience) | TexturePacker JSON (hash or array) is the only atlas format |
|
|
27
25
|
| A `stop()` for the whole scene | Not planned — already covered | `gameView.timeScale = 0` freezes everything the engine ticks while rendering and touch keep running. `gameView.pause()` stops the render loop entirely |
|
|
28
|
-
| Tweening `width`, `height
|
|
26
|
+
| Tweening `width`, `height` | Not planned | `animate()` only handles `x`, `y`, `scale`, `scaleX`, `scaleY`, `rotation`, `opacity`, `glowOpacity`, plus `frame` at the end. Everything else is an instant write — scale instead of sizing |
|
|
27
|
+
| Tweening `tintColor`, or `repeat`/`yoyo` on `animate()` | Planned (priority 9) | Re-launch the tween from its `complete` handler for a blink or a ping-pong, and step a tint by writing `tintColor` on a game-clock timer |
|
|
29
28
|
| Standard Titanium touch events on the GameView | By design | The view fires its own `press`, `tap`, `release` with scene coordinates. Sprites fire the richer set. Overlaid Titanium views keep their normal events |
|
|
30
29
|
| A per-frame event to hook your own logic into | By design, never | The whole architecture exists to keep JS out of the frame loop. Use `collision`, `land`, `complete`, `animationcomplete`, or a coarse timer for decisions |
|
|
31
30
|
|
|
@@ -33,7 +32,7 @@ Planned items come from the module's `TODO.md`. Planned is not shipped — when
|
|
|
33
32
|
|
|
34
33
|
The module's own `TODO.md` rules these out, each being a project the size of everything built so far:
|
|
35
34
|
|
|
36
|
-
- **2D lighting and shadows.** If it ever comes up: a screen-dim overlay plus additive light sprites (`blend: 'add'`) gets 90% of the look for 1% of the cost.
|
|
35
|
+
- **2D lighting and shadows.** If it ever comes up: a screen-dim overlay plus additive light sprites (`blend: 'add'`) gets 90% of the look for 1% of the cost, and `blend: 'multiply'` now does contact shadows under sprites.
|
|
37
36
|
- **Skeletal animation (Spine).** Frame animations from sheets only.
|
|
38
37
|
- **Full rigid-body physics (Box2D class).** What exists — velocity, gravity, solids with penetration resolution, restitution, circle and rect hitboxes, the car model, thrust — covers the target genres. Joints, torque, compound bodies and stacking do not exist.
|
|
39
38
|
- **Render-to-texture.** The only offscreen pass is the fullscreen `cameraEffect`.
|
|
@@ -49,7 +48,16 @@ Recent additions that are easy to miss and easy to reimplement badly in JS:
|
|
|
49
48
|
- `createSound` with separate low-latency effect and streaming music backends.
|
|
50
49
|
- `timeScale`, `maxFps`, `flipX`/`flipY`, `pixelSnap`, `idleAnimation`, `ySort`, `glowColor`/`glowBlur`/`glowOpacity`.
|
|
51
50
|
- `gameView.add([array])` committing a whole level in one bridge crossing.
|
|
51
|
+
- **Text is in the engine now** (2026-08-18): `createText` glyph sprites with an embedded pixel font, `createFont` for BMFont/AngelCode or monospace grid atlases, `align`/`letterSpacing`/`lineSpacing`, and `tools/genfont.py` to rasterize a TTF. Text objects are sprites — they tween, tint, flash, glow, z-sort and take taps. Do **not** fall back to `Ti.UI.Label` overlays for scores.
|
|
52
|
+
- **`screenFixed`** on any sprite: surface coordinates, camera position/zoom/shake ignored, touch mapped back. HUDs no longer need overlay views.
|
|
53
|
+
- **`swept: true`** per sprite: the frame's movement is path-tested (Minkowski + slab) for both collision events and solid blocking. Fast bullets stop tunneling; `hitboxScale` tricks along the travel axis are obsolete.
|
|
54
|
+
- **`collisionend`**: the exit half of the trigger lifecycle, including when the partner is removed, hidden or re-tagged mid-contact.
|
|
55
|
+
- **`followPath(points, opts)` and `play(name, { then })`** (2026-08-19): patrol routes, bullet arcs and animation chains run natively. Do not chain `animate()` legs from `complete`, and do not re-`play()` from an `animationcomplete` handler.
|
|
56
|
+
- **`gameView.raycast(x0, y0, x1, y1, groups)`** (2026-08-19): nearest hit as `{ x, y, distance, group, sprite, normal }`, or `null`. Line of sight, ledge probes and hitscan no longer need an invisible probe sprite — but it is a discrete query, not something to poll every frame.
|
|
57
|
+
- **Game-clock timers** (2026-08-19): `gameView.after(ms, cb)` / `every(ms, cb)` / `cancelTimer(id)` obey `timeScale` and pause with the render loop. Spawn waves and AI ticks belong here, not in `setTimeout`.
|
|
58
|
+
- **`scrollFactor`** (2026-08-19): per-sprite parallax against camera travel. A background layer is one property, not a hand-scrolled pair of copies.
|
|
59
|
+
- **`blend: 'multiply'` and `'screen'`** (2026-08-19) join `'normal'` and `'add'`, on sprites and emitters alike.
|
|
52
60
|
|
|
53
61
|
## Checking the current state
|
|
54
62
|
|
|
55
|
-
This file describes 0.3.0. Before relying on any "planned" item being still absent — or on a workaround still being necessary — read `TODO.md` and `README.md` at https://github.com/m1ga/ti.game, the module's upstream repo. `README.md` there is the canonical API documentation; the `documentation/` folder only points at it. The priorities quoted above are the maintainer's, not a roadmap this skill decides.
|
|
63
|
+
This file describes upstream `main` on 2026-08-19 — a moving target: eight items on this list shipped within three days of the skill being written, four of them in a single day, and the manifest version (`0.3.0`) did not change with any of them. Before relying on any "planned" item being still absent — or on a workaround still being necessary — read `TODO.md` and `README.md` at https://github.com/m1ga/ti.game, the module's upstream repo. `README.md` there is the canonical API documentation; the `documentation/` folder only points at it. The priorities quoted above are the maintainer's, not a roadmap this skill decides.
|