pub.dev GitHub TECS ECS

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.

Lunapulse Showcase

A game built with Tremble + TECS. Play it on itch.io →

Watch Trailer

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

Tremble gives you a raw Flutter 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.linear

Smooth 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

Camera
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
Follow
followSmooth 3.5f, 0.9z, 1.2r followCartoon 4.5f, 0.6z, 1.0r
UI
uiButton 6.0f, 0.7z, 0.0r uiMenu 4.0f, 0.9z, 0.0r uiNotification 7.0f, 0.5z, 0.0r
Aim
aimStable 8.0f, 1.0z, 2.0r aimElastic 7.0f, 0.7z, 1.5r
Recoil
recoil 10.0f, 0.5z, 0.0r
Spring
springLoose 2.0f, 0.3z, 0.0r springStable 3.0f, 0.5z, 0.0r
Default
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