Build a HTML5 “Helix Jump” prototype with Three.js and TypeScript

Talking about Helix Jump game, 3D, HTML5, Javascript and TypeScript.

You probably played Helix Jump, the popular hyper-casual game by Voodoo where a ball bounces down a rotating tower made of platforms.

It’s simple, addictive, and — most importantly — the kind of game that’s perfect to prototype with Three.js.

In this post, we’ll see how to start building a fully 3D Helix Jump-like game using Three.js, TypeScript, and Vite — a modern build tool that makes development fast and easy.

In case you are not that familiar with TypeScript Build tools, I wrote a free guide for you.

Three.js makes it easy to create 3D games directly in the browser.

Let’s see the final result of this first step:

Press A and D to rotate the tower counter clockwise and clockwise.

The central column is made of a simple THREE.CylinderGeometry, with attached a group of platforms that will form the spiral path the ball will follow.

Each platform is a partial cylinder (think about a pizza slice) made with CylinderGeometry, using the thetaStart and thetaLength parameters to carve out a sector of the full 360 degrees circle.

To close the open edges visually, I added two PlaneGeometry meshes as sides, positioned and rotated to perfectly align with the start and end angles of the slice.

Now, look at the completely commented source code, which consists in one HTML file, one CSS file and three TypeScript files.

index.html

The web page which hosts the game.

HTML
<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Helix Jump</title>
      <script type="module" crossorigin src="./assets/index-xBnltU75.js"></script>
      <link rel="stylesheet" crossorigin href="./assets/index-TZrNw7dA.css">
    </head>
    <body>
    </body>
</html>

style.css

The cascading style sheets of the main web page.

CSS
body {
    margin: 0
}

gameOptions.ts

Configurable game options. Changing these values affects the gameplay.

TypeScript
export const GameOptions = {
    columnRadius    : 1,                // column radius
    columnColor     : 0xffffff,         // column color
    totalPlaftforms : 10,               // total platorms in game
    platformGap     : 3,                // vertical gap between two platorms
    platformRadius  : 3,                // platform radius
    platformHeight  : 1,                // platform heignt
    minThetaLength  : Math.PI * 1.5,    // min theta length, minimum radians of the circular sector
    maxThetaLength  : Math.PI * 1.85,   // max theta length, maximum radians of the circular sector
    rotationSpeed   : 6,                // helix rotation speed
    platformColors  : [0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0x00ffff, 0xff00ff]
}

main.ts

This is where the game is created.

TypeScript
import * as THREE from 'three';                 // import all THREE.js components
import { GameOptions } from './gameOptions';    // import game options
import { Platform } from './platform';          // import game options
import './style.css';                           // import web page style sheet

// create the 3D scene container
const scene: THREE.Scene = new THREE.Scene();

// set up a perspective camera, then manually position and orient it
const camera: THREE.PerspectiveCamera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 4, 12);
camera.lookAt(0, -2, 0);

// create the WebGL renderer with antialiasing enabled
const renderer: THREE.WebGLRenderer = new THREE.WebGLRenderer({
    antialias: true
});

// set the renderer size to match the window
renderer.setSize(window.innerWidth, window.innerHeight);

// enable shadow rendering
renderer.shadowMap.enabled = true;

// use soft shadows for smoother lighting
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

// add the renderer canvas to the DOM
document.body.appendChild(renderer.domElement);

// create an ambient light to softly illuminate the scene
const ambientLight: THREE.AmbientLight = new THREE.AmbientLight(0xffffff, 0.3);

// add the ambient light to the scene
scene.add(ambientLight);

// create a directional light to simulate sunlight
const light: THREE.DirectionalLight = new THREE.DirectionalLight();

// manually position the light source
light.position.set(5, 10, 7.5);

// enable shadow casting from this light
light.castShadow = true;

// add the directional light to the scene
scene.add(light);

// create the geometry for the central column
const columnGeometry: THREE.CylinderGeometry = new THREE.CylinderGeometry(GameOptions.columnRadius, GameOptions.columnRadius, 50);

// create a standard material for the column
const columnMaterial: THREE.MeshStandardMaterial = new THREE.MeshStandardMaterial({
    color : GameOptions.columnColor
});

// create the column mesh using geometry and material
const column: THREE.Mesh = new THREE.Mesh(columnGeometry, columnMaterial);

// enable shadow reception on the column
column.receiveShadow = true;

// add the column to the scene
scene.add(column);

// create a group to hold all platforms
const platformGroup : THREE.Group = new THREE.Group();

// add the platform group to the scene
scene.add(platformGroup);

