Hit testing enemies vs. bullets

Next, we'll add another function that will get called by the enter frame handler. Inside this function, we need to cycle through all the enemies. Then for each enemy we consider, we have to also cycle through all the bullets. This is going to involve using a beast we haven't covered yet: a nested for loop. This might sound scary at first, but it's really not too bad. If you have a really good grasp of the for loop itself, you will be just fine.

In an ordinary for loop, there is a block of code that gets executed repeatedly, a certain number of times. In a nested for loop, the block of code that gets executed repeatedly includes another for loop!

I realize this is a huge head trip if you never encountered it before. But having an array of enemies and an array of bullets to use as an example is definitely going to help you understand it better. We have previously seen how arrays and for loops work well together, and that we can use a for loop to process an array. The fact that the array is zero-based, and the fact that we always use a loop counter that initializes at 0, and a condition that the loop counter be less than a certain number (that number being the number of times we wish to loop, and usually that's also the length of some array), we can be assured that our loop processes every element of the array exactly once each.

What we want to do is make the outer loop run through the array of enemies, and the inner loop run through the array of bullets. We will need to use a different letter for the inner loop's counter variable. Following convention, we will use the letter j. But don't let the loop counters confuse you, i and j are just convenient variable names. Here's the thing to really grasp: the inner loop runs once for each time the outer loop runs. This means that the inner loop will be run three times through altogether, once for each element of the outer loop.

So here's how this works: The outer loop begins processing. The first time through the outer loop, i = 0. Since this is so, we are dealing with spiderArray[0] this whole iteration. Next, the inner loop begins processing the bullets. The fact is that the inner loop will process all six bullets before it gives back control to the outer loop. We can thus test all six bullets for a hit test against the spiderArray[0]. Control is given back to the outer loop, and it begins its second iteration. Now we are processing spiderArray[1]. Once again, the inner loop begins and goes through all six of its iterations (again, with hit testing) before control is again given back to the outer loop. Next, the outer loop begins its third iteration, and now we are dealing wth spiderArray[2]. Once again, the inner loop processes all six bullets.

Hopefully, this is clear to you. The main thing to "get" is that we process the three enemy spiders, and that for each one of those, all six bullets are cycled through using the inner loop. If it helps, imagine a lineup of three of your friends. You walk up to the first one. You tap him or her on the shoulder six times. Then you move on to the next friend and do the same thing. Finally, you move to the third friend and do it one last time. The three friends are the outer loop, and the shoulder taps are the inner loop. And that's the same way we are processing our enemies and bullets.

The loop counters, i and j, are significant because while we are running the inner loop, we still have a reference to the enemy we are processing with the outer loop. Even inside the inner loop, the current enemy can be referenced with spiderArray[i]. Meanwhile, the six bullets are referenced with bulletArray[j]. We can thus process both arrays using this nested loop structure, and when we are done, we can be confident that every spider in the spiderArray has been hit tested against every bullet in the bulletArray:

function enemiesHitTest():void {
	//for each of the three spiders
	for(var i:int = 0; i < 3; i++) {
		//the each of the six bullets
		for(var j:int = 0; j < 6; j++) {
			if(spiderArray[i].hitTestObject(bulletArray[j])) {
				recycleEnemy(spiderArray[i]);
			}
		}
	}
}

We need to also make sure we place a call to this new function inside the enter frame handler:

function stage_onEnterFrame(event:Event):void {
	controlPlayer();
	moveBullets();
	moveEnemies();
	enemiesHitTest();
}

Using the hitTestObject function is pretty easy. It's a function that's built in to the MovieClip objects we are using. It takes this basic form:

mc1.hitTestObject(mc2);

Where mc1 and mc2 are a couple of movie clips. However, it works just as well if you turn it around the other way:

mc2.hitTestObject(mc1);

You can use either object's method to do a hitTest against the other object, in other words. It doesn't make any difference. Of course, we are using array references like spiderArray[i] and bulletArray[j] instead of mc1 and mc2. But the array references are really just pointers, and they point to the actual movie clip instances themselves. So when we use array references like this we are actually hitTesting the very objects that they are pointing to. An array reference is every bit as good as an instance name, and I am always emphasizing that in my tutorials!

If you try this new code, you will find that it works great! When a spider gets hit with a bullet, they get recycled to a random location above the top of the screen. The fact that they disappear when they do so makes it look like they really got shot! Now you see it, now you don't! Hey, we're mostly just illusionists, anyway! But one thing we forgot! When a spider is hit, the bullet doesn't disappear, but keeps right on going, and may even wipe out yet another spider! This probably isn't what we want, though. To fix it, let's borrow the two lines of code from the moveBullets function that placed a bullet at the player's feet, and copy them to the new function:

function enemiesHitTest():void {
	//for each of the three spiders
	for(var i:int = 0; i < 3; i++) {
		//the each of the six bullets
		for(var j:int = 0; j < 6; j++) {
			if(spiderArray[i].hitTestObject(bulletArray[j])) {
				recycleEnemy(spiderArray[i]);
				bulletArray[j].x = j * 70 + 100;
				bulletArray[j].y = 595;
			}
		}
	}
}

Notice that in the copied lines, we must change the i's to j's, because even though we are looping through the same array of bullets, we are using a "j" for a loop counter in this new location. Otherwise, it works exactly the same way, and places a bullet back at the player's feet after it hits a spider.

You may have guessed that we could have also written a special recycleBullet function and just called it from these two other locations, like we did with recycleEnemy, and that would be just fine, and a good thing to do. We will leave it as it is for now, but anytime you can refactor code that is essentially the same, but appearing in more than one place, you should do so. That way if you ever have to change it you can do it all in one place. Right now it's not too bad, because we can be pretty much certain that these will be the only two places where we'll need to recycle bullets.

But there is as yet one more obvious thing wrong with the above: there is no need to hit test a bullet that's not in play.
For one thing, it's not as efficient. But also, as it is, if the spiders get down to the bottom of the screen and hit the bullets below the player's feet, they get recycled immediately instead of making it past the bottom of the screen. So let's throw in an if condition that causes us to only hit test bullets that are in play. We can do this by making sure their y value is not greater than the player's, like we did before when moving the bullets:

function enemiesHitTest():void {
	//for each of the three spiders
	for(var i:int = 0; i < 3; i++) {
		//the each of the six bullets
		for(var j:int = 0; j < 6; j++) {
			//don't consider bullets that aren't in play:
			if(bulletArray[j].y > player.y) continue;
			if(spiderArray[i].hitTestObject(bulletArray[j])) {
				recycleEnemy(spiderArray[i]);
				bulletArray[j].x = j * 70 + 100;
				bulletArray[j].y = 595;
			}
		}
	}
}

The new line is this one:

if(bulletArray[j].y > player.y) continue;

The continue keyword is a special one that's used with loops. It means "cancel this iteration of the loop, and continue to the next one." This only applies to the inner loop, and makes the inner loop continue on to its next iteration, skipping the remaining lines that would have been executed. This is exactly what we want--any bullets not in play will therefore not be hit tested!

Here is all of the code so far:

import flash.events.KeyboardEvent;
import flash.events.Event;
import flash.display.MovieClip;

var rightArrow:Boolean;
var leftArrow:Boolean;
var spaceBar:Boolean;
var playerHalfWidth:Number = player.width / 2;
var bulletArray:Array = new Array(bullet0, bullet1, bullet2, bullet3, bullet4, bullet5);
var spiderArray:Array = new Array(spider0, spider1, spider2);
var bulletIndex:int = -1;

for(var i:int = 0; i < 3; i++){
	recycleEnemy(spiderArray[i]);
}

stage.addEventListener(KeyboardEvent.KEY_DOWN, stage_onKeyDown);
stage.addEventListener(KeyboardEvent.KEY_UP, stage_onKeyUp);
stage.addEventListener(Event.ENTER_FRAME, stage_onEnterFrame);

function stage_onKeyDown(event:KeyboardEvent):void {
	if(event.keyCode == 39) {
		rightArrow = true;
	}
	if(event.keyCode == 37) {
		leftArrow = true;
	}
	if(event.keyCode == 32 && spaceBar == false) {
		spaceBar = true;
		bulletIndex++;
		if(bulletIndex > 5) bulletIndex = 0;
		var bullet:Bullet = bulletArray[bulletIndex];
		bullet.x = player.x;
		bullet.y = player.y - 1;
	}
}
function stage_onKeyUp(event:KeyboardEvent):void {
	if(event.keyCode == 39) {
		rightArrow = false;
	}
	if(event.keyCode == 37) {
		leftArrow = false;
	}
	if(event.keyCode == 32) {
		spaceBar = false;
	}
}
function stage_onEnterFrame(event:Event):void {
	controlPlayer();
	moveBullets();
	moveEnemies();
	enemiesHitTest();
}
function controlPlayer():void {
	if(rightArrow == true) {
		player.x += 10;
		if(player.x > stage.stageWidth - playerHalfWidth) {
			player.x = stage.stageWidth - playerHalfWidth;
		}
	}
	if(leftArrow == true) {
		player.x -= 10;
		if(player.x < playerHalfWidth) {
			player.x = playerHalfWidth;
		}
	}
}
function moveBullets():void {
	//cycle through all six bullets in the bullet array
	//using a FOR LOOP:
	for(var i:int = 0; i < 6; i++) {
		//if the bullet is above the player, move it up 10 pixels every frame:
		if(bulletArray[i].y < player.y) {
			bulletArray[i].y -= 10;
			//if the bullet has gone off the top of the screen, 
			//put it back at the player's feet again:
			if(bulletArray[i].y < 0) {
				bulletArray[i].x = i * 70 + 100;
				bulletArray[i].y = 595;
			}
		}
	}
}
function moveEnemies():void {
	for(var i:int = 0; i < 3; i++) {
		spiderArray[i].y += 3;
		if(spiderArray[i].y > (stage.stageHeight + 25)) {
			recycleEnemy(spiderArray[i]);
		}
	}
}
function enemiesHitTest():void {
	//for each of the three spiders
	for(var i:int = 0; i < 3; i++) {
		//the each of the six bullets
		for(var j:int = 0; j < 6; j++) {
			//don't consider bullets that aren't in play:
			if(bulletArray[j].y > player.y) continue;
			if(spiderArray[i].hitTestObject(bulletArray[j])) {
				recycleEnemy(spiderArray[i]);
				bulletArray[j].x = j * 70 + 100;
				bulletArray[j].y = 595;
			}
		}
	}
}
function recycleEnemy(enemy:MovieClip):void {
	enemy.x = 25 + Math.random() * (stage.stageWidth - 50);
	enemy.y = -500 * Math.random() - 25;
}

And here is how the swf behaves with these new changes:

(Once again, you must click it once to activate the keyboard keys) We're getting there! This version is fairly cool, but the player is still impervious to contact with the spiders. We need to fix that with some more hit testing. Then we'll add some kind of scoring, and lives, and maybe even some other little touches!