HowToRts.github.io

Website/Blog about the things you need to know to make a modern RTS game


Steering Behaviours: Flocking

03 Jan 2014

Flocking behaviours are a subset of steering behaviours where we consider the locations of our neighbouring agents to decide our forces. They are modelled of how flocks of birds work. Again, a great reference from these is this video of Graham Penthenys GDC2013 presentation, or grab the slides.

We are going to implement the following:

  • Separation - Move away from those entities we are too close too
  • Cohesion - Move nearer to those entities we are near but not near enough to
  • Alignment - Change our direction to be closer to our neighbours

These will get our entities moving around grouped together. Check out the Completed Example

Let’s look at each of these behaviours individually first.

Separation

Separation calculates a force to move away from all of our neighbours. We do this by calculating a force from them to us and scaling it so the force is greater the nearer they are.

function steeringBehaviourSeparation(agent) {
	var totalForce = Vector2.zero;
	var neighboursCount = 0;

	//for each agent
	for (var i = 0; i < agents.length; i++) {
		var a = agents[i];
		//that is not us
		if (a != agent) {
			var distance = agent.position.distanceTo(a.position);
			//that is within the distance we want to separate from
			if (distance < agent.minSeparation && distance > 0) {
				//Calculate a Vector from the other agent to us
				var pushForce = agent.position.minus(a.position);
				//Scale it based on how close they are compared to our radius
				// and add it to the sum
				totalForce = totalForce.plus(pushForce.div(agent.radius));
				neighboursCount++;
			}
		}
	}

	if (neighboursCount == 0) {
		return Vector2.zero;
	}

	//Normalise the force back down and then back up based on the maximum force
	totalForce = totalForce.div(neighboursCount);
	return totalForce.mul(agent.maxForce);
}

Cohesion

Cohesion calculates a force that will bring us closer to our neighbours, so we move together as a group rather than individually. This is the opposite of what separation is trying to do, using them together is important but can tricky. We cover this later.

Cohesion calculates the average position of our neighbours and ourself, and steers us towards it

function steeringBehaviourCohesion(agent) {
	//Start with just our position
	var centerOfMass = agent.position;
	var neighboursCount = 1;

	for (var i = 0; i < agents.length; i++) {
		var a = agents[i];
		if (a != agent) {
			var distance = agent.position.distanceTo(a.position);
			if (distance < agent.maxCohesion) {
				//sum up the position of our neighbours
				centerOfMass = centerOfMass.plus(a.position);
				neighboursCount++;
			}
		}
	}

	if (neighboursCount == 1) {
		return Vector2.zero;
	}

	//Get the average position of ourself and our neighbours
	centerOfMass = centerOfMass.div(neighboursCount);

	//seek that position
	return steeringBehaviourSeek(agent, centerOfMass);
}

Alignment

Alignment calculates a force so that our direction is closer to our neighbours. It does this similar to cohesion, but by summing up the direction vectors (normalised velocities) of ourself and our neighbours and working out the average direction.

function steeringBehaviourAlignment(agent) {
	var averageHeading = Vector2.zero;
	var neighboursCount = 0;

	//for each of our neighbours (including ourself)
	for (var i = 0; i < agents.length; i++) {
		var a = agents[i];
		var distance = agent.position.distanceTo(a.position);
		//That are within the max distance and are moving
		if (distance < agent.maxCohesion && a.velocity.length() > 0) {
			//Sum up our headings
			averageHeading = averageHeading.plus(a.velocity.normalize());
			neighboursCount++;
		}
	}

	if (neighboursCount == 0) {
		return Vector2.zero;
	}

	//Divide to get the average heading
	averageHeading = averageHeading.div(neighboursCount);

	//Steer towards that heading
	var desired = averageHeading.mul(agent.maxSpeed);
	var force = desired.minus(agent.velocity);
	return force.mul(agent.maxForce / agent.maxSpeed);
}

Cohesion vs Separation and other behaviours

As mentioned under cohesion above, in a situation such as this: The agents at the front of the pack (far right) will have a cohesion force pulling them away from their seek target. This can mean that they do not reach their top speed as there is always a force slowing them down.

The solution I’ve decided upon is to scale down the effect of cohesion. For our purposes cohesion is not super important. So long as our units don’t spread apart too much while moving then we are doing good.

To implement this and to combine our steering behaviours together, we change our main loop as follows

//called periodically to update the game
//dt is the change of time since the last update (in seconds)
function gameTick(dt) {
	var i, agent;

	//Calculate steering and flocking forces for all agents
	for (i = agents.length - 1; i >= 0; i--) {
		agent = agents[i];

		//Work out our behaviours
		var seek = steeringBehaviourSeek(agent, destination);
		var separation = steeringBehaviourSeparation(agent);
		var cohesion = steeringBehaviourCohesion(agent);
		var alignment = steeringBehaviourAlignment(agent);

		//Combine them to come up with a total force to apply, decreasing the effect of cohesion
		agent.forceToApply = seek.plus(separation).plus(cohesion.mul(0.1)).plus(alignment);
	}

	//Move agents based on forces being applied (aka physics)
	for (i = agents.length - 1; i >= 0; i--) {
		agent = agents[i];

		//Apply the force
		agent.velocity = agent.velocity.plus(agent.forceToApply.mul(dt));

		//Cap speed as required
		var speed = agent.velocity.length();
		if (speed > agent.maxSpeed) {
			agent.velocity = agent.velocity.mul(agent.maxSpeed / speed);
		}

		//Calculate our new movement angle
		agent.rotation = agent.velocity.angle();

		//Move a bit
		agent.position = agent.position.plus(agent.velocity.mul(dt));
	}
}

You can see we have split our loop in two: First we calculate the forces on all of the agents, then we apply them. We must do it in two steps so that all agents see a consistent view of the world when calculating their behaviours.

Using this in a real RTS

I haven’t done this yet, but here are some notes.

Cohesion and Alignment probably only want to be concerned with other agents going to a similar location as us, otherwise we’ll get caught up when other agents move past.

Performance considerations

Each of our behaviours works out the agents neighbours in a very inefficient way, and it is done multiple times every tick. Instead we should store our agents in a data structure that provides efficient spatial searching (Quadtree, Cell Binning, Wikipedia: Spatial index). We also don’t need to calculate the neighbours every tick, we should be able to just recalculate them every couple and still have a good looking simulation.

Better than seek

For a real RTS there will probably be obstacles in the way that we need to path around. We’ll need to avoid these when we are close to them, and we’ll need to work out an efficient path to our target. We’ll come back to these in future articles!

comments powered by Disqus

Site By @daveleaver Theme by mattgraham