Friday, December 17, 2010

Project Jumper Part 2: The bare minimum

Now that we've got our mise en place set up, it's time to actually start making something that works.

This is pretty typically the stage where I panic and quit, so bear with me here.

Anyway, I want to get something that will at least compile and run as quickly as possible. So, I'm going to aim for a single stage for a player sprite to run around on.

Preloader

Well, before that, let's slap a preloader on here before I forget. Fortunately, this is easy. First, I just copy the preloader.as from, well, any flixel project, really. I don't even remember where I got this one, but it's the same one I've copy and pasted for each of my practice attempts.

Preloader.as

package  
{
    import org.flixel.FlxPreloader;
    public class Preloader extends FlxPreloader
    {
        
        public function Preloader() 
        {
            className = "Jumper";
            super();
        }
        
    }

}

I just have to change what's in the quotes after className there. Then, just throw in a quick edit to Jumper.as to tell it to use the preloader.Right after the line

[SWF(width = "640", height = "480", backgroundColor = "#000000")]
We add in the line

[Frame(factoryClass="Preloader")]

Making the Level
Well, that was the easy part. Now to make a level. There's lots of ways to do this, but I'm going to use the most basic and general: A tilemap and a tilesheet. The tilesheet is just a .png file with all the tiles I want to use lined up in a row. The tilemap is just a comma delineated spreadsheet that will tell Flixel which image to use for each space. It would be pretty easy to make these by hand, but I'm super lazy and I'm going to put a little work into doing less work. Instead of doing it by hand, I'm going to use Dame. Dame features lots of powerful export options that I'm not going to be using at all. I'm just going to slap some blocks down, hit export, and use the csv that it creates.


When we open up DAME, we get a nice blank canvas to scare us. To actually get a level started, we need to add a map layer to the project. Just right-click in the big white box labeled Layers, and choose "add new group", then check the "map" box and hit OK.




Just like this. The other check boxes and scroll dialogs I'm just going to ignore because I don't care about them right now.






A new dialog comes up with a bunch of options. All I change are the tileWidth and tileHeight, which are how many pixels in size each tile is, and the width and height options to say how big the map is. If I already had a tileset image created, I could select it now. But I don't, so I'm just leaving that blank. When I hit OK, it asks if I want to make a new tileset, which I do. I don't know if it's a bug in DAME, or in Flash, or in Windows, or just my computer, but at this point the new file dialog pops up BEHIND the DAME window, and DAME doesn't work properly until I find it. So if things are being weird for you, make sure DAME doesn't have an extra window open where you can't see it.

Anyway, now I've got something like this. That Tiles window is like a paint palette with no paint on it right now. Just right-click on the tiles window, and insert a new blank tile after the current tile. Now I've got something I can work with. Select Tile 1, and paint some tiles onto the map to make a level.










Those white boxes are the floor and walls of this terrible level. Just to add some color to this, choose the "draw on tiles" button. This lets you draw right onto the map and edit the tiles directly. It's not a great drawing program, but it works for fast and dirty stuff like this.



Now the level is kind of orangey-red! Yay! There's lots more we could be doing in here, but for now I don't care. I'll get back to it when it's time to actually make a decent level. For now, we're just going to export this as a .csv that flixel can use.










Go to File -> Export, and you should see something a little like this:

You might have to resize the window to get everything to show up. Almost everything on this page can be ignored, since we're not exporting any actual code. Just make sure the 'Export only CSV' box is checked, and that the CSV directory points somewhere useful. Hit export, and you should have a shiny new file full of zeroes, ones, and commas

Some actual code! Sort of.

OK, let's get this stuff into the code. In PlayState.as, at the beginning of the class before we start declaring variables or defining functions or anything, add these two lines of code:

[Embed(source = '../../../../levels/mapCSV_Group1_Map1.csv', mimeType = 'application/octet-stream')]public var levelMap:Class;
[Embed(source = '../../../../art/tilemap.png')]public var levelTiles:Class;

Except make sure that you use the right filenames and directories. You can just right-click on the filename in the project browser screen and choose Insert into Document, and FlashDevelop will do most of the work for you. Sweet.

Now we just have to get it to display. The map is going to be used by all sorts of functions, so we make it a public variable:

public var map:FlxTilemap = new FlxTilemap;

Then we just need to actually set the map up during the playstate's construction. Change the PlayState.create() function to look like this:

override public function create():void
{
    add(map.loadMap(new levelMap, levelTiles, 16, 16));
    super.create();
}


We can even compile and take a look at this point.

It works! It's also still a long way away from being an actual game. For now, let's try getting a player sprite added in there.

Enter the Player
We need a sprite to be our hero. Something awesome, heroic, and totally not ripped off from a well known NES game. Instead, we're using this guy.
Yeah. I'm sorry. Remember, when you make a game for real, you have to make your own sprite, or at least use one that you've actually got permission for. Anyway, this guy doesn't have nearly enough frames of animation for jumping, shooting, ducking, etc., so I'll have to replace him eventually. He'll work for now, though.

We need to make a new class to hold all our player stuff. Player.as sounds like as good a name as any. The basic bones will look something like this:
Player.as

package com.chipacabra.Jumper
{
    import org.flixel.FlxSprite;

    public class Player extends FlxSprite 
    {
        public function Player(X:int,Y:int):void
        {
            super(X, Y);
        }
    }
}

Nothing there is too surprising. FlxSprite is basically any sort of sprite object. Players, enemies, coins, whatever, will all be FlxSprite objects. The X and Y will be the starting coordinates we pass to the object when we actually create it. At this point, I'm basically looking at the Player.as from an existing game, in this case Adam Atomic's Mode and snagging anything I think I'll need.
In the declarations, we definitely need to include the sprite sheet.

[Embed(source='../../../../art/helmutguy.png')]public var Helmutguy:Class;
And we should define some basic physics.

protected static const RUN_SPEED:int = 80;
protected static const GRAVITY:int =420;
protected static const JUMP_SPEED:int = 200;
Those numbers are pretty arbitrary, and since they're in big capital letters right at the top of the class, they'll be easy to tweak later.

Then, down in the class constructor, let's start piecing it together. First off, telling it to use the spritesheet.

        public function Player(X:int,Y:int):void // X,Y: Starting coordinates
        {
            super(X, Y);
            loadGraphic(Helmutguy, true, true);
        }


Then we go ahead and define some animation.

            addAnimation("walking", [1, 2], 12, true);
            addAnimation("idle", [0]);

Finally, we need to actually define the physics that will govern helmutguy. A few more lines of code, still within the Player() constructor:

            drag.x = RUN_SPEED * 8;  // Drag is how quickly you slow down when you're not pushing a button. By using a multiplier, it will always scale to the run speed, even if we change it.
            acceleration.y = GRAVITY; // Always try to push helmutguy in the direction of gravity
            maxVelocity.x = RUN_SPEED;
            maxVelocity.y = JUMP_SPEED;
At this point, I'm feeling a little antsy to make sure everything's working so far. So let's just stick what we've got into our game and see if it works. Go back to PlayState.as, and introduce the player. The declarations and constructor should look like this:

        public var map:FlxTilemap = new FlxTilemap;
        public var player:Player;
        
        override public function create():void
        {
            add(map.loadMap(new levelMap, levelTiles, 16, 16));
            add(player = new Player(10, 10));
            super.create();
        }
Compile and run, and watch as little helmutguy appears on the left side of the screen, begins to fall... and keeps falling right through the floor and off the bottom of the screen. Well, that's because I didn't tell the code how to do things like stop falling when you hit the floor. Let's do that next. Actually, it's easy, since Flixel does almost all the work itself. Just add a line to the PlayState.update() function:

        override public function update():void 
        {
            super.update();
            player.collide(map);
        }

Make Him Move

Now we want helmutguy to actually walk around. That gets handled in the Player.update() function that we're about to make.

        public override function update():void
        {
            super.update();
            acceleration.x = 0; //Reset to 0 when no button is pushed
            
            if (FlxG.keys.LEFT)
            {
                facing = LEFT; 
                acceleration.x = -drag.x;
            }
            else if (FlxG.keys.RIGHT)
            {
                facing = RIGHT;
                acceleration.x = drag.x;                
            }    
        }
That code is pretty much pulled straight out of Mode. I suspect it'll look almost identical in any flixel platformer.
Here's a good time, incidentally, to point out a neat FlashDevelop trick. When I typed this in and hit compile, I got an error saying that FlxG was an undefined property. That's because I didn't import org.flixel.FlxG at the beginning of Player.as. All I have to do, though, is put the cursor on FlxG and hit ctrl-shift-1, and it automatically adds the import for me! Super convenient.

