Aiming feature like angry bird

Started by
13 comments, last by JoeJ 4 months, 1 week ago

I was tempted to move this, but leaving it here. Since this wasn't in “for beginners, instead posted to the gameplay programming section of the board:

This is one of those “if you have to ask, it doesn't matter for you” questions. If you have to ask, you really aren't ready for serious gameplay programming. These are basic game math equations, normally taught in school at age 13 or 14, at the same time you learn about parabolas, squares and square roots, and often the time you first graph equations.

You use exactly the same same basic math you intend to use to simulate the shot being taken. As mentioned, you've got whatever you are simulating gravity as, and you've got whatever direction the shot is launched. It is simple addition and multiplication, nothing more, and nothing fancy. You keep asking for the function but we don't have it, YOU have the function, it is whatever math you are doing in your simulation. Whatever math it is in your simulation, repeat the same math to compute the line. Then you do basic line drawing to draw the line segments, there are even dashed line drawing functions in most libraries.

If you are relying on an engine to do all the work and you really don't understand what the engine is doing — which is terrible to do as a gameplay programmer but understandable given how powerful many freely available engines are and unskilled many beginners start out — then you can even as a worst case just run the simulation a few ticks off screen to see where the object goes. Terribly wasteful, but with CPU power even on a cheap phone there is more than enough processing power to run the simulation to the first impact and then trace the line visually.

Yes it is harsh, but I think it is important to know. What you are describing just screams that you are in over your head. Back up a bit, learn the basics and fundamentals, then come back to it when you've got a better understanding of the basic math involved. You can't run a marathon until you've learned how to run, which comes after learning how to walk, which comes after learning how to crawl. Your posts show you really need to go back to the basics. In game development those basics tend to be first learning the basics of programming, then games like tic tac toe and guess the number and minesweeper; then move to games like pong or snake with basic animation; then move to games like breakout, space invaders, tetris, pac man, and other simple animations. Physics driven simulations are way down the list, even if you're leveraging powerful tools that do the heavy lifting for you.

Advertisement

frob said:
These are basic game math equations, normally taught in school at age 13 or 14, at the same time you learn about parabolas, squares and square roots, and often the time you first graph equations.

Hmpf. You know what they have teached me about math in my kind of art school at that age? Nothing.
What did they teach us instead? Programming in Pascal. Yes, ‘programming is for everybody’ they thought back then in the 90's. A kid which can not program will end under the bridge.
Ofc. nobody has learned anything about programming there. For me it was just basics i knew long before, and all the other kids simply did not get it at all. But maybe the teacher has learned a bit.

frob said:
You use exactly the same same basic math you intend to use to simulate the shot being taken.

Disagree on that, though. To simulate your objects, you will most likely integrate small timesteps.
But this does not tell you the angle the turret needs to hit the goal. We can use integration only to see how much we will miss the target. Eventually we use the error to adjust our angle, coming closer to our desired solution if we iterate and spend a large number of speculative integration steps.
But that's what we should not do here, since there is an analytical solution to this certain problem, which will give us an exact solution in one single step.
And because it's not obvious such solution exists, it's expected people ask for help, and it's a good question to be answered here. I would even say it's too hard for the beginners section, but actually a math / physics question.

I took my code and made a simple example out of it, but sadly turning it 2D did not made it any simpler.
Still, maybe the code is easier to to adopt if it's all in one place.

