Groups of units and collision response

Started by
15 comments, last by JoeJ 5 months, 2 weeks ago

JoeJ you often write so much that I find it difficult to keep up with you feedback wise. Your physics driven “unit” spreading screenshot looks interesting that’s all I can say. I understand the principle, making things behave like they would in real life but I don’t understand the laws of physics like formulas and stuff

at everyone else: thank you guys for sharing useful information and resources.

My project`s facebook page is “DreamLand Page”

Advertisement

Calin said:
I understand the principle, making things behave like they would in real life but I don’t understand the laws of physics like formulas and stuff

Well, i can give a little primer on that. Simulation can range from very to easy to very hard, but you should get away with the easy things for your needs.
And actually we do not even need a full simulation. This would mean you have to deal with velocity and acceleration for all your stuff, which i think is not common for RTS. Movement rather looks like constant velocity motion usually.

So let's say we just want to resolve traffic jams, ignoring stuff like integrating equations of motion.
Then we don't need a simulation, just a solver for our jam problem.

But the philosophy remains the same: Instead thinking about global logic to do things in some order, we think about simple local behavior, which then hopefully solves our jam problem if we observe global results.
The behavior is simply to move both units apart of each other for each pair that intersects. And to keep the math simple, we use simple shapes. Actually circles for units and rectangles for obstacles.
Expected problem: If A and B intersect, we move them apart. But now B intersets with C, which it did not do initially. And because we had already checked B vs. C before we have handled A vs. B, we now miss to resolve this collision we have created without intent.
Solution: Repeat the whole process for a number of iterations. That's expensive, but it will find the new collision and address it.
So our plan is to create an iterative solver.

I made one of my applets and here is what i got:

Initially, without any solver iterations, we have lots of intersections.

After one iteration, it;'s a bit better, but not yet good. But notice no more agents inside static obstacles.

With 10, it looks pretty good for many areas. But inside my 'building' those circles still often intersect.

… i drag my slider up to 100, and i can see how circles get pushed out through the ‘door’ of my building. Pretty cool to watch…

Better. You can see the trajectories (red→blue lines), and you see the heavy movement near the door.

With 1000, result looks acceptable to me.

But it's 2 fps. How can we make it faster?
1. Use some acceleration structure for collision tests, as discussed here:
2. But most importantly: Do just N iterations per frame of your game. Let's say we do 4 at 60 fps. Then we have our 1000 iterations result after 4 seconds while playing the game.
3. Make a better solver.

For a better solver, here is an idea:
Observing the trajectories, we see they form a vector field, pointing outwards from initially dense spots.
So we could, for each agent, look up for nearby agents in a larger area. From those nearby but many agents we can calculate a density gradient, and displace particles along then gradient until we have target density.
That's in part how SPH fluid simulation works.
The same idea can also give us flocking behavior: We calculate average velocity of nearby fish and then swim in this direction too.

You don't need a degree in math to experiment with such ideas. It's easy and fun. ; )

		static bool testParticleAgents = 1; ImGui::Checkbox("testParticleAgents", &testParticleAgents);
		if (testParticleAgents)
		{
			struct Agent
			{
				vec2 pos;
				float mass;
				float radius;

				void Vis (float r, float g, float b)
				{
					RenderCircle (radius, pos, vec(0,0,1), r,g,b);
				}
			};

			struct Obstacle
			{
				vec2 minCorner;
				vec2 maxCorner;

				void Vis (float r, float g, float b)
				{
					//assert(minCorner[0] < maxCorner[0] && minCorner[1] < maxCorner[1]);
					RenderAABBox(minCorner, maxCorner, r,g,b);
				}
			};

			// create some obstacels

			std::vector<Obstacle> obstacles(4);
			int i=0;
			obstacles[i].minCorner = vec2(-0.5f, -0.5f); obstacles[i].maxCorner = vec2(-0.3f, 0.5f); i++;
			obstacles[i].minCorner = vec2(0.5f, -0.5f); obstacles[i].maxCorner = vec2(0.7f, 0.1f); i++;
			obstacles[i].minCorner = vec2(-0.75f, -0.55f); obstacles[i].maxCorner = vec2(0.55f, -0.35f); i++;
			obstacles[i].minCorner = vec2(-0.75f, 0.35f); obstacles[i].maxCorner = vec2(0.55f, 0.55f); i++;		
			for (auto &r : obstacles) r.Vis(1,1,1);

			// create random agents
			static vec2 dbgOffset (0.f); ImGui::SliderFloat2("dbgOffset", (float*)&dbgOffset, -0.5f, 0.5f);
			//static float dbg (0.f); ImGui::DragFloat("dbg", &dbg, -0.01f);
			static int agentCount = 200; ImGui::DragInt("agentCount", &agentCount);
			std::srand(0);
			std::vector<Agent> agents;
			for (int i=0; i<agentCount; i++)
			{
				Agent a;
				vec2 pos(0.f);
				for (int d=0; d<2; d++)
				{
					float c = float(std::rand()) / float(RAND_MAX);
					c = (c-0.5f) * 2.f; 
					pos[d] = c;
				}
				a.pos = pos + dbgOffset;
				a.radius = 0.05f + 0.05f * float(std::rand()) / float(RAND_MAX);
				a.mass = a.radius * a.radius;
				agents.push_back(a);
			}
			for (auto &a : agents) a.Vis(0.5f,0.5f,0.5f);

			static int iterationCount = 500; ImGui::DragInt("iterationCount", &iterationCount);
			for (int n=0; n<iterationCount; n++)
			{
				float colorize = pow(float(n+1) / float(iterationCount), 0.25f);

				// collide each agent against each other
				for (int i=0; i<agentCount-1; i++)
				for (int j=i+1; j<agentCount; j++)
				{
					Agent &a0 = agents[i];
					Agent &a1 = agents[j];
					float minDist = a0.radius + a1.radius;
					vec2 diff = a1.pos - a0.pos;
					float dist = diff.Length();
					if (dist < minDist) // colliding
					{
						float massSum = a0.mass + a1.mass;
						vec2 sep = diff / dist * (minDist - dist); // vector to resolve the intersection
						RenderVector(a0.pos, -sep * (a1.mass / massSum), 1-colorize,0,colorize); // visualize trajectory
						RenderVector(a1.pos,  sep * (a0.mass / massSum), 1-colorize,0,colorize);
						a0.pos -= sep * (a1.mass / massSum); // we distribute the vector to both agents, weighted by mass
						a1.pos += sep * (a0.mass / massSum);
					}					 
				}

				// collide each agent against each obstacle 
				for (int j=0; j<obstacles.size(); j++)
				{
					Obstacle &o = obstacles[j];
					vec2 center = (o.minCorner + o.maxCorner) * .5f;
					vec2 half = o.maxCorner - center;

					for (int i=0; i<agentCount; i++)
					{
						Agent &a = agents[i];
						if (a.pos[0] < o.minCorner[0]-a.radius ||
							a.pos[1] < o.minCorner[1]-a.radius ||
							a.pos[0] > o.maxCorner[0]+a.radius ||
							a.pos[1] > o.maxCorner[1]+a.radius)
								continue; // skip if no collision
					
						// calculate closest point on obstacle boundary
						vec2 closest;
						bool inside;
						{
							vec2 diff = a.pos - center;
							vec2 clampedDiff (	min(max(diff[0], -half[0]), half[0]),
												min(max(diff[1], -half[1]), half[1]) );
							inside = (clampedDiff == diff);

							if (inside) // circle center is inside rectangle
							{
								closest = a.pos;
								int d = (half[0] - fabs(clampedDiff[0]) > half[1] - fabs(clampedDiff[1]));
								//RenderLabel (a.pos, 0,0.5f,1, "%i", d);
								closest[d] = center[d] + half[d] * (diff[d]<0.f ? -1.f : 1.f);
							}
							else // outside, in the voronoi regions of either edges or vertices
							{
								closest = center + clampedDiff;						
							}
						}

						// resolve collision
						{
							vec2 diff = a.pos - closest;
							float dist = diff.Length();
							if ((inside || dist < a.radius) && dist > FP_EPSILON)
							{
								float mag = (!inside ? a.radius - dist : -dist - a.radius);
								a.pos += diff / dist * mag;
							}
						}

						//RenderLine (a.pos, closest, inside,0.5f,!inside);								
					}
				}
			}
			for (auto &a : agents) a.Vis(0,1,0);
		}

I think I can understand bits of your primer JoeJ. You move the overlapping circles gradually away from the walls. The ”house“ entrance is where most movement takes place because you need to spew out all the extra circles that are found inside. Also I‘ve noticed you have a structure defined inside a function, it’s the first time I see a thing like that

My project`s facebook page is “DreamLand Page”