// build the platforms
for (let i : number = 0; i < GameOptions.totalPlaftforms; i ++) {

    const platform : Platform = new Platform(GameOptions.platformGap * -i);
    
    platformGroup.add(platform);

}
  
// store the key press timestamps or false when released
const keys : { [key: string]: number | false } = {};

// handle keydown events and store press time
window.addEventListener('keydown', (e : KeyboardEvent) => {
    const key : string = e.key.toLowerCase();
    if (!keys[key]) {
        keys[key] = Date.now();
    }
});

// handle keyup events and reset key state
window.addEventListener('keyup', (e : KeyboardEvent) => {
    const key : string = e.key.toLowerCase();
    keys[key] = false;
});

// three clock to measure time between frames
const clock : THREE.Clock = new THREE.Clock();

// function to be executed at each frame
function update() : void {

    requestAnimationFrame(update);
    
    // determine rotation direction according to pressed keys  
    let rotateDirection : number = 0;

    // get the time elapsed since the last frame
    const delta : number = clock.getDelta();
    
    // counter clockwise
    if (keys['a'] && !keys['d']) {
        rotateDirection = 1;
    }
    else  {
        // clockwise
        if (keys['d'] && !keys['a']) {
            rotateDirection = -1;
        }
        else {
            // both directions, so we see which one was the latest
            if (keys['d'] && keys['a']) {
                rotateDirection = (keys['a'] > keys['d']) ? 1 : -1;
            }
        }
    }        

    // apply rotation to the platform group
    platformGroup.rotation.y += rotateDirection * GameOptions.rotationSpeed * delta;
    
    // render the scene from the camera's point of view
    renderer.render(scene, camera);
}
  
update();

platform.ts

Custom class that handles platform group creation.

TypeScript
import * as THREE from 'three';                 // import all THREE.js components
import { GameOptions } from './gameOptions';    // import game options

// Platform class extends THREE.Group
export class Platform extends THREE.Group {
    
    constructor(posY : number) {
        
        super();

        // choose a random rotation angle around the column
        const angle : number = Math.random() * Math.PI * 2;
            
        // choose a random color for this platform
        const material : THREE.MeshStandardMaterial = new THREE.MeshStandardMaterial({
            color : GameOptions.platformColors[Math.floor(Math.random() * GameOptions.platformColors.length)]
        });
            
        // define the angular length of the platform arc
        const thetaLength : number = GameOptions.minThetaLength + Math.random() * (GameOptions.maxThetaLength - GameOptions.minThetaLength); 
          
        // create the curved surface of the platform using a cylinder segment
        const cylinderGeometry: THREE.CylinderGeometry = new THREE.CylinderGeometry(GameOptions.platformRadius, GameOptions.platformRadius, GameOptions.platformHeight, 32, 1, false, 0, thetaLength);
            
        // create a mesh with the cylinder geometry and material
        const cylinder: THREE.Mesh = new THREE.Mesh(cylinderGeometry, material);
        
        // the cylinder casts and receives shadows
        cylinder.castShadow = true;
        cylinder.receiveShadow = true;
            
        // add the cylinder to the platform group
        this.add(cylinder);
          
        // create the first side plane to close the cylinder slice
        const side1: THREE.Mesh = new THREE.Mesh(new THREE.PlaneGeometry(GameOptions.platformRadius, GameOptions.platformHeight), material);
        side1.position.x = 0
        side1.position.z = GameOptions.platformRadius / 2;
        side1.rotation.y = - Math.PI / 2;
        
        // side1 casts and receives shadows
        side1.castShadow = true;
        side1.receiveShadow = true;
        
        // add the side to the platform group
        this.add(side1);
          
        // create the second side plane to close the cylinder slice
        const side2: THREE.Mesh = new THREE.Mesh(new THREE.PlaneGeometry(GameOptions.platformRadius, GameOptions.platformHeight), material);
        side2.position.x = Math.sin(thetaLength) * GameOptions.platformRadius / 2;
        side2.position.z = Math.cos(thetaLength) * GameOptions.platformRadius / 2;
        side2.rotation.y = thetaLength - Math.PI * 3 / 2;
        
        // side2 casts and receives shadows
        side2.castShadow = true;
        side2.receiveShadow = true;
        
        // add the side to the platform group
        this.add(side2);

        // place the platform vertically
        this.position.y = posY;
           
        // rotate the platform around the column
        this.rotation.y = angle;
    }
}

And that’s it. Next time, I am adding a bouncing ball falling down the platforms. meanwhile download the commented source code along with the entire Vite project.

Don’t know where to start developing with Three.js and TypeScript? I’ll explain it to you step by step in this free minibook.