Managing Lots of Sprites

Robot Trash Collectors

Our example in this hour is a simulation of robot trash collectors. The robots are represented as white circles, and the trash items as red squares. The robot cleaners will look for the closest trash item and move toward it, getting rid of the trash when it is touched. Figure 11.1 shows the simulation running with just one robot.

Simulation of robotic trash collectors with population control A.I.
FIGURE 11.1 Simulation of robotic trash collectors with population control A.I.

To make the simulation more interesting, it has the capability to automatically manage the amount of trash produced based on the number of cleaners present. A minus button at the bottom left removes cleaners, and its complementary plus button at the lower right adds new cleaners. As long as there is trash remaining, new trash is added rather slowly. But if the number of cleaners increases and the trash goes down, more trash is added more quickly to keep the cleaners busy. Figure 11.2 shows the simulation with five cleaners. Note the respawn time.

Five robot cleaners are present, which increases the trash rate.
FIGURE 11.2 Five robot cleaners are present, which increases the trash rate.

An algorithm of two trash items to one robot cleaner is used to adjust the speed. If there is fewer than twice the number of trash items compared to the cleaners, the speed is increased (by speeding up the respawn time). By watching the simulation run over time, you can see that a balance is maintained as long as the numbers are reasonable. Adding up to 100 or more robot cleaners causes the trash production to shift into high gear to keep up! See Figure 11.3 for an example.

The robot cleaner population is out of control!
FIGURE 11.3 The robot cleaner population is out of control!

Don’t let the primitive artwork dissuade you from studying the example in this hour. The code shared here will be extremely valuable in your own future game projects.

Building the Example

The simulation project this hour has a few asset requirements that you can source from the resource files for this hour, or just make yourself. The button image is just a 64×64 square. The robots are represented by a 32×32 white circle with alpha transparency around the edges. The trash is represented by a 32×32 red square. You are free to use different images if you want, perhaps even little robot and trash images! You can also change the theme of the art; how about insects and food?

Button Class

A helper class is needed to handle the buttons used to increase and decrease the number of robots. The Button class (see Listing 11.1) inherits from Sprite, so it will have all of Sprite’s capabilities and then some.

LISTING 11.1 Source Code for the Button Class

[code]
public class Button : Sprite
{
public string text;
private SpriteBatch p_spriteBatch;
private SpriteFont p_font;
public Button(ContentManager content, SpriteBatch spriteBatch,
SpriteFont font) :
base(content, spriteBatch)
{
p_spriteBatch = spriteBatch;
p_font = font;
Load(“button”);
text = ““;
color = Color.LightGreen;
}
public void Draw()
{
base.Draw();
Vector2 size = p_font.MeasureString(text);
Vector2 pos = position;
pos.X -= size.X / 2;
pos.Y -= size.Y / 2;
p_spriteBatch.DrawString(p_font, text, pos, color);
}
public bool Tapped(Vector2 pos)
{
Rectangle rect = new Rectangle((int)pos.X, (int)pos.Y, 1, 1);
return Boundary().Intersects(rect);
}
}
[/code]

As you can see from the code, a constructor requires ContentManager and SpriteBatch parameters (which are just passed directly to the Sprite constructor), as well as a third parameter, SpriteFont, so that the button can print text on its own. Draw() calculates the size of the text and tries to center it. The font being used in this example is Wasco Sans Bold 36-point, but you are welcome to use a different font if you want. The Tapped() method receives as a parameter the position of a click/tap and checks to see whether it (the button) was tapped, returning true or false.

Main Source Code

Listing 11.2 contains the main source code for the Entity Grouping Demo program. We’ll go over the new helper methods after this.

