q5play logo

Teach with

q5play

Level up from block-based coding!

$6
per student
/ semester
Total: $180
Buy Now
+
LMS Bundle
  • q5 Web Editor with dedicated support for q5play
  • Assign, collect, and grade students' q5play projects
$3
per student
/ semester
Additional: $90 [50% discount applied]
Subscribe
interactive-textbook

Interactive Textbook

The Learn q5play interactive textbook makes Object Oriented Programming concepts tangible!

Available in JavaScript and Python editions.

Contains over 50 pages of reference documentation packed with 150 code examples that students can experiment with right in their web browser. Works great on iPads and Chromebooks.

Teachers can evaluate this resource for free.

game design

GDF Curriculum

q5play Game Design Fundamentals gives students a broad introduction to the art of game design.

Contains 7 lessons with slides and activities that facilitate class discussion and student collaboration.

Even if you're not a "gamer", you can still get competently teach game design!

certificate

How it Works

After purchase of the q5play Educational License, you'll be given a class ID for students to use.

No personal info is required for students to create an account on q5play.org and they can keep using their account after the licensing term ends.

Students fully own the content they make with q5play, and can even sell their games under the terms of the q5play Creator License for free.

teaching

Codevre Teach

Codevre's q5 Web Editor has powerful IDE features wrapped in a minimal UI that's easy for students to navigate. Inline documentation helps students learn about q5play's features as they code. No install required!

Codevre Teach provides an all-in-one LMS that teachers can use to assign, collect, and grade q5play projects.

q5play
Edu License
LMS Bundle
Academic use of q5play.js
Academic use of the Learn q5play interactive textbook
COPPA compliant q5play.org student accounts (no personal data)
q5play Game Design Fundamentals curriculum
q5play Visual Studio Code Extension
2 hour Professional Development session for teachers
COPPA compliant codevre.com student accounts (no personal data)
Cloud storage for over 2,000 projects made with Codevre's q5 Web Editor
LMS teachers can use to assign, collect & grade student's q5play projects
$6
per student /
semester
Total: $180
Buy Now

Looking for a school or district wide license?

Request a Quote

Compare q5play to...

Scratch 🐯

Scratch is an incredible introductory tool, perfectly suited for students ages 8–11. However, as students' imaginations grow, they rapidly outgrow its technical limitations.

Many of the featured projects on the Scratch website appear to have been created by adults using highly complex mathematical workarounds. This can set unrealistic expectations for students who are trying to build dynamic games using basic blocks. The fundamental difference between Scratch and modern game development is the underlying architecture: Scratch is a purely visual state-machine, whereas q5play is built on top of a professional-grade physics and rendering engine.

With q5play, students aren't burdened with writing tedious boilerplate code for basic physical interactions or debugging tangled webs of broadcast messages. Instead, q5play acts as a highly capable, natural bridge, allowing students to focus on the creative aspects of game design while learning industry-standard programming paradigms.

Technical Comparison

q5play vs. Scratch 🐯

Manual Math vs. Box2D Physics

Scratch has no built-in physics engine. Something as fundamental to modern games as rolling a ball down a slope or simulating realistic gravity requires discrete mathematical approximations that are tedious to implement and highly prone to glitches.

Because Scratch calculates movement frame-by-frame without spatial awareness, it suffers from tunneling (where fast-moving objects completely phase through thin walls before the engine registers a hit). To simulate realistic gravity, restitution (bounciness), and sloped collisions, students must manually compute vectors, update velocity arrays, and write loops to mathematically push sprites out of floors.

// Scratch blocks (Ball sprite)
// vx, vy, and slope_angle must be managed as custom variables
when green flag clicked
set [slope_angle v] to (15)  // Must manually match the Floor sprite's tilt
set [vx v] to (0)
set [vy v] to (0)
go to x: (-100) y: (80)

forever
  // 1. Apply discrete gravity and slope acceleration
  change [vx v] by ((sin of (slope_angle)) * (0.5))
  change [vy v] by ((cos of (slope_angle)) * (-0.5))
  change x by (vx)
  change y by (vy)

  // 2. Manual collision resolution (Prone to jitter and tunneling)
  if <touching [Floor v]?> then
    // Nudge the ball out of the floor pixel-by-pixel to prevent sinking
    repeat until <not <touching [Floor v]?>>
      change y by (1)
    end
    // 3. Apply fake friction and bounce decay
    set [vx v] to ((vx) * (0.8))
    set [vy v] to ((vy) * (-0.5))
  end
end

Note: Even with all this code, the Scratch ball will likely still jitter on the slope, and if vy gets too high, it will clip through the floor entirely.

In q5play, the integrated Box2D physics engine handles the entire simulation automatically. It uses Continuous Collision Detection (CCD) to ensure fast objects never phase through geometry, calculating mass, friction, and restitution under the hood:

await Canvas();
world.gravity.y = 10;

let ball = new Sprite(-100, -80, 30);
ball.bounciness = 0.5;

let floor = new Sprite(0, 50, 300, 10, 'static');
floor.rotation = 15;

Broadcast Spaghetti vs. Centralized Logic

