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.
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!
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.
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
|
<>
CODEVRE
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 | — | ✓ |
|
|
Looking for a school or district wide license?
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.
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;
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 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.
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!');
}
};
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.
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
}
};
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 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.
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;
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;
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'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.
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';
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++;
}
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;
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.
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;
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);
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 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.
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;
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;
};
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 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.
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');
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 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.
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
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 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.
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;
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);
};
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;
};
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;
};
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.
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';
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);
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;
};