Tile based/Object based Flash Tutorial

[Contents]

Part 1 - Scrolling

Introduction

This tutorial will explain one way to create scrolling games using Flash MX or later. Scrolling is a very CPU intensive operation in Flash, which requires some tricks to make it smooth. Many games involve scrolling, either in one or two dimensions. This tutorial describes scrolling in two dimensions, that is the screen can be scrolled both horizontally and vertically. If you only want to have vertical scrolling for example (like in many top view space shooters), it's just to remove the code which deals with the horizontal scrolling. It is recommended that you are at least vaguely familiar with tile based scrolling engines. The scrolling technique in this tutorial uses "super tiles" (thanks Squize for putting a name on it :-). Super tiles are several tiles grouped together and we only deal with them as a group, and not the individual tiles themselves. This speeds up the process greatly. For some good tutorials on more traditional tile based approaches, check out OutsideOfSociety's tutorials or Tonypa's tile based tutorial.

Creating objects

This scrolling technique is based on the idea that the game world consists of graphical objects (movie clips) that can be created/attached in some way at run-time. A container movie clip (From here on in, "screen") holds all the graphical objects. When a graphical object becomes visible due to the player's movement, it is created and placed once in the container movie clip, and scrolling is performed by moving the entire screen to create the sense of movement. When an object is no longer visible its movie clip is removed completely. This means that only visible objects exist in graphical form. That way you can have thousands of objects in the game without noticeable slowdown. The only thing that is affecting performance is basically the number of visible objects at the same time and the dimensions of those objects.

The simplest way in which these objects can be created is to attach a single movie clip from the library. Another way would be to create a new movie clip by using createEmptyMovieClip() and then use the drawing API to create the graphics. The third way, and the most powerful in my opinion, is to create a new movie clip, then attach several movie clips (tiles) in that empty movie clip to create the final object (this is the technique used in the Flash MX Platform Engine Demo). The tiles can have any dimension and be placed in any formation within that object movie clip, including overlapping. If you run the Flash MX Platform Engine Demo and press "b" when the screen has faded in you can see green borders around the objects and view the object numbers.

Divide and conquer

World is divided into areasThe "world" is divided into areas, where one area equals the screen size. So, if the screen size is 240x160 pixels, then area (0, 0) equals the pixels:

x = 0 to 239
y = 0 to 159

Area (1, 0) equals:

x = 240 to 479
y = 0 to 159

For every object, we pre-calculate which areas it occupies (every area has an array associated with it containing the objects occupying it). This is just done once, and it allows us to know the objects which appear in an area. The objects themselves can be placed with pixel precision in the world. This is an advantage compared to a gotoAndStop engine which has a grid of movie clips and the world is created by changing the frames of each individual movie clip. You can also have the objects overlap each other without slowdown. To perform this using a gotoAndStop engine you would have to implement "layers", which has a large impact on the overall performance.

Visibility tests

So, how do we know which tiles are visible on screen? Well, the solution consists of 2 parts:

Part 1: Since the areas have the same size as the screen, we know that the maximum number of areas visible at the same time is 4. The other possibilities are 1 visible area (if top left corner is located at coordinate (0, 0) for instance) or 2 visible areas (top left corner at say (10, 0)).

Now, one visible area is only occurring if the screen is located at coordinate (n*240, m*160) where n and m are integers. This basically never happens. Of all the possible screen positions, it only happens in

1/(240*160) = 0.000026%

of the cases. The chance that 2 areas are visible are

(240+159)/(240*160) = 0.01%

of the cases. So, instead of checking how many visible areas there are, we just assume 4 all the time because that is almost always going to be correct.

We can easily find out which area the top left corner of the screen is in. If we include the area to the right, the one below and the area one step to the right and down we have included all areas which may be visible. We create a list of all the objects appearing in these 4 areas. This whole operation is a bit CPU intensive, but we don't need to do it on every frame. We only need to do it every time the top left corner changes area.