LISTING 11.2 Source Code for the Entity Grouping Demo Program

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
TouchLocation oldTouch;
Random rand;
SpriteFont font, buttonFont;
Button plus, minus;
List<Sprite> cleaners;
Texture2D circleImage;
List<Sprite> trash;
Texture2D trashImage;
int lastTime = 0;
int target = -1;
int respawn = 500;
int score = 0;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
oldTouch = new TouchLocation();
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
rand = new Random();
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>(“WascoSans”);
buttonFont = Content.Load<SpriteFont>(“ButtonFont”);
//create minus button
minus = new Button(Content, spriteBatch, buttonFont);
minus.position = new Vector2(32, 480-32);
minus.text = “-”;
//create plus button
plus = new Button(Content, spriteBatch, buttonFont);
plus.position = new Vector2(800 – 32, 480 – 32);
plus.text = “+”;
//create cleaners group
cleaners = new List<Sprite>();
circleImage = Content.Load<Texture2D>(“circle”);
AddCleaner();
//create trash group
trash = new List<Sprite>();
trashImage = Content.Load<Texture2D>(“trash”);
AddTrash();
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
//get state of touch input
TouchCollection touchInput = TouchPanel.GetState();
if (touchInput.Count > 0)
{
TouchLocation touch = touchInput[0];
if (touch.State == TouchLocationState.Pressed &&
oldTouch.State == TouchLocationState.Released)
{
if (minus.Tapped(touch.Position))
RemoveCleaner();
if (plus.Tapped(touch.Position))
AddCleaner();
}
oldTouch = touch;
}
//add new trash item periodically
if ((int)gameTime.TotalGameTime.TotalMilliseconds >
lastTime + respawn)
{
lastTime = (int)gameTime.TotalGameTime.TotalMilliseconds;
AddTrash();
}
if (trash.Count > cleaners.Count * 2)
{
respawn += 1;
if (respawn > 1000)
respawn = 1000;
}
else
{
respawn -= 1;
if (respawn < 10)
respawn = 10;
}
//move the cleaners
for (int n = 0; n < cleaners.Count; n++)
{
//find trash to pick up
target = FindNearestTrash(cleaners[n].position);
if (target > -1)
{
float angle = TargetAngle(cleaners[n].position,
trash[target].position);
cleaners[n].velocityLinear = Velocity(angle, 2.0f);
cleaners[n].Move();
}
//look for collision with trash
CollectTrash(cleaners[n].position);
//look for collision with other cleaners
for (int c = 0; c < cleaners.Count; c++)
{
if (n != c)
{
while (cleaners[n].Boundary().Intersects(
cleaners[c].Boundary()))
{
cleaners[c].velocityLinear.X += 0.001f;
cleaners[c].velocityLinear.Y += 0.001f;
cleaners[c].Move();
}
}
}
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.DarkBlue);
spriteBatch.Begin();
minus.Draw();
plus.Draw();
foreach (Sprite spr in trash)
spr.Draw();
foreach (Sprite spr in cleaners)
spr.Draw();
spriteBatch.DrawString(font,
“Cleaners:”+cleaners.Count.ToString(),
Vector2.Zero, Color.White);
spriteBatch.DrawString(font, “Trash:”+trash.Count.ToString(),
new Vector2(0,25), Color.White);
spriteBatch.DrawString(font, “Score:” + score.ToString(),
new Vector2(0, 50), Color.White);
spriteBatch.DrawString(font, “Respawn:” + respawn.ToString(),
new Vector2(0, 75), Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
[/code]

Main Simulation Functionality Code

The “meat and potatoes” code that drives the simulation is coming up next, in Listing 11.3. Here we find helper methods that look for the nearest trash items, move the robots toward the trash, add new robots, remove robots, and calculate distance and velocity. Some of these methods, like AddCleaner() and RemoveCleaner(), could have been just coded directly where they are called in the program, but this approach is much cleaner and reusable.

LISTING 11.3 Entity Grouping Demo Program (Continued)

[code]
void RemoveCleaner()
{
if (cleaners.Count > 0)
cleaners.RemoveAt(0);
}
void AddCleaner()
{
Sprite obj = new Sprite(Content, spriteBatch);
obj.image = circleImage;
obj.position = new Vector2((float)rand.Next(0, 760),
(float)rand.Next(0, 450));
cleaners.Add(obj);
}
void AddTrash()
{
Sprite obj = new Sprite(Content, spriteBatch);
obj.image = trashImage;
obj.position = new Vector2((float)rand.Next(0, 760),
(float)rand.Next(0, 450));
trash.Add(obj);
}
int FindNearestTrash(Vector2 pos)
{
int target = -1;
float closest = 9999;
if (trash.Count == 0) return -1;
for (int n = 0; n < trash.Count; n++)
{
float dist = Distance(pos, trash[n].position);
if (dist < closest)
{
closest = dist;
target = n;
}
}
return target;
}
void CollectTrash(Vector2 pos)
{
if (trash.Count == 0) return;
for (int n = 0; n < trash.Count; n++)
{
float dist = Distance(pos, trash[n].position);
if (dist < 8)
{
score++;
trash.RemoveAt(n);
break;
}
}
}
float Distance(Vector2 A, Vector2 B)
{
double diffX = A.X – B.X;
double diffY = A.Y – B.Y;
double dist = Math.Sqrt(Math.Pow(diffX, 2) + Math.Pow(diffY, 2));
return (float)dist;
}
float TargetAngle(Vector2 p1, Vector2 p2)
{
return TargetAngle(p1.X, p1.Y, p2.X, p2.Y);
}
float TargetAngle(double x1, double y1, double x2, double y2)
{
double deltaX = (x2 – x1);
double deltaY = (y2 – y1);
return (float)Math.Atan2(deltaY, deltaX);
}
Vector2 Velocity(float angle, float acceleration)
{
double x = Math.Cos(angle) * acceleration;
double y = Math.Sin(angle) * acceleration;
return new Vector2((float)x, (float)y);
}
}
[/code]

