Search code examples
actionscript-3flashdevelop

AS3 - Space Shooter Enemy Shots


I'm very new to AS3 and programming in general and I've been developing a space shooter game in AS3 and have run into trouble regarding the enemy shots. The enemies fly vertically down from the top of the screen and should fire two shots (one going left and one going right) once their y coordinates equal that of the player. This works fine, except after anything from 30 seconds to 2 minutes, the following errors occur.

TypeError: Error #1009: Cannot access a property or method of a null object reference. at EnemyShip2/fireWeapon()[G:\Games Related\1942\src\EnemyShip2.as:78] at EnemyShip2/loop2()[G:\Games Related\1942\src\EnemyShip2.as:65]

TypeError: Error #1009: Cannot access a property or method of a null object reference. at EnemyShot/removeSelf()[G:\Games Related\1942\src\EnemyShot.as:43] at EnemyShot/loop()[G:\Games Related\1942\src\EnemyShot.as:36]

Below is the relevant code, for the enemy ship class as well as the enemy shot class.

Enemy Ship

package 
{
    import flash.display.Sprite;
    import flash.display.Bitmap;
    import flash.display.Stage;
    import flash.events.Event;
    import flash.events.TimerEvent;
    import flash.utils.Timer;

    /**
     * ...
     * @author D Nelson
     */

    [Embed(source = "../assets/enemyship2.png")]
    //This enemy moves relatively slowly and fires horizontal shots
    public class EnemyShip2 extends Bitmap
    {
        private var vy:Number = 3;
        //private var ay:Number = .2;
        private var target:HeroShip;
        private var enemyShip2:EnemyShip2;
        private var enemyfireTimer:Timer;
        private var enemycanFire:Boolean = true;

        public function EnemyShip2(target:HeroShip):void
        {
            this.target = target;
            scaleX = 0.3;
            scaleY = 0.3;
            x = Math.floor(Math.random() * 550);
            y = -105;
            addEventListener(Event.ENTER_FRAME, loop2, false, 0, true);
            enemyfireTimer = new Timer(1000, 1);
            enemyfireTimer.addEventListener(TimerEvent.TIMER, handleenemyfireTimer, false, 0, true);
        }

        private function handleenemyfireTimer(e:TimerEvent) : void
        {
            //the timer runs, so a shot can be fired again
            enemycanFire = true;
        }


        private function removeSelf2():void 
        { 
            removeEventListener(Event.ENTER_FRAME, loop2);
            if (stage.contains(this))
            {
                stage.removeChild(this);
            }
        }   

        private function loop2 (e:Event):void
        {
            //vy += ay;
            y += vy;
            if (y > stage.stageHeight)
            {
                removeSelf2();
            }

            if (y >= target.y && (enemycanFire))
            {
                fireWeapon();
                enemycanFire = false;
                enemyfireTimer.start();
            }

            if (this.x > 540 || this.x < 10)
            {
                removeSelf2();
            }
        }

        private function fireWeapon():void
        {
            stage.addChild(new EnemyShot(target, x, y, -4));
            stage.addChild(new EnemyShot(target, x, y, +4));
        }
    }
}

Enemy Shot

package 
{
    import flash.display.Sprite;
    import flash.display.Bitmap;
    import flash.events.Event;
    import flash.display.Stage;

    /**
     * ...
     * @author D Nelson
     */

    [Embed(source="../assets/enemyshot.png")]
    public class EnemyShot extends Bitmap
    {
        private var speed:Number;
        private var target:HeroShip;
        private var vx:Number;

        public function EnemyShot(target:HeroShip, x:Number, y:Number, vx:Number) 
        {
            this.target = target;
            this.x = x;
            this.y = y;
            this.vx = vx;
            scaleX = 0.3;
            scaleY = 0.3; 
            addEventListener(Event.ENTER_FRAME, loop, false, 0, true);
        }

        private function loop(e:Event) : void
        {
            x += vx;
            if (x >= 600 || x <= -50) 
            {               
                removeSelf();
            }
        }

        private function removeSelf():void 
        { 
            removeEventListener(Event.ENTER_FRAME, loop); 
            if (stage.contains(this))
            {
                stage.removeChild(this);
            }
        }    
    }
}

Also provided is the main class, in case that is of any help.

