arrays3_lists

advertisement
Boid Example Continued, and Lists
Move to Centroid
Let’s start by implementing the centroid concept. The idea is fairly simple – each boid can see some
distance away and wants to be close to the center of other boids. For simplicity, let’s make the visible
distance of a boid a circle with the boid at the center (yes, this would mean it can see behind itself).
For now, we’ll just make this a constant at the top of our form, where the constant represents the radius
of a circle, specified in pixels, within which the boid can see:
private const int BOID_VISION_RADIUS = 100;
// Radius for a boid's vision
For every other boid within this radius we’d like our current boid to try and move to the centroid of
those boids. The centroid is just the average of the x and y coordinates of all the boids within the visible
radius. This is illustrated in the diagram below:
Boid x moves slightly
toward centroid
B
100 pixels
A
D
X
Centroid of boids
A, B, C
Boid D Ignored, outside
vision range
X
C
In this diagram boid X is currently pointed up toward the top of the window. It finds all the other boids
(boids A, B, C) within a radius of 100 pixels from its location and ignores the rest (boid D). It then
calculates the centroid of boids A, B, and C, where the centroid’s x coordinate is the average of A,B, and
C’s x coordinates, and the centroid’s y coordinate is the average of A, B, and C’s y coordinates. In the
diagram, the centroid is depicted as a red dot. Boid X then shifts slightly closer toward the centroid.
This is shown as the red arrow to the upper left. To simplify things we’ll allow our boids to magically
shift location, even if it is moving in the opposite direction. A better (but somewhat more complex
approach) would be to figure out what angle the boid should turn so it is facing the centroid, and have it
turn a little bit toward that angle.
Before writing actual code, let’s start with some pseudocode.
To calculate the centroid for the boid b:
x1 = X coordinate of boid b
y1 = Y coordinate of boid b
totalX = 0
totalY = 0
numCloseBoids = 0
For each boid i in the array of boids
if (i != b) then
x2 = X coordinate of boid i
y2 = Y coordinate of boid i
if the distance from (x1,y1) to (x2,y2) < BOID_VISION_RADIUS
totalX = totalX + x2
totalY = totalY + y2
numCloseBoids++
End If
End if
Next
centroidX = totalX / NumBoids
centroidY = totalY / NumBoids
To calculate the distance from (x,y) to (x2,y2) we can make a function that returns:
 x  x 2 2   y  y 2 
