Create a Top-Down RPG in Flixel: Your First Room

In this tutorial we will go from asking "What is Flixel?" to having an indoor room and a keyboard-controlled character in the top-down role playing game style (think Zelda).

Final Result Preview

Let's take a look at the final result we will be working towards:

Step 1: Understanding the Project Structure

For the visual people among us, let's see how everything will be organized so the rest will make sense.

Basically, we have all of our artwork stored in the assets folder and all of our ActionScript files stored in the src folder. If you want to use this tutorial as the basis for your own game engine, the topdown folder contains the generic stuff (a.k.a. the engine) and the tutorial folder shows how to use it.

You'll probably notice rather quickly that the art files have really long names. Rather than showing you a tutorial filled with compelling red boxes (the apex of my artistic ability), we will use some open source artwork from OpenGameArt. Each file is named to show the source, the artist, and the license. So, for example, armor (opengameart - Redshrike - ccby30).png means it's an image of armor, downloaded from OpenGameArt, created by the artist known as Redshrike, and it uses the CC-BY-30 license (Creative Commons Attribution).

Long story short - these art files can be used for any purpose as long as we link back to the site and give credit to the artist.

tutorial/Assets.as - imports any images that we need to use in this tutorial

tutorial/IndoorHouseLevel.as - defines an indoor room with some objects lying around

tutorial/Player.as - a keyboard-controlled, animated Ranger

tutorial/PlayState.as - Flixel state that controls our game

Default.css - an empty file needed to prevent the Flex compiler from giving us a warning

Main.as - entry point for the application

Preloader.as - Flixel preloader

Now let's get down to business!

Step 2: Firing Up Flixel

Flixel is a 2D game engine for ActionScript 3. To quote the home page:

Flixel is an open source game-making library that is completely free for personal or commercial use.

The most important thing to know about Flixel is that it is designed to use bitmap images (raster graphics) instead of Flash-style vector graphics. You can use Flash movie clips, but it takes a little massaging. Since I don't feel like giving a massage today, we will be using images for all our art.

Flixel comes with a tool that creates a dummy project for you. This tool creates the three files that are in the root of our project: Default.css, Main.as, and Preloader.as. These three files form the basis for almost any project in Flixel. Since Default.css is just there to avoid a compiler warning, let's take a look at Main.as.

There are only three lines of importance here. First off, we tell Flash to use a 480x480 window with a white background. Then we tell Flash to use our Preloader class while loading. Finally, we tell Flixel to use a 240x240 window (zooming in by a factor of 2 to make things look bigger) and to use PlayState once everything is ready to go.

Let me share a quick word about Flixel's states. In Flixel, states are kind of like a window, but you can only have one at a time. So, for example, you could have a state for your game's main menu (MainMenu), and when a user clicks the Start Game button you switch to PlayState. Since we want our game to just get going immediately, we just need one state (PlayState).

Not much to see here. Since we extend from FlxPreloader, Flixel really just takes care of it. The only thing to note is that if you changed Main to some other name, you would have to change className here on the highlighted line.

We're almost up to seeing something on the screen now. All we need is a Flixel state to get the ball rolling, so here's PlayState.as.

If you compiled this code, you'd get a marvelous black screen with a mouse cursor. Never fear, it gets better from here.

Step 3: Creating a Basic Level

Now that we have Flixel up and running, it's time to make a top-down RPG level. I like to give you reusable classes so you can make your own levels, so we'll actually create a generic level class that we can use to make something more interesting later. This is topdown/TopDownLevel.as.

All of the variables have their own descriptions in the source code, so I won't bore you with too much repetition. I should, however, explain groups in Flixel.

