agentsclimarketplace

Ti game

Skill macCesar/titools/skills/ti-game

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, solids, carMode drifting, thrust), collision groups, trigger zones, swept AABB, raycast, paths, bitmap text, particles, 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, pathfinding, maze, 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, gamepads) do not exist yet — check references/roadmap.md before inventing an API.From its SKILL.md

Install
npx -y skills add macCesar/titools --skill ti-game

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

20.0 KB, as published. Nobody here has run it

ti.game — 2D game engine for Titanium SDK

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-23, manifest 0.4.0 — that bump is the first in a long run of feature landings (the text engine, screenFixed, swept collisions, collisionend, followPath, animation chaining, raycast, findPath, game-clock timers, scrollFactor parallax and the multiply/screen blend modes all shipped while it still said 0.3.0), so anything still calling itself 0.3.0 has none of them. The number lags the code in the other direction too: per-axis hitboxes (hitboxScaleX/hitboxScaleY), text word wrap (maxWidth), unload() on sheets and fonts, and the LiveView renderer lifecycle all landed on 2026-08-23 with the manifest still at 0.4.0, so date the build rather than trusting the version. 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.

Works in both Classic and Alloy projects. Nothing here depends on PurgeTSS.

The one rule everything follows

JS describes the scene and reacts to events. The engine runs every frame.

The Kroll bridge (JS ↔ native) is far too slow for per-frame work, so the API is a scene description: setting sprite.x, sprite.velocityY or car.throttle writes a volatile field the render thread reads on the next tick. Events flow back only when something discrete happens — tap, collision, land, complete. Nothing fires per frame by design.

The practical consequence, and the single biggest mistake to avoid:

If you are about to move a sprite from a setInterval or a requestAnimationFrame, stop and find the native property that does it. Coarse timers for decisions (an AI picking a direction every 80–150 ms, an autofire cadence, a spawn schedule) are fine and the official demos use them. Timers for motion are not.

You wantDo NOT writeUse
A sprite that fallssetInterval(() => s.y += 5)s.gravity = 2000
A sprite that moves at constant speedtimer updating xs.velocityX = 300
Move from A to B over timemanual easing loops.animate({ x, y, duration, easing })
A car that drives and driftsyour own physicscarMode: true + throttle/steering
A ship with inertiavector math per framethrust + angularVelocity
Drag a sprite with the fingertouchmove handlerdraggable: true
Scrolling background under a fixed cameratimer repositioning copieswrapX / wrapShift + velocityX
Parallax layers under a moving camerahand-scrolled layersscrollFactor on each layer
Camera that follows the playertimer writing cameraXgameView.follow(sprite, options)
A score, a title, a labelTi.UI.Label overlaysGame.createText({ text, screenFixed: true })
A dialog box that fits the screenbreaking the lines with \n after measuring in JSmaxWidth on the text sprite
A bullet fast enough to skip past a thin wallshrinking the time stepswept: true on the bullet
To know the player left a zonepolling on a timerthe collisionend event
An enemy on a fixed patrol routechained animate() legssprite.followPath(points, { loop })
Play an attack, then go back to idlean animationcomplete handlerplay('attack', { then: 'idle' })
Line of sight, a ledge probe, a hitscan shotan invisible probe spritegameView.raycast(x0, y0, x1, y1, ['group'])
Walking around obstacles to a tapped pointyour own A* in JS, or a straight line through the wallgameView.findPath(from, to, opts) → sprite.followPath(path)
A spawn wave or an AI tick that respects pausesetIntervalgameView.every(ms, cb)
Smoke, sparks, explosionspooled sprites in JSGame.createEmitter(...)
A swinging chain or capeverlet solver in JSGame.createRope(...)
Slow motion or a pause that still drawspausing your own timersgameView.timeScale = 0.5 / 0

Minimal scene

const Game = require('ti.game');

const win = Ti.UI.createWindow({
	backgroundColor: '#000',
	navBarHidden: true,                           // iOS: no nav bar
	theme: 'Theme.Titanium.DayNight.NoTitleBar'   // Android: no title bar
});
const gameView = Game.createGameView({ backgroundColor: '#8ed8f8' });