package 
{
    import flash.display.Sprite;
    import flash.display.MovieClip;
    import flash.display.DisplayObject;
    import flash.display.Stage;
    import flash.events.*;
    import flash.events.Event;  
    import flash.events.EventDispatcher;
    import flash.events.KeyboardEvent;
    import flash.events.TimerEvent;
    import flash.ui.Mouse;
    import flash.utils.*;
    import flash.utils.ByteArray;
    import flash.utils.Timer;
    import flash.text.*;
    import flash.media.Sound;
    import flash.media.SoundChannel;

    /**
     * ...
     * @author D Nelson
     */

    public class Main extends MovieClip
    {
        [Embed(source = "snd/gamemusic2.mp3")]
        private var MySound : Class;         
        private var mainsound : Sound;
        private var shootsound: Sound = new ShootSound;
        private var sndChannel:SoundChannel = new SoundChannel;
        public var heroShip:HeroShip;
        public var enemyShip1:EnemyShip1
        public var enemyShip2:EnemyShip2
        public var enemyShip3:EnemyShip3
        public var enemyShip4:EnemyShip4
        public var bossShip: BossShip
        public var enemyShot: EnemyShot;
        public var playerShot: PlayerShot;
        private var background1:Background1;
        private var background2:Background2;
        public static const scrollspeed:Number = 2;
        public var playerShotArray:Array = new Array()
        public var enemyShotArray:Array = new Array()
        public var enemies1Array:Array = new Array()
        public var enemies2Array:Array = new Array()
        private var fireTimer:Timer; //this creates a delay between each shot
        private var canFire:Boolean = true; //this checks if a shot can be fired                

        public function Main():void 
        {
            if (stage) init();
            else addEventListener(Event.ADDED_TO_STAGE, init);
            mainsound = (new MySound) as Sound;
            shootsound = (new ShootSound) as Sound;
            mainsound.play();
            background1 = new Background1();
            background2 = new Background2();
            //setting the backgrounds one below another
            background1.y = 0;
            background2.y = background1.height;
            //add background at the lowest depth level
            stage.addChildAt(background1,0);
            stage.addChildAt(background2, 0);           
            //sets up the timer and its listener
            fireTimer = new Timer(250, 1);
            fireTimer.addEventListener(TimerEvent.TIMER, handlefireTimer, false, 0, true);
            stage.addEventListener(KeyboardEvent.KEY_DOWN, handleCharacterShoot);
            //background scrolling effect
            stage.addEventListener(Event.ENTER_FRAME, backgroundScroll);
            //loop for enemy1 variety
            stage.addEventListener(Event.ENTER_FRAME, loop1, false, 0, true);
            //loop for enemy2 variety
            stage.addEventListener(Event.ENTER_FRAME, loop2, false, 0, true);
            //speed of player shots
            setInterval(playerShootMovement, 10);           
        }

        private function init(e:Event = null):void 
        {
            removeEventListener(Event.ADDED_TO_STAGE, init);
            //entry point           
            heroShip = new HeroShip();
            stage.addChild(heroShip);

            //test values for enemies
            /*enemyShip1 = new EnemyShip1();
            stage.addChildAt(enemyShip1, 2);
            enemyShip1.y = stage.stageWidth * 0.3;
            enemyShip1.x = 300;

            enemyShip2 = new EnemyShip2();
            stage.addChildAt(enemyShip2, 2);
            enemyShip2.y = stage.stageWidth * 0.3;
            enemyShip2.x = 200;

            enemyShip3 = new EnemyShip3();
            stage.addChildAt(enemyShip3, 2);
            enemyShip3.y = stage.stageWidth * 0.3;
            enemyShip3.x = 100;

            enemyShip4 = new EnemyShip4();
            stage.addChildAt(enemyShip4, 2);
            enemyShip4.y = stage.stageWidth * 0.3;
            enemyShip4.x = 400;

            bossShip = new BossShip();
            stage.addChildAt(bossShip, 1);
            bossShip.y = 10;
            bossShip.x = 130;*/         

            Mouse.hide();           
        }

        private function handlefireTimer(e:TimerEvent) : void
        {
            //the timer runs, so a shot can be fired again
            canFire = true;
        }

        public function playerShoot():void
        {
            //if canFire is true, allow a shot to be fired, then set canFire to false and start the timer again
            //else, do nothing          
            if (canFire)
            {
                //Add new line to the array
                playerShotArray.push(playerShot = new PlayerShot);
                //Spawn missile in ship
                playerShot.y = heroShip.y;
                playerShot.x = heroShip.x+11;
                addChild(playerShot);
                canFire = false;
                fireTimer.start();              
                sndChannel = shootsound.play();
            }
        }

        public function playerShootMovement():void
        {
            //code adapted from the Pie Throw tutorial
            for (var i:int = 0; i < playerShotArray.length; i++) 
            {
                playerShotArray[i].y -= 10;

                if (playerShotArray[i].y == 850)
                {
                    playerShotArray.shift();
                }
            }           
        }

        public function handleCharacterShoot(e:KeyboardEvent):void
        {
            /** 
             * SpaceBar = 32
             */                                 
            if (e.keyCode == 32)
            {
                playerShoot();
            }           
        }       

        public function backgroundScroll (evt:Event):void
        {
            background1.y += scrollspeed;
            background2.y += scrollspeed;
            if (background1.y >= stage.stageHeight)
            {
                //the background is below the visible stage area, put it above the other background       
                background1.y = background2.y - background2.height;
            }
            else if (background2.y >= stage.stageHeight)
            {

                background2.y = background1.y - background2.height;
            }           
        }

        //EnemyShip 1 spawning
        private function loop1(e:Event):void
        {
            //generates probability for the ship to spawn (lower number to increase odds, decrease it to decrease odds)
            if (Math.floor(Math.random() * 55) == 5)
            {
                var enemyShip1:EnemyShip1 = new EnemyShip1(heroShip); 
                enemyShip1.addEventListener(Event.REMOVED_FROM_STAGE, removeEnemy1, false, 0, true);
                enemies1Array.push(enemyShip1); 
                stage.addChild(enemyShip1);
            }
        }

        private function removeEnemy1(e:Event):void
        {
            //removes the enemy that most recently left the screen from the array
            enemies1Array.splice(enemies1Array.indexOf(e.currentTarget), 1);
        }

        //EnemyShip2 spawning
        private function loop2(e:Event):void
        {
            if (Math.floor(Math.random() * 40) == 5)
            {
                var enemyShip2:EnemyShip2 = new EnemyShip2(heroShip); 
                enemyShip2.addEventListener(Event.REMOVED_FROM_STAGE, removeEnemy2, false, 0, true);
                enemies2Array.push(enemyShip2); 
                stage.addChild(enemyShip2);
            }
        }

        private function removeEnemy2(e:Event):void
        {
            enemies2Array.splice(enemies2Array.indexOf(e.currentTarget), 1);
        }
    }
}

