Dev Blog: Engineering the “Infinite Run” – Cinematic

The Evolution: From Simple Stickmen to Cinematic Neon Giants

https://mero.live/games/run

What started as a basic experiment in 3D movement has evolved into a high-stakes, cinematic “Data Stream” simulation. We moved away from “balloon men” into Director Mode—a system where physics, HERO jumps, and cartoonish deaths are captured through a dynamic lens.


1. Scaling the Threat: The 2X Giant

We realized that to make the “Infinite Run” feel impactful, the runners needed to dominate the screen. We doubled their scale to make them feel like “Titan” data packets.

  • The Idea: Move from human-scale to giant-scale (2X taller).
  • The Code Change: We scaled the CapsuleGeometry and added a complex joint system for the arms and hands.

code JavaScript

// Engineering the 2X Giant
const bodyGeo = new THREE.CapsuleGeometry(1.0, 2.0, 4, 10); // Double height
const limbGeo = new THREE.CapsuleGeometry(0.35, 1.6, 4, 8); // Thick reinforced limbs
const handGeo = new THREE.SphereGeometry(0.5, 8, 8);      // Large glowing contact points

2. Hand Motion & “Wrist Flick”

To prevent the giants from looking like stiff robots, we implemented a secondary animation layer for the wrists.

  • The Logic: As the arm swings (Pendulum physics), the hand should “flick” in the opposite direction to simulate weight and momentum.

code JavaScript

// Hand/Wrist flick logic
const swingX = Math.sin(t);
const swingY = Math.cos(t);

this.lHand.rotation.x = swingX * 0.5;   // Wrist flick back/forth
this.lHand.position.z = swingY * 0.2;   // Subtle depth "drag"
this.lArm.rotation.z = Math.abs(swingX) * 0.2; // Natural arm flare

3. “Hero Jump” vs. “Cartoon Flatten”

The core gameplay mechanic involves a high-speed ball. We wanted the interaction to feel like a high-budget action movie.

  • Hero Jump: Only 1 or 2 runners per roll are “chosen” to jump. They launch into a parabolic arc, leaning forward like a superhero.
  • Flatten Physics: If a runner fails to jump or is hit, they don’t just disappear—they are instantly crushed into a 0.02-scale pancake, sliding across the mirror floor.

code JavaScript

// The "Pancake" Death Logic
flatten() {
    this.state = 'flat';
    this.group.scale.set(2.0, 0.02, 2.0); // Extreme vertical squash
    this.group.position.y = 0.05;         // Stick to the mirror floor
}

4. Director Mode: Cinematic Camera Cuts

A fixed camera is boring. We built a Virtual Director that watches the race and decides when to “cut” to a better angle.

  • The Logic: When a Hero Jump is detected, the camera snaps to a random cinematic preset (Side-view, Birds-eye, or Low-Front) and zooms in using Bullet Time (Slow Motion).

code JavaScript

// Director Camera Angle Presets
const jumpCamAngles = [
    new THREE.Vector3(40, 15, 0),   // Dramatic Profile
    new THREE.Vector3(-40, 15, 5),  // Reverse Angle
    new THREE.Vector3(0, 8, 50),    // Low Hero Angle
];

// Switch to Cinematic Focus
if (anyoneJumping) {
    camTargetPos.set(hero.x + angle.x, hero.y + angle.y, hero.z + angle.z);
    bulletTime = 0.2; // Slow down the universe
}

5. Visible Data: KB/MB Tracking

To tie the theme together, we treated every object as a “Download.”

  • The Concept: Spawning a runner isn’t just a 3D call; it’s a 120.4 KB data payload.
  • Tracking: Every tap to launch the ball costs data, and every death results in “Packet Loss.”

Final Optimized Code

