Lab 6: Critters

Concept

This week we extend our engagement with object orientation into the field of artificial life -- in a very primitive sense, at least.

What you see here is a very simple population simulator. We start with two bugs, who generate offspring every time they collide, provided there are no more than 20 bugs currently alive (a nice bit of population management not common in nature). Every bug dies in 2-3 seconds, unless it is a member of the sole remaining pair, in which case it sticks around long enough to breed.

At least some of the code here will be familiar: the wandering bugs of this project have evolved out of the bouncing ball you scripted at the beginning of the course. But since the bugs are objects -- instances of the Critter class -- they can also interact and reproduce. More about that presently.

Architecture

There are two files in play here: a Flash source file called critterDemo.fla and an ActionScript 2.0 class file called Critter.as.

The source file contains one frame, one layer, and one frame script (see below). Its Library contains one Symbol, a Movie Clip called bug. This Movie Clip is linked to the Critter class. The cycling internal behavior of the bug, the motion of its legs and antennae, is all done as frame animation in the Movie Clip.

Root script in the source file

The Stage in the main Flash movie is empty. To introduce cast members, we use the root script (i.e., a script written on the single frame of the movie). This script sets up some crucial variables and then attaches Movie Clips to begin our simulation.

_root.population = new Array();
_root.serialNum = 0;

for(i=0; i<2; i++)
{
	_root.serialNum ++;
	_root.attachMovie('bug','bug'+serialNum,
      _root.getNextHighestDepth());
	_root.population.push('bug'+serialNum);
}

Much should be familiar here. We used attachMovie() in a similar way last week to bring in the bullets with which we defeated the space invaders. But our bullets all had the same instance name. Here we vary the instance name of each bug, in this case by concatenating a serial number to the instance name. Thus our first two creatures are named bug0 and bug1; more important, we'll be able to generate a unique name for all bugs created in the future.

Next we use the push() method of the root array population. This method adds the instance name of the newly created bugs to the end of the array. We'll use this array as a cast list, because as you'll see, we will eventually need to check the names of all the active bugs.

Class definition

Now we come to the major scripting of this project, the class definiton for Critter, found in the file Critter.as. There's quite a bit to this script; let's start with the list of variables:

//old stuff from bouncing ball
  var xDir:String;
  var yDir:String;
  var xChange:Number;
  var yChange:Number;
  var coinToss:Number;
  var rotateAmount:Number;
//new stuff
  var checkString:String;
  var contact:Boolean;
  var contactClear:Boolean;
  var lifespan:Number;

The first five variables listed here are the same used in the bouncing ball project. The variable called rotateAmount is used to rotate each bug from negative 30 to positive 30 degrees.

The variable checkString comes into play when we do collision testing, as do the Booleans contact and contactClear.

Finally we have lifespan, which specifies how long each bug stays active in the simulation.

Now here's what all these variables do.

Let's start at the bottom of the code, with the Constructor method:

function Critter()
  {
  //placement
    this._x = Math.floor(Math.random()*550);
    this._y = Math.floor(Math.random()*400);
    reSlope();
    flip("vertical");
    flip("horizontal");
    rotateAmount = 3;
  //set lifespan
    lifespan = getTimer()+Math.floor(Math.random()*1000)+2000;
}

The first six statements here (placement and animation parameters) come straight from the bouncing ball, with the addition of that rotation effect.

We use the getTimer() method to set the critter's lifespan to a random number between 2000 and 3000 milliseconds (2-3 seconds). You may make this value larger or smaller if you like.

We'll skip over the custom functions in this class, because they are mostly brought over unchanged from the bouncing ball project. Likewise we'll skip the sections of the enterFrame handler that cover basic animation, because these too are mostly identical to the earlier example.

We'll concentrate on the most complex and interesting part of the enterFrame handler, the section on collision detection and its consequences. Remember, what you see below is all written within the enterFrame handler.

  contact = false;
  for(var i:Number=0; i<_root.population.length; i++)
  {
    checkString = "_level0."+_root.population[i];
    if(_root.population[i] != this._name 
      && this.hitTest(checkString))
      {
        contact = true;
        break;
      }
    }
		
  if(contact == true && contactClear != true)
  {
    bounceBack();
    contactClear = true;
    //MAKE NEW CRITTER
    if(_root.population.length < 20) //limit of 20 bugs
      {
    	_root.serialNum ++;
        _root.attachMovie('bug','bug'+_root.serialNum,
          _root.getNextHighestDepth());
        _root.population.push('bug'+_root.serialNum);
      }
  }
		
  if(contact == false && contactClear == true)
  {
    contactClear = false;
  }

The first thing you see here is a line assigning the value false to a flag variable called contact. This variable tells us whether the current object is touching any other object. We set it to false every time we enter a frame because we need to perform a complete check against all the other objects (bugs) in the movie.

That check takes place within a for loop that runs the length of the array _root.population, which we last saw in the root script of the main Flash movie. As you'll remember, this array is our cast roster. It lists by instance name all the bugs we have created.