There he is, all sliding around. Isn't he cute?
Now let's get him to jump around. Again taking my cue from Mode, I add this after the move left and move right code:

            if(FlxG.keys.justPressed("UP") && !velocity.y)
            {
                velocity.y = -JUMP_SPEED;
            }
Strangely, though, it doesn't work! Helmutguy remains glued to the ground. It took a little poking and experimenting, but I figured out that when you call super.update() matters. I just moved it down to the end of the Player.update() code, and everything works fine.

OK, one last thing before I call it good for now; animation. Again, this one's easy. Just need to tell it to play the animation if he's moving sideways, and stop when he's not moving. Just add this somewhere after the movement logic:

    //Animation
    if (velocity.x > 0 || velocity.x <0 ) { play("walking"); }  // Make sure to check for positive or negative velocity, so the animation will play when moving in both directions
    else if (!velocity.x) { play("idle"); }


Now helmutguy has a proper wiggle when he moves! It would be better if he had a jumping animation, but beggars and choosers and all that.

If you'd like to take a look at the progress so far, I crammed all the source code and resource files into a zip file here: JumperPart2.zip.
And here's the little guy in action: http://www.swfupload.com/view/142731.htm

10 comments:

  1. Great tutorial Chipacabra!

    Couple of small things I noticed:
    You changed up your scaling of your map between the last step in Jumper.as:

    super(320, 240, PlayState, 2); //Create a new FlxGame object at 320x240 with 2x pixels, then load PlayState to something like

    super(320, 240, PlayState, 1); //Create a new FlxGame object at 320x240 with 2x pixels, then load PlayState

    Might be worth mentioning how these numbers play out (zoom, camera width / height), or mention that these will be further discussed in step 3.

    Also, the walking animation for the Helmutguy does not work when facing left:

    if (velocity.x > 0) { play("walking"); }
    (does not animate when going left because the velocity will be negative)

    Just need to take the absolute value of the velocity for this to work:

    if (Math.abs(velocity.x) > 0) { play("walking"); }

    ReplyDelete
  2. Yep, good catch. I actually fixed those in the code after I posted, forgot to make a note.

    ReplyDelete
  3. Thanks for doing this blog! It's making for a very unique tutorial.

    One issue I had with this post as I'm following along, I didn't fall, and I wasn't moving at all. I looked at the source code and realized you left out mention of all of this stuff:

    drag.x = RUN_SPEED * 8;
    acceleration.y = GRAVITY;
    maxVelocity.x = RUN_SPEED;
    maxVelocity.y = JUMP_SPEED;

    ReplyDelete
  4. Oops, I don't know how I forgot that! Thanks, I went ahead and added it back in.

    ReplyDelete
  5. I dunno if there has been a version change in DAME since this tutorial, or if maybe I missed something, but I found that after tiling the map and drawing on the tiles, I had to right-click the "tiles" window, "View Raw Image Data", then click "Save" for the tiles to show up in game.

    Great tutorial, by they way!

    ReplyDelete
    Replies
    1. Honestly...I still can't get the tiles to show correctly... I assumed that by "view raw image data" you were reffering to "edit raw image data". I click on the then saved the map, exported the png and cvs file, but my map still has grey tiles instead of the color i painted them, and not to mention the map seems to be cut off.

      Delete
  6. Excellent tutorial!

    I got a problem with the collision test, as the collide function doesn't exist anymore on the player object.
    In the latest version of flixel, we have to write the following instruction:
    FlxG.collide(player,map);

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
  7. For some reason when I run the compiled program, my tiles are grey, and the map is cutoff. I did paint my tiles a red color and dame...it didn't say in the tutorial but I assumed I needed to and went ahead and exported the map as an image. Here's my play state code

    package com.chipacabra.Jumper
    {
    import org.flixel.*;


    public class PlayState extends FlxState
    {
    [Embed(source = "../../../../levels/mapCSV_Group1_Map1.csv", mimeType = "application/octet-stream")]
    public var levelMap:Class;
    [Embed(source = "../../../../art/tilemap.png")]
    public var levelTiles:Class;
    public var map:FlxTilemap = new FlxTilemap;
    override public function create():void
    {
    add(map.loadMap(new levelMap, levelTiles, 16, 16));
    super.create();
    }
    }
    }

    ReplyDelete
    Replies
    1. I am having a similar problem, except the screen is black. My code is the same. Perhaps something to do with a newer version of DAME?

      Delete