In game development, “feel” is everything. When building a parkour engine, movement must feel continuous. If a player performs an action and the character suddenly “snaps” or “teleports” from one height to another, the immersion is shattered.
Recently, while developing a parkour prototype using Three.js, I encountered a critical failure in the Swing Mechanic. This article documents the evolution of that feature: a journey from a rigid, ground-only movement to a fully realized, height-aware physics system.
Phase 1: Solving Input Rigidity
Initially, the swing mechanic was functionally primitive. The system was logically bound to a single state: the player had to be standing on the floor to interact with the environment.
The Problem: State-Dependent Input
The input handler restricted the KeyF (Swing) action inside a conditional block that only checked if the player’s state was ‘ground’. This created a “dead zone” where jumping or falling made the mechanic unreachable.
The Problematic Code:
code JavaScript
// ❌ THE PROBLEM: Input is locked to the 'ground' state
function onPress(code) {
if (state === 'ground') {
switch (code) {
case 'KeyE': tryHang(); break;
case 'KeyF': trySwing(); break; // Logic fails if state is 'air'
}
}
}
The Fix: Universal Input Triggering
To achieve true parkour flow, a player must be able to grab a bar mid-air. I refactored the input logic to decouple the swing trigger from the grounded state.
The Optimized Code:
code JavaScript
// ✅ THE FIX: Decoupling input from the 'ground' state
function onPress(code) {
if (state === 'ground') { /* ...ground-only actions... */ }
// The swing trigger is now state-agnostic
if (code === 'KeyF') {
trySwing(); // Works during 'ground' OR 'air' states
}
}
Phase 2: The “Teleportation Pop” Bug
Once the player could grab the bar from the air, a much more jarring issue emerged: Kinematic Discontinuity. If a player jumped from a high building and grabbed a bar, they would instantly “teleport” to the floor the moment they touched the bar.
The Problem: Hard-Coded Coordinate Snapping
The bug originated in the trySwing function. Upon engagement, the system was hard-coding the player’s vertical position (Y_pos) to 0 (the ground plane).
The Problematic Code:
code JavaScript
// ❌ THE PROBLEM: Hard-coded ground snap
function trySwing() {
// ... logic to find bar ...
// This '0' forced the player to the floor instantly.
pos.set(targetX, 0, targetZ);
state = 'swing';
}
The resulting visual “jitter” followed this sequence:
- Grab: Player engages the bar mid-air.
- Teleport: Character instantly snaps to
Y=0Y=0(the ground). - Correction: The physics engine realizes the player is actually above a block.
- Pop: The character is violently “yanked” back up to the block.
Phase 3: The Advanced Physics Solution
To achieve a professional “feel,” I implemented a three-tier optimization strategy: Height Memory, Sinusoidal Interpolation, and Smart Landing.
Step 1: Temporal Height Memory
We moved away from hard-coded values by implementing Height Memory. The system now captures the player’s exact vertical position (Y_start) at the moment of engagement.
The Optimized Code:
code JavaScript
// ✅ Inside trySwing()
swingStartY = pos.y; // Capture the exact height of the jump/fall
// Snap to the BAR height, not the floor, preserving verticality
pos.set(targetX, bar.y, targetZ);
Step 2: Sinusoidal Path Interpolation
A natural swing follows an arc, not a straight line. I used Linear Interpolation (LERP) to transition between the starting height (Y_start) and the landing height (Y_land), combined with a Sine Wave to simulate gravitational momentum.
The Optimized Code:
code JavaScript
// ✅ THE FIX: Smooth height interpolation via Sinusoidal Arc
function updateSwing() {
const t = clamp(activeAction.time / duration, 0, 1);
const u = t * t * (3 - 2 * t); // Smoothstep easing
// LERP: Move from captured start height to predicted landing height
const heightTransition = THREE.MathUtils.lerp(swingStartY, swingLandY, u);
// Sinusoidal Arc: Adds a "swing boost" to create a physical curve
const arcHeight = Math.max(swingStartY, swingLandY, 1.0) * 0.5;
pos.y = heightTransition + arcHeight * Math.sin(Math.PI * u);
}
Step 3: Smart Landing (Conditional State Switching)
Finally, we addressed the “end-of-animation” transition. If the swing ends over a platform, we snap to it. If the swing ends in mid-air, we must avoid snapping to the ground and instead transition the player into a natural falling state.
The Optimized Code:
code JavaScript
// ✅ THE FIX: Conditional landing logic
function finishSwing() {
if (swingLandY > 0) {
// Scenario A: Surface detected -> Transition to Grounded
pos.x = swingTargetPos.x;
pos.y = swingLandY;
state = 'ground';
} else {
// Scenario B: No surface detected -> Transition to Airborne (Natural Fall)
state = 'air';
airMode = 'fall';
}
}
Conclusion
By moving away from hard-coded coordinate snapping and embracing a State-Based Physics model, we transformed a glitchy, broken mechanic into a fluid movement system.
Key Takeaway for Developers: In any physics-based engine, always respect the player’s current momentum and position. Never force a coordinate unless you have mathematically accounted for the transition from the previous state.
Keywords: Three.js, Game Development, Physics Engine, JavaScript, Parkour Game, Bug Fixing, Lerp, Kinematics, Animation.