Initially I thought it was to do with enemies firing shots while being too far to either side of the screen, but that doesn't seem to be the case. Any help would be greatly appreciated.


Solution

  • You need to make sure you remove the ENTER_FRAME listeners whenever you remove the object.

    Actually, my advise is to not add ENTER_FRAME handlers throughout your game objects. This gets hard to manage and leads to bugs like you've encountered. Instead, add a single ENTER_FRAME as your core game loop, and update objects by calling an update() function on a list of game objects you maintain in your main game class. When you remove an object you simply won't call update() anymore. It also becomes easy to pause the game by just removing the ENTER_FRAME handler.

    For example, here's a pattern I like to use for simple games:

    interface IGameObject {
        update():void;
    }
    
    class Enemy extends Sprite implements IGameObject {
        public update():void {
            // move, fire, etc
        }
    }
    
    class Player extends Sprite implements IGameObject {
        public update():void {
            // move, fire, etc
        }
    }
    
    class Bullet extends Bitmap implements IGameObject {
        public update():void {
            // move, collide, etc
        }
    }
    
    class Main extends Sprite {
        private objects:Vector.<IGameObject> = new <IGameObject>[];
    
        public start():void {
            addEventListener(Event.ENTER_FRAME, update);
        }
    
        public stopGame():void {
            removeEventListener(Event.ENTER_FRAME, update);
        }
    
        private function update(e:Event):void {
            for each (var object:IGameObject in objects) {
                object.update();
            }
        }
    
        public addObject(object:IGameObject):void {
            objects.push(object);
            addChild(object as DisplayObject);
        }
    
        public removeObject(object:IGameObject):void {
            objects.splice(objects.indexOf(object), 1);
            removeChild(object as DisplayObject);
        }
    }
    

    Add and remove objects using addObject and removeObject. You can invoke them through a reference to Main or through event handlers you dispatch from your objects.