🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Trying to implement simple seek steering behaviour

Started by
4 comments, last by nortski 3 weeks, 1 day ago

Having my first look at seek steering behaviour. After watching a lecture on Youtube and given this formula, where scale is a float anywhere between 0.0 and 1.0 that affects the sharpness of the seek curve:

desired = target - pos
desired = desired / desired.length (normalised)
desired = desired * vel.length
steering = desired - vel
steering = steering * scale
actual = vel + steering

I tried giving it a go but I get odd results. It seems to work well enough when the scale is set to 1.0 but anything below 0.5 the agent won't seek the target that is positioned in it's opposite direction. I create an agent from a struct and give it an initial, normalised, velocity and I set the target to the mouse cursor position.

Demo: https://streamable.com/k42cms

struct Entity
{
    sf::CircleShape shape;
    Vec2 position;
    Vec2 velocity;
    Vec2 desired;
    Vec2 steering;
    Vec2 actual;
    float steerScale = 0.4f;
    float speed = 2.0f;    

    Entity(sf::RenderWindow& win)
    {
        position = { win.getSize().x / 2.0f, win.getSize().y / 2.0f };
        shape.setPosition(sf::Vector2f(position.x, position.y));        
        velocity = { 2.0f, 2.0f };
        velocity.normalize();
        velocity *= speed;
    }
};
void sMovement(Entity& agent, sf::RenderWindow& window)
{
    Vec2 mPos = { (float)sf::Mouse::getPosition(window).x, (float)sf::Mouse::getPosition(window).y };

    agent.desired               = mPos - agent.position;
    agent.desired.normalize();
    agent.desired               = { agent.desired.x * agent.velocity.getLength(), agent.desired.y * agent.velocity.getLength() };
    agent.steering              = agent.desired - agent.velocity;
    agent.steering              = { agent.steering.x * agent.steerScale, agent.steering.y * agent.steerScale };
    agent.actual                = agent.velocity + agent.steering;
    agent.position              = agent.position + agent.actual;

    std::cout << agent.actual.getLength() << std:: endl;
}
void update(Entity& agent)
{
    agent.shape.setPosition(sf::Vector2f(agent.position.x, agent.position.y));
}
Advertisement

nortski said:
After watching a lecture on Youtube and given this formula

A problem with the formula is it's terminology. What is ‘actual’ or ‘steering’? Those are no physical terms, which would be helpful to relate the method to other alternatives.

A physical object has a velocity describing it's movement.
Changing the movement means to apply acceleration ('steering').
After that change, the velocity becomes a different value, but neither the formula nor your code ever change velocity. Seemingly it remains constant to the value you gave it in the constructor, so i wonder it works at all.
Instead velocity, you use ‘actual’, which does change but is simply the wrong term imo, causing confusion.

Now you may say you're not much interested in math or physics eventually, you just want to make games.
Nothing wrong with that if so, but moving objects is physics, and using related terminology helps a lot with learning from multiple sources, which will for a majority agree on those established terms. It also helps with communication - asking for help or exchanging ideas, etc.

I've written a small the demo to show some basic control methods.
Yours is probably similar to the first method (mode 0) with smoothing close to one.

The next adds a speed limit.

The next uses a limit on acceleration, and it's similar to the Super Mario moons / flying turtles. (iirc you are the guy talking about that some time ago)

The last uses 'critical damping' from a code snippet i've found on the internet.
For it's simplicity this works pretty well for many applications. It's function parameters use physics terms, so using it becomes easier if we do this too. ; )

This is really just simple examples. All of this can be combined as needed, but it's not really intelligent behavior ofc., just physics. But often it's what we want, because it's easy to predict for the player.

To simplify the physics math, it helps to use a timestep of one. Then it is as simple as your example and initially easier to learn.
But using correct timesteps matching realtime helps to have consistent results across different hardware.
(I'm just assuming you're not super experienced with physics yet ; )