Calin said:

Also I‘ve noticed you have a structure defined inside a function, it’s the first time I see a thing like that

Reminds me i always complained about this. Back then, when it did not work. But they added this feature with C++11. Seems you did not care for a decade about what has changed on the language. ;D

It's a real game changer imo. Look at this, i have changed the code slightly:

static bool testParticleAgents = 1; ImGui::Checkbox("testParticleAgents", &testParticleAgents);
		if (testParticleAgents)
		{
			struct Agent
			{
				vec2 pos;
				float mass;
				float radius;

				void Vis (float r, float g, float b)
				{
					RenderCircle (radius, pos, vec(0,0,1), r,g,b);
				}
			};

			struct Obstacle
			{
				vec2 minCorner;
				vec2 maxCorner;

				void Vis (float r, float g, float b)
				{
					//assert(minCorner[0] < maxCorner[0] && minCorner[1] < maxCorner[1]);
					RenderAABBox(minCorner, maxCorner, r,g,b);
				}
			};

			struct Collision // making this only to have a local function
			{
				static void ResolvePairOfAgents (Agent &a0, Agent &a1, float colorize)
				{
					float minDist = a0.radius + a1.radius;
					vec2 diff = a1.pos - a0.pos;
					float dist = diff.Length();
					if (dist < minDist) // colliding
					{
						float massSum = a0.mass + a1.mass;
						vec2 sep = diff / dist * (minDist - dist); // vector to resolve the intersection
						RenderVector(a0.pos, -sep * (a1.mass / massSum), 1-colorize,0,colorize); // visualize trajectory
						RenderVector(a1.pos,  sep * (a0.mass / massSum), 1-colorize,0,colorize);
						a0.pos -= sep * (a1.mass / massSum); // we distribute the vector to both agents, weighted by mass
						a1.pos += sep * (a0.mass / massSum);
					}
				}
			};

			auto ResolveAgentObstaclePair = [](Obstacle &o, Agent &a) // lambas also give us local functions, but it's more convenient
			{
				if (a.pos[0] < o.minCorner[0]-a.radius ||
					a.pos[1] < o.minCorner[1]-a.radius ||
					a.pos[0] > o.maxCorner[0]+a.radius ||
					a.pos[1] > o.maxCorner[1]+a.radius)
						return; // skip if no collision
					
				vec2 center = (o.minCorner + o.maxCorner) * .5f;
				vec2 half = o.maxCorner - center;
				
				// calculate closest point on obstacle boundary
				vec2 closest;
				bool inside;
				{
					vec2 diff = a.pos - center;
					vec2 clampedDiff (	min(max(diff[0], -half[0]), half[0]),
										min(max(diff[1], -half[1]), half[1]) );
					inside = (clampedDiff == diff);

					if (inside) // circle center is inside rectangle
					{
						closest = a.pos;
						int d = (half[0] - fabs(clampedDiff[0]) > half[1] - fabs(clampedDiff[1]));
						//RenderLabel (a.pos, 0,0.5f,1, "%i", d);
						closest[d] = center[d] + half[d] * (diff[d]<0.f ? -1.f : 1.f);
					}
					else // outside, in the voronoi regions of either edges or vertices
					{
						closest = center + clampedDiff;						
					}
				}

				// resolve collision
				{
					vec2 diff = a.pos - closest;
					float dist = diff.Length(); // todo: if the agent is axactly at the wall this becomes zero and won't work. fix: introduce a contact normal
					if ((inside || dist < a.radius) && dist > FP_EPSILON)
					{
						float mag = (!inside ? a.radius - dist : -dist - a.radius);
						a.pos += diff / dist * mag;
					}
				}
			};

			// create some obstacels

			std::vector<Obstacle> obstacles(4);
			int i=0;
			obstacles[i].minCorner = vec2(-0.5f, -0.5f); obstacles[i].maxCorner = vec2(-0.3f, 0.5f); i++;
			obstacles[i].minCorner = vec2(0.5f, -0.5f); obstacles[i].maxCorner = vec2(0.7f, 0.1f); i++;
			obstacles[i].minCorner = vec2(-0.75f, -0.55f); obstacles[i].maxCorner = vec2(0.55f, -0.35f); i++;
			obstacles[i].minCorner = vec2(-0.75f, 0.35f); obstacles[i].maxCorner = vec2(0.55f, 0.55f); i++;		
			for (auto &r : obstacles) r.Vis(1,1,1);

			// create random agents
			static vec2 dbgOffset (0.f); ImGui::SliderFloat2("dbgOffset", (float*)&dbgOffset, -0.5f, 0.5f);
			//static float dbg (0.f); ImGui::DragFloat("dbg", &dbg, -0.01f);
			static int agentCount = 200; ImGui::DragInt("agentCount", &agentCount);
			std::srand(0);
			std::vector<Agent> agents;
			for (int i=0; i<agentCount; i++)
			{
				Agent a;
				vec2 pos(0.f);
				for (int d=0; d<2; d++)
				{
					float c = float(std::rand()) / float(RAND_MAX);
					c = (c-0.5f) * 2.f; 
					pos[d] = c;
				}
				a.pos = pos + dbgOffset;
				a.radius = 0.05f + 0.05f * float(std::rand()) / float(RAND_MAX);
				a.mass = a.radius * a.radius;
				agents.push_back(a);
			}
			for (auto &a : agents) a.Vis(0.5f,0.5f,0.5f);



			static int iterationCount = 500; ImGui::DragInt("iterationCount", &iterationCount);
			for (int n=0; n<iterationCount; n++)
			{
				float colorize = pow(float(n+1) / float(iterationCount), 0.25f);

				// collide each agent against each other
				for (int i=0; i<agentCount-1; i++)
				for (int j=i+1; j<agentCount; j++)
				{
					Collision::ResolvePairOfAgents(agents[i], agents[j], colorize);
				}

				// collide each agent against each obstacle 
				for (int j=0; j<obstacles.size(); j++)
				for (int i=0; i<agentCount; i++)
				{
					ResolveAgentObstaclePair (obstacles[j], agents[i]);
				}
			}
			for (auto &a : agents) a.Vis(0,1,0);
		}

