Overview
Tremble is a lightweight Flutter game engine that follows the simple setup → update → draw pattern found in frameworks like p5.js, Processing, Raylib, and LÖVE. No complex architecture — just write a controller and draw to the canvas.
Setup
GameArea
The root widget. Wrap it with SizedBox + FittedBox for a fixed-resolution letterbox canvas.
DartFittedBox( child: SizedBox( width: 480, height: 640, child: GameArea(controller: DemoController()), ), )
ScreenController
Dartclass DemoController extends ScreenController { void setup(BuildContext context, double width, double height) { } void update(double deltaTime) { } void draw(Canvas canvas, Size size) { } void dispose() { } }
Preload
Load assets before the first frame. Report progress via the callback and call done():
DartFuture<void> preload(progress, done) async { progress(0.5); // await loadAssets(); progress(1.0); done(); }
Show a loading UI with loadingBuilder:
DartGameArea( controller: DemoController(), loadingBuilder: (context, progress) => Center(child: LinearProgressIndicator(value: progress)), )
Input
Keyboard
Dartvoid keyDown(LogicalKeyboardKey key) { } void keyUp(LogicalKeyboardKey key) { }
Hold detection via a Set:
Dartfinal keys = <LogicalKeyboardKey>{}; void update(double dt) { if (keys.contains(LogicalKeyboardKey.space)) { /* held */ } } void keyDown(LogicalKeyboardKey k) { keys.add(k); } void keyUp(LogicalKeyboardKey k) { keys.remove(k); }
Mouse
Dartvoid mouseMove(int id, double x, double y) { } void mousePressed(int id, int button, double x, double y) { } void mouseReleased(int id) { } void mouseScroll(Offset scroll) { }
Rendering
Canvas object in the draw() method.
This means everything Flutter's canvas API supports works here —
shapes (drawRect, drawCircle, drawLine, drawPath, etc.),
images, text (drawParagraph), gradients, and custom clipping.
Sprite/Animation helpers are optional conveniences, not limitations.
SpriteBatch
Loads a GDX texture atlas and renders all sprites in a single draw call. Supports flipping and masking without canvas transforms.
Dartfinal batch = await SpriteBatch.fromOldGdxPacker( "assets/batches/sprites.atlas", flippable: true, ); // Newer: SpriteBatch.fromGdxPacker(...) // Custom: SpriteBatch.custom(image: ..., textures: ..., frames: ...) final rect = batch.getTexture("table"); final anim = batch.getAnimation("hero-idle", speed: 10); batch.draw(canvas, [hero, table]);
Sprite
Dartfinal s = Sprite( texture: batch.getTexture("table"), x: 100, y: 100, ); s.originX = 0.5; s.originY = 0.5; s.opacity = 255; s.scale = 1.0; s.rotation = 0; s.flip = false; s.tint = Colors.white; s.mask = false;
Animation
Extends Sprite with frame-based animation, multiple states, and looping.
Dartfinal hero = Animation( animations: [ batch.getAnimation("hero-idle", speed: 10), batch.getAnimation("hero-run", speed: 10), ], x: 200, y: 300, ); hero.update(deltaTime); hero.setAnimation("hero-run", fromFrame: 0); hero.playing = false; hero.finished; // true when a non-looping animation ends
Tooling
Signals
A lightweight pub-sub system (inspired by Godot signals). Callbacks return true to keep listening or false to unsubscribe.
Dartfinal sig = Signal<int>(); sig.listen((v) { print(v); return true; }); sig.dispatch(42); sig.unlisten(myFn); sig.clear(); sig.length;
SignalBuilder
DartSignalBuilder<int>( signal: sig, builder: (ctx, val) => Text("$val"), )
SignalsBuilder
Listens to multiple signals and rebuilds when any fires.
Wait Events
Timer utilities driven by your game loop. Call wait.update(dt) each frame.
Dartfinal wait = WaitEvents(); wait.wait(time: 1.5, onEnd: () { }); wait.waitAndDo(time: 2, onUpdate: (dt, t) { }, onEnd: () { }); wait.waitUntil(onUpdate: (dt) => alive, onEnd: () { }); wait.periodic(time: 1, onTick: (p) { return true; }); wait.update(deltaTime); wait.clear(); wait.hasEvent; wait.length;
WaitChain
Chain timed actions, animations, dialogs, and callbacks into a single sequential pipeline. Each step waits for the previous one to finish before starting.
Dartwait.chain() // 1. Snap the camera .run(() => camera.setPosition(0, 0)) // 2. Smoothly zoom in over time .waitAndDo(1.5, (dt, remaining) { zoom = MathUtils.lerp(1.0, 2.0, 1.0 - remaining / 1.5); }) // 3. Pan left until reaching a threshold .waitUntil((dt) { camera.x -= 40 * dt; return camera.x < -200; }) // 4. Pause for dramatic effect .wait(1.0) // 5. Spawn entities and swap textures .run(() { world.spawn("debris", x: 120, y: 300); world.spawn("shockwave", x: 120, y: 300); bossSprite.setTexture("boss_rage"); }) // 6. Wait for a dialog to finish (external callback) .blockUntil((continueFn) { dialog.show("You activated my trap card!", onEnd: continueFn); }) // 7. Shake the camera briefly .waitAndDo(0.4, (dt, t) { camera.shake(4.0 * (t / 0.4)); }) .build();
The chain is driven by the same wait.update(dt) call — no extra plumbing needed.
State Machine
Generic typed FSM with enter / update / draw / exit hooks.
Dartfinal fsm = StateMachine<String>(); fsm.add("walking", onEnter: () { }, onUpdate: (dt) { }, onExit: () { }, onDraw: (c, s) { }, ); fsm.state = "walking"; fsm.previousState; fsm.restartState(); fsm.reset(); fsm.onBeforeStateChange = (from, to) { }; fsm.onAfterStateChange = (from, to) { };
Utilities
Vector2
Dartfinal v = Vector2(3, 4); v.magnitude; // 5 v.normalized(); v.distanceTo(other); v.dot(other); v.rotated(angle); v.reflected(normal); v + other; v - other; v * other; v / other; // Vector2 or scalar -v;
Tween
Interpolates a double with a parametric curve.
Dartfinal t = Tween(0, 100, time: 2, curve: Parametrics.smoothStop2); t.forward(); t.backward(startFrom: 1); t.stop(stopAt: 0.5); t.value; // current interpolated value t.isTweening; t.changeTime(3); t.update(deltaTime);
Second Order Dynamics
Spring-based smooth motion for cameras, UI, and character follow.
Dartfinal cam = SecondOrderDynamics.cameraSmooth(Vector2.zero()); final pos = cam.update(deltaTime, target); final pos2 = cam.update(deltaTime, target2);
ColorUtils
DartColorUtils.randomColor(ColorMood.bright); ColorUtils.randomColor(ColorMood.pastel); ColorUtils.randomColor(ColorMood.dark); ColorUtils.randomHSV(mood, opacity: 0.8);
MathUtils
DartMathUtils.randInt(1, 10); MathUtils.randDouble(0, 1); MathUtils.randPick(list); MathUtils.randTake(list); MathUtils.flipCoin(); MathUtils.flipCoinWith(0.7); MathUtils.shuffle(list); MathUtils.seedRandom(42); MathUtils.lerp(0, 10, 0.5); MathUtils.inverseLerp(0, 10, 5); MathUtils.remap(5, 0, 10, 100, 200); MathUtils.constrain(15, 0, 10); MathUtils.lcm(12, 18); MathUtils.normalizeAngle(a); MathUtils.lerpAngle(from, to, t);
ImageUtils
DartImageUtils.loadImageFromBytes(bytes); ImageUtils.loadImageFromAssets("img.png"); ImageUtils.loadImageFromPath("/path/img.png"); ImageUtils.generateFlipped(image); ImageUtils.generateMasked(image); ImageUtils.saveImage(image, "out.png");
Extensions
Dartrect.split(count: 4, axis: Axis.vertical); rect.grid(4, 3); (3.14).fract; // 0.14
Reference
Parametrics
All easing functions — type ParametricFunc = double Function(double t), where t is in [0, 1].
Linear
Parametrics.linearSmooth Start (ease-in)
smoothStart2 smoothStart3 smoothStart4
Smooth Stop (ease-out)
smoothStop2 smoothStop3 smoothStop4
Smooth Step (ease-in-out)
smoothStep2 smoothStep3 smoothStep4
Special
arch2 t·(1−t) parabola
bellcurve6 smoothStart3 × smoothStop3
Elastic (overshoot)
elasticStart(t, [s])
elasticStop(t, [s])
elasticStep(t, [s])
Utility
mix(f1, f2, t, blend) blend two curves
Vector2 API
Constructors
Vector2(x, y)
Vector2.zero()
Vector2.one()
Vector2.fromAngle(rad, [mag])
Vector2.copy(other)
Vector2.fromJson(map)
Properties
.x .y
.magnitude
.magnitudeSquared
.angle
.isZero
Instance Methods
normalized() normalize()
clamped(max) clamp(max)
distanceTo(v) distanceSquaredTo(v)
dot(v) cross(v)
rotated(a) rotate(a)
lerp(v, t)
reflected(n) reflect(n)
clone()
set(x, y) setFrom(v)
toJson()
Operators
+ - * / unary -
== hashCode
Static Methods
.add(a, b) .subtract(a, b)
.multiply(a, b) .divide(a, b)
.scale(v, s)
.dotBetween(a, b) .crossBetween(a, b)
.distance(a, b) .distanceSquared(a, b)
.lerpBetween(a, b, t)
.min(a, b) .max(a, b)
.clampMagnitude(v, max)
.angleBetween(a, b)
.reflectBetween(d, n)
Second Order Dynamics Presets
cameraSmooth 2.5f, 1.0z, 1.0r
cameraLively 3.0f, 0.8z, 1.0r
cameraCinematic 1.5f, 1.2z, 0.8r
cameraImpact 6.0f, 0.6z, 0.0r
followSmooth 3.5f, 0.9z, 1.2r
followCartoon 4.5f, 0.6z, 1.0r
uiButton 6.0f, 0.7z, 0.0r
uiMenu 4.0f, 0.9z, 0.0r
uiNotification 7.0f, 0.5z, 0.0r
aimStable 8.0f, 1.0z, 2.0r
aimElastic 7.0f, 0.7z, 1.5r
recoil 10.0f, 0.5z, 0.0r
springLoose 2.0f, 0.3z, 0.0r
springStable 3.0f, 0.5z, 0.0r
defaultPreset 3.5f, 0.85z, 1.0r
Raw constructor: SecondOrderDynamics(f, z, r, initial) — frequency, damping, response.
WaitChainBuilder API
Each call returns the builder for fluent chaining. Finish with .build().
.wait(seconds) pause for duration
.waitAndDo(seconds, onUpdate) per-frame while waiting
.waitUntil(onUpdate) wait for condition (return false to stop)
.periodic(seconds, onTick) repeat on interval until false
.run(fn) execute immediately
.blockUntil(continueFn) pause chain until continueFn() is called
.build() start executing the chain
StateMachine API
.add(name, {onEnter, onUpdate, onDraw, onExit}) register a state
.state = name change state (triggers exit → enter)
.state current state
.previousState previous state (null if none)
.restartState() re-fire exit + enter on current state
.reset() clear state and history
.update(dt) call per-frame
.draw(canvas, size) optional per-state draw
.onBeforeStateChange callback(from, to)
.onAfterStateChange callback(from, to)
Types
Darttypedef SubscriptionCallback<T> = bool? Function(T); typedef VoidCallback = void Function(); typedef PeriodicCallback = bool Function(int phase); typedef UpdateCallback = void Function(double dt); typedef UpdateSubscriptionCallback = bool Function(double dt); typedef TimeUpdateCallback = void Function(double dt, double remaining); typedef StateChangeCallback<T> = void Function(T? from, T? to);
Tremble — MIT License — GitHub