Beyond physics, game architecture in Scratch scales very poorly. Scratch relies heavily on global variables and asynchronous broadcast messages to trigger events across dozens of different sprite scripts.

When a game gets complex, this "broadcast spaghetti" makes it harder for students to track the flow of execution. Worse, because Scratch processes scripts asynchronously, it frequently creates race conditions, bugs where two scripts try to update the score or delete a sprite at the exact same time, leading to unpredictable game behavior.

// Scratch blocks (Coin sprite)
when I receive [collect_coin v]
change [score v] by (1)
delete this clone

// Scratch blocks (Player sprite)
forever
  if <touching [Coin v]?> then
    // Which script processes first? The engine has to guess.
    broadcast [collect_coin v]
  end
end

In q5play, logic is deterministic and centralized. The game flow lives in a single update loop and reads naturally from top to bottom. Using built-in collision methods like overlaps, q5play guarantees that interactions are handled safely in the exact order the programmer specifies:

q5.update = function () {
    // Synchronous, predictable, and clean
    if (player.overlaps(coin)) {
        coin.remove();
        score++;
    }
};

Unity ⚙️

Unity is an incredibly powerful engine built for professional game studios, but its complexity makes it poorly suited for introductory computer science courses.

The learning curve is steep. Teaching Unity requires a lot of prior experience or professional development. In contrast, teachers can prepare to teach with q5play in under an hour.

Also, Unity cannot run on Chromebooks or iPads, which are commonly used in schools. q5play runs entirely in the web browser, making it compatible with virtually all school-issued devices.

Students can typically accomplish more with q5play after a few days of practice than they could after weeks with Unity.

Furthermore, Unity takes a few seconds to compile every time the user wants to test out changes. q5play runs instantly! Unity's web export carries substantial bloat and overhead compared to q5play's lightweight web-native approach.

Under the hood, q5play uses the same Box2D v3 physics engine as Unity 6.3. Once students have a solid foundation in programming logic and physics simulation through q5play, transitioning to Unity becomes a much more natural progression.

Technical Comparison

q5play vs. Unity ⚙️

Workflows & Collision Detection

Creating a simple 2D game in Unity requires navigating a dense graphical interface to configure rigidbodies, colliders, and materials, while simultaneously writing C# scripts to bind them all together. Even basic physics-based movement requires boilerplate involving FixedUpdate, Time.fixedDeltaTime, and vector math.

In q5play, that entire workflow, creating the player with built-in physics and assigning an image, is accomplished in just a few lines of highly readable code:

// q5play JS
let player = new Sprite(0, 0, 50);
player.img = 'assets/player.webp';

Detecting collisions in Unity requires implementing an OnCollisionEnter2D callback on a MonoBehaviour subclass:

// Unity C#
public class Player : MonoBehaviour {
  void OnCollisionEnter2D(Collision2D col) {
    if (col.gameObject.CompareTag("Floor")) {
      Debug.Log("landed!");
    }
  }
}

In q5play, collision checks are readable boolean conditions that can be used directly inside the update loop:

// q5play JS
q5.update = function () {
    if (player.collides(floor)) {
        console.log('landed!');
    }
};

ECS vs. Direct OOP Architecture

Creating game logic in Unity means adhering to its Entity-Component pattern (GameObjects and MonoBehaviours). This structure requires students to manage states across a visual Inspector UI and scattered C# scripts. It frequently creates a disconnect for beginners who are just trying to understand how a variable actually controls an outcome.

In contrast, q5play is built on straightforward Object-Oriented Programming (OOP) principles. A Sprite is treated as a unified object where its physical properties (x, y, velocity, mass, bounciness) are manipulated directly through code. This 1:1 mapping between the typed code and the resulting on-screen behavior reinforces core computer science fundamentals without the cognitive load of a complex editor interface.

Input Complexity vs. Readability

Handling inputs in Unity requires navigating either the legacy Input Manager (with string-based axis lookups) or the newer, robust but highly complex Input System, which demands configuring Action Maps, binding events, and passing callback contexts.

In Unity C#, a simple physics jump looks like this:

// Unity C#
void Update() {
  if (Input.GetKeyDown(KeyCode.Space)) {
    GetComponent<Rigidbody2D>().AddForce(new Vector2(0, 10), ForceMode2D.Impulse);
  }
}

q5play handles device inputs natively with a syntax tailored specifically for readability. Keyboard, mouse, and touch states are accessible directly within the update loop, requiring zero prior configuration:

// q5play JS
q5.update = function () {
    if (kb.presses('space')) {
        player.vel.y = -10; // Instantly apply vertical velocity
    }
};

Engine Bloat vs. Web-Native Performance