Look at the final loop doing the solve. How clean and easy to read it is. The whole concept and idea becomes visible instantly.

Ofc. i could just make the Resolve() functions global like with C, but then we pollute our code base with tons of little helper functions we need only in one spot. The crap shows up in intellisense pop ups, and it becomes hard to find the important functions within all that noise.

With C++11 we can finally write our helper functions inside the one function where we need them. So they are easy to find while working on that, but completely hidden when working on anything else.
Also, think about all those copy pasted code sections. We can use local lambdas to get rid of this noise, and write significantly less code for the same result.

Personally i was lazy and slow with learning the new syntax of lambdas, so i was using local structs instead. Under the hood both is the same anyway, but today i say it's worth to learn about lambdas asap.
However, in the code i used both ways to give an example.

Calin said:
The ”house“ entrance is where most movement takes place because you need to spew out all the extra circles that are found inside.

Yes. If a bottle has a hole the will spill out due to gravity or pressure, and we try to simulate this behavior.
Maybe you remember my alternative path finding method some years ago. The idea was to fill honey into the goal of a labyrinth. Once the honey has reached all exits, we can find the shortest path to the goal simply by following increasing height of honey.
Both those ideas are closely related and typical ‘simulation’ examples, where we solve problems more with math and less with logic.

Calin said:
I think I can understand bits of your primer JoeJ. You move the overlapping circles gradually away from the walls.

