Generating Almost Equally-Spaced Points Along a Parabola

10 hours ago 1

July 12, 2025

A trick I recently used to produce aesthetically pleasing arcs of coins in an endless runner game–all it took was hand-waving away the rules of calculus

The final two weeks of my freshman year were consumed by the making of an endless runner game for an introductory software development class. A core part of the intended experience was coin generation: given sufficient room between randomly-spaced obstacles, coins should spawn along arcs that perfectly corresponded to the trajectory of a correctly-timed jump by the player.

It sounded simple enough, but there was one problem: every implementation I (sidenote: This was a highly collaborative project with two other students, but the coin generation feature described here was one of the few parts of it developed by me alone.) tried looked ugly. The straightforward approaches to this problem resulted in me generating coins that got squashed together near the top… Coins correctly generated along the parabolic arc that a player takes when jumping but with too much arclength in between near the bottom and too little arclength near the top … or that frustrated the player with no chance to collect all of them. Evenly-spaced coins in a circular path that are mostly out of reach of the parabolic arc of a player

Much of graphics programming is a tradeoff between accuracy and effort, but I was convinced that I could do better than in the approaches depicted above.

The Context

First, we need to take a step back to set up the problem–why do we even care about parabolas in the first place?

In the simple two-dimensional world (sidenote: I'm handwaving away the details of the unit system used here because they're not essential to the central focus of the post. For those curious, the game space was a 1000 x 500 grid of pixels. The event loop was called semi-periodically (usually in 8-12 millisecond increments), so all kinematics calculations had to be normalized per second to take this into account. Constants were tuned as follows to give aesthetically pleasing jumps: \(u_x = 200, u_y = 250, g = 300 \), all in the appropriate units per second) of the game, our player sprite had a constant forward velocity \(+u_x\) along the horizontal axis, so their horizontal displacement \(s_y \) would vary with time as

$$s_x(t) = u_x t$$

When the player jumped, the sprite would instantaneously gain a velocity \( +u_y \) in the vertical axis, and immediately experience downward acceleration \(-g\) due to gravity. Since the player experiences no other forces, the equations of motion tell us that their position in the vertical axis will vary with time \(t\) as

$$s_y(t) = u_y t - \frac{1}{2} gt^2$$

So when a player jumped, their trajectory could be completely described by the above two equations in terms of the parameter \(t\)—giving a parabola!

A parabolic path as described by the equations above

We’re interested in picking some points along this trajectory at which to render coins, which would be collected by the player as their sprite passes through each one of them.

Approach 1: Sampling along regular time intervals

Equipped with the parametric equations above, we can try calculating the positions along the player’s trajectory that correspond to equally-spaced time intervals–just divide the time we expect the player to take to return to their original \(y\)-displacement by the number of coins we want to generate.

We can get the total time it’ll take for the player to reach the vertex of the parabola–where \(y\)-velocity is zero–will be \(\frac{u_y}{g} \). Since the parabola is symmetric, the total time taken to return to the original \(y\)-displacement will then be \(\frac{2u_y}{g} \).

Unfortunately, dividing this time interval into equal slices results in coins that are awkwardly close together near the top of the curve and awkwardly far apart near the bottom.

Coins along a parabolic trajectory. They’re much closer together near the top with significantly more space between them closer to both lower ends

It’s okay, and a reasonable compromise if all else fails, but it’s unsatisfactory.

Approach 2: Sampling along regular horizontal intervals

Instead of uniformly sampling time values, what if we took evenly-spaced \(x\)-values? Given a sequence of evenly-spaced \(x_1 \dots x_n\), we can generate coins at the corresponding \(y_i \dots y_n\) to get coordinates for the coins.

However, our old equation to calculate \(s_y\) was parametrized in terms of \(t\). Luckily, we know that if a sprite has covered distance \(x\) at a constant velocity \(u_x\), then time \( t = \frac{x}{u_x} \) must have elapsed. So we can reparametrize the equation as $$ s_y(x) = u_y \frac{x}{u_x} - \frac{1}{2} g \left( \frac{x}{u_x} \right)^2 $$

