Skip to main content

2D games: physics, input, camera, animation

These four modules make up a platformer or top-down game. Sky Run (examples/engine-demo) uses all of them, together with scenes, particles and a pen HUD.

Sky Run

Physics​

import * as Physics from "tts/physics"

Each sprite that calls these functions has its own body. Collisions are pixel-perfect against a solid sprite: draw every wall and floor on one sprite's costume (or tiles of it).

Function
configure(gravity, friction, maxFall)e.g. 0.8, 0.8, 15. Friction multiplies horizontal speed every frame.
setHitbox(costume)collide using a plain rectangle costume, so animation frames can't snag on walls
setStepUp(pixels)small steps and slopes the body climbs automatically (default 6)
push(dx, dy) / setVelocity(x, y)change the velocity
jump(power)jump if on the ground
step(solid)platformer step: gravity, friction, move and collide
stepTopDown(solid)the same without gravity (friction on both axes)
moveX(dx, solid) / moveY(dy, solid)move one axis with collision
groundBelow(solid)is there ground right below?
bounceInStage(halfW, halfH)reverse velocity at the stage edges
vx, vy, onGround, hitWall, hitCeilingthe body's state after a step
Physics.configure(0.8, 0.8, 14);
Physics.setHitbox("physics");
forever(() => {
Physics.push(Input.axisX() * 1.2, 0);
if (Input.jumpPressed()) Physics.jump(12);
Physics.step("Level");
});

Input​

import * as Input from "tts/input"

Function
axisX() / axisY()-1, 0 or 1 from arrow keys and WASD
jumpHeld()space, up or W held
jumpPressed()true only on the frame a jump key goes down
mouseClicked()true only on the frame the mouse goes down
pressedOnce(key)true only on the frame key goes down

"Pressed" functions remember the previous frame per sprite, so call each one once per frame.

Camera​

import * as Camera from "tts/camera"

Keep positions in world coordinates and let the camera put sprites on screen. The camera is shared by all sprites.

Function
follow(x, y, smoothing)ease toward a point (0.1–0.2 feels good)
lookAt(x, y) / clampTo(minX, minY, maxX, maxY)snap / keep inside the level
shake(strength) / setZoom(percent)effects
place(worldX, worldY)put this sprite on screen; hides it when off screen and sets onScreen
placeTile(worldX, worldY, halfW, halfH)the same for big sprites such as level tiles
toWorldX(screenX) / toWorldY(screenY)screen to world
toScreenX(worldX) / toScreenY(worldY)world to screen
mouseWorldX() / mouseWorldY()the mouse in world coordinates
view.cameraX, view.cameraY, view.zoomthe shared camera

Scrolling platformers​

Scratch won't move a sprite fully off stage, so:

  • Build levels from screen-sized tiles. Give the level sprite costumes tile1, tile2, ..., make one clone per tile, and call placeTile(i * 480, 0, 240, 180) every frame.
  • Run physics on screen, then convert back to world coordinates:
Camera.place(wx, wy); // 1. put the player on screen
Physics.step("Level"); // 2. collide with the tiles on screen
wx = Camera.toWorldX(me.x); // 3. back to world coordinates
wy = Camera.toWorldY(me.y);
Camera.follow(wx, wy, 0.12); // 4. move the camera last
  • Let the player's script be the only one moving the camera, and make world objects clones, so they update after the player each frame.
  • A sprite can't go much below y = -180, so detect "fell in a pit" just under the floor (e.g. wy < -165).

Animation​

import * as Anim from "tts/anim"

Name your frames <name><n>: walk1, walk2, ... The quick form plays one animation at a time per sprite:

if (!Physics.onGround) Anim.show("jump");
else if (Math.abs(Physics.vx) > 1) Anim.play("walk", 2, 8); // 2 frames at 8 fps, looping
else Anim.show("idle"); // a single costume: no animation
Function
play(name, frames, fps)loop name1..nameN; restarts from frame 1 when switching animations
playOnce(name, frames, fps)play once, hold the last frame; returns true when finished
show(costume)show one costume, no animation
restart()start the current animation over

Or keep configured animations as objects (each clone gets its own):

const walk = new Anim.Animation("walk", 4, 10);
const die = new Anim.Animation("die", 6, 12, false); // false = play once

onMessage("hit", () => die.restart());
forever(() => {
walk.show();
if (die.finished()) deleteClone();
});

Animation has restart(), show(), frame(), finished() and the fields prefix, frames, fps, loop and speed.