Broadly yes, although i do not resolve static collisions gradually. I project the circles to the boundary of the rectangle in one step. It's not gradual. Static collision is not our problem.
The problem is the many dynamic objects. That's why we need many iterations at all, and why we seemingly approach a solution 'gradually'.

But your use of the word ‘gradually’ shows you understand the concept. So let's dig deeper.

I claim that our solver is bad. Because if we use it in a game, our units may jitter. Look at the trajectories again. They form zig zag paths. If i see jitter in a game, i refund and uninstall this amateurish crap immediately, and play a proper game instead.

So what do you think where this jitter comes from, and what could we eventually do about it?

I claim that our solver is bad

My math class marks were embarrassing from the beginning of the high school until the end. And I haven’t improved since then. I don’t think I can help you build a better solver. I’ve noticed you make heavy use of motion vectors which is probably a normal thing to do in a situation like this. A couple of years ago I was naive enough to think I can build a ray tracing engine on my own. I had to look at the source of a professional made ray tracing demo to realize it’s not my league.

In recent months I have been trying to understand how you could simulate RTS unit micromanagement. That gave me a bit of insight into vectors. But I still don’t understand them properly. And it doesn’t bother me. I can build my game without them.

My project`s facebook page is “DreamLand Page”

Calin said:
In recent months I have been trying to understand how you could simulate RTS unit micromanagement. That gave me a bit of insight into vectors. But I still don’t understand them properly. And it doesn’t bother me. I can build my game without them.

I see. I also lack related education completely and had to learn everything myself.
It's true for 2D games you can get away without vectors, but it's also true that vectors are not really something new. We can understand them geometrically, including transformation matrices. And i wanted to make 3D games, which gave me the motivation to learn it.
To rewrite the above code without vectors, any line involving them would become two lines, doing the math on both x and y separately. And to calculate the length of a vector formed form x and y, we use Pythagoras as learned in school: A^2 + B^2 = C^2, so length = sqrt(xx + yy).

Today i think the point when i started to use vectors and matrices was the point when i transitioned from trial and error on C64 towards serious programming. And it also made everything so much easier. I just loved those vectors.
It's the foundation of both graphics and physics, including related simulations. So we can't get further with the simulation primer until you did some linear math homework. : )

You use DirectX, which comes with a linear math lib. XMM vectors and that stuff, or how they call it. Assuming they also have a 2D vector not just 3D and 4D, you should use this. Otherwise there is GLM and others.
Or you write your own. E.g. a vec2 could be as simple as this:

struct Point2D
	{
		float x, y;

		Point2D () {};

		Point2D (float x, float y) : x(x), y(y) {};


		Point2D operator + (const Point2D &p) const 
		{
			return Point2D (x + p.x, y + p.y);
		}
		Point2D operator - (const Point2D &p) const
		{
			return Point2D (x - p.x, y - p.y);
		}
		Point2D operator * (const float v) const
		{
			return Point2D (x * v, y * v);
		}
		
		float Dot (const Point2D &p) const
		{
			return x * p.x + y * p.y;
		}		
		
float Length
 () const
		
{
		
	return sqrt(x * x + y * y);
		
}

	}; 

It should be called Vec2D, not Point2D. But otherwise that's already enough for my example i guess.

The only ‘new' thing here for you should be the dot product, so there may be not as much to learn as you might think. Totally worth the time.

This topic is closed to new replies.

Advertisement