Funded with Kickstarter

Catch ‘em All

And we’re back! Last time, we added a bouncing ball and hooked up the controls so a player can move it around our previously created map. Today, we’ll introduce collectibles, add them (and the player) to our level in Tile, and hook up level reloading without recompiling the SWF. As your game and levels become more complex this becomes a huge time saver. Tweak, reload, playtest, repeat!

Objects in the Rear View

Let’s start by revisiting our level and adding some objects. Open up level.tmx in Tiled. Create a new object layer (Layer > Add Object Layer) and name the new layer entities. Objects in Tiled are one of its most powerful features but also one of its clumsiest.

tiled-layers.png

Tiled Gotcha 1: The default width and height of a newly created object is 0 tiles. With these default dimensions an object appears to have a width and height of 2 tiles. The object’s actual x and y position is the top left corner of the bottom right tile it covers. Comically unintuitive.

Tiled Gotcha 2: When setting or changing an object’s properties, always tab to the next property. Otherwise, Tiled will not save the updated value when you hit Enter/Return to confirm the changes and dismiss the Object Properties window. This may not be a problem if you manually click the OK button.

tiled-object-tools-insert.png

Select the Insert Object tool and click somewhere on your map. Your first object! Now we need to give it a type. Control-click on the bottom right corner of the new object and select Object Properties… from the contextual menu. Set type to Ball and click the OK button.

tiled-object-tools-select.png

Select the Select Object tool and click and drag the bottom right corner of the object to reposition it. (If the object gets stuck to the mouse, click the bottom right corner again to drop it. So buggy!)

Insert and position five more objects. Set their type to Pod. When you’re done your level.tmx should look something like this:

tiled-objects.png

Don’t forget to save level.tmx when you’re done!

Pod Race

Now let’s update the project to make use of the objects we just added. We’ll start by adding collectibles. Download the hovering pod spritesheet:

pod.png

Move the file into your project’s assets folder.

Open up FDT and open ROM.as and embed the pod spritesheet by adding:

[Embed(source="assets/pod.png")] static public var ImgPod : Class;

ROM.as should now look like this:

package {
    public class ROM {
        [Embed(source="assets/bg.png")] static public var ImgBg : Class;
        [Embed(source="assets/ball.png")] static public var ImgBall : Class;
        [Embed(source="assets/pod.png")] static public var ImgPod : Class;
    }
}

Now create a new Pod ActionScript Class in your project (File > New > ActionScript Class). Set the Class name to Pod, the Superclass to org.flixel.FlxSprite, check “Explicitly invoke superclass constructor”, and click the Finish button.

In Pod.as, we need to remove the unnecessary SimpleGraphic argument from the constructor and super calls. Replace:

public function Pod(X : Number = 0, Y : Number = 0, SimpleGraphic : Class = null) {

with:

public function Pod(X : Number = 0, Y : Number = 0) {

And:

super(X, Y, SimpleGraphic);

with:

super(X+4, Y+4);

We’re going to make the hitbox of each pod smaller than its frame so we need to offset its position by the difference. The frame will be 16x16 pixels and the hitbox will be 8x8 so we need to subtract each of frame dimension from its corresponding hitbox dimension and then halve the result. Since width and height are the same we only need to do this math once (phew!). 16 - 8 = 8. 8 / 2 = 4. So we position each pod at X+4, Y+4 when we call the super() constructor.

Let’s load our spritesheet! Add the following inside the Pod constructor:

loadGraphic(ROM.ImgPod, true, false, 16, 16);
addAnimation("hover", [0,1,2,3], 10);
play("hover");
width = 8;
height = 8;
offset.x = 4;
offset.y = 8;

This should look familiar. We set up Ball.as almost exactly the same way except we use a custom hitbox size by setting the width and height, and the corresponding offsets with offset.x and offset.y. (In this case offset.y is greater than offset.x because we want our pod to appear as if it is hanging in the air, not sitting on the ground.)

With these additions Pod.as should look like this:

package {
    import org.flixel.FlxSprite;

    public class Pod extends FlxSprite {
        public function Pod(X:Number, Y:Number) {
            super(X+4, Y+4);
            loadGraphic(ROM.ImgPod, true, false, 16, 16);
            addAnimation("hover", [0,1,2,3], 10);
            play("hover");
            width = 8;
            height = 8;
            offset.x = 4;
            offset.y = 8;
        }
    }
}

Close Pod.as and open up PlayState.as.

Flixel Aside: A sprites hitbox and offset can be hard to grasp abstractly. The best way to get a feel for how it works is to play with a bunch of different values and see what effect they have. The easiest way to see that efffect is to turn on visual debugging. Open up Primer.as and beneath:

super(240,160,PlayState,2);

add:

FlxG.visualDebug = true;

Then run your game. The red outlines are hitboxes, the blue are individual tiles of your tilemap. To turn visual debug off, just remove or comment out the line we just added like so:

// FlxG.visualDebug = true;

Hanging Around

Now we’re going to update our level loading logic to load the Ball and Pods from our TMX. In PlayState.as find:

private var ball:Ball;

and beneath it add:

private var podGroup:FlxGroup = new FlxGroup();

Then, inside the loadStateFromTmx() function, find:

ball = new Ball(16,16);
add(ball);

and replace it with:

var group : TmxObjectGroup = tmx.getObjectGroup('entities');
for each (var object:TmxObject in group.objects) {
    spawnObject(object);
}
add(podGroup);

This code will turn the objects we added to our level into game objects. But first we need to define the spawnObject() function.

Add the following below the loadStateFromTmx() function:

private function spawnObject(obj : TmxObject) : void {
    switch(obj.type) {
        case "Ball":
            ball = new Ball(obj.x, obj.y);
            add(ball);
        break;

        case "Pod":
            podGroup.add(new Pod(obj.x, obj.y));
        break;
    }
}

Let’s add the collection logic. Inside the update() function, beneath:

FlxG.collide(background, ball);

add:

FlxG.overlap(podGroup, ball, collectPod);

The overlap() function will check each pod in the podGroup to see if it overlaps with ball. When one does the collectPod() function is called.

Then after the update() function, add:

private function collectPod(aPod:Pod, aBall:Ball):void {
    aBall; // silence FDT warning about unused arg
    aPod.kill();
}

Finally, we add the reloading code. Inside the update() function, beneath:

if (!hasLoaded) return;

add:

if (FlxG.keys.justPressed("ENTER")) {
    FlxG.switchState(new PlayState); // reload to test map changes
}

With all the new additions, PlayState.as should now look like this:

package {
    import org.flixel.*;
    // TMX Loading
    import net.pixelpracht.tmx.*;
    import flash.events.Event;
    import flash.net.URLLoader;
    import flash.net.URLRequest;

    public class PlayState extends FlxState {
        private var hasLoaded:Boolean = false;
        private var background:FlxTilemap = new FlxTilemap();
        private var ball:Ball;
        private var podGroup:FlxGroup = new FlxGroup();

        override public function create():void {
            // initiate variables, add sprites, tilemaps, etc

            loadTmxFile();
        }

        // Level loading
        private function loadTmxFile():void {
            var loader:URLLoader = new URLLoader();
            loader.addEventListener(Event.COMPLETE, onTmxLoaded);
            loader.load(new URLRequest('level.tmx'));
        }

        private function onTmxLoaded(e:Event) : void {
            var loader:URLLoader = e.target as URLLoader;
            var xml:XML = new XML(loader.data);
            var tmx:TmxMap = new TmxMap(xml);
            loadStateFromTmx(tmx);
            hasLoaded = true; // allow updates
        }

        private function loadStateFromTmx(tmx:TmxMap) : void {
            var csvBg:String = tmx.getLayer('bg').toCsv(tmx.getTileSet('bg'));
            background.loadMap(csvBg, ROM.ImgBg, 16, 16, FlxTilemap.OFF,0,0,2);
            add(background);

            var group : TmxObjectGroup = tmx.getObjectGroup('entities');
            for each (var object:TmxObject in group.objects) {
                spawnObject(object);
            }
            add(podGroup);

            FlxG.camera.setBounds(0, 0, background.width, background.height, true);
            FlxG.camera.follow(ball, FlxCamera.STYLE_TOPDOWN_TIGHT);
        }

        private function spawnObject(obj : TmxObject) : void {
            switch(obj.type) {
                case "Ball":
                    ball = new Ball(obj.x, obj.y);
                break;

                case "Pod":
                    podGroup.add(new Pod(obj.x, obj.y));
                break;
            }
        }

        override public function update():void {
            // listen for user input, move sprites, etc

            if (!hasLoaded) return;

            if (FlxG.keys.justPressed("ENTER")) {
                FlxG.switchState(new PlayState); // reload to test map changes
            }

            // handle user input
            var ballVelocity:Number = 64;

            ball.velocity.x = 0;
            ball.velocity.y = 0;

            if (FlxG.keys.RIGHT) {
                ball.velocity.x = ballVelocity;
            }
            else if (FlxG.keys.LEFT) {
                ball.velocity.x = -ballVelocity;
            }

            if (FlxG.keys.UP) {
                ball.velocity.y = -ballVelocity;
            }
            else if (FlxG.keys.DOWN) {
                ball.velocity.y = ballVelocity;
            }

            // update after user input
            super.update();

            // collision logic
            FlxG.collide(background, ball);
            FlxG.overlap(podGroup, ball, collectPod);
        }

        private function collectPod(aPod:Pod, aBall:Ball):void {
            aBall; // silence FDT warning about unused arg
            aPod.kill();
        }
    }
}

Now run your game. Collect the pods! Edit your level in Tiled, reposition the ball and pods, redraw parts of the background map. Save, switch back to Flash Player, and hit Enter to test your changes. You’re now a level designer!

Time to add some danger.

Copyright © 2012–2014 Retro Game Crunch.