static bool visAimTurret = 1; ImGui::Checkbox("visAimTurret", &visAimTurret);
if (visAimTurret)
{
	static float timestep = 0.16f; ImGui::DragFloat("timestep", (float*)&timestep, 0.01f); 
	static vec2 launchPos (0,0); ImGui::DragFloat2("launchPos", (float*)&launchPos, 0.01f);
	static vec2 targetPos (15,3); ImGui::DragFloat2("targetPos", (float*)&targetPos, 0.01f);
	static float launchVelocity = 20.f; ImGui::DragFloat("launchVelocity", &launchVelocity, 0.01f); 
	static bool takeHigherAngle = 1; ImGui::Checkbox("takeHigherAngle", &takeHigherAngle); 
	static float gravityMagnitude = 10.f; ImGui::DragFloat("gravityMagnitude", &gravityMagnitude, 0.01f); 
	static vec2 gravityDir (0,-1); ImGui::DragFloat2("gravityDir", (float*)&gravityDir, 0.01f);
	gravityDir.Normalize();
	timestep = max(0.001f, timestep);

	// calculate turret angle to hit the target
	vec2 launchDirection;
	bool outOfReach;
	{
		// projecting the target difference to gravity direction,
		// so only the vertical component is affected by acceleration.
		// the horizontal component has no acceleration, so it moves simply at constant speed.

		vec2 diff = targetPos - launchPos;
		float diffV = gravityDir.Dot(diff); // vertical component (along gravity)
		vec2 perp = diff - diffV * gravityDir; // project to gravity plane to get the horizontal component
		float diffH = perp.Length(); // horizontal distance

		// if gravity direction equals Y axis, we could do this instead, and also simplify the 'construct velocity' code below a bit
		//diffV = -diff[1]; // vertical 
		//diffH = fabs(diff[0]); // horizontal
		//perp = vec2(diff[0],0);


		if (diffH < FP_EPSILON) // target has no vertical distance, so we shoot straight upwards to hit it
		{
			launchDirection = gravityDir * -1.f;
			outOfReach = false;
		}
		else
		{
			float angle;
			{
				float vel2 = launchVelocity * launchVelocity;
				float r = vel2*vel2 - gravityMagnitude * (gravityMagnitude * diffH*diffH - 2.f*diffV*vel2);
				if (r>0.f) // we can hit the target
				{
					r = sqrt (r);
					outOfReach = false;
				}
				else // we can not, would need to increase launch velocity 
				{
					r = 0.f;
					outOfReach = true;
				}

				if (takeHigherAngle) r *= -1.f;
				float s = vel2 - r;
				float t = s / (gravityMagnitude*diffH);
				angle = atan(-t);
			}

			// construct velocity direction to apply to projectile
			launchDirection = perp / diffH * cos(angle);
			launchDirection += gravityDir * sin(angle);
		}
	}

	// calculate time to target
	{
		float vel = gravityDir.Dot(launchDirection) * launchVelocity;
		float acc = gravityMagnitude;
		float disp = gravityDir.Dot(launchPos - targetPos);

		float sq = vel*vel - 2.f*acc*disp;
		float r = (sq > 0.f ? sqrt (sq) : 0.f);

		float timeToTarget = (r - vel) / acc;
		
		ImGui::Text("predicted time: %f", timeToTarget);
	}

	// visualize trajectory
	{
		vec2 start = launchPos; // initial height of the particle
		vec2 intialVelocity = launchDirection * launchVelocity;
		vec2 a = gravityDir * gravityMagnitude; // constant acceleration of gravity
	
		vec2 pI = start; // current position
		vec2 vI = intialVelocity; // current velocity
		vI += a * timestep * .5f; // simple trick to get the accuracy of a midpoint integrator
	
		vec2 pI0 = start, pA0 = start; // cache last step to draw lines
		for (float t = 0; t <= 20.f; t += timestep) // simulate time for 2 seconds
		{
			// analytical solution:
			vec2 pA = start + intialVelocity*t + 0.5f * a * t*t;
			vec2 vA = a * t;
		
			// plot results to show p==pA and v==vA, besides integration error
			RenderPoint (pI, 1,0,0);
			//RenderLine (pI0, pI, 1,0,0);
			//RenderPoint (pA, 0,1,0);
			RenderLine (pA0, pA, 0,1,0);

			// integrating, similar to how our physics engine would do
			pI += vI * timestep;
			vI += a * timestep;

			//check if we hit the target here
			vec2 lineSegment = pI - pI0;
			float sqLen = lineSegment.Dot(lineSegment);
			vec2 diff = targetPos - pI0;
			if (diff.Dot(diff) < sqLen)
			{
				float s = lineSegment.Dot(diff) / sqLen;
				if (s>=0.f && s<=1.f)
					RenderLabel (pI0 + lineSegment * s, 1,1,1, "measured time: %f", t + timestep * s);
			}

			pI0 = pI;
			pA0 = pA;
		}
	}
	RenderArrow (launchPos, launchDirection, 1,1,1,1);
	RenderCircle (0.5f, targetPos, vec(0,0,1), outOfReach,!outOfReach,0);
	
}

It's a lot of code, but notice most of it is just setup stuff. Not all of it is needed, but it sure is some work to dig through it and to reproduce it in your code. If you feel totally overwhelmed, then it's probably in deed to early to work on this.

Here is how it looks:


