Tile based/Object based Flash Tutorial
Introduction
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
The "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:
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:
- The tiles can have any size, in a gotoAndStop all tiles must be of the same size
- It's very easy to animate objects, like moving platforms
- Relatively easy to add parallax effect on an object per object basis. In a gotoAndStop you would have to make a couple of "layers" and then scroll them with different speed.
- You can place tiles in the same object on top of each other
- Objects can be on top of each other. You can easily have 5 objects overlapping, and it will not be slower than if they were placed edge to edge.
- It's easy to make interactive objects, like swings (see the Flash MX Platform Demo at the end of the level)
What drawbacks are there?
- If you want 100% of the screen covered in tiles at all times, this method could get a bit slower than a gotoAndStop unless you group the tiles very carefully.
- It's a bit more difficult to create a level editor. With a gotoAndStop engine, it's very easy to create an editor to build maps. However, there is a nice way to actually build the levels in Flash using the tile set and then loop through the scene to create the map data.
Labels: flash, flash tutorial
0 Comments:
Post a Comment
Subscribe to Post Comments [Atom]
<< Home