We have three groups defined here: floorGroup, wallGroup, and guiGroup. Flixel uses groups to determine in what order to render sprites (to decide what's on top when they overlap) and to handle collisions. We want the player to be able to walk around on a floor (no collisions needed), but we also want walls and objects (collisions definitely needed) so we need two groups. We also need a separate group for our user interface (guiGroup) so we can make sure it gets rendered on top of everything else.

Groups are rendered in the order they are added, which is determined in our addGroups() function. Since we want guiGroup to always be on top, we call add(guiGroup) after all the other groups. If you make your own groups and forget to call add(), they won't show up on the screen.

In our constructor, we store some useful values (like the number of tiles in the level) and call create(). The create() function shows you what goes into a Flixel level - a map, a player, an interface, groups (to control rendering order and collisions), and a camera view. Each of these gets its own function to help keep things more readable and so we can re-use common functionality. For instance, take a look at createCamera().

We won't need to change this function to make our own indoor level. Flixel has a built-in camera for top-down games (FlxCamera.STYLE_TOPDOWN). All we're really doing here is telling the camera not to leave the level (by calling setBounds()) and telling the camera to follow the player (by calling follow()) if the level is bigger than the screen and requires scrolling. This will work for almost every kind of level, so we can keep it here rather than re-coding this for each of our levels.

FlxG.collide(wallGroup, player) causes the player to bump into walls rather than walking through them. Since we don't call FlxG.collide(floorGroup, player), the player can walk all over the floors with nary a collision in sight (same thing for guiGroup, too).

Remember to call this.add(LEVEL) unless you want to stare at a black screen forever. As the comment states, I used public static var LEVEL as a convenience for the future. Suppose you add some artificial intelligence to your game and your AI needs to know where the player is located; this way, you can call PlayState.LEVEL.player and keep things nice and easy. It's not necessarily the prettiest way to do things, but it'll get the job done if used sparingly.

Step 4: Creating a Basic Entity

An entity is something that needs to be displayed and can move around. This could be the player, a computer-controlled character, or perhaps even something like an arrow. Since there can be many entities on a level, we want a generic class that we can use to save ourselves some time. Take a look at topdown/TopDownEntity.as.

Notice that we extend from FlxSprite. This gives us access to much of the power behind Flixel. makeGraphic() creates a rectangular bitmap of the given size (16x18 in this case), using the color you pass in. This color is in 0xAARRGGBB format, so 0xFFFF0000 means we're creating a solid red box (I warned you about my artistic abilities). You can mess around with this value to see how the color changes. In fact, we now have something other than a blank screen!

Still not too exciting, but at least we can see something, right?

Step 5: Creating an Indoor Room

I don't know about you, but I'm tired of looking at that black background. Let's make it look like a room. Here's tutorial/IndoorHouseLevel.as.

The first thing you notice are those two giant arrays of numbers, FLOORS and WALLS. These arrays define our map layers. The numbers are tile indices based on the artwork we're using. I've zoomed in on the image we're using for our walls to show you what I'm talking about.

Notice that zero is blank (draw nothing). The floor image, on the other hand, is just one tile, repeated, at the moment. That means we want to draw every tile (including zero). So if you look at the createMap() function, our code to load in the floor is longer than our code to load in the walls.

We start with FlxTilemap.arrayToCSV(FLOORS, 15), which converts our big array into a format Flixel likes (CSV). The number at the end tells Flixel how many values are in each row. Next we tell Flixel which image to use (Assets.FLOORS_TILE - I'll explain what that's about in the next step). After defining the size of each block in the image, we have four more values for our floor than for our walls. Since we want all the tiles (including zero) drawn for our floor, we need to pass in these extra values.

The only one that's a little weird is the last: uint.MAX_VALUE. Every tile number (zero through the number of tiles in our image) that is above the number passed at this parameter will be marked for collisions. Everything below this number will ignore collisions by default. So, if you had a wall that the player could walk through, you could put it at the end of your image (a high index) and use this value to have Flixel ignore collisions. Since we never want any collisions to happen with the floor, we use uint.MAX_VALUE because every tile index will be below this value and will therefore not have collisions.

Finally, we have to remember to add our tilemaps to a group or they won't show up on the screen. Before we can run the project, though, we need to load in our artwork.

Step 6: Loading Assets

Since we're using images, we need to let Flash know about them. One of the more straightforward ways to do this is by embedding them in your SWF. Here's how we're doing that in this project (found in tutorial/Assets.as).

I'm giving you all the artwork at once, because it's not all that complicated once you get the hang of it. Let's take a look at the highlighted lines. Here we are loading in two images: one for our walls and one for our floor. If you remember in the last step, we told Flixel to use Assets.WALLS_TILE and Assets.FLOORS_TILE when loading in the map layers. This is where we define those variables.

Notice that we use a path relative to the Assets.as file. You can also embed things like XML files, SWF files, and a ton of other assets. All we need, however, are images. For more information on embedding assets in Flash, check out this article from the Nightspade blog.

Now that we have our images embedded and accessible, we can tell PlayState.as to use our newfangled level.

decalGroup will allow us to add some rugs (purely visual eye candy), while objectGroup will allow us to add some furniture that will get in the player's way. The other variables are the pieces of furniture we will be adding in a moment.

Next, we need to add these objects to the level. We're adding the highlighted line and everything beneath it.

I'm using an extra function, createObjects(), simply to keep things easier to read. The comments explain each individual object, but let me offer a few general observations. First, we always need to remember to call add() for each object or it won't get displayed. In addition, we need to use the right group (mapGroup, floorGroup, decalGroup, objectGroup, etc.) when calling add() or it will mess up our render order and our collision detection.

Also take notice of all the various ways we can decide where to place our objects and decals. We can hard code the values (like we do with the first rug), we can use tileSize to align it with the floor and wall tiles (like we do with the second rug), and we can mix and match to our heart's content. Just know that Flixel won't detect it if we place something off the level or overlapping another object - it assumes we know what we're doing.

Now we need to display our new groups in the right order and handle collisions. Add these functions to the bottom of IndoorHouseLevel.

Since we want our new groups to render on top of the floors and walls, we need to completely re-do the addGroups() function that we had in TopDownLevel. We also need to add collision detection for our furniture in objectGroup. Once again, since we don't call FlxG.collide() for decalGroup, the player won't be stymied by our imposing rugs. Now our room is looking a little less vacant.

Step 8: Creating Our Player

I keep talking about collisions, but it's hard to collide with an immobile red box. Over the next three steps we will add keyboard controls to our red box before finally making it a proper animated sprite. Let's create tutorial/Player.as.

We've added a new constant, RUNSPEED, that determines how quickly our entities move. Then we set maxVelocity and drag (deceleration) in our constructor. After that, we call updateControls() each frame so we can check for keyboard, mouse, or AI (depending on our needs). Finally, we add some helper functions for moving in each direction. Notice that we update facing in each of these. This is a handy way to know which animation to use later down the line.

Now we need to actually use the keyboard inside Player. Add this function after the constructor.

So every frame we check what keys are being pressed. Flixel allows us to test keys in different ways. Here we're using pressed(), which is true for as long as the key is being held down. If we used justPressed(), it would only be true immediately after the player presses the key, even if the key is held down after that. That would be reversed if we used justReleased().

As I state in the comments, I want to handle the case where the user is pressing left and right (for example) at the same time by not moving. Incrementing or decrementing movement.x based on which arrow is pressed allows us to do that because movement.x would be zero if both left and right were being pressed.

If you run the project now, you should be able to move the red box around with the arrow keys and see collisions happen between the box, and the walls or furniture (but not the rugs).

You'll probably notice that the box doesn't go all the way over to the left and right walls. This is a more esoteric aspect of Flixel. Flixel uses rather simple collision detection (but allows us to make it more complicated if we want to). Since the images we're using for all the walls are the same size (16x16), Flixel uses 16x16 as the collision size even though most of the left and right wall images are transparent. Fixing that behavior is beyond the scope of this tutorial, but it can be done.

Step 10: Adding Animations

I promised we wouldn't stick with the red box (endearing though it is), so here we go with an animated sprite. Since we probably want the ability to animate future entities, we will be adding the basic functionality to TopDownEntity instead of Player. Here are the new constructor, createAnimations(), and update() functions for TopDownEntity.

FlxSprite assumes that, if we're animating, we have multiple frames of animation stored in a single image (called a sprite sheet). While our red box doesn't have frames to animate, the artwork we will be using does. If you use artwork that is arranged differently in your own game, you would need to change these frame numbers. Additionally, if you look at the idle animations you'll notice that we need to pass in an array of frame indices even if we only have one.

Here's the ranger sprite sheet we'll be using for our player just so you can see a little more clearly.

Note that we included some blank frames in the sprite sheet. This was mainly for convenience in editing the animations since our bottom four animations (attacking) have one more frame than our top four animations (walking). Also notice that we are using the middle frame from each of the walking animations as our idle animation.

As long as all of our entities in our game use these same frame numbers, we never have to change the animation code. If our artwork used a different number of frames to animate walking and attacking, we would have updated the frames passed into addAnimation() accordingly. Since the animations are in their own function - createAnimations(), you can also override this function to make some entities have different animations than the rest.

We also made another new function to show the right animation: updateAnimations().

This is more laborious than it is complicated. Basically we are calculating how much we're moving vertically and horizontally. Whichever has more movement, we use that direction's animation. This would come into play if you are moving at an angle and suddenly bump into something. Whichever direction you can still move in will determine the animation used.

We have only one more thing to do before we can finally put the red box out of its misery. We need to tell Player to use the ranger sprite sheet.

Once again we are going to our Assets class to pull in the image we want. The comments tell you what's going on, but let me tell you a bit about "flipped" images. Instead of generating different animations when travelling left/right and up/down, Flixel can just flip the "right" animation to make it "left" and flip the "up" animation to make it "down" (or vice versa). Our "up" and "down" animations look very different (and we already have the artwork with all the directions), so we tell Flixel not to bother with flipping the animations.

Now we have a true indoor top-down RPG level!

Step 11: Adding a GUI

As a bonus, let's see how to add GUI elements to the screen. We're going to add a simple set of instructions at the top so users know what to do once they load up this level. Let's add a GUI to IndoorHouseLevel.