The total distance covered by the player in the jump can be found by multiplying the total time \(\frac{2u_y}{g} \) by the constant horizontal velocity \(u_x\). Slicing this into equal pieces gives…

Comparison of constant x and t intervals. The figure shows the legend with two icons but the plot shows seemingly just one set of points

Oops–it seems like we got the exact same results as earlier! The linear relationship between \(x\)-displacement and time means that the above two approaches are equivalent.

Why’d I show you this derivation, then? Because the substitution we did above opens a new possibility.

Approach 3: Calculating arc length

This whole time, I’ve been handwaving away what I didn’t like: I’ve been saying the points didn’t look ’evenly spaced’. But what does that mean?

Mathematically, we can say that I want to find pairs of points \(x_i, y_i \) with constant arclength \(S\) between them. Going back to high school calculus, we know the arclength between two points \(a \) and \( b\) will be $$ \begin{align*} S &= \int_a^b \sqrt{1 + \left(\frac{dy}{dx}\right)^2} dx \end{align*} $$

Conveniently, we have an equation for \(s(y) \) in terms of \( x \): \( s(y) = u_y \frac{x}{u_x} - \frac{1}{2} g \left( \frac{x}{u_x} \right)^2 \). Its derivative with respect to \(x \) is \( \frac{u_y}{u_x} - \frac{g}{u_x^2} x\). Plugging this into the equation for arclength gives

$$ S = \int_a^b \sqrt{1 + \left(\frac{u_y}{u_x} - \frac{g}{u_x^2}x\right)^2} dx $$

