Game Build Breakdown
Peak Hopper: Making a 3D Platformer Readable Instead of Random
A 3D platformer fails quickly when the player cannot judge distance. Peak Hopper is a useful teaching case because every system has to support one question: can I land there?
Production context
This guide studies Peak Hopper, a published Supagames game. Repository source: biggames/peak-hopper/src/main.js.
The snippets below are intentionally smaller than the production file. They isolate transferable systems so you can reuse the idea in your own browser game without copying every detail of the original project.
1. Design the Jump Before the Level
Platform distances should come from jump capability, not vibes. Measure horizontal speed, jump impulse, gravity and air control, then build platform gaps that sit inside that envelope. Once the movement is stable, level generation has real numbers to respect.
This is even more important in 3D because perspective can make a safe jump look impossible or an impossible jump look close.
function estimateJumpRange(stats) {
const airtime = (stats.jumpVelocity * 2) / Math.abs(stats.gravity);
const idealDistance = stats.runSpeed * airtime;
return idealDistance * 0.78; // reserve margin for human timing
}
2. Moving Platforms Need Predictable Timing
A moving platform should look like it belongs to a pattern. If it reverses instantly without easing or visual warning, the player cannot plan. Store endpoints, phase and speed, then derive position from time so the platform is deterministic.
When the player stands on the platform, add platform delta to the player position. Otherwise the surface slides out from under them even though contact is visually correct.
function updatePlatform(platform, time) {
const t = (Math.sin(time * platform.speed + platform.phase) + 1) * 0.5;
platform.previous.copy(platform.mesh.position);
platform.mesh.position.lerpVectors(platform.a, platform.b, t);
platform.delta.copy(platform.mesh.position).sub(platform.previous);
}
3. Enemy Contact Needs Feedback and Recovery
When a monster touches the hero, the player needs more than a floating '-1'. Health should change, the hero should react, and rapid repeated damage should be prevented for a short interval. Knockback and invulnerability are clarity tools, not only balance tools.
A simple damage pipeline keeps enemy AI, collision and UI consistent. Collision asks for damage; the player system decides whether it is allowed right now.
function damagePlayer(sourcePosition) {
if (player.invulnerableFor > 0) return;
player.health -= 1;
player.invulnerableFor = 1.1;
player.velocity.subVectors(player.position, sourcePosition)
.normalize()
.multiplyScalar(7);
showDamageFlash();
}
4. Camera Framing Is Part of Collision Fairness
Good collision can still feel unfair if the camera hides the landing zone. For platforming, the camera should show the player, target platform and next hazard. Looking slightly ahead in movement direction is often more important than centering the character perfectly.
Avoid sudden camera snaps after respawn or teleports. The player needs stable orientation before the next jump.
function updatePlatformerCamera(dt) {
const ahead = player.velocity.clone().multiplyScalar(0.18);
const lookTarget = player.position.clone().add(ahead).add(new THREE.Vector3(0, 1.2, 0));
const desired = lookTarget.clone().add(new THREE.Vector3(0, 5.5, 9));
camera.position.lerp(desired, 1 - Math.exp(-6 * dt));
camera.lookAt(lookTarget);
}
5. Build checklist
- Use movement numbers to decide platform spacing.
- Make moving platforms deterministic and carry the player with platform delta.
- Use invulnerability and knockback so contact damage is visible and fair.
- Frame landing targets before decorative camera composition.