This code includes the 2X Scale Giants, Hand/Wrist Motion, Director Mode Camera, Hero Jump Logic (1-2 per roll), and Flattening Physics.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Infinite Run - Director Mode</title>
    <style>
        body { margin: 0; overflow: hidden; background-color: #000; font-family: 'Courier New', Courier, monospace; }
        canvas { display: block; width: 100vw; height: 100vh; cursor: crosshair; }
        #ui-left { position: absolute; top: 15px; left: 15px; color: #00ffcc; background: rgba(0,0,0,0.8); padding: 12px; border-left: 3px solid #00ffcc; pointer-events: none; font-size: 11px; text-transform: uppercase; z-index: 10; }
        #instruction { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); color: #ff00ff; font-weight: bold; text-transform: uppercase; letter-spacing: 2px; pointer-events: none; animation: pulse 1.5s infinite; font-size: 12px; }
        @keyframes pulse { 0%, 100% { opacity: 0.3; } 50% { opacity: 1; } }
        .highlight { color: #fff; font-weight: bold; font-size: 16px; }
        .label { opacity: 0.6; font-size: 9px; display: block; margin-top: 5px; }
    </style>
</head>
<body>

<div id="instruction">Tap to Launch Ball | Cinematic Jumps</div>
<div id="ui-left">
    <div>SYSTEM: <span style="color: #00ff00;">DIRECTOR_V5_STABLE</span></div>
    <span class="label">Total Streamed Data</span>
    <div><span id="total-kb" class="highlight">0.00</span> MB</div>
    <span class="label">Jumps This Roll</span>
    <div><span id="jump-count-ui" class="highlight">0</span> / <span id="jump-max-ui">0</span></div>
    <span class="label">Active Runners</span>
    <div id="unit-count" class="highlight">0</div>
</div>

<script type="importmap">
    { "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js", "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" } }
</script>

<script type="module">
    import * as THREE from 'three';
    import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
    import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
    import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
    import { Reflector } from 'three/addons/objects/Reflector.js';
    import Stats from 'three/addons/libs/stats.module.js';

    let totalKB = 0, jumpsThisRoll = 0, maxJumpsForThisRoll = 0, ballActive = false;
    const uiTotal = document.getElementById('total-kb'), uiJumpCount = document.getElementById('jump-count-ui'), uiJumpMax = document.getElementById('jump-max-ui'), uiUnits = document.getElementById('unit-count');

    function logData(kb) { totalKB += kb; uiTotal.innerText = (totalKB / 1024).toFixed(2); }

    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x020205);
    scene.fog = new THREE.FogExp2(0x020205, 0.006);
    const camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 1000);
    let camTargetPos = new THREE.Vector3(0, 30, 100), lookTargetSmooth = new THREE.Vector3();

    const renderer = new THREE.WebGLRenderer({ antialias: false });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setPixelRatio(window.devicePixelRatio);
    document.body.appendChild(renderer.domElement);
    const stats = new Stats();
    stats.dom.style.left = 'auto'; stats.dom.style.right = '0px';
    document.body.appendChild(stats.dom);

    const clock = new THREE.Clock(), raycaster = new THREE.Raycaster(), mouse = new THREE.Vector2();
    scene.add(new THREE.AmbientLight(0xffffff, 0.2));
    const sun = new THREE.DirectionalLight(0xffffff, 2.5);
    sun.position.set(50, 100, 50); scene.add(sun);

    const ground = new Reflector(new THREE.PlaneGeometry(2000, 2000), { clipBias: 0.003, textureWidth: 512, textureHeight: 512, color: 0x111111 });
    ground.rotateX(-Math.PI / 2); scene.add(ground);

    const headGeo = new THREE.SphereGeometry(1.2, 12, 12), bodyGeo = new THREE.CapsuleGeometry(1.0, 2.0, 4, 10), limbGeo = new THREE.CapsuleGeometry(0.35, 1.6, 4, 8); 
    limbGeo.translate(0, -0.8, 0);
    const handGeo = new THREE.SphereGeometry(0.5, 8, 8); 

    let runners = [];
    const colors = [0x00ffff, 0xff00ff, 0x00ff88, 0xffff00, 0xff3333];
    const jumpCamAngles = [new THREE.Vector3(40, 15, 0), new THREE.Vector3(-40, 15, 5), new THREE.Vector3(0, 8, 50)];
    let currentJumpAngle = new THREE.Vector3();

    class Runner {
        constructor(x, z, speed, color) {
            this.group = new THREE.Group();
            this.mat = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 2 });
            const body = new THREE.Mesh(bodyGeo, this.mat); body.position.y = 3.2;
            const head = new THREE.Mesh(headGeo, this.mat); head.position.y = 5.6;
            this.lLeg = new THREE.Mesh(limbGeo, this.mat); this.rLeg = new THREE.Mesh(limbGeo, this.mat);
            this.lLeg.position.set(0.7, 2, 0); this.rLeg.position.set(-0.7, 2, 0);
            this.lArm = new THREE.Mesh(limbGeo, this.mat); this.rArm = new THREE.Mesh(limbGeo, this.mat);
            this.lArm.position.set(1.5, 4.2, 0); this.rArm.position.set(-1.5, 4.2, 0);
            this.lHand = new THREE.Mesh(handGeo, this.mat); this.rHand = new THREE.Mesh(handGeo, this.mat);
            this.lHand.position.y = -1.6; this.rHand.position.y = -1.6;
            this.lArm.add(this.lHand); this.rArm.add(this.rHand);
            this.group.add(head, body, this.lLeg, this.rLeg, this.lArm, this.rArm);
            this.group.position.set(x, 0, z);
            this.speed = speed * 120; this.animOffset = Math.random() * 10;
            this.state = 'running'; this.jumpProgress = 0;
            scene.add(this.group); logData(120.4);
        }
        update(elapsed, delta, ball) {
            if (this.state === 'flat') return;
            const distZ = this.group.position.z - ball.position.z, distX = Math.abs(this.group.position.x - ball.position.x);
            if (this.state === 'running' && ballActive && jumpsThisRoll < maxJumpsForThisRoll) {
                if (distZ < 30 && distZ > 15 && distX < 10) {
                    this.state = 'jumping'; this.jumpProgress = 0; jumpsThisRoll++;
                    uiJumpCount.innerText = jumpsThisRoll;
                    currentJumpAngle.copy(jumpCamAngles[Math.floor(Math.random() * jumpCamAngles.length)]);
                }
            }
            if (this.state === 'jumping') {
                this.jumpProgress += delta * 1.8;
                this.group.position.y = Math.sin(this.jumpProgress * Math.PI) * 26;
                this.group.rotation.x = -Math.sin(this.jumpProgress * Math.PI) * 0.6;
                if (this.jumpProgress >= 1.0) { this.state = 'running'; this.group.position.y = 0; this.group.rotation.x = 0; }
            } else {
                if (distZ < 60 && distZ > 0 && distX < 22) this.group.position.x += (this.group.position.x > ball.position.x ? 1 : -1) * 35 * delta;
            }
            const t = elapsed * 15 + this.animOffset, swingX = Math.sin(t), swingY = Math.cos(t);
            this.lLeg.rotation.x = swingX; this.rLeg.rotation.x = -swingX;
            this.lArm.rotation.x = -swingX * 1.2; this.rArm.rotation.x = swingX * 1.2;
            this.lHand.rotation.x = swingX * 0.5; this.rHand.rotation.x = -swingX * 0.5;
            if (this.state === 'running') this.group.position.y = Math.abs(Math.cos(t * 1.5)) * 0.6;
            this.group.position.z += this.speed * delta;
        }
        flatten() {
            if (this.state === 'flat') return;
            this.state = 'flat'; this.group.scale.set(2.0, 0.02, 2.0); this.group.position.y = 0.05;
            setTimeout(() => { scene.remove(this.group); runners = runners.filter(r => r !== this); }, 3000);
        }
    }

    const ballRadius = 8;
    const deathBall = new THREE.Mesh(new THREE.SphereGeometry(ballRadius, 32, 32), new THREE.MeshStandardMaterial({ color: 0x000000, emissive: 0xff0044, emissiveIntensity: 5 }));
    deathBall.position.set(0, ballRadius, -1000); scene.add(deathBall);

    function launchBall(event) {
        const x = (event.clientX || (event.touches && event.touches[0].clientX)), y = (event.clientY || (event.touches && event.touches[0].clientY));
        mouse.x = (x / window.innerWidth) * 2 - 1; mouse.y = -(y / window.innerHeight) * 2 + 1;
        raycaster.setFromCamera(mouse, camera);
        const intersects = raycaster.intersectObject(ground);
        if (intersects.length > 0) {
            deathBall.position.set(intersects[0].point.x, ballRadius, 140);
            ballActive = true; jumpsThisRoll = 0; maxJumpsForThisRoll = Math.floor(Math.random() * 2) + 1;
            uiJumpCount.innerText = "0"; uiJumpMax.innerText = maxJumpsForThisRoll; logData(480.0);
        }
    }
    window.addEventListener('mousedown', launchBall); window.addEventListener('touchstart', launchBall);

    let spawnTimer = 0;
    function spawnSingleRunner() {
        const x = (Math.random() - 0.5) * 120, color = colors[Math.floor(Math.random() * colors.length)];
        runners.push(new Runner(x, -550, 0.18 + Math.random() * 0.1, color));
    }

    const composer = new EffectComposer(renderer);
    composer.addPass(new RenderPass(scene, camera));
    composer.addPass(new UnrealBloomPass(new THREE.Vector2(256, 256), 1.3, 0.4, 0.85));

    function animate() {
        stats.begin();
        let rawDelta = clock.getDelta();
        const jumpingHero = runners.find(r => r.state === 'jumping');
        const anyoneJumping = !!jumpingHero;
        const bulletTime = (anyoneJumping && jumpingHero.group.position.y > 5) ? 0.2 : 1.0;
        const delta = rawDelta * bulletTime, elapsed = clock.getElapsedTime();

        spawnTimer += rawDelta; if (spawnTimer >= 0.4) { spawnSingleRunner(); spawnTimer = 0; }
        deathBall.position.z -= 135 * delta; deathBall.rotation.x -= 10 * delta;
        if (deathBall.position.z < -600) ballActive = false;

        for (let i = runners.length - 1; i >= 0; i--) {
            runners[i].update(elapsed, delta, deathBall);
            if (runners[i].group.position.distanceTo(deathBall.position) < ballRadius + 2.2 && runners[i].group.position.y < ballRadius + 1.5) runners[i].flatten();
            if (runners[i].group.position.z > 160) { scene.remove(runners[i].group); runners.splice(i, 1); }
        }

        if (anyoneJumping) {
            camTargetPos.set(jumpingHero.group.position.x + currentJumpAngle.x, jumpingHero.group.position.y + currentJumpAngle.y, jumpingHero.group.position.z + currentJumpAngle.z);
            lookTargetSmooth.lerp(jumpingHero.group.position, 0.1);
        } else {
            let avgX = 0; if(runners.length > 0) { runners.forEach(r => avgX += r.group.position.x); avgX /= runners.length; }
            camTargetPos.set(avgX * 0.5, 30, 110); lookTargetSmooth.lerp(new THREE.Vector3(avgX, 10, -20), 0.05);
        }
        camera.position.lerp(camTargetPos, 0.1); camera.lookAt(lookTargetSmooth);
        uiUnits.innerText = runners.length; composer.render(); stats.end();
        requestAnimationFrame(animate);
    }
    window.addEventListener('resize', () => { camera.aspect = window.innerWidth/window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); });
    animate();
</script>
</body>
</html>