Inside the loop, we construct a string variable called checkString, consisting of the instance name of the cast member from _root.population, added to the string "_level0." . We have to do this rather arcane maneuver because we need the complete designation of an object in order to perform a hit test against it.

Next we come to a very complicated if statement:

  if(_root.population[i] != this._name && this.hitTest(checkString))

We execute the contents of this if clause if two conditions are met. The first is that the instance name of the current movie does not match the instance name we are checking from the population array. Why do we make this condition? Because we do not want to check for a hit test against the object itself! Otherwise, as you'll see, every bug would make itself instantly pregnant. Which is okay if we're simulating slime molds, which can actually do this, but not if we want to create remotely realistic bugs.

The second part of the compound if condition is a conventional hit test. If we've determined that the object is not testing against itself, and if it tests positively against the other object, then we set contact to true, meaning that we've registered contact with at least one other object. We also execute a simple statement called break which terminates the loop and saves us having do to any more calculations. While this statement doesn't really affect the performance of this simple simulation, it might make a huge difference in a more ambitious context.

So far so good; but there are many twists and turns yet to explain.

Next we come to another if condition. This one checks both the contact variable and a second flag variable called contactClear. Why do we need a second flag variable? Because the bugs have non-zero height and width, and because they don't necessarily move fast enough to clear each other's rectangles in 1/12 of a second, bugs will continue to detect hits against each other even as they start moving apart; and since the result of a hit detection is immediate course reversal, our bug will get pretty confused if it picks up multiple hits. In fact, it will quiver, thrash, or oscillate: not that pretty at all.

We use the second flag, contactClear, to suppress the hit response until the two object have cleaed each other's geometries.

If contact is true and contactClear is false, then two bugs have just come into contact, so we're ready to do something interesting. Three things follow as consequences of the if test above. First, we call a function called bounceBack(), which is a simpler version of the standard reversal routines we use when bugs hit the edges of the runtime window. In bounceBack(), we simply switch both xDir and yDir to their opposites.

Next we set the flag variable contactClear to true, meaning we have detected and responded to a collision, and therefore do not want to respond again until we are out of contact.

Finally we do something really interesting: we create a new object, using the attachMovie() method. Most of the code here will look familiar from last week. Note that we're generating an instance name for the new movie including a number. This number uses the _root.serialNum variable, which just keeps growing so long as we go on making little critters. Again, we need to introduce the number here to distinguish one bug from another.

As we did in the root script in the main movie source, we perform a push() on _root.population, adding the designation of the new bug to the cast roster.

One more move remains unexplained. Following the if test discussed above is yet another compound if:

if(contact == false && contactClear == true) contactClear = false;

In this case we're asking if contact is false while contactClear is true. The first condition occurs only when all the hit tests we perform against objects in the movie come up false -- we're not in anyone else's space. The second condition obtains when we have previously detected a collision. In other words, this last if statement clears the contactClear flag once we have completed an encounter with another bug.

Now that we've seen how new bugs are made, it's time to consider that other, graver fact of life: how bugs exit the scene. Here's another sequence of code from the enterFrame handler:

//MORTALITY
  if(_root.population.length < 3)
  {
    lifespan = getTimer()+5000;
  }
  else //R.I.P.
  {
    if(getTimer() > lifespan)
      {
        //clean up population array
        for(var i:Number=0; i<_root.population.length; i++)
        {
          if(this._name == _root.population[i])
          {
            _root.population.splice(i,1);
            break;
          }
        }
        removeMovieClip(this);
    }
  }

First we ask how many live bugs are currently onstage, a number that corresponds to the current length of the cast list. If that number is lower than three, then the remaining pair aren't allowed to die yet, because we only get new bugs when a pair collide. To keep the ancestral pair going, we bump their lifespan counters up by five seconds each time we enter the frame, so long as they are the last bugs on earth.

If the survivors have company, that's the end of their special treatment. Once population exceeds two, we check everyone's lifespan setting against the runtime clock. If time has run out, we start another for loop to run through the cast list yet again. This time we are looking simply for a match between one member of that list (or array) and the name of the current object. When we find a match, we execute the splice() method of the array. This method deletes a specified number of items (in our case, one) from an array beginning at a specified index number. In this case the index number is the current index of our for loop.

Once again, we follow up a successful match with a break statement to gain some speed.

Outside the loop, we use removeMovieClip(This) to erase the current object from the system.

As we said earlier, this discussion covers only about a third of the code in the Critter class. The rest is pretty much the same as what you saw in the bouncing ball, with a few additions. You can examine the code in its entirety by downloading the source files from the server.

Source Files

You'll find the source files for the main Flash movie, critterDemo.fla, and the Critter class, Critter.as, in the shared SDE folder on student-iat.ubalt.edu. Look in the idia610 directory, and within that, in lab06.

Acknowledgements

Earlier and more elegant versions of this project have been built by several of my former students, including Faye Levine and Shannon Tucker. They first suggested the idea of pushing new names into a roster array.

Kevin Carter from Game Concept and Design made several improvements on my original version of this code.


University of Baltimore Logo

Copyright © 2004 School of Information Arts and Technologies