const sheet = Game.createSpriteSheet({
	image: 'assets/hero.png',
	frameWidth: 32,
	frameHeight: 48,
	smoothing: false            // nearest-neighbor — crisp pixel art
});

let initialized = false;
gameView.addEventListener('resize', (e) => {
	if (initialized) {
		return;
	}
	initialized = true;
	buildLevel(e.width, e.height);
});

function buildLevel(W, H) {
	const hero = Game.createSprite({
		sheet: sheet,
		x: W / 2,
		y: H / 2,
		width: 96,
		height: 144,
		animations: {
			idle: { frames: [0], fps: 1, loop: true },
			walk: { frames: [1, 2], fps: 6, loop: true }
		}
	});
	hero.play('idle');
	gameView.add(hero);
}

win.add(gameView);
win.open();

Build the level on resize, never from displayCaps

Ti.Platform.displayCaps includes the system bars and reports points on iOS, while the scene lives in surface pixels. Sizing a level from it puts bottom-anchored sprites below the visible area. The GameView fires resize with the real surface size — that is the coordinate space. The guard flag matters: resize fires again on rotation and on some lifecycle transitions, and rebuilding the level there would duplicate every sprite.

When you genuinely need dp→scene units (to keep a floor above 80dp on-screen buttons), measure the surface rather than trusting the density factor, because the iOS Simulator renders at 1x while still reporting the device scale:

const density = Ti.Platform.osname === 'android'
	? Ti.Platform.displayCaps.logicalDensityFactor
	: H / Ti.Platform.displayCaps.platformHeight;
const buttonZone = Math.round(130 * density);
const groundTop = H - buttonZone;

Coordinates

Top-left origin, y-down, surface pixels. Touch coordinates map 1:1. (x, y) is the sprite's anchor (default 0.5/0.5 = center, so y is the middle of the sprite, not its top). Rotation in degrees, positive clockwise. All JS durations are milliseconds.

Pick a movement model

ModelPropertiesFits
Plain velocityvelocityX/Y (px/s), gravity (px/s²)Flappy, projectiles, falling objects, notes
Platformervelocity + solidWith, read-only onGround, land eventMario-style run & jump
Bouncing bodyrestitution (0..1) on top of solidWithBalls, pinball, breakout
CarcarMode: true, throttle, steering (-1..1)Top-down racer with emergent drift
Newtonian flightthrust (px/s² along heading), angularVelocity (deg/s), maxSpeedAsteroids, lunar lander
Tweenanimate({ x, y, duration, easing })Point-and-click walking, UI cards, patrols

Gravity is per sprite, not global — set it on each body that should fall. A sprite with no gravity and no velocity simply stays put.

Block versus react: solidWith and collidesWith are independent

  • 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.
  • 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.

A sprite can use both — a player that stands on platforms (solidWith) and dies on spikes (collidesWith).

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.

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.

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.

And when the question is "how do I get over there?", gameView.findPath(from, to, { cellSize, groups, clearance, bounds }) runs grid A* around the same tagged sprites and hands back waypoints for followPath — tap-to-walk that rounds a tree, a hound re-routing to the player on an 800 ms timer. Also discrete: the walk itself runs natively between queries, so pathfind on taps and AI ticks, never per frame. Pass clearance of about half the walker's width, since the route is a line for the sprite's center.

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.

Tune fairness with hitboxScale (art rarely fills its frame; slightly small hitboxes feel better), hitboxScaleX/hitboxScaleY when the drawing fills its frame by a different fraction on each axis (a 20×44 hero in a 32×48 frame wants 0.62 wide and 0.92 tall — the two multiply hitboxScale rather than replace it), 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.

