Outstream
Outstream

Reputation: 61

AS3 Null Object Reference After Null Check

I have been looking for a solution to this for a while now but cannot find anything.

This problem is very odd so I'll give some background. I am making a tower defense game, and while implementing status effects on enemies (slowed, on fire, etc.) I ran into a weird problem. When the tower kills the enemy with a projectile everything is fine, but for some reason when the enemy is killed by a status effect, I get a null object reference error (both are handled with the method "Enemy.damage(damage:int)"). The weird thing about this is that the status effects are stored in an array that is only referenced in 3 spots. In the ENTER_FRAME event (one ENTER_FRAME event is used to process every aspect in the Game), in the constructor (status = new Array();) and in the destroy method.

One weird part is that if the "status" variable was null, other variables from the destroy method would have given me errors a long time ago. The really weird part is that I have a null check right before I get the error.

Here is where I get the error:

        //STATUS EFFECTS
        if (status == null) {
            trace("test");
        }
        if (status != null && status.length != 0) {
            for (var i:int = 0; i < status.length; i++) {//LINE 101
                status[i].applyEffect();
            }
        }

Here is the error:

TypeError: Error #1009: Cannot access a property or method of a null object reference.
at game.enemies::Enemy/update()[C:\flash\game\enemies\Enemy.as:101]

"test" never gets printed, and even with the null check I still get the error

EDIT: Here is the Enemy class: http://pastebin.com/LAyMMB1P

The StatusEffect & FireEffect classes: http://pastebin.com/GTGDmjt8 (I can only post 2 links, each is in its own file)

status variable is referenced in StatusEffect.destroy() and here:

override public function onHit(e:Enemy) {

        if (upgradeLevel >= 1) {
            var onFire = false;
            for (var i:int = 0; i < e.status.length; i++) {
                if (e.status[i].name_ == "fire") {
                    onFire = true;
                }
            }
            if (!onFire) {
                e.status.push(new FireEffect(e));
            }
        }
        if (upgradeLevel >= 2) {
            if (enemiesHit.indexOf(e) == -1) {
                enemiesHit.push(e);
                e.damage(1);
            }
            if (upgradeLevel == 2 && enemiesHit.length == 2) {
                destroy();
            }
        } else {
            e.damage(super.damage);
            destroy();
            return;
        }
    }

Upvotes: 1

Views: 136

Answers (1)

BadFeelingAboutThis
BadFeelingAboutThis

Reputation: 14406

This is happening because you can potentially destroy your enemy in the middle of the for loop on line 101.

Destroying the enemy sets the status array to null, so on the next iteration when it checks if i < status.length, status is null, thus the error.

So to illustrate, stepping through the code, let's assume at line 101 there are two items in the status array:

  • First loop, item at index 0 gets applyEffect() called on it.

  • inside applyEffect() the specificApplication() method can be called.

  • inside specificApplication() the super.enemy.damage(1); method is called.

  • inside the enemy.damage() method, you can potentially call the enemy.destroy method

  • inside the enemy.destroy method, you set status = null.

  • NOW the loop from line 101 loops, but status is now null.

Fix this by breaking out of your loop if the enemy is destroyed.

for (var i:int = 0; i < status.length; i++) {//LINE 101
    status[i].applyEffect();
    if(!status) break;
}

You also have some issue with splicing:

enemy.status.splice(enemy.status.indexOf(this), 1);

While there is nothing wrong with that line on it's own, let's step through the code:

line 101 of Enemy class:

for (var i:int = 0; i < status.length; i++) {
  • You loop through the status array. For this example, let's say you have two items in the array.

  • Item A is at position 0. Item B is at position 1 in the array.

  • First loop, i has a value of 0.

  • You call status[i].applyEffect(), since i == 0, that is item A.

  • Inside applyEffect let's say your condition is met so you call destroy (on the StatusEffect class)

  • Inside the StatusEffect.destroy() method you splice the status array.

  • Now status has only one item, item B, and item B now has an index of 0 (instead of 1).

  • Your loop loops, now i == 1.

  • However, there is no longer any item at index 1 so the loop exits, skipping item B.

To solve it, iterate backwards instead, then your indices won't get out of whack when you splice:

var i:int = status.length
while(i--){
    status[i].applyEffect();
}

As an aside, it would be much cleaner if you used var status:Vector.<StatusEffect> instead of an var status:Array. You'd get compile time checking and type safety that way.

Upvotes: 1

Related Questions