5 minutes
Evaluating Motion
Before we implement the function, there’s one small simplification we need to mention: In this chapter we will always evaluate the motion every 16 ms (0.016 seconds), which corresponds to roughly 60 frames per second.
Using a fixed time step makes the formulas easier to understand for this article.
Note: In real simulations or game engines, you usually don’t assume a constant time value. Instead, you take the actual delta time from the game loop, so that the motion stays consistent even if the frame rate changes. More on that later.
But for the sake of clarity in this chapter: We assume that every update happens exactly every 16 ms. This keeps the examples simple and lets us focus on the physics first.
Now that we know how acceleration works and how gravity affects the coaster depending on the slope, we can finally build the first real part of our simulation. The idea is simple. Every small time step we calculate how much the coaster changes its speed and how much distance it travels.
We start with a minimal simulation state.
type SimulationState = {
velocity: number;
distanceTraveled: number;
};
const simulationState: SimulationState = {
velocity: 0,
distanceTraveled: 0,
};
In the beginning, the coaster does not move and has not traveled any distance.
The evaluateMotion function
Our simulation works by creating a new state based on the previous state. Think of it like this:
initial state → evaluate → new state → evaluate → new state → …
To calculate the acceleration along the slope, we now implement the downhill-slope acceleration formula from Chapter 2. This formula tells us how much of gravity actually acts in the direction of the slope:
$$acceleration = gravity * sin(slopeAngle)$$So we end up basically with:
const acceleration = gravity * Math.sin(slopeAngle);
const evaluateMotion = (state: SimulationState, acceleration: number, deltaTime: number) => {
/// ...
};
- slopeAngle controls how steep the slope is
- deltaTime is how much time passed since the last calculation (we use 16 ms)
Calculating the velocity
Acceleration is measured in m/s². That means:
m/s² tells us how much the velocity changes in one full second.
So if acceleration is 9.81 m/s²:
- after 1 second, velocity increases by 9.81 m/s
- so after 0.016 seconds, velocity increases by \(9.81 * 0.016\) Just multiply it with 0.016 :) that does the trick
We take the previous velocity and add the small increase for this frame.
const velocity = state.velocity + acceleration * deltaTime;
Calculating the distance traveled
Velocity is measured in m/s. This means:
velocity tells us how much distance is to travel in one full second.
Again we only want the part that matches our 0.016 seconds.
$$distanceToTravel = velocity \cdot deltaTime$$We take the previous distance traveled and add the small increase for this frame.
const distanceTraveled = state.distanceTraveled + velocity * deltaTime;
So we end up with something like:
const evaluateMotion = (
state: SimulationState,
acceleration: number,
deltaTime: number,
): SimulationState => {
const velocity = state.velocity + acceleration * deltaTime;
const distanceTraveled = state.distanceTraveled + velocity * deltaTime;
return { velocity, distanceTraveled };
};
This is already a complete physics step without friction or air resistance.
Interactive demo
Just like in the last chapter, we again have an interactive demo. This time, we’ve implemented a simple motion-evaluation method that applies basic physics. In this demo you can play around with the slope and already see a physical motion emerging.
Important note: Earlier in this article I said we would evaluate everything every 16 ms, right? Well… that was kind of a lie, at least for this demo.
Since we’re using THREE.js, we can make use of theuseFramehook, which gives us the delta time of the previous frame, and we benefit from using it. This is the proper way to handle updates, instead of forcing a fixed 16 ms step, which I briefly covered in the introduction of this chapter.
What comes next?
In the next chapter we’ll add a basic roller coaster track, just a straight, linear segment. This is a preparation for the real coaster curvatures we’ll build later.
Demo code
Click here to view the code on GitHub (opens in a new tab)
import React, { useEffect } from 'react';
import { useFrame } from '@react-three/fiber';
import { useControls } from 'leva';
import { MathUtils, Vector3 } from 'three';
import { Arrow } from '../../components/Arrow';
import { ControlPoint } from '../../components/ControlPoint';
import Line from '../../components/Line';
import { evaluateMotionByAcceleration } from '../../helper/physics';
import useColors from '../../hooks/useColors';
import OrthographicScene from '../../scenes/OrthographicScene';
const EvaluatingMotion = () => {
const colors = useColors();
const [{ slope, gravity, sinSlope, trackLength, ...simulationState }, setSimulationState] =
useControls(() => ({
slope: {
value: 0,
step: 5,
min: -180,
max: 180,
},
trackLength: {
value: 8,
step: 1,
min: 0,
max: 20,
},
gravity: {
value: 9.81665,
pad: 5,
},
sinSlope: {
disabled: true,
label: 'sin(slope)',
pad: 5,
value: 0,
},
velocity: 0,
distanceTraveled: 0,
acceleration: {
value: 0,
pad: 5,
},
}));
useEffect(() => {
const sinSlope = Math.sin(MathUtils.degToRad(slope));
const acceleration = gravity * sinSlope;
setSimulationState({ acceleration, sinSlope });
}, [slope, gravity, setSimulationState]);
// Main motion evaluation per frame
useFrame((state, deltaTime) => {
setSimulationState(
evaluateMotionByAcceleration(simulationState, simulationState.acceleration, deltaTime),
);
});
// Reset simulation state if train overshoots track
useEffect(() => {
if (simulationState.distanceTraveled < 0 || simulationState.distanceTraveled > trackLength) {
setSimulationState({
velocity: 0,
distanceTraveled: 0,
});
}
}, [simulationState.distanceTraveled, trackLength, setSimulationState]);
return (
<>
<group position={[-5, 0, 0]} rotation={[0, 0, MathUtils.degToRad(slope)]}>
<Arrow
position={[-trackLength / 2, 0, 0]}
rotation={[0, 0, Math.PI / 2]}
color={colors.secondary}
/>
<ControlPoint
position={new Vector3(trackLength / 2 - simulationState.distanceTraveled, 0, 0)}
/>
<Line
points={[new Vector3(-trackLength / 2, 0, 0), new Vector3(trackLength / 2, 0, 0)]}
color={colors.secondary}
/>
</group>
</>
);
};
export const EvaluatingMotionScene = () => {
return (
<OrthographicScene>
<EvaluatingMotion />
</OrthographicScene>
);
};