2
Here it is in actual code:
// A helper function to calculate the distance between two points
private double distance(int x1, int y1, int x2, int y2)
{
return Math.Sqrt(Math.Pow(x1-x2,2) + Math.Pow(y1-y2,2));
}
// Calculate the centroid of other boids close to boid b
private void Centroid(int b)
{
int x1, y1, x2, y2;
// x1,y1 = coordinate of boid b
// x2,y2 = coordinate of target boid in array
int totalX = 0, totalY = 0; // Total of coordinates for centroid
x1 = Xs[b];
y1 = Ys[b];
// Set to coordinates of boid b
// Loop through every boid's coordinates
// to calculate centroid
int boidsSeen = 0;
for (int i = 0; i < NUMBOIDS; i++)
{
// Don't include boid b in centroid calculations
if (i != b)
{
x2 = Xs[i];
y2 = Ys[i];
// Check if target boid is visible
if (distance(x1, y1, x2, y2) < BOID_VISION_RADIUS)
{
totalX += x2;
totalY += y2;
boidsSeen++;
}
}
}
// Do nothing if no boids in sight
if (boidsSeen == 0)
{
return;
}
// Calculate coordinates of the centroid
int centroidX = totalX / boidsSeen;
int centroidY = totalY / boidsSeen;
// Do nothing if we're already at the centroid
if ((centroidX == x1) && (centroidY == y1))
{
return;
}
// More code will go here to move the boid
}
This code calculates the centroid’s X and Y coordinate, so now we need to move the boid toward that
location. As mentioned earlier, ideally we’d change the boid’s heading so it would be going toward the
centroid, but to keep things simple we’ll just “transport” the boid a few pixels so that its closer to the
centroid:
// Move 2% of the way toward centroid,
// or not at all if within 50 pixels of the centroid
double deltaX = (centroidX - x1) / 50.0;
double deltaY = (centroidY - y1) / 50.0;
Xs[b] += Convert.ToInt32(deltaX);
Ys[b] += Convert.ToInt32(deltaY);
This code would be placed at the end of the Centroid() method. It computes the X and Y distance away
from the centroid, then moves it 1/50th of the way, or 2%, of the way there. I’ve used a small number so
the boid doesn’t look like it suddenly jumps to a new location in space. Note that there is no change if
the centroid is within 50 pixels of the boid, due to the integer conversion.
Lastly, we should update the heading toward the centroid for every boid before we draw it. This can be
done by simply adding a call to Centroid in the timer:
New
Code
// When the timer goes off, update the location of each boid
private void timer1_Tick(object sender, EventArgs e)
{
for (int i = 0; i < NUMBOIDS; i++)
{
// Centroid rule
Centroid(i);
// Set the location of the boid to a new spot moving at
// speed BOID_VELOCITY
Xs[i] += Convert.ToInt32(Math.Cos(Dirs[i]) * BOID_VELOCITY);
Ys[i] += Convert.ToInt32(Math.Sin(Dirs[i]) * BOID_VELOCITY);
// Check for going off the edge of the screen
// and wrap around to the other side
if (Xs[i] > pboxWorld.Width)
{
Xs[i] = Xs[i] - pboxWorld.Width;
}
if (Xs[i] < 0)
{
Xs[i] = pboxWorld.Width + Xs[i];
}
if (Ys[i] > pboxWorld.Height)
{
Ys[i] = Ys[i] - pboxWorld.Height;
}
if (Ys[i] < 0)
{
Ys[i] = pboxWorld.Height + Ys[i];
}
}
// Invalidate the picturebox so it calls the paint subroutine
// and re-draws the boid in the new location
pboxWorld.Invalidate();
}
Upon running the program the boids that are going in close to the same direction should now be
somewhat aligned as the centroid draws them closer to each other. Note that at this point the boids
can’t change direction, so we only see grouping for the boids going the same direction.
Move away from neighbor that’s too close
Whew, we’ve written a fair bit of code, but the good news is that we can reuse a lot of it to implement
the final two rules. To counteract the rule of moving toward the centroid, we can add a new rule that
we don’t like to be too close to another boid. It might result in a collision (although we don’t detect
one) and invades a boid’s personal space.
Let’s add a simple rule that we find the distance to the closest boid. If that distance is less than some
threshold, say 30 pixels, then the boid should move a short distance in the OPPOSITE direction. To do
this we need to find the coordinates of the closest boid. Here is pseudocode to find the boid closest to
boid b:
x1 = X coordinate of boid b
y1 = Y coordinate of boid b
closestDistanceOtherBoid = Some big number
For each boid i in the array of boids
if (i != b) then
x2 = X coordinate of boid i
y2 = Y coordinate of boid i
if the distance from (x1,y1) to (x2,y2) < closestDistanceOtherBoid then
closestX = x2
closestY = y2
closestDistanceOtherBoid = distance from (x1, y1) to (x2, y2)
End If
End if
Next
Return if (closestX = x1 and closestY = y1) // If closest is on top of you, do nothing
// closestX and closestY are the coordinates of the closest boid so now check if
// it’s within the repel distance
if (closestDistanceOtherBoid < BOID_REPEL_DISTANCE)
move away from (closestX, closestY)
You might recognize that the code is fairly similar to calculating the centroid, but instead we remember
the coordinates and distance of the closest boid (as long as it’s at least within the boid’s vision). Here is
working code. First let’s define another constant, BOID_REPEL_DISTANCE, set to 30:
private const int BOID_REPEL_DISTANCE = 30; // Move away if closer
The method to find the closest boid’s coordinates is below:
// Find the closest boid to boid b and move away if it's too close
private void Repel(int b)
{
int x1, y1, x2, y2;
// x1,y1 = coordinate of boid b
// x2,y2 = coordinate of target boid in array
int closestX = -1, closestY = -1; // Coords of closest boid,
// -1 initially for no boid
int closestDistance = 99999; // Initially, nothing is close
x1 = Xs[b];
// Set to coordinates of boid b
y1 = Ys[b];
// Loop through every boid's coordinates
// to find closest
for (int i = 0; i < NUMBOIDS; i++)
{
// Don't include boid b
if (i != b)
{
x2 = Xs[i];
y2 = Ys[i];
// Check if target boid is visible
if (distance(x1, y1, x2, y2) < closestDistance)
{
// Remember distance and coordinates of closest one
closestDistance =
Convert.ToInt32(distance(x1, y1, x2, y2));
closestX = x2;
closestY = y2;
}
}
}
// Do nothing if closest boid is on top of you,
// we'll be moving off in some direction anyway
if ((closestX == x1) && (closestY == y1))
{
return;
}
// Move away from closest if it's too close
if (closestDistance <= BOID_REPEL_DISTANCE)
{
// Move 10% away from the too-close boid
double deltaX = (x1 - closestX) / 10.0;
double deltaY = (y1 - closestY) / 10.0;
Xs[b] += Convert.ToInt32(deltaX);
Ys[b] += Convert.ToInt32(deltaY);
}
}
Note that we compute (x1 – closestX) and (y1 – closestY) whereas with the centroid we calculated
(centroidX – x1) and (centroidY - y1). This time we’re reversing the subtraction so we add on a value
that moves us away from (closestX, closestY). The last step is to add a call to Repel(i) into the timer:
for (int i = 0; i < NUMBOIDS; i++)
{
// Centroid rule
Centroid(i);
// Repulse rule
Repel(i);
Running the new version will make the boids scatter when they get too close.
Move in the same direction as our neighbors
It’s sometimes good to go with the flow, so our final rule is to influence each boid to move in the same
direction as its neighbors. To implement this we just calculate the average direction of all other boids
within the vision radius, then make our boid move a little bit toward that average direction.
Pseudocode for boid i:
x1 = X coordinate of boid b
y1 = Y coordinate of boid b
totalDir = 0
numCloseBoids = 0
For each boid i in the array of boids
if (i != b) then
x2 = X coordinate of boid i
y2 = Y coordinate of boid i
if the distance from (x1,y1) to (x2,y2) < BOID_VISION_RADIUS
totalDir += Direction of Boid i
numCloseBoids++
End If
End if
Next
If numCloseBoids = 0 return
' No nearby boids
averageDir = totalDir / numCloseBoids
Here is the corresponding code to calculate the average direction for the boids in sight:
// Align boid b in the same direction as nearby boids
private void Alignment(int b)
{
int x1, y1, x2, y2;
// x1,y1 = coordinate of boid b
// x2,y2 = coordinate of target boid in array
double totalDir = 0;
// total of dir's for nearby boids
double aveDir;
x1 = Xs[b];
// Set to coordinates of boid b
y1 = Ys[b];
// Loop through every boid
int boidsSeen = 0;
for (int i = 0; i < NUMBOIDS; i++)
{
// Don't include boid b in alignment calculations
if (i != b)
{
x2 = Xs[i];
y2 = Ys[i];
// Check if target boid is visible
if (distance(x1, y1, x2, y2) < BOID_VISION_RADIUS)
{
totalDir += Dirs[i];
boidsSeen++;
}
}
}
// Do nothing if no boids in sight
if (boidsSeen == 0)
{
return;
}
// Calculate average
aveDir = totalDir / boidsSeen;
// Will add more code here to change
// directions toward the average direction
}
Now that we have an angle representing the average direction, we have to figure out which way our
boid should turn, toward the left (subtract) or to the right (add). We should turn in a direction that gets
us to the centroid more quickly. Unfortunately, it’s not as simple as adding if the average direction is
bigger, and subtracting if the average angle is smaller. Here are two scenarios:
Average heading, 7/4 or 315 degrees
Θ1 = |7/4 - /4| = 6/4 =
3/2
Θ2 = 2 - Θ1
Our heading, /4 or 45 degrees
We should subtract from our heading to move closer to the average since Θ2 is smaller than Θ1
Rule:
If our heading is smaller than the target heading
then if Θ1 < Θ2 then subtract
else add
Our heading, 7/4 or 315 degrees
Θ1 = |7/4 - /4| = 6/4 =
3/2
Θ2 = 2 - Θ1
Average heading, /4 or 45 degrees
We should add to our heading to move closer to the average since Θ2 is smaller than Θ1
Rule:
If our heading is greater than the target heading
then if Θ1 > Θ2 then add
else subtract
Here are the rules combined together:
If our heading <= target heading
theta1 = target heading – our heading
theta2 = 2*PI - theta1
if theta1 > theta2 then
Subtract from our heading
Else
Add to our heading
Else
// Our heading > target heading
theta1 = target heading – our heading
theta2 = 2*PI – theta1
if theta1 > theta2 then
Add to our heading
Else
Subtract from our heading
For now, let’s just say that a boid is able to change directions at a rate of /64 (set in the constant
BOID_TURN_RADIUS) so we’ll add or subtract that much every time. For example, it wouldn’t be very
realistic to allow a boid to suddenly change directions by , or 180 degrees.
private const double BOID_TURN_RADIUS = Math.PI / 64; // Max Turn Amount
Here is the pseudocode turned into a method:
// Return how many radians we should turn to get closer
// to the target heading
private double calcTurnAmount(double curHeading,
double targetHeading)
{
double theta1, theta2;
double turnAmount;
if (curHeading <= targetHeading)
{
theta1 = targetHeading - curHeading;
theta2 = 2 * Math.PI - theta1;
if (theta1 > theta2)
{
turnAmount = -1 * BOID_TURN_RADIUS;
}
else
{
turnAmount = BOID_TURN_RADIUS;
}
}
else
{
theta1 = curHeading - targetHeading;
theta2 = 2 * Math.PI - theta1;
if (theta1 > theta2)
{
turnAmount = BOID_TURN_RADIUS;
}
else
{
turnAmount = -1 * BOID_TURN_RADIUS;
}
}
return turnAmount;
}
Now we should invoke our calcTurnAmount method from the Alignment method:
// Calculate average
aveDir = totalDir / boidsSeen;
// Move toward the average
double myHeading = Dirs[b]; // Current heading of b
myHeading += calcTurnAmount(myHeading, aveDir);
// Save new direction back into array,
// ensure range no more than 2PI
Dirs[b] = myHeading % (2 * Math.PI);
}
And finally, invoke Alignment from the Timer Tick:
for (int i = 0; i < NUMBOIDS; i++)
{
// Centroid rule
Centroid(i);
// Repulse rule
Repel(i);
// Alignment rule
Alignment(i);
We now get nicely aligned flocks/schools of boids!
We can experiment a little by varying some of the constants. For example, a smaller vision radius of 30
and a larger turn radius of /16 gives us more insect-like swarms.
We can increase the turn radius, but if we increase it too much we get un-natural turning because the
boids will overshoot the target. For example, if the boid only wants to turn /32 degrees, but
BOID_TURN_RADIUS is set to /8 degrees, then it is always forced to turn /8 degrees. We can correct
this by allowing the boid to move to the exact heading if it’s within the turning radius:
// Returns the
private double
{
if (num1 <
{
return
}
else
{
return
}
}
minimum of two numbers passed in
min(double num1, double num2)
num2)
num1;
num2;
private double calcTurnAmount(double curHeading,
double targetHeading)
{
double theta1, theta2;
double turnAmount;
if (curHeading <= targetHeading)
{
theta1 = targetHeading - curHeading;
theta2 = 2 * Math.PI - theta1;
if (theta1 > theta2)
{
turnAmount = -1 * min(BOID_TURN_RADIUS, theta2);
}
else
{
turnAmount = min(BOID_TURN_RADIUS, theta1);
}
}
else
{
theta1 = curHeading - targetHeading;
theta2 = 2 * Math.PI - theta1;
if (theta1 > theta2)
{
turnAmount = min(BOID_TURN_RADIUS, theta2);
}
else
{
turnAmount = -1 * min(BOID_TURN_RADIUS, theta1);
}
}
return turnAmount;
}
Running the program now results in much smoother behavior as boids fall in line behind one another. In
fact it gets a little boring quickly as all the boids eventually adopt the same alignment as everyone else.
We could throw in some random factors or obstacles to shake things up.
There’s one last enhancement to make, and that is to add buttons to start/stop the animation, and
trackbar controls to change the constants we added for the Boid’s VISION_RADIUS, TURN_RADIUS, and
REPEL_DISTANCE.
Here are the controls on the form:
Here are the control’s initial values:
tbRepelDistance:
Minimum = 1
Maximum = 100
TickFrequency = 10
Value = 30
tbTurnRadius:
Minimum = 1
Maximum = 64
TickFrequency = 10
Value = 16
tbVisionRadius
Minimum = 1
Maximum = 400
TickFrequency = 20
Value = 150
For the buttons we just enable or disable the timer:
private void btnGo_Click(object sender, EventArgs e)
{
timer1.Enabled = true;
}
private void btnStop_Click(object sender, EventArgs e)
{
timer1.Enabled = false;
}
We don’t need to add any events for the sliders, but we should use their values in place of the
constants. In calcTurnAmount we (Math.Pi / tbTurnRadius.Value) instead of the constant
BOID_TURN_RADIUS:
private double calcTurnAmount(double curHeading, double targetHeading)
{
double theta1, theta2;
double turnAmount;
if (curHeading <= targetHeading)
{
theta1 = targetHeading - curHeading;
theta2 = 2 * Math.PI - theta1;
if (theta1 > theta2)
{
turnAmount = -1 * min(Math.PI / tbTurnRadius.Value, theta2);
}
else
{
turnAmount = min(Math.PI / tbTurnRadius.Value, theta1);
}
}
else
{
theta1 = curHeading - targetHeading;
theta2 = 2 * Math.PI - theta1;
if (theta1 > theta2)
{
turnAmount = min(Math.PI / tbTurnRadius.Value, theta2);
}
else
{
turnAmount = -1 * min(Math.PI / tbTurnRadius.Value, theta1);
}
}
return turnAmount;
}
In Alignment and Centroid, replace BOID_VISION_RADIUS with tbVisionRadius.Value:
if (distance(x1, y1, x2, y2) < tbVisionRadius.Value)
In Repel, replace BOID_REPEL_DISTANCE with tbRepelDistance.Value:
if (closestDistance <= tbRepelDistance.Value)
We can now fiddle with these parameters and watch the results unfold! It is also easy to change the
number of boids by recompiling with a different number for NUMBOIDS. Too many boids will make the
program to slower, though (there are however many ways to make it run faster).
There are a lot of other enhancements we could make. Just a couple are listed here:







Rewrite as an object-oriented program, with objects for boids, object for the world
Add variable velocities instead of constant velocity
Add collision detection, physical obstacles
Action of wind or current
Tendency to a particular place, e.g. nest or food
Perching
Predator and scattering of prey
Lists
The List object behaves like a dynamic array. We can add and remove from it at will, and also delete
items at specific locations. This can be useful when you don’t know how many items will be processed
in your array and you can then use a List instead. The textbook doesn’t discuss Lists, but it does discuss
an older (and similar) object called the ArrayList. The List object is pretty straightforward to use if you
understand arrays. Lists are just a simpler version of a Collection (The collection allows you to index
values with different keys, while the list forces you to index values with a key identical to the value).
The following code sample illustrates the usage:
List<string> stringList = new List<string>();
stringList.Add("foo");
stringList.Add("bar");
stringList.Add("zot");
stringList.Add("bah");
Console.WriteLine("Size of List: " + stringList.Count);
Console.WriteLine("Contents:");
for (int i = 0; i < stringList.Count; i++)
{
Console.WriteLine("At index " + i + " Value=" + stringList[i]);
}
stringList.Remove("bar");
Console.WriteLine("Contents after remove bar:");
for (int i = 0; i < stringList.Count; i++)
{
Console.WriteLine("At index " + i + " Value=" + stringList[i]);
}
stringList.RemoveAt(0);
Console.WriteLine("Contents after remove:");
for (int i = 0; i < stringList.Count; i++)
{
Console.WriteLine("At index " + i + " Value=" + stringList[i]);
}
Output:
Size of List: 4
Contents:
At index 0 Value=foo
At index 1 Value=bar
At index 2 Value=zot
At index 3 Value=bah
Contents
At index
At index
At index
after remove bar:
0 Value=foo
1 Value=zot
2 Value=bah
Contents after remove:
At index 0 Value=zot
At index 1 Value=bah
As you can see, the List is convenient because we don’t need to declare the size up front like an array,
and we are able to delete (or insert) items at specific places in the List. Both of these operations are not
allowed with normal arrays.
One last operation that you may find useful is the IndexOf function. It tells you the position of a
matching item, and -1 if there is no match. For example:
stringList.Add("foo");
stringList.Add("bar");
stringList.Add("zot");
stringList.Add("bah");
Console.WriteLine(stringList.IndexOf("foo"));
Console.WriteLine(stringList.IndexOf("zot"));
Console.WriteLine(stringList.IndexOf("moo"));
This code outputs:
0
2
-1
The string “foo” is found at position 0, and “zot” at position 2. , and “moo” gives us -1 because it does
not exist in the List.
Exercise: Convert the Trivia program to use Lists instead of arrays.
Download