Implementing curved paths for objects in 2D platformers

Started by
14 comments, last by JoeJ 1 year, 10 months ago

Hi,

I'm interested to know how to make objects follow curved paths in a 2D platformer.

Here are some examples:

In this video (at around 1 minute) , the boss follows a curved path, and its velocity along that path seems to increase over time.

I tried to implement something like this by using Bezier curves parameterised by arc length, but it's still not obvious how to control the speed along the curve to make it look natural.

Is there a typical way this is done in this type of game?

Also, there is a lot of information out there on the internet about making 2D platformers, but I've never found anything that discusses this kind of issue, so if anyone can point me to any material about it that would be really helpful.

Thanks a lot for your help!

Advertisement

Instead of an arc you can have a sequence of straight lines. I wouldn't be surprised if only a few lines work convincingly already. The creatures are moving so fast you cannot even see the precise trajectory.

For a more smooth animation you may want to allow a creature to deviate from the line. Give it some mass, speed, and a force towards the (moving) point at the line. Never tried this though, but sounds like fun to experiment.

Another way to get lines is to convert your arcs to lines. Start with a point at the arc, then move away from it along the arc until the distance from that start point is some value D. You found 1 line segment of the arc. Repeat this starting from the new point until you run out of arc.

Alberth said:
For a more smooth animation you may want to allow a creature to deviate from the line. Give it some mass, speed, and a force towards the (moving) point at the line. Never tried this though, but sounds like fun to experiment.

Ha, i came to the same thought but from the other direction. : )

I was working on motion controllers to drive physics. Imagine a simple 1D case, like controlling the distance in a slider joint, e.g. connecting a elevator platform to a static base.
The elevator may have some upwards or downwards velocity, and a current height. That's the initial state, and the goal is to drive the height to some target, and velocity should be zero (or some other given value).
We have a motor which can only produce a constant force, either positive or negative to move platform up or down. There are no external forces like gravity.

Given that, we can calculate analytically a time period of positive force to accelerate the platform, and a time period of negative force to decelerate it, so it smoothly arrives at the desired height.
The resulting motion is smooth. There is no discontinuity in trajectory or velocity, only force changes abruptly, which we do not notice. (Not like the usual actuators used in real world elevators, which may drive at constant velocities and then suddenly stop.)

If we plot the height to a graph to visualize trajectory, we get very nice and natural curves. It seemed the perfect curve for me. Visually, but also in terms of its physical origin, not requiring to design some math to guarantee smoothness.
In practice it would be similar than a bezier curve. Instead two points with a handle on each to define tangent directions, we have a velocity on each point.
Constant force value probably controls the tension of the curve.
Certainly interesting. But i assume, once we want to have variable tension, so non constant force, analytical math is still possible, but too complex to be practical. So we probably better stick at splines.

Himura78 said:
Is there a typical way this is done in this type of game?

The two most common curve types are Catmull-Rom splines and Bezier splines. Unlike the more advanced variants used in NURBS for example, those are pretty simple to use.

Catmull-Rom takes input of N points and generates a smooth curve. No need to provide tangents:

	inline sVec3 CatmullRom3D (const float tF, const sVec3 k0, const sVec3 k1, const sVec3 k2, const sVec3 k3) 
	{
		float sF = 1.0f - tF;

		sVec3 g0 = k2 - k0;
		sVec3 g1 = k3 - k1;
		sVec3 gL = g0 * sF + g1 * tF;
		sVec3 x0 = k1 + (gL + g0) * tF * 0.25f;
		sVec3 x1 = k2 - (gL + g1) * sF * 0.25f;

		float tI = tF * tF * (3.0f - 2.0f*tF); // cubic (smoothstep)
		//if (base_debug::dbgi[0] & 0x10000) tI = tF * tF * tF * (tF * (tF * 6.0f - 15.0f) + 10.0f); // quintic 
		return x0 * (1.0f - tI) + x1 * tI;
	}
	