Part 2: Just because an object appears in an area which is (partially) visible doesn't mean that the object is actually visible on the screen. We need to loop through this list of objects and check if the object's bounding box is on the screen. If so, we flag this object as visible in an array, create the object movie clip and then place it on the object's coordinate in the container movie clip. We never move an object movie clip after that (unless it's animated, but that's another story). Instead, scrolling is performed by moving the screen.

Remove movie clips no longer visible

We somehow need to remove object movie clips which are no longer on screen. We can easily do that by looping through the movie clips in the container using for.. in and for every found movie clip we check if the flag in the visible array is true. If not, this movie clip is not on screen and can therefore be removed.

In the demo for this tutorial I've added a break which stops looping after we have removed one old movie clip. The reason is that when there are movie clips to remove, it's often only one or two. By breaking the loop and thus only removing max one each frame we speed up the looping (it will only speed up when there are old movie clips in the container). And if there are more then one old movie clip to be removed that frame the remaining movie clips will be removed the next frames instead. Breaking the loop like this does not make a huge difference, and you can skip this if you want. The idea is to remove movie clips from the container which are no longer visible to speed up the scrolling, but this check is a bit costly. Another approach would be to only do this check say every 2:nd or 3:rd frame.

Demo file

This is a small demo with everything mentioned above implemented. There are a couple of different sized tiles, ranging from a 16x16 bitmap to a 64x64 bitmap. There are four object types, and they are just generated at random positions instead of being loading from a file, which would be the method used in a real game. You can easily add more tiles to the library and experiment with the demo. The grey grid marks the different areas, and the colour borders show which tiles are grouped into an object.

Download commented .fla: scrollingDemo_v1.2.zip (Zipped Flash MX FLA, 12 Kb)

Implementation

In this section I will explain the important parts of the code in the demo file more in detail.

oX = []; //- x coord for the top left corner of the object
oY = []; //- y coord for the top left corner of the object
oTiles = []; //- array of the form [tileNum 1, x pos 1, y pos 1, tileNum 2 etc...)
oW = []; //- the width of the object
oH = []; //- the height of the object

The arrays above store the map data. Each element holds information for a particular object. oX and oY are the object position coordinates. The hotspot for the position coordinates is the top left corner of the object movie clip.

The array oTiles stores an array for each object on the form: [t1, x1, y1, t2, x2, y2, ... tn, xn, yn], where t1 is the first tile type, x1, y1 are it's position inside the object movie clip. Then the same again for second tile etc. The first tile defined in the array will be on top of all the others in that object if they are placed on top of each other. The tile movie clips have the hotspot in the bottom right corner, which means that the entered coordinates in the oTiles array will have the tile's bottom right corner placed at that coordinate. The reason is because of the shifting bitmap bug, and by placing the tile's hotspot in the bottom right corner it is avoided (you can read more about this solution here). The attached tiles must not be on negative coordinates, otherwise they are clipped when scrolling. So, if a tile is 16x32, it can not have a lower x-coordinate than 16 and a lower y-coordinate than 32 (placing that tile on coordinate (16, 32) results in top left corner being on (0, 0), which is fine).

oW and oH are the object width and height. This value can be calculated automatically, but in the demo it's entered manually.

The reason the data structure consists of 1-dimensional arrays as much as possible is mainly because of speed. They are not meant to be edited manually. Just as an example, here's how you create the object shown to the right: Example of an object built using 4 tiles

oX.push(100);
oY.push(50);
oTiles.push([1, 16, 16, 1, 32, 16, 1, 32, 32, 2, 64, 32]);
oW.push(64);
oH.push(32);
It's placed on coordinate (100, 50). Tile type 1 is the 16x16 tile and 2 is the 32x32 tile. The bounding box is drawn as a green rectangle. You can try changing oW to 20 and then scroll to the right until the object scrolls out if the screen. Notice that the object disappears too early.

Once the objects exist, we can add them to the areas they appear in. We do this calculation once in the following function:

