Skip to content
JT
  • Home
  • Projects
  • Extras
  • Contact
  • Home
  • Projects
  • Extras
  • Contact

© 2026 Joshua Tjhie. All rights reserved.

GitHubLinkedIn
Back to Projects
  1. Home
  2. Projects
  3. Copperfall

Copperfall

An 8×8 PNG becomes a playable chunk. Underneath Copperfall is a JavaScript engine I built for scenes, collisions, controls, and a world that remembers where you have been.

Games
Copperfall

Details

JavaScriptCanvas 2DHTMLCSS

Links

Play in the BrowserSource on GitHub

An 8 by 8 pixel image is enough to give Copperfall a new chunk of world. White pixels become rocks, green pixels become copper pickups, and other colours place enemies. Editing the route through an encounter can mean moving a handful of pixels.

I built Copperfall and its 2D engine for my first semester in Creative Computing at St. Pölten University of Applied Sciences. JavaScript and Canvas draw the game. HTML and CSS handle the menus, HUD, and control prompts. The source runs as browser modules without a build step.

Copperfall gameplay, with the player beside a spaceship inside a rocky map containing enemies and pickups.
The player beside the starting ship. Rocks, enemies, and pickups occupy the generated chunks around it.

What the Engine Owns

The structure takes inspiration from Unity. A scene creates game objects; each object has a transform and a collection of components. A sprite renderer draws the object, a collider describes its contact area, and effects such as squash and stretch can be attached separately.

Creating a game object registers it with the engine and the active scene. Adding a component attaches its owner and calls its start method. Each object forwards update and render calls to its components. This let me reuse effects across objects without putting every behaviour in a shared parent class.

Scene changes have an explicit cleanup path. The old scene marks its objects as destroyed, except those marked persistent. The engine removes destroyed objects from its active list on the next frame. The input handler is persistent, so a scene change does not need another set of keyboard listeners.

Copperfall’s Engine and World
  • Data & Assets
  • Logic & Services
  • Interface & Presentation
  • Platform & External

Loading diagram...

Startup runs from index.html through bootstrap.js into main.js. This is an object-and-component engine. Chunk state survives unloading; only registered objects take part in the active frame and collision passes.

Explore the Source
  • Browser Bootstrap
  • Engine Loop
  • Game Object
  • Chunk Manager
  • Blueprint Decoder
  • Collision System

Copperfall’s Engine and World

Scroll to explore · 1:1 for readable labels
  • Data & Assets
  • Logic & Services
  • Interface & Presentation
  • Platform & External

The map separates the active object list from retained chunk state. That distinction determines which objects consume frame time and which changes survive a return visit.

Following One Frame

The loop uses requestAnimationFrame and calculates elapsed time in seconds. It clears the canvas, removes destroyed objects, and sorts the active list by sprite order. It then updates and draws each object before the collision pass.

One Frame in Copperfall

Loading diagram...

This is the order in Engine.js. Each object's update and render happen together; collision resolution follows the drawing pass.

One Frame in Copperfall

Scroll to explore · 1:1 for readable labels

That ordering is a useful limit to make explicit. A position correction from a collision appears in the following draw. The loop also uses the elapsed browser-frame time directly, rather than a fixed simulation step. Those are simple choices for this project, with consequences for how it responds to uneven frame timing.

The camera and sprite renderer share a world-to-screen conversion. They subtract the camera position, apply its scale, and offset the result to the centre of the canvas. They also reverse the vertical axis, because positive world height and Canvas pixel coordinates run in opposite directions.

Seeing the Shapes Behind the Sprites

Debug mode draws the information the artwork normally hides. Green outlines are solid colliders, magenta outlines are triggers, and red outlines mark chunk boundaries. A grid at 50 world-unit intervals gives those shapes a common reference.

Copperfall running in debug mode. Green collision boxes surround the rocks and player, magenta circles mark pickup and interaction triggers, and a coordinate grid crosses the starting chunk.
A fresh capture of the game's existing debug renderer. The player's physical box and larger circular trigger have different jobs.

