Demonstration
The demonstration movie above shows a complete solution to Assignment 3: a virtual fishtank with six sprites and simple collision behavior: if the first fish to make contact is larger than the fish it hits, it increases 20% in size while the other fish decreases 20%; if it is smaller, then it increases 10% and the other fish remains unchanged. As you can see, over time this simple rule produces a tank populated by one or two big fish, with others much smaller, sometimes just tiny specks.
The source file for this movie, fishtankDemo2.fla, can be found on Crow in MMShare/fishtankDemos.
Generalizing animation
The code for this movie departs significantly from the more direct but less elegant scripting used in the first fishtank demo. Most of the crucial operations in the movie--animation and collision detection, for instance--are handled in generalized functions that are invoked on enterFrame by each sprite. You'll remember that in the first demo, each sprite's enterFrame handler contained all the instructions to handle animation and detection. Now each of the six sprites' handlers looks exactly like this:
onClipEvent(enterFrame){
_root.swim(this);
_root.bumpCheck(this);
}
The two lines in the enterFrame handler are function invocations. They refer to functions defined at the root level. To define a function at the root you simply write the function on a keyframe somewhere in the main timeline. Most coders put this keyframe in a layer of its own and name it "scripts," which is what we've done here.
Note the use of the special variable this in the enterFrame handler. Remember that this contains the full name of the current Movie Clip-- for example, _level0.fish1 or _level0.fish3. (We'll explain that "_level0" prefix a little later.) Using this full name, the generalized function can set properties for the Movie Clip in question. We do this by passing the full name of the Movie Clip, contained in this, into a variable used in the generalized script, in our case arbitrarily named which.
Here's what the animation function, swim(which), looks like:
function swim(which){
if(which.direction == "right"){
//swim right
which._x += which.speed;
}
if(which.direction == "left"){
//swim left
which._x -= which.speed;
}
}
As you can see, this function is a bit more complicated than the one we used in the original demo, mainly because in this version sprites may travel either left-to-right or right-to-left. The direction of travel is controlled by an arbitrarily named variable called direction which is defined individually for each sprite--that's why it's indicated as which.direction. Remember that the variable which has been passed the value of the special variable this from the individual sprite. So if the code above was invoked from the fish1 sprite, we're really talking about _level0.fish1.direction.
It's this principle of reference--passing values from this into a main variable within the generalized script--that allows this very useful technique to work. Thus you can think of the generalized function as a formula into which we plug various instance names and variables.
A couple of notes about the swim(which) function before we move on. First, the actual code you'll see in the source file contains more instructions than we've shown here. The omitted lines fine-tune the function and are not essential to the explanation we're giving here.
Second, you may be curious about how that direction variable gets its value. Since it is local to the individual sprite it must be defined on a load handler belonging to the sprite. So in the script for fish1 we have in addition to its enterFrame handler, the following:
onClipEvent(load){
this.direction = "left";
this.leftFish._visible = true;
this.rightFish._visible = false;
this.speed = 3+Math.round(Math.random()*8);
this.startWidth = this._width;
this.startHeight = this._height;
this.currentWidth = this._width;
this.currentHeight = this._height;
}
As you can see, the first line of the script determines that the fish will initially move to the left. The next two lines make the left-facing fish image (a Movie Clip within the Movie Clip) visible, while the right-facing component image is made invisible. If the sprite changes direction, this situation will be reversed. The next line sets travel speed to some value between 3 and 11. The last four lines record the original dimensions of the sprite and record them in two pairs of variables. You'll see why this is necessary a bit later on.
Generalizing the hitTest
You probably remember how tedious it was to write multiple stacks of hitTests to make each of your sprites sensitive to collision with each of the other sprites. There is a better way, though it's harder to understand at a single glance unless you're an experienced coder. Here's the function that handles collision detection in the redesigned fishtank demo:
function bumpCheck(which){
for(x=0; x<characters.length; x++){
if(_root.bumpus == false && which != characters[x] && which.hitTest(characters[x])){
_root.bumpus = true;
if(which.currentWidth > characters[x].currentWidth){
which._width = which.currentWidth+which.currentWidth*0.2;
which._height = which.currentHeight+which.currentHeight*0.2;
characters[x]._width = characters[x].currentWidth-characters[x].currentWidth*0.2;
characters[x]._height = characters[x].currentHeight-characters[x].currentHeight*0.2;
}
else{
which._width = which.currentWidth+which.currentWidth*0.1;
which._height = which.currentHeight+which.currentHeight*0.1;
}
if(which._width > which.startWidth*2){
which._width = which.startWidth*2;
which._height = which.startHeight*2;
}
which.currentWidth = which._width
which.currentHeight = which._height
characters[x].currentWidth = characters[x]._width;
characters[x].currentHeight = characters[x]._height;
reset(characters[x]);
}
}
}
There's a lot going on here, so we'll proceed section by section.
The first feature you may notice here is a for loop. For loops repeat a set of instructions for a given range of values of an index variable, in this case called x. (You may name a loop index anything you like, so long as that variable name is not used elsewhere in the same context.)
This for loop begins at zero and runs sequentially through all the values of an array called characters. The limiting term is the length property of characters, making use of the handy fact that every array stores in its length property the number of items it contains. The array called characters (the name is arbitrary) is set up and populated in a directly executed statement at the beginning of the root script:
characters = new Array(_level0.fish1,_level0.fish2,_level0.fish3, _level0.fish4,_level0.fish5,_level0.fish6);
The characters array contains the full names of all the sprites used in the movie. Each begins with the prefix _level0. We have to do this because ActionScript will only allow us to set the properties of a sprite if we use its full name. The level specification does NOT refer to the layers of the main timeline. Layers are not the same thing as levels; they're a higher-level structure. The entire main timeline occupies level zero of the current movie. (If you're thinking ahead, you'll realize that it's possible to have other movies loaded into other levels. We won't go there until later in the course.)
If you've worked with scripts and arrays before, you may have worked primarily with strings. A string is a series of letters and/or numbers, always indicated by being placed in double quote marks (e.g., "I'm a string"). We're not using strings here but rather object names, which means in effect that the characters array is not a list of names but a set of objects. If you find this distinction too fine to grasp, don't worry. Just notice that the technique shown here will not work if you put the contents of characters in quotes.
The characters array lets us march through the for loop, testing each sprite in the movie against the sprite that invoked this script, whose name has been passed to the main variable which. So the objects of comparison are which on the one hand and another sprite defined as characters[x], where x is the current index number of the loop, a value that goes from 0 to 5 in order to accommodate each of the six elements in the characters array.
Thus the for loop lets us check each sprite against every sprite in the movie--which of course includes the testing sprite (which) itself. But we don't want to hitTest a sprite against itself. So immediately inside the beginning of the for loop you see an if construction. Actually this if construction has three conditions:
- Our old friend the flag variable _root.bumpus must not have been set
already--you'll remember this reserves the victory to the first fish that
detects a collision;
- The value currently being examined from the
characters array must not be the same as the value of the sprite that
has invoked the script--eliminating self-referential hitTests;
- The hitTest expression must evaluate to true, i.e., there must be a collision between the testing sprite and the sprite being tested.
If all three conditions are met, then the sprite that invoked this script has made contact with another sprite and it is the first to detect the contact (in other words, the winner of our little head-butting contest). So we set the flag variable _root.bumpus to true in order to claim unique victory. Then we evaluate another if construction, this one with an else clause, to determine what happens visually. This is where the interaction logic comes in. If the testing sprite is bigger than its opponent, it increases 20% in size while the opponent decreases 20%. If the testing sprite is smaller, it increases 10% and the opponent does not change size. (All of these consequences are arbitrary, of course, and could be changed.)
Immediately following the if/else construction you'll see yet another if, this one concerned with the size of the testing sprite. If that sprite has already increased to 200% its original size, then it is reassigned height and width values that remain at 200%, thus preventing the sprite from filling the screen as in our deliberately flawed first prototype. Note that the if condition only checks the width property; since both height and width always change by the same percentage, this is safe.
The next four instructions, outside the last if construction, do essential housekeeping. They update the value of which.currentWidth and which.currentHeight, local variables attached to the testing sprite. We'll need those values for later tests. Similarly we update characters[x].currentWidth and characters[x].currentHeight, local variables that record the dimensions of the opponent sprite.
The final line of the script invokes a function called reset(which). This function, not discussed here, places the losing sprite at a new position offstage and resets the flag variables. Once again, we're passing a particular value--the value of characters[x]--to the main variable which in this function. Thus you can see that one generalized function can invoke another, extending the chain of reference even further.
Summary
If all the above seems dreadfully complicated, try not to panic. Generalized scripts are not the easiest things in the world to figure out unless you've had some previous experience with scripting. It's perfectly acceptable to use more tedious and redundant scripts, as in our first example, if you find them easier to understand and control.
If you want to become a serious coder, however, you should try to graduate from one-of-a-kind local scripts to more generalized solutions. Try revising a project in which you use one-of-a-kind scripts, gradually shifting features off to generalized functions.