Excellent–we just need to solve this integral, rearrange, keep \(S\) fixed and rearrange to find \(x\). Then, starting from the initial position, to calculate the exact optimum points at which to render coins, we can recursively find the \(x \)-displacement from the previous point that progresses us by the required arclength \(S\) along the parabola. Here’s what sympy thinks (sidenote: Wolfram Alpha gave up and integral-calculator.com give a different solution that's equally impractical for our purposes.) the solution is:

An evaluation of the integral in a Jupyter notebook with sympy

$$ \frac{v_{x}^{2} \left(- \frac{u_{y}^{2}}{v_{x}^{2}} - \frac{1}{2} + \frac{u_{y}^{2} + v_{x}^{2}}{v_{x}^{2}}\right) \log{\left(\frac{2 g^{2} x}{v_{x}^{4}} - \frac{2 g u_{y}}{v_{x}^{3}} + \frac{2 \sqrt{\frac{g^{2} x^{2}}{v_{x}^{4}} - \frac{2 g u_{y} x}{v_{x}^{3}} + \frac{u_{y}^{2}}{v_{x}^{2}} + 1} \left|{g}\right|}{v_{x}^{2}} \right)}}{\left|{g}\right|} + \\ \left(\frac{x}{2} - \frac{u_{y} v_{x}}{2 g}\right) \sqrt{\frac{g^{2} x^{2}}{v_{x}^{4}} - \frac{2 g u_{y} x}{v_{x}^{3}} + \frac{u_{y}^{2}}{v_{x}^{2}} + 1} $$

We want to solve for \(x\)–but it’s nested across the square root inside the logarithm in the first term, the coefficient of the second term, and the square root in the second term. It seems intractable to rearrange the expression to get a clean equation for \(x\).

We could try to use numerical methods to approximate a solution for \(x\) given \(S \), but that’d be far too impractical for generating hundreds of coins in the background of a browser-based game. No matter what, our solution will, at best, minimize–but not avoid–imperfection.

Sidestepping calculus

We can think of infinitesimal changes \( dS \) in arclength as being derived, from the Pythagorean theorem, as the square root of the sum of the squares of infinitesimal changes in \(x\) and \(y \)-displacement. $$ dS = \sqrt{dx^2 + dy^2} = \sqrt{1 + \frac{dy}{dx}^2} dx $$

We can reframe our goal as being was for the arclength \(\Delta S\) between points \((x_i, y_i)\) and \((x_{i+1}, y_{i+1})\) to be constant. If we approximate \(dS \approx \Delta S\) and \(dx \approx \Delta x\), it becomes easy to solve for \(\Delta x \)!

$$ \begin{align*} \Delta S &= \sqrt{1 + \left(\frac{u_y}{u_x} - \frac{g}{u_x^2}x\right)^2} \Delta x\\ \implies \Delta x &= \frac{\Delta S}{\sqrt{1 + \left(\frac{u_y}{u_x} - \frac{g}{u_x^2}x\right)^2}} \end{align*} $$

Everything in the above equation is a constant aside from the current \(x\)-value, so it’s extremely easy to evaluate–we start with \(x_0 \), find \(x_1 = x_0 + \Delta x \) with the above equation, and repeat this pattern for every \(x_i\) and \(x_{i+1}\) until we reach the end of (sidenote: We can define this as the first point where the \(y\)-displacement is negative relative to the starting position, or where the \(x\)-displacement is more than the expected horizontal length of the trajectory) the trajectory.

Points that look relatively evenly spaced along a parabola

There’s still not as much breathing room near the top of the curve as at the bottom–but it looks much more consistent than before. Here’s a comparison:

You can see that the approach which used constant \(x\)-intervals or \(t\)-intervals results in points that are squashed together near the top and more spaced out near the bottom of the curve compared to the new approach.

One limitation with this method is that it doesn’t guarantee a symmetrical structure with one coin placed at the beginning and one at the end of the curve, as was produced by the previous approaches which evenly split the interval along the horizontal axis. This was an acceptable tradeoff for generating coins, and had the nice side effect of enabling a skilled player to take early jumps such that their sprite was within the collision radius of all of the coins while also gaining more breathing room before upcoming obstacles.

Concluding thoughts

No approximation is perfect, and I’m certain a better approach exists, but it’s still interesting to see that applying some relatively straightforward math can get you much further than intuitive approaches without compromising performance.

In some ways, these kinds of challenges in graphics programming remind me of machine learning, where you know that there isn’t a perfect solution to a problem but making principled mathematical approximations based on some carefully selected assumptions can get you remarkably close to one.

Code

For those curious, below is the verbatim code we wrote to implement the above algorithm in the game project.

c code snippet start

list_t *parabolic_path(state_t *state, vector_t min_start_pos, vector_t max_end_pos) { double expected_x_dist_with_jump = (2 * get_curr_jump_vel(state).y / get_curr_gravity(state).y) * state->bg.bg_3_building_vel.x; double delta = max_end_pos.x - min_start_pos.x; // Need tolerance between expected x_dist and delta if (delta < 0 || delta - expected_x_dist_with_jump < SPACING_LIMIT) { return NULL; } double x_offset = min_start_pos.x + (rand() % ((int)(delta - expected_x_dist_with_jump))); list_t *coin_positions = list_init(V_LARG_NUM_COINS, free); // Constants to use in calculation of points along arc double u_y = get_curr_jump_vel(state).y; double v_x = state->bg.bg_3_building_vel.x; double g = get_curr_gravity(state).y; double DS = PARABOLIC_COIN_SPACING; double x = 0; double y = 0; // These are values required for our approximation of dx which we reuse double t1 = pow(u_y / v_x, 2); double t2 = 2 * u_y * g / pow(v_x, 3); double t3 = pow(g / (v_x * v_x), 2); /* Local approximation of ds approxeq s, dx approxeq x to give constant arc length */ do { vector_t *pos = malloc(sizeof(vector_t)); *pos = (vector_t){.x = x_offset + x, .y = min_start_pos.y + y}; list_add(coin_positions, pos); double squared_deriv = t1 - (t2 * x) + (t3 * pow(x, 2)); x += DS / sqrt(1 + squared_deriv); y = u_y * (x / v_x) - (0.5 * g) * pow((x / v_x), 2); } while (x_offset + x < max_end_pos.x && y >= 0); return coin_positions; }

c code snippet end

The structure of the class incentivized you to care about code quality and performance, and you can see the effects of that near the end, where as many static values as possible are cached outside the loop to minimize repeated computation.

All the figures in this post were generated in Python with matplotlib in this IPython notebok.

Read Entire Article