The player makes that distinction visible. Its box stops it passing through solid objects. Its circular trigger detects interaction range, including the spaceship zone. A trigger can report contact without pushing either object away.

The collision system gathers box and circle colliders from active objects. It supports box-to-box, circle-to-circle, and mixed checks. Each collider remembers its current contacts, so a new overlap produces an enter callback, a continuing overlap produces stay, and separation produces exit.

Detection and physical response have different coverage. The overlap resolver handles axis-aligned boxes. It separates them along the axis with the least penetration, moves the dynamic object away from a static one, or divides the correction between two dynamic objects. Circle contacts can trigger events, but this is not a general circle-physics solver.

Painting the Pieces of a World

BaseChunk draws a blueprint into an offscreen canvas, optionally rotates it by 90, 180, or 270 degrees, then reads its pixel colours. The starter chunks keep their authored orientation. A normal chunk chooses an image from its type's pool before rotation.

Blueprint ColourObject Created
WhiteRock
GreenCopper pickup
BlueDestructible rock
RedBasic enemy
MagentaPatrol enemy
YellowFast enemy

Each chunk spans 400 world units. Its eight cells along each side are therefore 50 units wide. The loader places an object at the centre of its cell, reverses the image's vertical direction, and adds the chunk's world offset. Unmapped colours and transparent pixels leave space empty.

The colour lookup produces at most one object per cell. Layout changes stay in the images, while a new object type needs a constructor and a colour mapping. This keeps encounter authoring small enough to inspect in an image editor.

Chunk type also changes with distance from the starting ship's chunk. The nearby pool gives way to another at 1,600 world units and a third at 3,200. The world is assembled from authored pieces, with variation in which piece appears and how it is turned.

Leaving a Chunk Without Forgetting It

The chunk manager activates chunks whose centres are within 800 world units of the player. Moving away removes their objects from the engine's active list, so those objects stop updating, drawing, and colliding. Their chunk records remain in memory.

What Happens When You Return

Loading diagram...

The saved chunk contains the existing object instances. Reactivation skips children already marked destroyed.

What Happens When You Return

Scroll to explore · 1:1 for readable labels

Returning restores the surviving instances. A destroyed rock or collected pickup does not come back simply because its chunk becomes active again. The ship's starter chunk is an exception to distance unloading and stays registered.

This matters especially because collision detection checks every pair of active colliders. With 100 colliders, that is 4,950 pairs; with 200, it is 19,900. These are counts from the loop's structure, not measured frame rates. Keeping distant colliders out of that list limits the work, while retaining visited chunks still costs memory.

One Dash Across Two Input Devices

Keyboard and gamepad input feed shared actions such as dash and interact. Objects subscribe to those actions instead of checking device-specific buttons. The input handler normalizes movement direction and applies a deadzone to the gamepad stick.

Movement has its own acceleration and deceleration states. The player reaches normal speed through an easing curve, while dash uses a separate duration, speed, cooldown, and rechargeable charges. Sprite changes, particles, and screen shake provide feedback around those movement rules.

The browser UI also sends menu-open and menu-close events. The player listens to those events to stop accepting movement while a menu is open. Drawing the menus in HTML made buttons and layout easier to author, but it also made that boundary between gameplay input and UI input something I had to handle.

What I Would Change Next Time

A broader world would need a better collision search before it needed more objects. A spatial grid could reduce candidate pairs. Separating all simulation updates from rendering would also make the frame order easier to reason about, and visited chunks would need an eviction or serialization policy if memory became a constraint.

The part I would keep is the short path between an authored pixel, the object it creates, and the debug shape visible in the game. Each stage is small enough to inspect when the result is wrong.

Generating a 3D Arena

The Circussy One
The Circussy One
A Unity roguelite where the audience climbs over itself to reach you. I built its gameplay architecture and content tools as the sole programmer in a four-person team.
Project•Games

Questions About This Project?

Ask about how it works, or point out something I missed.

Contact me