inline sVec3 CatmullRom3D (const float tSpline, const int numKnots, const sVec3 *knots, int const stride = 1) // clamps indices in range
	{
		//float tW = tSpline * float(numKnots);
		float tW = tSpline * float(numKnots-1); // bugfix
		float ftW = floor (tW);
		float tF = tW - ftW;

		int i1 = (int) ftW;
		int i2 = i1 + 1;
		int i3 = i1 + 2;
		int i0 = i1 - 1;

		if (i0 < 0) i0 = 0;
		if (i1 < 0) i1 = 0;
		if (i2 < 0) i2 = 0;
		if (i3 < 0) i3 = 0;

		if (i0 >= numKnots) i0 = numKnots-1;
		if (i1 >= numKnots) i1 = numKnots-1;
		if (i2 >= numKnots) i2 = numKnots-1;
		if (i3 >= numKnots) i3 = numKnots-1;

		return CatmullRom3D (tF, knots[i0*stride], knots[i1*stride], knots[i2*stride], knots[i3*stride]);
	}

 

It works like this:

If we want to calculate the curve segment between points 2 and 3, we make adjacency vectors 1-3 and 2-4, half them, and then we interpolate them using smoothstep.
That's pretty simple, but we should know how smoothstep works, which is difficult given the optimized formula we usually see (f1):

We generate green and yellow curves, guaranteed to be smooth at 0 or 1 by squaring (f2, f3), then we lerp those two curves and done. Our result (f4) is the same as smoothstep - more instructions but easier to understand imo.

That's probably ideal for what you want to do.

Bezier curves are the kind you know from Photoshop, Illustrator, etc. All font letters in postscript also are made from them.
They can have any number of points, but we usually only use those with two points and two handles.
Due to the handles, we have intuitive control of the curve tangent at control points, which can be an advantage over Catmull-Rom, especially if human design is involved.

 inline void EvaluateBezier4 (sVec3 &pos, sVec3 &tang, sVec3 cp1, sVec3 cp2, sVec3 cp3, sVec3 cp4, float t)
	{
		float u = 1.0f - t;		
	
		float vvv = u*u*u;
		float vvu = u*u*t*3;
		float vuu = u*t*t*3;
		float uuu = t*t*t;
	
		pos =  vvv * cp1;
		pos += vvu * cp2;
		pos += vuu * cp3;
		pos += uuu * cp4;

		// tangent... 
		vvv = -3*u*u; // opt
		vvu = -6*u*t + 3*u*u; 
		vuu =  6*u*t - 3*t*t; 
		uuu =  3*t*t;

		tang =  vvv * cp1;
		tang += vvu * cp2;
		tang += vuu * cp3;
		tang += uuu * cp4;
	}

This code also gives the tangent direction at any position, which would be useful to orient a sprite to the direction of the curve, for example.

To use the example code, you generate points by increasing the t parameter from 0 to 1.
Engines should have such functionality built in.

Edit: Link to the graph: https://graphtoy.com/?f1(x,t)=x%20*%20x%20*%20(3%20-%202*x)&v1=true&f2(x,t)=x%20*%20x&v2=true&f3(x,t)=1%20-%20(1-x)%20*%20(1-x)&v3=true&f4(x,t)=(1-x)%20*%20(x%20*%20x)%20+%20x%20*%20(1%20-%20(1-x)%20*%20(1-x))&v4=false&f5(x,t)=&v5=false&f6(x,t)=&v6=false&grid=1&coords=0,0,2.158305478910566

Another edit: Just found a bug in my code, fixed it also here.

Thanks for your replies, really appreciate it ?

@Alberth I like the idea of using straight line segments, though I'm not sure if that's what they're doing in the video I attached - if they were using straight line segments, when I slow the video down I would expect to see that in the trajectory, but when I do that the motion still looks curved. I also like the idea of using a force to follow the straight line segments, which would give a curved trajectory, but it seems that it'd be very difficult to achieve the result in the video using that technique, because in the video, the boss follows the same circular path at an ever-increasing speed. I don't see how it'd be possible to calculate the required force to guarantee that the same path would be followed for any given speed.