Simulating Group Dynamics

The key to this simulation example is a pair of linked lists called cleaners and trash. A linked list is a managed collection for a single type of data, like our Sprite class. In C#, the list is defined with the data type (Sprite) as a template in brackets:

[code]
List<Sprite> cleaners;
The list is created in LoadContent():
cleaners = new List<Sprite>();
[/code]

At this point, the list is ready to be filled with Sprite objects. Each object is a separate, complete Sprite object with its own properties and methods. A new object is added to the list with the Add() method. This code creates a new Sprite object and adds it to the list:

[code]
Sprite sprite = new Sprite(Content, spriteBatch);
sprite.position = new Vector2(0, 0);
cleaners.Add(sprite);
[/code]

Testing revealed that the simulation locks up when there are more than about 250 robots, at which point it is difficult to click the minus button to reduce the population.

You can add as many objects to the list as you want, using a for loop, or by reading data in from a level file, or by any other means, and the list will grow as needed to contain all the objects being added. When objects have been added to the list, they can be forgotten to a certain degree. It’s up to us to make sure we can identify each object inside the list later if we want to use a specific object. For instance, if all the objects for a game are stored in a list like this, and you want to highlight the player’s character, then there must be a way to uniquely identify that sprite among all the others. This can be done by adding an identifier to the Sprite class, such as an ID number or name string. We aren’t doing that in this simulation because every object has equal treatment.

Using List Objects

There will be at least two different points where all the objects in the list must be accessed—from Update() and Draw(). That is, again, the very least number of times, while you might need to access the list elsewhere to test for collisions or for other purposes. There are two ways to iterate the list, using a for loop or using a foreach iterator.

A property of List provides the number of items it contains, called Count. For example:

[code]
for (int n = 0; n < cleaners.Count; n++)
{
//reference cleaners[n]
}
[/code]

Another way to access a list is with a foreach loop such as this:

[code]
foreach (Sprite sprite in cleaners)
{
//reference sprite
}
[/code]

Creating and Using a List

A List is like an array, but it is much easier to use. Let’s learn how to create a List container for other objects.

  1. Define the new List variable in the program’s global variables section. In the brackets, you will define the type of data that the List will contain— which can be anything, such as Sprite or int or string.
    [code]
    List<string> groceries;
    [/code]
  2. Create or initialize the List variable, again with the data type in brackets. Plus, don’t forget the parentheses at the end! The parentheses mean this is a function—or rather, method—call, the constructor method to be precise.
    [code]
    groceries = new List<string>();
    [/code]
  3. Add items to the list like so:
    [code]
    groceries.Add(“butter”);
    groceries.Add(“milk”);
    groceries.Add(“bread”);
    [/code]
  4. Access the items in the list using either a for loop with the groceries.count property, or a foreach loop with the groceries object.
    [code]
    foreach (string item in groceries)
    {
    // … do something with the item here
    }
    [/code]

Iteration Techniques Compared

There are distinct advantages to both iteration mechanisms, depending on programming needs. The numeric for loop is helpful when you want to find the index position of a certain object in the list and then reference it later with indexers ([]). For instance, you might keep track of a “clicked” object with a number variable, and then just index inside the list with that variable, like so:

[code]
cleaners[clicked].Move();
[/code]

The foreach loop is easier to use if you just want to do a quick update of every object, and is commonly used to update the position or draw the object. Of course, there are many other purposes for a foreach iteration, and this is just one example. In the simulation project, all the robot cleaners are drawn with just two lines of code:

[code]
foreach (Sprite spr in cleaners)
spr.Draw();
[/code]

Very complex-appearing A.I. behaviors can seem to derive from surprisingly simple algorithms. When it comes down to analysis, simple rules determine human behavior in social environments too!

But in another part of the project, an index for loop is used to find the nearest trash. This snippet of code from the FindNearestTrash() method looks for the closest trash item by calculating the distance to every item in the trash list, keeping track of the index of the closest one with an int variable called closest. That specific trash item can then be found by indexing into trash with the variable.

[code]
for (int n = 0; n < trash.Count; n++)
{
float dist = Distance(pos, trash[n].position);
if (dist < closest)
{
closest = dist;
target = n;
}
}
[/code]

That’s all there is to it! In the final analysis, we use the distance calculation for many, many things in an ordinary video game!


Posted

in

by

Tags: