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 fantastic platform for students ages 8–11, but its guardrails quickly become limiting for older students ready to graduate from blocks to text-based coding.
Scratch has no built-in physics engine, so something as simple as rolling a ball down a slope requires significant math that's tedious to implement in Scratch's block-based environment.
Many of the featured projects on the Scratch website were evidently made by adults challenging themselves to push beyond Scratch's practical limits. This sets unrealistic expectations for students.
In contrast, q5play empowers students with beginner-friendly APIs that make professional-grade physics accessible to anyone. By handling common boilerplate tasks under the hood, students can focus on the creative aspects of game design.
Something as basic as rolling a ball down a slope is a nightmare to implement in Scratch.
Naive attempts are typically inefficient and brittle, with user code that manually manages velocity, gravity, and collision resolution. Often the result is jittering, tunneling, and other glitches inexplicable to beginners.
// Scratch blocks (Ball sprite)
// Students must manually manage vectors, slopes, and collision resolution.
when green flag clicked
set [slope_angle v] to (15)
set [vx v] to (0)
set [vy v] to (0)
forever
// 1. Calculate 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 severe jitter and tunneling)
if <touching [Floor v]?> then
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
q5play eliminates this frustration by using Box2D for physics. Gravity, collision detection, and resolution are handled automatically by the engine. This allows students to create interesting interactions with just a few lines of code, without needing to understand the underlying physics math.
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;
Because Scratch scripts cannot easily communicate, the engine relies heavily on asynchronous broadcast messages to trigger events across dozens of isolated sprite scripts.
This creates "Broadcast Spaghetti": a tangled web of trigger responses that makes it hard for students to track the flow of their own code. Worse, because Scratch processes these scripts asynchronously, it causes race conditions. If a trigger response tries to delete a sprite that another area of code depends on, it could lead to game-breaking, unpredictable bugs.
// 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 executes naturally from top to bottom. By using built-in contact methods like
overlaps, q5play guarantees that interactions are handled safely,
predictably, and in the exact order the programmer specifies.
q5.update = function () {
// Synchronous, predictable, and structurally clean
if (player.overlaps(coin)) {
coin.delete();
score++;
}
};
Unity is the industry standard for professional game development, but its editor-first workflow and vast enterprise toolset introduces significant cognitive overhead for beginners.
For university-level Game Design and CS courses, Unity’s complex GUI can obscure fundamental game programming concepts. Students often spend more time navigating the menu mazes of this specific software than mastering core logic.
Unity has to compile after every minor edit students make, which only takes a few seconds, but these micro-interruptions add up as friction that stifles creative experimentation. In contrast, q5play runs instantly in the browser, resulting in a more rewarding feedback loop.
Under the hood, q5play uses the same Box2D v3 physics engine as Unity, so even if your primary goal is teaching Unity, q5play can serve as a pedagogical stepping stone. Once students have a solid foundation in programming logic and physics simulation via q5play, transitioning to Unity becomes far less overwhelming. Although adult students will be able to breeze through learning q5play, it's not just a toy engine for kids. It's designed to be a joy to use for developers of all ages interested in rapidly prototyping games and sharing them on the web.
At the high school level, teaching Unity is often unfeasible. Unity is a massive desktop application, so it can't run on Chromebooks or iPads. q5play runs in the browser, requiring no installation. Additionally, while teaching Unity demands extensive training due to its notoriously steep learning curve, educators can comfortably get up to speed with q5play in a matter of hours. By lowering the barrier to entry, students can accomplish more in a few weeks with q5play than they could after months of struggling with Unity.
Creating a 2D physics interaction in Unity requires constant context switching. A developer must navigate the Inspector to configure Rigidbodies, adjust physics materials, define Layer Collision Matrices, assign Tags, and then switch to an IDE to bind it all together with C#.
Even idiomatic, well-written Unity C# requires boilerplate to cache components and safely hook into the physics step:
// Unity C#
[RequireComponent(typeof(Rigidbody2D))]
public class Player : MonoBehaviour {
private Rigidbody2D rb;
void Awake() {
rb = GetComponent<Rigidbody2D>();
}
void OnCollisionEnter2D(Collision2D col) {
if (col.gameObject.CompareTag("Floor")) {
Debug.Log("Landed!");
}
}
}
In q5play, that entire workflow, creating the player with a physics collider and assigning an image, is accomplished in just a few lines of highly readable code. This unified approach eliminates Editor-IDE friction, making it ideal for rapid prototyping:
let player = new Sprite(0, 0, 50);
player.img = 'assets/player.webp';
q5.update = function () {
if (player.collides(floor)) {
console.log('landed!');
}
};
Unity enforces a strict Component-based architecture (GameObjects and MonoBehaviours). While essential for scaling AAA games, this scatters game state across serialized Inspector variables, prefabs, and isolated scripts. For students or developers testing a quick mechanic, tracing how a variable mutates across these layers can be needlessly complex.
q5play is built on straightforward, code-first Entity principles. A Sprite is a unified object where physical properties (x, y, velocity, mass, bounciness) are manipulated directly in the script. This 1:1 mapping between the typed code and the on-screen outcome reinforces foundational programming paradigms and allows developers to test gameplay systems instantly without configuring Editor serialization.
Handling inputs in Unity for production requires robust solutions like the newer Input System package, which entails configuring Action Maps, creating input assets, and passing callback contexts. It is brilliant for handling simultaneous controller support, but massive overkill for testing a jump mechanic in a prototype.
Even using the legacy immediate-mode input requires verbose code:
// Unity C#
if (Input.GetKeyDown(KeyCode.Space)) {
GetComponent<Rigidbody2D>().AddForce(new Vector2(0, 10), ForceMode2D.Impulse);
}
q5play handles device inputs natively with a syntax tailored for readability. Keyboard, mouse, and touch states are accessible directly within the update loop, requiring zero prior configuration:
if (kb.presses('space')) {
player.vel.y = -10;
}
Unity is fundamentally designed as a heavy-duty production engine. Exporting a Unity game to the web, even with modern WebGPU support, relies on IL2CPP (compiling C# to C++ to WebAssembly) alongside heavy rendering pipelines. This results in significant compilation times and large payload sizes, which creates friction for peer review, game jams, and portfolio building.
q5play is web-native from the ground up. It leverages modern WebGPU rendering alongside Box2D compiled to WebAssembly to deliver hardware-accelerated performance directly in the browser.
Because the library is lightweight, there are zero compilation cycles. The code executes instantly on a page refresh. For courses prioritizing iteration velocity, peer playtesting, and portfolio deployment, q5play provides a much tighter, more rewarding design loop than wrangling WebGL builds in a traditional engine.
p5.js is a fantastic tool for creative coding, but it's 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. q5 offers up to ~100× better performance than p5's fastest renderer (Canvas2D).
For teachers already familiar with p5.js, the transition is virtually frictionless. q5's API is largely compatible with p5, so existing knowledge carries over pretty seamlessly.
Importantly, Codevre's q5 Web Editor supports using q5play in Python, whereas 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 an engaging starting point for young coders, but it's too restrictive for students ready to graduate from block-based coding.
While constraints can inspire creativity, MakeCode Arcade's fixed 160×120 screen resolution, basic arcade physics, and inability to load external images are frustrating bottlenecks for older students with greater ambitions. These severe limitations exist because MakeCode is narrowly designed to remain compatible with ultra-low-spec handheld devices, that schools can purchase. q5play allows students to fully utilize the modern computing power already sitting on their desks.
Although MakeCode technically supports switching between blocks, JavaScript, and Python, the platform was clearly designed with block coding as the primary interface. The text-based coding experience is marred by incredibly verbose namespacing, overuse of enums, and callback heavy design patterns. Furthermore, as a closed ecosystem, MakeCode prevents students from exporting their games as standalone web pages, integrating third-party APIs, or interacting with the broader browser environment.
q5play bridges the massive gap between MakeCode Arcade and the complexity of engines like Unity and Godot. By providing professional-grade Box2D physics simulation under the hood without the intimidating overhead of a full AAA toolset, it serves as the ideal next step for students ready to explore modern game design.
MakeCode Arcade forces students to make assets through its built-in pixel-art editor. While convenient for first-time coders, it inherently limits a game's visual identity. Importing high-resolution external images is effectively walled off in the standard web interface.
// MakeCode Arcade: Assets are hardcoded and limited to low-res pixel art grids.
let player = sprites.create(
img`
. . . . . . . . . .
. . . f f f f . . .
. . f e f f e f . .
f f f f f f f f f f
`,
SpriteKind.Player
);
q5play allows students to decouple their visual assets from their code, seamlessly loading high-resolution images in modern formats like ".webp" and ".avif". This lets students collaborate with digital artists and use assets from the vibrant ecosystem of game art available online.
let player = new Sprite();
player.img = 'player.avif';
But q5play also has a spriteArt function for creating pixel art with text
and q5 has displayMode(MAXED, PIXELATED) to achieve a classic retro look
when desired. With q5play, students have the freedom to choose their own artistic style.
MakeCode Arcade relies heavily on an event-driven architecture. Students register asynchronous callbacks (onOverlap, onEvent) that trigger when specific actions occur. This is an excellent way to introduce event listeners, but as games grow in complexity, scattering logic across multiple asynchronous blocks can make it difficult for students to debug the chronological flow of their game state. Unpredictable execution order can lead to race conditions. Although MakeCode Arcade does have a main update loop, students can not check input or contact states in it.
// MakeCode Arcade
controller.A.onEvent(ControllerButtonEvent.Pressed, function () {
player.vy = -200;
});
sprites.onOverlap(SpriteKind.Player, SpriteKind.Coin, function (player, coin) {
coin.destroy();
info.changeScoreBy(1);
});
q5play has its own event system, but it primarily encourages students to check inputs and collisions directly within the main update loop. This promotes a more intuitive, top-to-bottom flow of logic that is easier to reason about and debug as game states become more complex.
if (contro.presses('a')) {
player.vel.y = -10;
}
if (player.overlaps(coin)) {
coin.delete();
score++;
}
The most significant technical limitation of MakeCode Arcade is its physics engine. It utilizes a bare-bones, axis-aligned bounding box (AABB) model. Objects exist as rigid rectangles that cannot rotate. While perfect for recreating grid-based classics like Pac-Man, it cannot handle angular momentum, sloped terrain, or dynamic mass. Furthermore, its lack of Continuous Collision Detection (CCD) means fast-moving objects can easily "tunnel" through walls.
// MakeCode Arcade: Movement is strictly linear. No rotation or dynamic mass.
let crate = sprites.create(img`...`, SpriteKind.Enemy);
crate.vy = 100; // Falls straight down
crate.setBounceOnWall(true); // Basic linear bounce, no energy loss modeling
q5play is powered by Box2D v3, the same industry-standard 2D physics simulator used in the dynamic, physics-driven games that dominates the modern indie market. This unlocks entirely new genres of mechanics for students. Objects possess mass, friction, and restitution (bounciness). Characters can tumble down angled slopes, joints can swing like pendulums, and angular velocity can be manipulated.
let crate = new Sprite(200, 50, 40, 40);
crate.mass = 5;
crate.bounciness = 0.6;
crate.friction = 0.8;
// the crate will naturally tumble and rotate down this slope
let ramp = new Sprite(200, 300, 300, 20, 'static');
ramp.rotation = 15;
q5.update = function () {
// Apply a rotational force (torque) dynamically
if (kb.pressing('right')) {
crate.applyTorque(10);
}
};
Code.org’s Game Lab was an important stepping stone for a generation of students. But despite millions in annual funding from Big Tech corporations and billionaires, it hasn't received a meaningful update in a decade.
Its custom fork of p5.play v1 operates within the fixed resolution of a 400×400 canvas. On typical student hardware its performance is so poor, it can't handle in realtime more than a dozen sprites at once.
Although Game Lab technically supports switching between blocks and JavaScript code, it's a closed sandbox that prevents projects from interacting with the broader browser environment, integrating external JS libraries, or exporting to a standalone web page.
Code.org's shift towards "Hour of AI" initiatives, left their creative coding tools in the dust. These days, Game Lab can only offer disappointment to students capable of more ambitious work with modern web technologies.
If you're looking for a natural upgrade from Code.org's p5.play v1 fork, look no further! q5play takes everything you loved about Code.org Game Lab's p5.play v1 API and puts it in an open platform designed to empower students to build real games backed by professional-grade Box2D physics simulation.
A core philosophy of Game Lab is explicit configuration. Sprites are rendered as generic squares without physics colliders unless a student manually defines them. This explicit approach requires students to manage the underlying state of every object constantly. If a student wants to see a collider's shape, they must manually enable debug mode.
In this Game Lab example, creating a bouncing ball requires manually defining the shape, setting the bounciness, and manipulating velocity every frame:
// 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');
// Gravity must be applied manually every frame
ball.velocityY += 0.5;
ball.bounce(floor);
drawSprites();
}
By contrast, q5play operates on "smart defaults" driven by an underlying physics engine (Box2D). Sprites are automatically assigned colliders that match their visual dimensions. Because physics are simulated globally rather than per-object, properties like gravity are defined once for the world, resulting in cleaner, more readable code that mirrors professional game engines:
await Canvas(400, 400);
world.gravity.y = 10; // Defined globally once
let ball = new Sprite(0, -150, 50);
ball.bounciness = 0.5;
let floor = new Sprite(0, 150, 400, 10, STATIC);
floor.rotation = 10;
Game Lab teaches collision through explicit, pairwise function calls. The
sprite.collide(other) method resolves displacement between two specific
sprites at the exact moment it is called in the draw loop.
While this effectively teaches beginners about the order of operations, it quickly becomes unmanageable for complex scenes. To stack multiple boxes, a student must manually check collisions from the bottom up. If the order is wrong, or if a dynamic structure (like a pyramid of crates) is introduced, the pairwise logic fails, and objects clip through one another.
// Code.org Game Lab: Order-dependent pairwise logic
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;
// Brute-force order of operations is required
if (box2.collide(floor)) vy2 = 0;
if (box1.collide(box2)) vy1 = 0;
if (box1.collide(floor)) vy1 = 0;
drawSprites();
}
In q5play, the Box2D engine handles collision mathematically and globally. Because objects possess mass, friction, and restitution by default, they interact organically. Stacking a tower of boxes requires zero explicit collision code in the draw loop; the engine resolves all contacts simultaneously during each physics step. This enables students to focus higher up the design stack, rather than getting bogged down in the minutiae of collision order.
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);
// No explicit collision checks needed; Box2D handles stacking natively.
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's team ported the Box2D v3 physics simulator to JavaScript and open sourced it, which was a meaningful contribution to the web gaming 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's no built-in physics engine and no 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
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;
};