function registerObjectsToAreas() {
        a = []; //- holds all the lists of objects in all the area squares (where objects exist)

        var mMax = Math.max; //- use local variable for speed

        for (var n=0;n<noo;n++) {
                var xMin = mMax(int(oX[n]/sW), 0);
                var yMin = mMax(int(oY[n]/sH), 0);
                var xMax = mMax(int(oX_oW[n]/sW), 0);
                var yMax = mMax(int(oY_oH[n]/sH), 0);

                for (var x=xMin;x<=xMax;x++) {
                        for (var y=yMin;y<=yMax;y++) {
                                if (a[x] == undefined) {
                                        a[x] = [];
                                }
                                if (a[x][y] == undefined) {
                                        a[x][y] = [];
                                }
                                a[x][y][n] = true;
                        }
                }
        }
}
We loop through all the objects (noo = number of objects) and find the top left area coordinates (xMin, yMin) it appears in and the bottom right area coordinates (xMax, yMax) the object appears in (it's often the same area).

For every area the object appears in, we set the element corresponding to the object number to true in the area's array. If for example object 3 appears in area 1,0 then a[1][0][3] = true, otherwise a[1][0][3] = undefined. When we later loop through this array using for.. in we will only get the elements appearing in the area. What value you give to the element is not important, only that a value actually exists for it. It is equally important that the elements representing objects which are not in the area are left undefined. If those elements are defined (even if the values are 0) it will still be interpreted as if those objects are in the area.

If the object dimensions are equal or smaller than the screen size the object can be in no more than 4 areas at the same time. I would not recommend creating objects larger than the screen size since that would make the engine slow down when large objects like that appear. But the engine can also slow down if you make many small objects (objects which only contain one small tile). Let's say you plan to have a floor which is built by using 20 16x16 tiles placed edge to edge. It's a bad idea to make each tile an object, since those results in 20 objects which needs to be iterated through. And making it one object containing 20 tiles would probably be a bit much, and you could see a noticeable delay when big objects like that is being created. I would recommend splitting up the floor into at least 2 objects, each built up by 10 tiles.

The following code snippets are performed on every frame. The first thing to do is to create some local variables:

var sX = int(xPos); //- screen left edge x coord (use local variables for speed)
var sY = int(yPos); //- screen top edge y coord (use local variables for speed)
var ax = int(sX/sW); //- x coord of the area in which coord sX, sY is in
var ay = int(sY/sH); //- y coord of the area in which coord sX, sY is in
(sX, sY) is the coordinate of the top left corner of the screen. sW, and sH are the screen width and screen height. We then calculate in which area the top left corner is in. Once we know the area, we compare it to the area we were in last time, if it's the same area, we delete old movie clips. If it's different, we re-create the list of objects to check.
if (ax == oldax && ay == olday) { //- if no new area, we use the time to delete old clips no longer visible
        for (var n in scr) { //- Remove object MC:s not visible on screen
                if (!oV[n]) {
                        removeMovieClip(scr[n]);
                        nOfVisObj--; //- holds number of visible objects, just for DEMO/DEBUG purposes
                        break; //- if we removed a mc, break the search. We only delete one old mc per frame
                }
        }
}
scr is the movie clip containing all the object movie clips. oV is an array which contains elements corresponding to the visible objects. So, if we encounter a movie clip number which is not an element in the oV, that movie clip is not visible and can be removed. This code does not have to be performed once every frame, but I would recommend doing it on every frame or every other. If you only do it say every 10th frame you may see a small lag when it is performed, since there are more old movie clips. And we want to avoid having more movie clips at the same time than absolutely necessary since that slows down the engine a lot. In our case we delete old movie clips on all frames where we don't change area. When we do change area, we have some area calculations to do, and therefore skip this loop. That way we smooth out calculation "peaks".

If the area did change, we re-create the list of objects to check:

else { //- if we have entered a new area, calculate list of objects to check
        oldax = ax; //- set this new area to the current area
        olday = ay; //- set this new area to the current area

        //- create new list of objects which MAY be visible
        ar = []; //- clear the list
        for (var n in a[ax][ay]) { //- the area in which top left corner is in
                ar[n] = true; //- set the object to visible if it was in the area
        }
        for (var n in a[ax+1][ay]) { //- the area in which top right corner is in
                ar[n] = true; //- set the object to visible if it was in the area
        }
        for (var n in a[ax][ay+1]) { //- the area in which bottom left corner is in
                ar[n] = true; //- set the object to visible if it was in the area
        }
        for (var n in a[ax+1][ay+1]) { //- the area in which bottom right corner is in
                ar[n] = true; //- set the object to visible if it was in the area
        }
}
The array ar will now contain elements corresponding to the objects occupying the 4 areas.

The following two lines are the scrolling of the screen. (sOffx, sOffy) is the coordinate (in the demo it's (40, 40)) the top left corner of the screen should be placed on in the movie:

scr._x = sOffx - sX; //- scroll screen
scr._y = sOffy - sY; //- scroll screen
And then we loop through all the object numbers in the array ar and check that the bounding box is inside the screen. If so, we set it as visible in the array oV and then create the object movie clip if it's not already created:
var x1 = oX; //- object left edge x coord (use local variables for speed)
var y1 = oY; //- object top edge y coord (use local variables for speed)
var x2 = oX_oW; //- object right edge x coord (use local variables for speed)
var y2 = oY_oH; //- object bottom edge y coord (use local variables for speed)
var sX_sW = sX + sW; //- screen right edge x coord
var sY_sH = sY + sH; //- screen bottom edge y coord

oV = []; //- Reset array which stores if an object is visible or not

for (var n in ar) { //- for every object in the visible areas...
        if (x2[n] > sX) { //...check if it's on screen in the x-dimension...
                if (x1[n] < sX_sW) { // (faster to write each test as a separate if-statemen)
                        if (y2[n] > sY) { //...and that it's on screen in the y-dimension
                                if (y1[n] < sY_sH) {
                                        oV[n] = true; //- set current object as VISIBLE
                                        if (!scr[n]) { //- if the object movie clip doesn't exist...
                                                crObjMC(n); //- ...create and place it in the scr movie clip
                                        }
                                }
                        }
                }
        }
}
To create an object movie clip, we call the following function:
function crObjMC(n) {
        var c = scr.createEmptyMovieClip(n, n); //- create empty object MC
        c._x = oX[n]; //- place object MC in x
        c._y = oY[n]; //- place object MC in y

        var t = oTiles[n]; //- use local variable for speed
        var i = t.length/3; //- calculate number of tiles in the object

        while(i--) { //- loop through and create all tiles from the end to the first tile
                tellTarget(c.attachMovie(t[i*3], i, i)) { //- use tellTarget for speed
                        _x = t[i*3+1]; //- place tile within object MC
                        _y = t[i*3+2]; //- place tile within object MC
                }
        }
}
We send in the object number as an argument. A movie clip instance will be created with the number as instance name and at the same depth as the number. This means that objects defined before an object will be placed under it if they are overlapping. We then place the object movie clip. Then we loop through the tiles array and create the tiles and place them. We start with the last tile in the array and work out way to the beginning.

Conclusions

This is just the very basic idea, but it's general enough to work as a platform for a wide range of games. Depending on the requirements you can add all sorts of things. Why is this method fast? The reason is that we don't have any nested for loops. There are actually only 2 for.. in loops which are always performed each frame. When we enter a new area then we have 4 additional for.. in loops, but that happens far from every frame (unless the scrolling speed is very high). A simple data structure and pre-calculations also help, and by using local variables as much as possible. And I'm sure there is still room for improvement...

This method is not necessarily faster than a gotoAndStop engine, but it has many advantages in my opinion:

What drawbacks are there?