Trajectory prediction for “Mikan Pile” (aim assist)
2025-12-22 ・ ~7 min read
Motivation: show the path before throwing
- Visually show how the mikan will fly before the throw
- Makes it easier to adjust direction and power
- Improves overall control feel
Approach: simple ballistic approximation
Use a lightweight prediction based on initial velocity and gravity to sample points along time. Render the sampled points as a line/markers in three.js.
- Sample several timesteps ahead (e.g., 30–60 points)
- Convert joystick input to launch direction and magnitude
- Render a preview line that updates as the player adjusts input
Implementation Code
private predictTrajectory(
start: THREE.Vector3,
initialVelocity: THREE.Vector3,
gravityY: number
): { points: THREE.Vector3[]; end: THREE.Vector3; endTangent: THREE.Vector3 } {
// Height where the ball touches the plate top
const plateTopY = GameParams.plate.height / 2;
const targetY = plateTopY + GameParams.ball.radius;
// Sampling settings (lightweight for visual purposes)
const dt = 1 / 30; // 30fps equivalent
const maxSteps = 120; // Max 4 seconds
const g = new THREE.Vector3(0, gravityY, 0);
const points: THREE.Vector3[] = [];
let prev = start.clone();
points.push(prev.clone());
for (let i = 1; i <= maxSteps; i++) {
const t = dt * i;
// p(t) = p0 + v0*t + 0.5*g*t^2
const p = start
.clone()
.add(initialVelocity.clone().multiplyScalar(t))
.add(g.clone().multiplyScalar(0.5 * t * t));
points.push(p);
// If crossing targetY downward, use linear interpolation for intersection
if (prev.y >= targetY && p.y <= targetY) {
const denom = prev.y - p.y;
const alpha = denom !== 0 ? (prev.y - targetY) / denom : 1;
const hit = prev.clone().lerp(p, alpha);
points[points.length - 1] = hit;
break;
}
prev = p;
}
const end = points[points.length - 1].clone();
const endTangent = points.length >= 2
? end.clone().sub(points[points.length - 2]).normalize()
: new THREE.Vector3(0, 0, -1);
return { points, end, endTangent };
}
Visual Preview Rendering
- Render trajectory line
- Use three.js Line object to connect calculated points
- Change line color based on mikan color (orange or green)
- Display arrow tip
- Show a cone-shaped arrow at the trajectory end point indicating direction
- Calculate trajectory tangent to determine arrow orientation
- Preview update timing
- Recalculate and update trajectory display whenever joystick position changes
- Also update preview at regular intervals when camera rotates
Trajectory Line Rendering Implementation
private buildTrajectoryLine(
points: THREE.Vector3[],
color: number
): THREE.Line {
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({
color,
transparent: true,
opacity: 0.85,
depthTest: true,
});
const line = new THREE.Line(geometry, material);
line.name = 'TrajectoryLine';
return line;
}
// Arrow tip (cone) rendering
private buildArrowHead(
tipPosition: THREE.Vector3,
direction: THREE.Vector3,
color: number
): THREE.Mesh {
const headLength = 0.22;
const headRadius = 0.08;
const geometry = new THREE.ConeGeometry(headRadius, headLength, 16);
const material = new THREE.MeshStandardMaterial({
color,
metalness: 0.2,
roughness: 0.6,
transparent: true,
opacity: 0.95,
});
const cone = new THREE.Mesh(geometry, material);
// Set direction
const q = new THREE.Quaternion().setFromUnitVectors(
new THREE.Vector3(0, 1, 0),
direction.normalize()
);
cone.quaternion.copy(q);
cone.position.copy(tipPosition);
return cone;
}
UX notes
- Keep the preview subtle (low opacity / thin line) so it doesn’t distract
- Clamp max power to keep prediction stable and readable