static bool visSteeringGame = 1; ImGui::Checkbox("visSteeringGame", &visSteeringGame);
		if (visSteeringGame)
		{
			// timing and mouse
			std::this_thread::sleep_for(std::chrono::milliseconds(16)); // throttle for 60 fps
			static auto prevTime = std::chrono::system_clock::now();
			auto curTime = std::chrono::system_clock::now();
			std::chrono::duration<float> deltaTime = curTime - prevTime;
			prevTime = curTime;
			float timestep = deltaTime.count(); // measured from real time

			vec2 target (application.mousePosX - 1000.f, -application.mousePosY + 1000.f);

			// settings
			static int mode = 2; ImGui::SliderInt("mode", &mode, 0, 3);
			static float maxVel = 500; ImGui::DragFloat("maxVel", &maxVel, 0.1f);
			static float maxAcc = 2000; ImGui::DragFloat("maxAcc", &maxAcc, 0.1f);
			static float maxTargtetDist = 50.f; ImGui::DragFloat("maxTargtetDist", &maxTargtetDist, 0.1f);
			static float smoothing = 0.5f; ImGui::SliderFloat("smoothing", &smoothing, 0.f, 1.f); // smooths the target - set to zero to see most difference between the methods
			static float dampingTime = 0.5f; ImGui::DragFloat("dampingTime", &dampingTime, 0.1f);



			struct Agent
			{
				vec2 pos = vec2(0.f);
				vec2 vel = vec2(0.f);

				void Integrate (float timestep)
				{
					pos += vel * timestep;
				}
			};
			static Agent agent;


			// simple steering behavior

			vec2 smoothTarget = agent.pos * smoothing + target * (1.f-smoothing); 
			vec2 diff = smoothTarget - agent.pos;
			float dist = diff.Length();
			if (dist > maxTargtetDist) diff *= maxTargtetDist / dist;
			smoothTarget = agent.pos + diff;
			
			if (mode == 0) // hit the target immideately
			{
				vec2 velToHitTargetOnNextFrame = diff / timestep;
				agent.vel = velToHitTargetOnNextFrame;
			}

			if (mode == 1) // some maximum speed, causing motion with mostly constant velocity
			{
				vec2 velToHitTargetOnNextFrame = diff / timestep;
				agent.vel = velToHitTargetOnNextFrame;
				float mag = agent.vel.Length();
				if (mag > maxVel) agent.vel *= maxVel / mag;
			}

			if (mode == 2) // some maximum acceleration, realistic motion but overshooting the target in a springy way
			{
				vec2 velToHitTargetOnNextFrame = diff / timestep;
				vec2 accel = (velToHitTargetOnNextFrame - agent.vel) / timestep;
				float mag = accel.Length();
				if (mag > maxAcc) accel *= maxAcc / mag;
				agent.vel += accel * timestep;
			}

			if (mode == 3) // critical damping, prevents the overshoot
			{
				auto SmoothCD = [](vec2 from, vec2 to, vec2 &vel, float smoothTime, float deltaTime)
				{
					// from Programming Gems 4
					float omega = 2.f/smoothTime;
					float x = omega*deltaTime;
					float exp = 1.f/(1.f+x+0.48f*x*x+0.235f*x*x*x);
					vec2 change = from - to;
					vec2 temp = (vel+omega*change)*deltaTime;
					vel = (vel - omega*temp)*exp;
					return to + (change+temp)*exp;
				};

				SmoothCD (agent.pos, target, agent.vel, dampingTime, timestep);
				float mag = agent.vel.Length();
				if (mag > maxVel) agent.vel *= maxVel / mag;
			}

			agent.Integrate(timestep);
			
			// visualize


			RenderPoint (target, 1,1,1);
			RenderPoint (agent.pos, 1,0,0);
			RenderLine (agent.pos, smoothTarget, 1,0,0);
			
			static std::vector<vec2> trajectory;
			if (trajectory.size() > 1000) trajectory.erase(trajectory.begin());
			trajectory.push_back(agent.pos);
			for (size_t i=1; i<trajectory.size(); i++)
				RenderLine(trajectory[i-1], trajectory[i], 0,0.5f,1);
			
			vec2 topLeft (-640, -320); vec2 botRight (640, 320);
			RenderAABBox(topLeft, botRight, 1,1,1);
		}

@JoeJ thank for the detailed reply. I shall read through it, several times, and try to understand what is happening.

🙂

nortski said:
I shall read through it, several times, and try to understand what is happening.

Uhh - this would not have worked for me.

I suggest you copy the code into your project and make it work with your mouse driven target. Try different settings, etc.
This way you can feel what it does. That's the only way to understand simulations imo. Math text books do not help much. ; )

@JoeJ yeh good idea, I'll give it a go.

Advertisement