Unity was fundamentally designed as a desktop and console engine. Even with its recent support for WebGPU, exporting a Unity game to the web still relies on IL2CPP (compiling C# code to C++ and then to WebAssembly) alongside heavy rendering pipelines like URP. This results in massive initial download sizes, often tens of megabytes, and long compilation times.

q5play bypasses this entirely by being native to the web from the ground up. It leverages q5.js and modern WebGPU rendering to deliver lightning-fast, hardware-accelerated graphics directly in the browser.

Because the entire framework is so lightweight, a q5play project loads instantly in a browser tab. Students can test, share, and deploy their games continuously without waiting for agonizing compilation cycles, resulting in a much tighter, more rewarding feedback loop.

p5.js 🌸

p5.js is a fantastic tool for creative coding, but it is not a game engine. It has no built-in support for game physics, collision resolution, sprite management, state machines, or hardware game controllers.

q5, the underlying graphics library q5play is built on, was heavily inspired by p5.js but utilizes WebGPU rendering. For projects like generative art, particle systems, or games with massive amounts of shapes on screen, q5 offers up to ~100× better performance than p5.js's standard Canvas2D renderer.

For teachers already familiar with p5.js, the transition is virtually frictionless. q5's API is largely compatible with p5.js, so existing knowledge carries over pretty seamlessly.

Importantly, q5 and Codevre's q5 Web Editor support Python, whereas p5.js, OpenProcessing, and the p5.js Web Editor do not.

Technical Comparison

q5play vs. p5.js 🌸

Manual Math vs. Box2D Physics

Without a physics engine, p5.js requires writing gravity and collision math entirely by hand. Adding a tilted floor means transforming to floor-local space to detect the collision, computing the surface normal to reflect velocity correctly, and nudging the ball out of the floor along that normal:

// p5.js
let ball = { x: 150, y: 50, r: 25, vx: 0, vy: 0 };
let floor = { cx: 200, cy: 300, w: 300, h: 20, angle: 15 };

function setup() {
    createCanvas(400, 400);
}

function draw() {
    clear();

    // 1. Apply gravity to velocity FIRST
    ball.vy += 0.2;

    // 2. Apply velocity to position BEFORE checking collisions
    ball.x += ball.vx;
    ball.y += ball.vy;

    // 3. Transform the NEW position into the floor's local space
    let rad = (floor.angle * Math.PI) / 180;
    let dx = ball.x - floor.cx;
    let dy = ball.y - floor.cy;

    let localX = dx * Math.cos(-rad) - dy * Math.sin(-rad);
    let localY = dx * Math.sin(-rad) + dy * Math.cos(-rad);

    // 4. Transform velocity into local space
    let localVx = ball.vx * Math.cos(-rad) - ball.vy * Math.sin(-rad);
    let localVy = ball.vx * Math.sin(-rad) + ball.vy * Math.cos(-rad);

    // 5. Collision Check
    let floorTop = -floor.h / 2;

    if (Math.abs(localX) < floor.w / 2 + ball.r && localY + ball.r > floorTop && localY - ball.r < floor.h / 2) {
        if (localVy > 0) {
            // Only bounce if it's moving downwards into the floor

            // Bounce (restitution)
            localVy = -localVy * 0.5;

            // Friction (so it doesn't slide like it's on pure ice)
            localVx *= 0.99;

            // Push ball safely out of the floor
            localY = floorTop - ball.r;

            // 6. Convert the corrected local values BACK to world space
            ball.x = floor.cx + localX * Math.cos(rad) - localY * Math.sin(rad);
            ball.y = floor.cy + localX * Math.sin(rad) + localY * Math.cos(rad);

            ball.vx = localVx * Math.cos(rad) - localVy * Math.sin(rad);
            ball.vy = localVx * Math.sin(rad) + localVy * Math.cos(rad);
        }
    }

    // --- DRAWING ---
    push();
    translate(floor.cx, floor.cy);
    rotate(rad);
    fill(100);
    rect(-floor.w / 2, -floor.h / 2, floor.w, floor.h);
    pop();

    fill(255, 100, 100);
    circle(ball.x, ball.y, ball.r * 2);
}

In q5play, Box2D handles gravity, collision detection, and normal resolution automatically:

await Canvas(400);
world.gravity.y = 10;

let ball = new Sprite(0, -150, 50);
ball.bounciness = 0.5;

let floor = new Sprite(0, 150, 400, 10, STATIC);
floor.rotation = 10;

Manual vs. Automatic Animations

Loading a spritesheet animation in p5.js requires manually tracking a frame index, advancing it each draw call with a timer, and cropping the spritesheet with 9-argument image() to render each frame:

// p5.js
let sheet;
let frameW = 32,
    frameH = 32,
    numFrames = 8;
let frameIndex = 0,
    frameTimer = 0,
    frameDelay = 6;
let player = { x: 200, y: 200 };

function preload() {
    sheet = loadImage('assets/run.png');
}

function setup() {
    createCanvas(400, 400);
}

function draw() {
    clear();

    frameTimer++;
    if (frameTimer >= frameDelay) {
        frameTimer = 0;
        frameIndex = (frameIndex + 1) % numFrames;
    }

    let sx = frameIndex * frameW;
    image(sheet, player.x - frameW / 2, player.y - frameH / 2, frameW, frameH, sx, 0, frameW, frameH);
}

In q5play, animations can be loaded from a sprite sheet with a single line of code. The ani property of a Sprite is an instance of the built-in Animation class, which automatically handles frame timing and rendering:

let player = new Sprite(0, 0);
player.addAni('assets/run.png', 8);
player.ani.frameDelay = 6;

Arrays vs. Groups Entity Management

Managing groups of enemies in vanilla p5.js means storing object states in arrays and writing manual for loops to update, draw, and check collisions for every single entity.

// p5.js
let enemyImg;
let enemies = [];

function preload() {
    enemyImg = loadImage('assets/enemy.webp');
}

function setup() {
    createCanvas(400, 400);
    for (let i = 0; i < 5; i++) {
        enemies.push({ x: random(width), y: random(height), w: 48, h: 48, vx: random(-2, 2), vy: random(-2, 2) });
    }
}

function draw() {
    clear();
    for (let e of enemies) {
        e.x += e.vx;
        e.y += e.vy;
        image(enemyImg, e.x - e.w / 2, e.y - e.h / 2, e.w, e.h);
    }
}

In q5play, a Group manages shared behavior and images for all its sprites automatically:

await Canvas(400, 400);

let enemies = new Group();
enemies.img = 'assets/enemy.webp';

for (let i = 0; i < 5; i++) {
    let e = new enemies.Sprite(jit(200), jit(200), 48);
    e.vel.x = jit(2);
    e.vel.y = jit(2);
}

Microsoft MakeCode Arcade 🕹️

Microsoft's MakeCode Arcade is primarily a block-based game engine.

It forces students to work within a fixed screen resolution of 160×120 and use arcade physics. But students who grew up playing modern 2D games like Celeste, Terraria, and Hollow Knight are unlikely to be inspired by the prospect of building an 8-bit Space Invaders or Pac-Man clone.

From the perspective of today's students, games from the 2010s like Angry Birds are retro. While classic 80s games are relics worth studying, see our Asteroids demo as an example, today's students are best served by cutting edge tools that empower them to create the games of the future.

MakeCode Arcade is closely tied to its visual block editor. While it does offer JavaScript and Python modes, text-based coding in MakeCode is highly verbose, relying heavily on enums and registered callbacks rather than straightforward procedural logic.

Technical Comparison

q5play vs. Microsoft MakeCode Arcade 🕹️

Pixel Editing vs. External Images

In MakeCode Arcade, asset generation is restricted to its built-in pixel-art editor. Importing external image files dynamically via URL is not supported in the standard web interface, which prevents students from using high-resolution assets.

// MakeCode Arcade forces inline, generated pixel art
let player = sprites.create(
    img`
    . . . . . . . . . . . . . . . .
    . . . f f f f . . . . . . . . .
    . . f f f f f f . . . . . . . .
    . . f e f f e f . . . . . . . .
    `,
    SpriteKind.Player
);

In q5play, images can be loaded seamlessly:

let player = new Sprite(0, 0, 64, 64);
player.img = 'assets/player.webp';

Event Callbacks vs. Centralized Game Loop

Furthermore, collision logic in MakeCode relies on registering event callbacks outside of the main update loop.

// MakeCode Arcade
sprites.onOverlap(SpriteKind.Player, SpriteKind.Coin, function (player, coin) {
    coin.destroy();
    info.changeScoreBy(1);
});

In q5play, overlap detection reads naturally inside the update loop:

if (player.overlaps(coin)) {
    coin.delete();
    score++;
}

Event Driven vs. Real-Time Input

MakeCode Arcade's controller input is also event-based.

// MakeCode Arcade
controller.A.onEvent(ControllerButtonEvent.Pressed, function () {
    player.vy = -200;
});

In q5play, controller input handling can also be done in the main update loop.

if (contro.presses('a')) player.vel.y = -10;

Code.org Game Lab 🧪

Despite tens of millions in annual funding from corporate and philanthropic sources, Code.org's Game Lab hasn't been meaningfully updated in a decade.

Given that classes were added to JavaScript in 2015, Game Lab was already outdated upon its 2016 release for the purpose of teaching object oriented programming. Also, despite the editor's ability to toggle between blocks and JavaScript, student's Game Lab code exists inside a sandbox that can't interact with the browser environment or interface with other JS libraries. Student's projects are stored on Code.org and can't be easily exported to a standalone website.

Code.org's Game Lab uses a custom fork of p5.play v1 that only supports basic arcade physics, not the high-quality simulation capabilities of q5play. Projects are confined to a 400×400 canvas, and performance limitations in practice restrict students to just a handful of sprites on screen at once. These are significant constraints, particularly for high school students capable of more ambitious work.

Following Code.org's 2025 "Hour of AI" shift away from creative coding, a number of their original curriculum developers departed from the company, leaving Game Lab without a clear roadmap for the future and probably no further updates.

Technical Comparison

q5play vs. Code.org Game Lab 🧪

Configuration vs. Automatic Defaults

Sprites do not have physics colliders by default; they must be added manually. Furthermore, sprites display as generic squares by default, which rarely correspond to the actual shape of their underlying collider. This is a common source of confusion for students and teachers! Only by setting debug to true can students see a green outline of the collider shape.

In this example, a ball bounces off a rotated floor.

// Code.org Game Lab
var ball = createSprite(20, 50);
ball.setCollider('circle', 0, 0, 25);
ball.debug = true;
ball.bounciness = 0.8;

var floor = createSprite(200, 350);
floor.rotation = 10;
floor.setCollider('rectangle', 0, 0, 400, 10);
floor.debug = true;
floor.immovable = true;

function draw() {
    background('white');

    ball.velocityY += 0.5;

    ball.bounce(floor);

    drawSprites();
}

In q5play, sprites have a collider by default. Shapes representing the collider are drawn automatically, unless an image or animation is applied to the sprite. Box2D handles the bouncing and collision resolution without any additional configuration:

await Canvas(400, 400);
world.gravity.y = 10;

let ball = new Sprite(0, -150, 50);
ball.bounciness = 0.5;

let floor = new Sprite(0, 150, 400, 10, STATIC);
floor.rotation = 10;

Pairwise vs. Global Collision Resolution

In Game Lab, the sprite.collide(other) function only resolves displacement between the two specific sprites in that call; every other sprite is unaffected. To stack boxes on a floor, you must manually call collide for every pair and in the right order: settling lower boxes first, then upper ones. Miss a pair and sprites fall through each other. Note that this technique doesn't work for arrangements where the order of collisions isn't strictly bottom-to-top, such as a pyramid.

// Code.org Game Lab, every pair needs its own collide() call
var box1 = createSprite(200, 50, 50, 50);
var box2 = createSprite(200, 150, 50, 50);
var floor = createSprite(200, 380, 400, 20);
floor.immovable = true;

var vy1 = 0,
    vy2 = 0;

function draw() {
    background('white');
    vy1 += 0.5;
    vy2 += 0.5;
    box1.y += vy1;
    box2.y += vy2;

    // order matters, settle from bottom to top
    if (box2.collide(floor)) vy2 = 0;
    if (box1.collide(box2)) vy1 = 0;
    if (box1.collide(floor)) vy1 = 0; // fallback if box1 falls past box2

    drawSprites();
}

In q5play, Box2D resolves all contacts simultaneously every physics step. Sprites can be stacked naturally based solely on their position.

await Canvas(400, 400);
world.gravity.y = 10;

let box1 = new Sprite(0, -200, 50);
let box2 = new Sprite(0, -100, 50);
let floor = new Sprite(0, 150, 400, 10, STATIC);

Math vs. Overlap Mouse Input

Game Lab also has no way to check sprite-level pointer input. Detecting a click on a specific sprite requires manually testing whether the mouse position is inside the sprite's bounding box every frame:

// Code.org Game Lab
var clicked = false;

function draw() {
    if (
        !clicked &&
        mouseDown('left') &&
        Math.abs(mouseX - coin.x) < coin.width / 2 &&
        Math.abs(mouseY - coin.y) < coin.height / 2
    ) {
        clicked = true;
        coin.visible = false;
        score++;
    }
    drawSprites();
}

In q5play, sprites respond to pointer input directly:

q5.update = function () {
    if (mouse.presses() && mouse.overlapping(coin)) {
        coin.delete();
        score++;
    }
};

GameMaker Studio 🎮

GameMaker Studio uses its own scripting language, GML (GameMaker Language).

Its workflow revolves around object behaviors split across separate event scripts (Create, Step, Collision, Draw) and visual editors. While GameMaker is powerful when utilizing its "no-code" visual Room and Object editors, handling everything purely in code requires a lot of boilerplate.

Furthermore, GameMaker relies on Box2D v2 for its internal physics simulation, a library that is nearly two decades old. q5play, uses Box2D v3 compiled to WebAssembly, which delivers substantially improved performance, modern JavaScript syntax, and highly accurate simulation.

Technical Comparison

q5play vs. GameMaker Studio 🎮

Scattered Nodes vs. Direct Physics Setup

In GameMaker, supporting a rotated floor purely via code requires enabling the room's Box2D physics world and creating fixture bodies manually.

// GameMaker GML - Room creation code (physics room required)
physics_world_gravity(0, 10);

// Floor object - Create event
sprite_index = spr_floor_white; // Requires a pre-made 1x1 white sprite asset
image_xscale = 400; // Stretch visual to match physics
image_yscale = 10;
var floor_fix = physics_fixture_create();
physics_fixture_set_box_shape(floor_fix, 200, 5); // 400x10 total size (uses half-measurements)
physics_fixture_set_density(floor_fix, 0); // 0 density = static body
physics_fixture_set_restitution(floor_fix, 0.8);
physics_fixture_bind(floor_fix, id); // Must bind before setting phy_rotation
physics_fixture_delete(floor_fix);
phy_rotation = 10;

// Ball object - Create event
sprite_index = spr_ball_white; // Requires a pre-made 50x50 circle sprite asset
var ball_fix = physics_fixture_create();
physics_fixture_set_circle_shape(ball_fix, 25);
physics_fixture_set_restitution(ball_fix, 0.5);
physics_fixture_bind(ball_fix, id);
physics_fixture_delete(ball_fix);

In q5play, all of this is expressed in a few readable lines of JavaScript:

await Canvas(400, 400);
world.gravity.y = 10;

let ball = new Sprite(0, -150, 50);
ball.bounciness = 0.5;

let floor = new Sprite(0, 150, 400, 10, 'static');
floor.rotation = 10;

Game Controller Input Handling

Gamepad input in GameMaker requires polling a specific slot index, managing axis deadzones manually, and mapping actions to numeric button constants each frame:

// GameMaker GML - Step event
if (gamepad_is_connected(0)) {
    var axis = gamepad_axis_value(0, gp_axislh);

    // Manual deadzone check and physics velocity application
    if (axis < -0.5) phy_linear_velocity_x = -50;
    else if (axis > 0.5) phy_linear_velocity_x = 50;
    else phy_linear_velocity_x = 0;

    // gp_face1 represents the bottom face button (e.g., 'A' on Xbox)
    if (gamepad_button_check_pressed(0, gp_face1)) {
            physics_apply_impulse(x, y, 0, -500);
    }
}

In q5play, controller input abstracts away slot checking and deadzones automatically. It handles controllers using the same readable string inputs as the keyboard:

q5.update = function () {
    if (contro.pressing('left')) player.vel.x = -5;
    else if (contro.pressing('right')) player.vel.x = 5;
    else player.vel.x = 0;

    if (contro.presses('a')) player.vel.y = -10;
};

Disconnected Events vs. Inline Collision Logic

In GameMaker, reacting to a collision (such as a player collecting a coin) typically requires leaving your Step event script and creating a completely separate "Collision Event" tab in the IDE, which interrupts the flow of reading and writing code:

// GameMaker GML - Collision Event with obj_Coin
instance_destroy(other);
global.score += 1;

In q5play, collision and overlap rules can be defined dynamically and passed as inline callbacks, keeping your logic centralized and easy to track:

player.overlaps(coins, (player, coin) => {
    coin.remove();
    score += 1;
});

Phaser 👽

Phaser is a capable JavaScript game engine, but its steep learning curve and boilerplate requirements make it a poor fit for introductory courses.

For example, loading animations from a sprite sheet requires a substantial amount of configuration code in Phaser, whereas the same task takes only a few lines in q5play.

Phaser itself is free and open source, but their optional visual Phaser Editor is priced at $12 per month, compared to q5play's $12 per year for academic use.

Phaser's team ported the Box2D v3 physics simulator to JavaScript and open sourced it, which was a meaningful contribution to the ecosystem. However, this decision was driven by the constraints of mobile advertising platforms, which don't allow WebAssembly. Since q5play is built for full game development rather than ad delivery, it uses Box2D v3 compiled to WASM, which offers 2–3x better performance compared to Phaser's JavaScript port.

Technical Comparison

q5play vs. Phaser 👽

Adding Animations

Loading and playing a sprite sheet animation in Phaser requires preloading the asset, defining an animation configuration object, creating the physics sprite, and then playing the animation:

// Phaser
this.load.spritesheet('player', 'assets/player.png', {
    frameWidth: 32,
    frameHeight: 48
});

this.anims.create({
    key: 'walk',
    frames: this.anims.generateFrameNumbers('player', { start: 0, end: 3 }),
    frameRate: 10,
    repeat: -1
});

this.player = this.physics.add.sprite(100, 450, 'player');
this.player.anims.play('walk', true);

In q5play:

let player = new Sprite(0, 0, 32, 48);
player.addAnis({ walk: { row: 0, frames: 4, frameDelay: 6 } });
player.changeAni('walk');

Binding vs. Direct Keyboard Input

Supporting both arrow keys and WASD in Phaser requires calling createCursorKeys() for arrow keys and then separately registering each WASD key with addKeys() using verbose Phaser.Input.Keyboard.KeyCodes constants, all in the scene's create method before any input can be read:

// Phaser
// Inside create():
this.cursors = this.input.keyboard.createCursorKeys();
this.wasd = this.input.keyboard.addKeys({
    up: Phaser.Input.Keyboard.KeyCodes.W,
    left: Phaser.Input.Keyboard.KeyCodes.A,
    down: Phaser.Input.Keyboard.KeyCodes.S,
    right: Phaser.Input.Keyboard.KeyCodes.D
});

// Inside update():
if (this.cursors.left.isDown || this.wasd.left.isDown) {
    this.player.setVelocityX(-200);
} else if (this.cursors.right.isDown || this.wasd.right.isDown) {
    this.player.setVelocityX(200);
} else {
    this.player.setVelocityX(0);
}
if (this.cursors.up.isDown || this.wasd.up.isDown) {
    this.player.setVelocityY(-500);
}

In q5play, no setup is required. Direction names (up, down, left, right) refer to the arrow and WASD keys.

q5.update = function () {
    if (kb.pressing('left')) player.vel.x = -5;
    else if (kb.pressing('right')) player.vel.x = 5;
    else player.vel.x = 0;

    if (kb.presses('up')) player.vel.y = -10;
};

Pygame 🐍

Pygame is a Python library for game development and a common recommendation for educators who teach Python. However, it is considerably more low-level than q5play: there is no built-in physics engine, no sprite sheet management, and no scene or animation system. Students end up writing a substantial amount of infrastructure code before they can build anything meaningful.

Pygame also requires a local Python installation, which creates friction in school environments. Projects cannot be easily shared via a URL without relying on third-party compilation tools like pygbag. Students must otherwise distribute files or set up a runtime environment for others to run their work, which is cumbersome.

q5play supports Python and runs entirely in the browser with no installation or setup required. Students can share their games with a single link. For CS teachers who want their students to write Python while building engaging, shareable projects, q5play is the more practical choice.

Technical Comparison

q5play vs. Pygame 🐍

Manual Loops vs. Automated Physics Math

Adding a tilted floor in Pygame requires the same coordinate-space transformation as p5.js; there is no physics engine, so students must transform to floor-local space, compute the surface normal, reflect velocity, and nudge the ball out along that normal. On top of all that, Pygame requires a full game loop with manual event processing and explicit draw calls every frame:

# Pygame
import pygame, math

pygame.init()

screen = pygame.display.set_mode((400, 400))
clock = pygame.time.Clock()
ball = {'x': 150, 'y': 50, 'r': 25, 'vx': 0, 'vy': 0}
floor = {'cx': 200, 'cy': 300, 'w': 300, 'h': 10, 'angle': 10}
running = True

while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
    ball['vy'] += 0.5  # gravity
    rad = math.radians(floor['angle'])
    dx = ball['x'] - floor['cx']
    dy = ball['y'] - floor['cy']
    lx = dx * math.cos(-rad) - dy * math.sin(-rad)
    ly = dx * math.sin(-rad) + dy * math.cos(-rad)
    if abs(lx) < floor['w'] / 2 and ly + ball['r'] > 0 and ly - ball['r'] < floor['h']:
        nx = -math.sin(rad)
        ny = math.cos(rad)
        dot = ball['vx'] * nx + ball['vy'] * ny
        if dot < 0:
            ball['vx'] -= 2 * dot * nx
            ball['vy'] -= 2 * dot * ny
        overlap = ball['r'] - ly
        ball['x'] -= overlap * math.sin(rad)
        ball['y'] += overlap * math.cos(rad)
    ball['x'] += ball['vx']
    ball['y'] += ball['vy']
    hw = floor['w'] / 2
    corners = [(-hw, 0), (hw, 0), (hw, floor['h']), (-hw, floor['h'])]
    rotated = [
        (floor['cx'] + x * math.cos(rad) - y * math.sin(rad),
         floor['cy'] + x * math.sin(rad) + y * math.cos(rad))
        for x, y in corners
    ]
    screen.fill((220, 220, 220))
    pygame.draw.polygon(screen, (50, 50, 50), rotated)
    pygame.draw.circle(screen, (0, 100, 200), (int(ball['x']), int(ball['y'])), ball['r'])
    pygame.display.flip()
    clock.tick(60)

pygame.quit()

In q5play's Python mode:

Canvas(400, 400)
world.gravity.y = 10

ball = Sprite.new(0, -150, 50)

floor = Sprite.new(0, 150, 400, 10, STATIC)
floor.rotation = 10

Separated Checks vs. Synchronous Event Loops

Keyboard input in Pygame splits across two separate mechanisms: continuous state requires polling pygame.key.get_pressed() each frame, while single-press detection requires checking KEYDOWN events inside the event loop. Logic that belongs together ends up in two different places:

# Pygame, continuous and single-press input live in different places
while running:
    for event in pygame.event.get():
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_SPACE:  # jump: only detectable here
                player['vy'] = -10

    keys = pygame.key.get_pressed()
    if keys[pygame.K_LEFT] or keys[pygame.K_a]:
        player['vx'] = -5
    elif keys[pygame.K_RIGHT] or keys[pygame.K_d]:
        player['vx'] = 5
    else:
        player['vx'] = 0
    # ... physics update, draw calls

In q5play's Python mode, pressing and presses unify all input in the same place:

if kb.pressing('left') or kb.pressing('a'):
    player.vel.x = -5
elif kb.pressing('right') or kb.pressing('d'):
    player.vel.x = 5
else:
    player.vel.x = 0

if kb.presses('space'):
    player.vel.y = -10

Godot 🤖

Godot is a free, open-source game engine that has grown significantly in popularity, particularly following Unity's 2023 licensing controversy. It is a capable and well-designed tool for students who want to pursue game development seriously.

That said, Godot is a full desktop application that must be installed and is not well suited for introductory CS courses. Its primary scripting language, GDScript, is Python-inspired but distinct enough to require dedicated learning. C# is also supported, but adds additional setup complexity. The engine's node-and-scene architecture, while powerful, introduces conceptual overhead that can slow progress for beginners.

q5play is a better starting point for students new to programming. Once students have a solid foundation in JavaScript or Python through q5play, transitioning to Godot for more advanced 3D or desktop game projects is a natural progression.

Technical Comparison

q5play vs. Godot 🤖

Scene Nodes vs. Single Lines Architecture

In Godot, a ball bouncing off a tilted floor requires separate RigidBody2D and StaticBody2D nodes, each with a CollisionShape2D child. Setting bounce requires creating and assigning a PhysicsMaterial. Without the scene editor, all of this must be done programmatically across two node scripts:

# Godot GDScript - Ball (RigidBody2D)
extends RigidBody2D

func _ready():
    var shape = CircleShape2D.new()
    shape.radius = 25.0
    var col = CollisionShape2D.new()
    col.shape = shape
    add_child(col)
    var mat = PhysicsMaterial.new()
    mat.bounce = 0.8
    physics_material_override = mat

# Godot GDScript - Floor (StaticBody2D)
extends StaticBody2D

func _ready():
    rotation_degrees = 10
    var shape = RectangleShape2D.new()
    shape.size = Vector2(400, 10)
    var col = CollisionShape2D.new()
    col.shape = shape
    add_child(col)

In q5play:

await Canvas(400, 400);
world.gravity.y = 10;

let ball = new Sprite(0, -150, 50);
let floor = new Sprite(0, 150, 400, 10, STATIC);
floor.rotation = 10;

Math vs. Expressive Movement Vectors

Moving a player toward the mouse in Godot requires computing a direction vector, normalizing it, scaling by a speed constant, and calling move_and_slide() inside the _physics_process callback:

# Godot GDScript
extends CharacterBody2D

const SPEED = 200.0

func _physics_process(delta):
    var target = get_global_mouse_position()
    var direction = (target - global_position).normalized()
    velocity = direction * SPEED
    move_and_slide()

In q5play, a single call handles all of that:

q5.update = function () {
    player.moveTowards(mouse, 0.1);
};

Action Configs vs. Instant Input Systems

Keyboard movement in Godot requires using the Input singleton with named action bindings that must be configured separately in the project settings:

# Godot GDScript
func _physics_process(delta):
    var direction = Input.get_axis("move_left", "move_right")
    velocity.x = direction * SPEED
    if Input.is_action_just_pressed("jump") and is_on_floor():
        velocity.y = JUMP_VELOCITY
    move_and_slide()

In q5play:

q5.update = function () {
    if (kb.pressing('left')) player.vel.x = -5;
    else if (kb.pressing('right')) player.vel.x = 5;
    else player.vel.x = 0;

    if (kb.presses('up') && player.colliding(floor)) player.vel.y = -10;
};

Abstract Mapping vs. Immediate Controller Bindings

Godot's gamepad support also routes through the Input singleton, requiring device index tracking and action bindings registered in the project settings:

# Godot GDScript
func _physics_process(delta):
    var axis = Input.get_joy_axis(0, JOY_AXIS_LEFT_X)
    velocity.x = axis * SPEED
    if Input.is_joy_button_pressed(0, JOY_BUTTON_A) and is_on_floor():
        velocity.y = JUMP_VELOCITY
    move_and_slide()

In q5play:

q5.update = function () {
    if (contro.pressing('left')) player.vel.x = -5;
    else if (contro.pressing('right')) player.vel.x = 5;
    else player.vel.x = 0;

    if (contro.presses('a') && player.colliding(floor)) player.vel.y = -10;
};

KaPlay 💥

Kaboom.js was a JavaScript game library that gained some traction in hobbyist communities. It was subsequently rebranded as Kaplay following a change in maintainership.

KaPlay's API introduces a non-standard mental model of creating objects that does not transfer to other libraries or real-world codebases.

Technical Comparison

q5play vs. KaPlay 💥

Function Arrays vs. Declarative Object Creation

KaPlay's core API requires students to create game objects by passing an array of function calls to an add() function, where each element is the return value of a component factory:

// Kaplay
add([
    rect(40, 40),
    pos(100, 200),
    color(0, 0, 255),
    area(),
    body() // huh?
]);

This pattern (calling functions inside an array literal) is not how object oriented programming is typically done in JavaScript or any other programming language. It introduces a non-standard mental model that does not transfer to other libraries or real-world codebases.

In q5play, the same physics-enabled object is created with standard JS class instantiation and property assignment:

let box = new Sprite(100, 200, 40, 40);
box.color = 'blue';

Object Literals vs. Setup Parameters

Even basic setup requires knowledge of object literal syntax. Resizing the canvas means passing a configuration object to the kaplay() initializer:

// Kaplay
kaplay({
    width: 640,
    height: 480
});

For beginners who have not yet learned about objects and key-value pairs, this is a barrier on day one, before a single sprite has been drawn. In q5play, the canvas is created with a straightforward function call:

await Canvas(640, 480);

Separate Listeners vs. Per-Frame Input

Keyboard input in Kaplay requires registering separate onKeyDown and onKeyRelease event handlers for each direction:

// Kaplay
onKeyDown('left', () => {
    player.vel.x = -5;
});
onKeyDown('right', () => {
    player.vel.x = 5;
});
onKeyRelease('left', () => {
    player.vel.x = 0;
});
onKeyRelease('right', () => {
    player.vel.x = 0;
});

In q5play, the same logic is a single conditional block inside the update loop:

q5.update = function () {
    if (kb.pressing('left')) player.vel.x = -5;
    else if (kb.pressing('right')) player.vel.x = 5;
    else player.vel.x = 0;
};