@JoeJ Thanks for explaining the Catmull-Rom and Bezier curves. As I was saying in my earlier post, I played around with a Bezier curve that was parameterised by arc length when trying to replicate this kind of motion… but the problem is, I'm not sure how to control the speed along the curve in a similar way to what's done in the video. The boss seems to speed up over time, but also reduce its speed when turning more sharply. To achieve this using the parameterised curve it seems I would need to have some kind of function that dictates the speed along the curve over time, but I'm not clear on how to generate this function, and it seems a difficult way of approaching it. I guess I was hoping there was some more obvious / natural way of doing it that I was missing.

Himura78 said:
As I was saying in my earlier post, I played around with a Bezier curve that was parameterised by arc length when trying to replicate this kind of motion… but the problem is, I'm not sure how to control the speed along the curve in a similar way to what's done in the video. The boss seems to speed up over time, but also reduce its speed when turning more sharply. To achieve this using the parameterised curve it seems I would need to have some kind of function that dictates the speed along the curve over time, but I'm not clear on how to generate this function, and it seems a difficult way of approaching it.

Yeah, idk either how to move along a spline at constant speed. For an approximation, relating current tangent length to desired speed might work well enough.
But you could look up libraries like this, which can solve such problems (random find): https://github.com/andrewwillmott/splines-lib

In the video it looks like they use arcs. With arcs that's easy: Just move at constant angle increments.
Arcs are also more predictable to the player eventually.

Himura78 said:
I like the idea of using straight line segments

Some reasonable idea might be to support splines and arcs in tools, using such library to precompute tessellated line segments of constant length.
The game runtime then needs no spline functionality, and moving at constant speed is easy too.

Himura78 said:
like the idea of using straight line segments, though I'm not sure if that's what they're doing in the video I attached - if they were using straight line segments, when I slow the video down I would expect to see that in the trajectory

Depends on the length of the line segments. Nothing stops you from having line segments of 2 pixels, although 10-20 pixels for the curvy parts is likely more realistic.

JoeJ said:

In the video it looks like they use arcs. With arcs that's easy: Just move at constant angle increments.
Arcs are also more predictable to the player eventually.

Some reasonable idea might be to support splines and arcs in tools, using such library to precompute tessellated line segments of constant length.
The game runtime then needs no spline functionality, and moving at constant speed is easy too.

What do you mean by arcs?

In the video, at around 1 minute, the boss seems to follow an elliptical or maybe a capsule sort of shape… how would this be done with arcs?

I think your idea of precomputing tessellated line segments of constant length is the same as what I meant by saying “parameterising the Bezier spline by arc length” (I meant to do this approximately using line segments).

I can do that, and so I can then move an object along the curve at constant speed… I'm okay with that part. It's more the next step that I struggle with, which is choosing the appropriate speed at any time point.

Alberth said:

Depends on the length of the line segments. Nothing stops you from having line segments of 2 pixels, although 10-20 pixels for the curvy parts is likely more realistic.

That's true, maybe it's as simple as using a lot of line segments. I guess I could try it out and see ?

I have code to convert a series of points (a trajectory) into a Bézier curve. The key thing is that my code takes an arbitrary number of points, rather than, say, 4 points.

Will this code help?

P.S. The code is:

// https://stackoverflow.com/questions/785097/how-do-i-implement-a-bézier-curve-in-c
vector_4 getBezierPoint(vector<vector_4> points, float t)
{
	int i = points.size() - 1;

	while (i > 0)
	{
		for (int k = 0; k < i; k++)
		{
			points[k].x += t * (points[k + 1].x - points[k].x);
			points[k].y += t * (points[k + 1].y - points[k].y);
			points[k].z += t * (points[k + 1].z - points[k].z);
			points[k].w += t * (points[k + 1].w - points[k].w);
		}

		i--;
	}

	return points[0];
}
for (size_t i = 0; i < all_4d_points.size(); i++)
{
	vector<vector_4> p;

	for (float t = 0; t <= 1.0f; t += 0.01f)
	{
		vector_4 v = getBezierPoint(all_4d_points[i], t);
		p.push_back(v);
	}

	bezier_paths.push_back(p);
}

This topic is closed to new replies.

Advertisement