The green circle is the target, and you see we hit it precisely.
The red dots show the integrated projectile position at each timestep, the green line shows the analytical prediction.
Depending on how accurate your integrator is, they can diverge a little bit. (I assume you already have some physics simulation running to simulate the projectile. If not, learn about that first.)
We give a launch velocity, which is the speed the projectile has when we fire it.
We also define gravity.
Then we calculate the turret angle / direction based on these inputs (white arrow).
I can not explain the math better than the code tries to do, because i have forgotten how exactly i have derived this a decade ago. But it works.
I also calculate the time duration the projectile will need to hit the target, which is often needed for something.

After that, i verify the results while visualizing the trajectory. So there is a lot of code you might not need but is useful to give context.

It could be simplified a little but if gravity is aligned with Y direction, see comments, but that's not much.

Are you sure that is the math being used in the simulation, though? We are both just assuming the basic acceleration, but don't know what is actually implemented.

The most basic form is exactly what you wrote, but I don't think it is all that complex. X is constant, unless there is wind or something. Y is simple acceleration, which is the standard 1/2*at^2+vt+y0. The y0 is easy enough as the start point, initial velocity is probably 0 so vt cancels out, leaving just the gravity acceleration being used in the game. You can simplify it further if you just want to see the change one step at a time in the x direction.

You also solved it in a few lines of code. It is a bit of knowledge understanding a formula for acceleration, but it is commonly taught around age 13-14 or so in most of the modern world's education systems. Generally it is one of the first things kids learn to graph as they learn functions can be turned into curves and not just lines. It's not like we're in dark ages or minimal mathematics understanding, nor college level mathematics in 2023, although it could be advanced to someone without a modern education.

frob said:
Are you sure that is the math being used in the simulation, though? We are both just assuming the basic acceleration, but don't know what is actually implemented.

I'm very sure it works for anybody using a physics engine, assuming any physics engine uses some higher order integrator like RK4.

But while i wrote this example i was surprised how large the error is with the simple euler integrator as shown in my very first code snippet.
That's maybe a problem if we don't use a physics engine, e.g. because our game feels simple enough. So maybe it's worth to talk a bit more about this.

Initially my intergration code was like this:

vel += acc * timestep;
pos += vel * timestep;

But it diverged from the analytical solution a lot already on the first step.
So i have changed order:

pos += vel * timestep;
vel += acc * timestep;

This gave the same amount of error. It only changed the integrator from overshooting to undershooting.
So have tried a mid point method, because it became obvious the correct result lies between those two attempts:

vel += acc * timestep * .5f;
pos += vel * timestep;
vel += acc * timestep * .5f;

This worked perfectly. No more visible error.
But because our acceleration is constant and there are no other forces to calculate here, i can just optimize this, going technically back to simple euler but setting off initial velocity with half of acceleration:

vel += acc * timestep * .5f;
for (;;)
{
	pos += vel * timestep;
	vel += acc * timestep;
}

So i found away to get high accuracy for free, but i agree people lacking experience might not easily come up with such tweaks, and then accuracy can be a problem.
But i still assume it works well enough for practical needs even if integration error is bigger.
And i also think that's likely true no matter how exactly people simulate their physics.
Because any attempt of simulation will try to approximate the behavior of real world physics, the given analytical and exact solution should always be a good fit to what really happens in the game. We just need to consider a small margin of error in game design.
(But i agree this would not hold for early 8 or 16 bit games, when physics was all about faking it while working with limited precision and performance budgets.)

frob said:
initial velocity is probably 0 so vt cancels out,

No, initial velocity is not zero. It is a high velocity our projectiles have at launch, and this initial velocity is also our open variable we try to calculate. We know its magnitude, but not its direction.

I think you may confuse the problem with the much simpler problem to calculate trajectory under gravity. E.g. answering a question like ‘which height will i fall in 1 second if i jump out of the window?’, or ‘how long does it take until i hit the bottom when jumping from a height of 10m?’.
But the given problem is much harder. I confuse it with other similar toy problems i had worked at the time, but i think it took me quite some time to figure it out. You can not just intersect a parabola with a line, because you do not yet know how the parabola should look like.
They surely never gave me such a difficult calculus problem in school. Hell, they did not even tell me the difference between calculus and linear algebra. I do not even know what's the german word for calculus. : /
I really have picked the wrong school, but at least it was a good time. : )

This topic is closed to new replies.

Advertisement