When you see ROBLOX characters moving in-game, their motion occasionally appears to “stutter.” The problem is magnified in certain scenarios; for example, two characters standing in close proximity on a moving conveyor will appear to stutter dramatically in each other’s camera. ROBLOX Client Physics and Networking Lead Kevin He recently dove deep into this problem, as it applies to characters and vehicles, and has some observable improvements to share.
First off, let’s take a look at some before-and-after video. In both clips, there are three players in a vehicle and the video is captured from the camera of a non-driver passenger. On the “new” side, it’s clear that much of the vehicle’s motion stuttering has been eliminated.
If we were to watch this footage from the driver’s perspective, passenger stuttering would be reduced, too. In the rest of this article, Kevin provides the gritty technical details about how he fixed motion stuttering.
Motion stuttering on the surface seems like a simple problem, but it actually involves ROBLOX’s physics, networking and rendering engines — all running in 3D space. The best way to solve this sort of a widespread engineering problem is to simplify the testing environment and break the problem into manageable pieces.
First, let’s define the problem. When two characters — from here known as Player1 and Player2 — are standing in close proximity on a moving, square conveyor, they appear to stutter dramatically in each other’s cameras as they move. You can see this in the following video, which is captured from Player1’s camera. Player2 stutters, especially when viewed from an angle perpendicular to the direction of movement.
This video shows our testing environment: a bare-bones place with very few parts and a moving conveyor.
Addressing the Problem
Now that we know what we’re up against, we can break motion stuttering into manageable pieces:
- Numeric errors
- Perception issue caused by camera motion
- RakNet timestamp shifting
- Interpolation Algorithm
- Interpolation Window Re-centering
- Out-of-sync rendering and physics updates
1. Numeric Errors
The friction force between the character and the conveyor floor is modeled as a spring connector. The spring minimizes the velocity difference between the character and the floor. Due to the nature of discrete simulation steps and numeric errors, there is always a slight vibration of the character relative to the floor. We aren’t addressing the spring connector right now, but it could be modified in the future to reduce numeric error.
2. Perception issue caused by camera motion
When Player1’s camera is moving along the conveyor with slight numeric error, the camera essentially vibrates. This magnifies Player2’s stuttering. Setting the camera in a fixed position helps smooth motion, but it’s not a solution, as every player generally has control of their view.
Instead of fixing the camera right now, we’ll leverage the way it magnifies stuttering as a method of verifying that we’re fixing the source of stuttering.
3. RakNet timestamp shifting
The first fix involves the timestamp on network packets exchanged between the server and players. Before diving in, we can use a chart to plot the trajectory of Player2 from Player1’s perspective to see where we’re starting and where we can improve.
The above chart represents Player2’s position from a stationary Player1’s perspective. For the sake of simplicity, we’re only tracking position along the X-coordinate of the conveyor (i.e., side-to-side movement); the flat, narrow peaks and valleys form due to movement across the Z-coordinate (in-and-out movement).
On the chart, the X-axis shows the passage of time and the Y-axis is the position of Player2, both as received by Player1. Look at the data points on the chart. What’s good is the slopes are relatively straight, meaning Player2 is consistently moving in the right direction on the conveyor. What’s bad is the distance between positional data — we want the spacing to be roughly even.
This piece of the problem stems from RakNet, the open-source networking library on which our networking engine is built. It attempts to compensate for differences in time, such as time zones, and network lag when creating a timestamp for each network packet traded between the server and players. This is useful, but in this application it’s undesirable, as it causes erratic distances between positional data points.
The first fix is to add a new timestamp field to our physics packets, which are independent of RakNet, absolute and without network compensation. We’ll keep the original sender timestamps to maintain the trajectory and interpolate the motion.
4. Bi-linear Interpolation
Interpolation is the process of inserting, estimating, or finding an intermediate term in a sequence. A good interpolation algorithm should absorb as much stuttering as possible. The following graph shows the output of ROBLOX’s existing interpolation algorithm.
Again, the X-axis shows the passage of time and the Y-axis is the position of Player2, both as received by Player1. The shape of the interpolated trajectory is not bad; however, if you look closely, you see curvatures (circled in red) that should be straight. To fix these blips in character motion trajectory, we have to work on ROBLOX’s interpolation algorithm. Currently, it’s a single-hop Lerp, or linear interpolation, which fills motion gaps by calculating the average between two data points (e.g., n and n+1).
If data points n, n+1 and n+2 have a zig-zag curvature in them due to errors, the current interpolation algorithm will spit out the exact same zig-zag curvature at high resolution. We want the interpolation algorithm to be more resilient to slight disturbances and smooth out bumps from the input trajectory.
We could do higher-order interpolation, such as cubic spline. However, those algorithms are too resource-intensive for the large-scale, multiplayer experiences of ROBLOX. Instead, we chose a simpler, bi-linear interpolation algorithm:
- p1 = lerp(n, n+1)
- p2 = lerp(n, n+2)
- p3 = lerp(p1, p2)
It blends three points and is more resilient to small glitches. The following graph shows the output with bi-linear interpolation. The slope of the trajectory is straighter and free of curvature. This translates to less stuttering of Player2.
5. Interpolation window re-centering
ROBLOX’s interpolation algorithm uses a circular buffer that stores the five most recent data points for a part. The time between the first sample and the last sample is the optimal window — or range of time — we can use to interpolate motion for it. This window slides forward along with time and new data.
Sometimes the local rendering ticks faster (or slower) than the remote simulation. To correct that error, the current interpolation algorithm “snaps” the current sample point back to the center of the sliding window with every network frame, or about every 50 miliseconds. The distance of the snap can cause visible, periodic stuttering.
To fix this part of the motion-stuttering problem, we implemented a new version of the algorithm. It essentially snaps the current sample point back toward the center of the sliding window at a higher rate, or about every 16 miliseconds, and does so more gradually. This way, the snaps are more frequent and the error correction is spread across a greater time period. Think of it like correcting a mistake as soon as it happens, rather than letting many mistakes accumulate and eventually result in an exponentially large problem.
The effect is more evenly-spaced and higher-fidelity positional data, as you can see on the following chart.
6. Out-of-sync rendering and physics updates
These fixes make the stuttering of Player2 almost unnoticeable from a stationary Player1. However, there is still a high-frequency, low-amplitude stuttering when both Player1 and Player2 are moving. This is because ROBLOX’s rendering task (60Hz) runs at twice the frequency of our physics task (30Hz).
For locally simulated objects, the rendering task pulls the CFrame (coordinate frame) of parts directly from the kernel at 60Hz. Since the physics task is stepped at 30Hz, it only pulls a new position half as often. For remotely simulated objects, the rendering task pulls the CFrame of parts from the interpolator. The interpolator always outputs a new position of the part’s trajectory because it continuously performs linear interpolation. This creates relative stuttering between locally simulated parts and remotely simulated parts.
There’s a simple fix: force the network interpolator to only spit out a new part-position value if the physics “Step ID” advances. This forces the interpolator to synchronize with the physics task. In the future, when we bump physics to 240Hz, the network interpolator should automatically beef up its interpolation frequency to 240Hz. Ultimately, they’re better off synchronized.
The results are not yet perfect. Keep in mind we are capturing the video at close distance, under the magnifier of a moving camera and from the toughest (perpendicular) angle. Character stuttering is a surprisingly complex and deep-seated issue, touching many sub-systems of our simulation engine. In the future we will continue to iterate and improve this from alternative angles.
For now, however, we will move the fixes explained in this article to ROBLOX’s production environment so you can enjoy a better gameplay experience.