Performance rules that actually matter

  • 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.
  • 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.
  • 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.
  • Keep collision group lists targeted. Cost is O(colliders × candidates) per frame. A bullet checking ['asteroid'] tests 5 sprites, not the whole scene.
  • 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.
  • Read properties in handlers, not in loops. ball.x is a synchronous snapshot of the live native value — perfect inside a collision handler or an 80 ms AI tick, wasteful in a tight loop.
  • maxFps: 60 on the GameView keeps 120 Hz ProMotion displays from doubling render work. 0 (default) follows the display refresh rate.
  • 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.

Text is a sprite

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.

const score = Game.createText({ text: 'SCORE 0', screenFixed: true, x: W / 2, y: H * 0.08, scale: 3, zIndex: 100 });
gameView.add(score);
score.text = 'SCORE 10';   // re-lays out natively

One long string wraps itself: maxWidth breaks lines on word boundaries and re-wraps every time text changes, so a dialog box needs no hand-broken \n. It is measured in font-space px, before scale — divide the screen width you want by the scale you are drawing at.

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).

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.

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.

Clean up what you created

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().

Two things the engine now cleans up on its own, so do not hand-roll them: a LiveView reload retires the previous runtime's render loops, SoundPool, MediaPlayers and audio proxies, so reloads stop stacking game loops; and releasing a sheet or font frees its GL texture. What you can now do explicitly is spriteSheet.unload() / font.unload() when streaming levels — permanent, so only after nothing draws from them.

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.

win.addEventListener('close', () => {
	clearInterval(aiTimer);
	clearTimeout(spawnTimer);
	music.stop();          // a looping track outlives the window otherwise
	gameView.pause();
	gameView.removeAllSprites();
});

Reference files

Read the one that matches the task instead of guessing — the API surface below was verified against the module source, not reconstructed from memory.

NeedReference
Exact properties, methods, events, defaults for every objectapi.md
Working code per genre: flappy, platformer, top-down, racer, asteroids, endless runner, rhythm, cards, point & click, particles, ropes, camera — plus patrol paths, raycast probes, pathfinding, parallax and game-clock timersrecipes.md
Install the module, tiapp.xml, Alloy vs Classic wiring, assets, sprite sheets, atlases, lifecycle, testingproject-setup.md
What does NOT exist yet, what is coming, and how to work around it todayroadmap.md

When this skill runs out — a brand-new feature, a demo's exact code, a build question — read upstream itself at https://github.com/m1ga/ti.game. Each file there is authoritative for something different:

Upstream fileWhat it is authoritative for
README.mdThe module's own API documentation, and the demo index. Canonical, but it has lagged the code: verify a defaulted value against the source before relying on it
example/26 runnable demos, one per feature area — the reference implementations this skill's recipes are distilled from
TODO.mdWhat the maintainer has planned, with priorities. The source of roadmap.md
tutorial.mdA step-by-step first scene (sprite, animation, tap-to-move). For a human learning the module, not for looking things up
ios/README.mdThe iOS port's threading, coordinates and touch constants. Mostly for module developers; the app-facing facts are already here
AGENTS.mdRules for changing the module itself — irrelevant to building a game with it
android/src/ti/game/, ios/Classes/The truth. Defaults, clamping and edge cases in this skill were read here, not from prose

Before writing game code, check these

  1. Is the level built inside a guarded resize handler, using e.width/e.height?
  2. Does any timer move a sprite? Replace it with velocity, gravity, a tween, carMode or thrust.
  3. Are all the level's objects added in one gameView.add([...]) call?
  4. Does every moving body have the right model — solidWith to be blocked, collidesWith to be notified?
  5. Is hitboxScale set on sprites whose art does not fill the frame — with hitboxScaleX/hitboxScaleY when the fit differs per axis?
  6. Do triggers (scores, goals, kill zones) use sheet-less sprites instead of visible art?
  7. Is every createSound, setInterval and setTimeout stopped when the window closes?
  8. Is the HUD Game.createText with screenFixed: true, rather than a stack of overlaid labels?
  9. Does anything travel far enough per frame to tunnel? Give it swept: true.
  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.
  11. Are you using an API that actually exists? Anything involving tilemaps, sprite parenting, joysticks, gamepads or slopes — check roadmap.md first.

Keep looking

Skills are one crate of 628,971. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.