I never thought development of web games can be that simple until I started development of my first game with Adobe Flex. I made two games, without any prior experience with Action script, in only ten months: "Fitter" and "RGB".

I found many answers to my questions on different forums and web sites, and it is time to share my knowledge with Flex community.

Saturday, March 13, 2010

Tiled background

In chapter 20 we implemented scrolling background, which was big improvement compared to the previous version, which had solid colour background only. Because the grass image is only 640 pixels high, it soon starts to repeat itself and becomes boring. Of course, bigger image with nicer and more diverse graphics would certainly make the game look much better, but at what cost?

Let's make some calculations. Background scrolls by one pixel per frame, which makes 24 pixels per second or 1440 per minute. Let's say that average time to complete each stage in the game, would be five minutes. In case it is required, that background image does not repeat itself, it must be more than 7200 pixels high. In case JPEG compression is used and quality is not to low, file size of the image would be somewhere between one and two megabytes. For one stage...

If you are developing a web game and you plan to keep the size of the game as small as possible, you should consider implementing tiled background.



Tiles

Tiles are small images which are used to create large images, maps or terrain. Tiles are not easy to make, creation of tile sets requires skill and lots of hard work. Fortunately, there are lots of free tile sets on the internet, that you can use in your own game.

There are different ways to create a background, map, terrain... from tiles. Game developers usually use a special editor to create them. Instead of complete bitmap, only location and index (or name) of each tile is stored into file. File size of even big maps is usually measured in kilobytes.

For this chapter, I made an image that contains 72 tiles. Size of each tile is 48x48 pixels and there are two pixels of empty space between each tiles. There are 12 rows of tiles in the image (600 pixels), each row consist of six tiles (300 pixels). Image Background.jpg can be found inside project package for this chapter under Images folder.


If you already tried to play the game, you most certainly noticed, that now it takes more time before background starts to repeat itself. By using tiles from new Background.jpg, which is a slightly smaller than previous version, background image is now almost three times bigger then before.

Implementation of tiles did not require many changes in the code. On the other hand, creation of tile set and creation of background map was not an easy task.

Level.xml

XML is probably not the best format to the store structure of the background, map or terrain. But XML documents are so well supported in ActionScript, that I just had to use it.

Bellow is the XML document that can be found inside project package for this chapter inside Levels folder under the name Level_1.xml.

<level>
  <row>05-6,01-2,01-3,01-1,01-2,01-3,01-1,01-2,07-1,07-2</Row>
  <row>02-1,02-2,02-3,02-1,02-2,02-3,02-1,02-2,02-3,08-2</Row>
  <row>03-1,03-2,03-3,03-1,03-2,03-3,03-1,03-2,03-3,09-2</Row>
  <row>10-2,10-2,10-3,01-1,01-2,01-3,01-1,10-1,10-2,10-2</Row>
  <row>11-2,11-2,11-3,02-1,02-2,02-3,02-1,11-1,11-2,11-2</Row>
  <row>11-2,11-2,11-3,03-1,03-2,03-3,03-1,11-1,11-2,11-2</Row>
  <row>12-2,12-2,12-3,01-1,01-2,01-3,01-1,12-1,12-2,12-2</Row>
  <row>02-1,02-2,02-3,02-1,02-2,02-3,02-1,02-2,02-3,02-1</Row>
  <row>04-3,03-2,03-3,03-1,03-2,03-3,03-1,03-2,03-3,03-1</Row>
  <row>04-6,01-2,01-3,01-1,01-2,01-3,01-1,01-2,01-3,04-2</Row>
  <row>06-3,06-4,02-3,02-1,02-2,02-3,02-1,02-2,02-3,05-2</Row>
  <row>07-3,07-4,03-3,03-1,03-2,03-3,03-1,03-2,06-1,06-2</Row>
  <row>05-6,01-2,01-3,01-1,01-2,01-3,01-1,01-2,07-1,07-2</Row>
  <row>02-1,02-2,02-3,02-1,02-2,02-3,02-1,02-2,02-3,03-4</Row>
  <row>04-3,03-2,03-3,03-1,03-2,03-3,03-1,03-2,01-4,06-2</Row>
  <row>05-3,01-2,01-3,01-4,01-5,01-6,01-1,01-2,02-4,02-5</Row>
  <row>08-3,02-2,02-3,03-4,03-5,02-6,02-1,02-2,03-4,03-5</Row>
  <row>09-3,03-2,03-3,04-4,04-5,04-6,03-1,03-2,05-4,07-2</Row>
  <row>04-3,01-2,01-3,05-4,05-5,05-6,01-1,01-2,01-3,04-4</Row>
  <row>05-3,02-2,02-3,02-1,02-2,02-3,02-1,02-2,02-3,05-4</Row>
  <row>03-6,03-2,03-3,03-1,03-2,03-3,03-1,03-2,03-3,03-1</Row>
  <row>04-6,01-2,01-3,01-1,01-2,01-3,01-1,01-2,01-3,04-2</Row>
  <row>06-3,06-4,02-3,02-1,02-2,02-3,02-1,02-2,02-3,05-2</Row>
  <row>07-3,05-6,03-3,03-1,03-2,03-3,03-1,03-2,06-1,06-2</Row>
  <row>05-6,01-2,01-3,01-1,01-2,01-3,01-1,01-2,07-1,07-2</Row>
  <row>02-1,02-2,02-3,02-1,02-2,02-3,02-1,02-2,02-3,03-4</Row>
  <row>04-3,03-2,03-3,03-1,03-2,03-3,03-1,03-2,01-4,06-2</Row>
  <row>05-3,01-2,01-3,01-1,01-2,01-3,01-1,01-2,02-4,02-5</Row>
  <row>08-3,06-6,02-3,02-1,02-2,02-3,02-1,02-2,03-4,03-5</Row>
  <row>09-3,03-2,03-3,03-1,03-2,03-3,03-1,03-2,05-4,07-2</Row>
  <row>04-3,01-2,01-3,01-1,01-2,01-3,01-1,01-2,01-3,04-4</Row>
  <row>05-3,02-2,02-3,02-1,02-2,02-3,02-1,02-2,02-3,05-4</Row>
  <row>03-6,03-2,03-3,03-1,03-2,03-3,03-1,03-2,03-3,03-1</Row>
  <row>04-6,01-2,01-3,01-1,01-2,01-3,01-1,01-2,01-3,04-2</Row>
  <row>06-3,06-4,02-3,02-1,02-2,02-3,02-1,02-2,02-3,05-2</Row>
  <row>07-3,07-4,03-3,03-1,03-2,03-3,03-1,03-2,06-1,06-2</Row>
</Level>

Inside this document is the structure of the background for stage one. Because all tiles have size 48x48 pixels, there are exactly ten tiles required in one row. Columns are separated with commas, each tile is described with vertical and horizontal index, according to both values we know exactly which tile has to be displayed.

Elements row in the document are positioned in the same direction as the tiles will be displayed in the background. Element at the end of the document represents the bottom row, that will be visible at the beginning of the game. Top most row element represents the row at the top of the background, which will become visible after approximately one minute of play.

XML object

ActionScript 3.0 includes a group of classes for XML which feature powerful and easy-to-use functionality for working with XML data. Before we can use the Level_1.xml document, we have to embed it.

I put the statement, that embeds the XML document, inside Game.as file, just before the New_Game function.

[Embed(source="Levels\\Level_1.xml", mimeType="application/octet-stream")] private var level_1XML:Class;

This is not much different than the statement that takes care of embedding image files, but this time additional parameter mimeType is required.

Create_Level

This is the new function in the Game.as, that will take care of the creation of every stage. It will be called from New_Game and Restart_Game functions.

private function Create_Level():void
{
  var xml:XML = null;

  switch (level)
  {
  case 1:
    xml = XML(new level_1XML());
    break;
  }

  scrollPos = 0;

  background = new Background(xml);
  snake = new Snake(SCREEN_WIDTH / 2,SCREEN_HEIGHT / 3,20);
  egg = CreateRandomEgg(true);
}

According to the value of level variable, new XML object is made from embedded XML documents. Because at the moment, there is only one stage implemented, switch statement with only one case looks kind of funny. But I hope it will help you understand how the system works: each stage has its own XML document, that contains all the data required to create a stage. XML documents have to be embedded before they can be used. At the beginning of the stage, XML object is made from XML document and data inside the document is used during initialization phase.

The code after the switch statement used to be part of the implementation of New_Game and Restart_Game functions. This code was replaced with single statement that calls Create_Level function. The only difference is the xml parameter in Background constructor.

Class Background ver.3

Class has been changed completely, again. Constructor will scan the row elements in XML document and extract the data from the elements value. This data will be used to display visible tiles during the game.

public class Background
{
  private static const SCREEN_WIDTH:int = 480;
  private static const SCREEN_HEIGHT:int = 480;
  private static const TILE_SIZE:int = 48;
  private static const TILE_DIST:int = 50;

  [Embed(source="../Images/Background.jpg")] private var backgroundImg:Class;

  private var bitmap:BitmapData = null;
  private var rows:Array = new Array();

  public function Background(xml:XML)
  {
    var rowCount:int = 0;
    var rowXML:XML = xml.Row[rowCount];

    while (rowXML != null)
    {
      var cellStr:Array = rowXML.text().split(",");
      var cells:Array = new Array();

      for (var i:int = 0; i < cellStr.length; i++)
      {
        var str:Array = cellStr[i].split("-");

        cells.push(new Point(int(str[1]),int(str[0])));
      }

      rows.unshift(cells);

      rowXML = xml.Row[++rowCount];
    }
  }

  public function draw(screenBuffer:BitmapData,visible:Rectangle):void
  {
    if (bitmap == null)
    {
      bitmap = new BitmapData(300,600,true,0x00000000);
      bitmap.draw(new backgroundImg());
    }

    var startRow:int = (visible.y - visible.height) / TILE_SIZE;
    var endRow :int = visible.y / TILE_SIZE + 1;

    for (var i:int = startRow; i <= endRow; i++)
    {
      var cells:Array = rows[i % rows.length];

      if (cells != null)
        for (var j:int = 0; j <= 9; j++)
        {
          var tile:Point = cells[j];

          screenBuffer.copyPixels(bitmap,
                                  new Rectangle((tile.x - 1) * TILE_DIST,(tile.y - 1) * TILE_DIST,TILE_SIZE,TILE_SIZE),
                                  new Point(j * TILE_SIZE,visible.y - TILE_SIZE - i * TILE_SIZE));
        }
    }
  }
}

At the top of the file there are two new constant definitions: TILE_SIZE holds the value of horizontal and vertical size, TILE_DIST is the value of offset between tiles.

Data from row elements in the XML document are stored into rows Array. As you can see, class constructor now requires XML object. row elements from XML object are accessed inside the while loop until the element with rowCount index exist. Value of each element is split by comma separator, horizontal and vertical index of the tile are stored into Point object. All tiles for one row are stored into Array and inserted into rows Array.

At the beginning of draw function are calculated indexes of first and last visible row. All this rows are visible and tiles in the row have to be drawn. Horizontal and vertical index, that was stored into Point object, is transformed into coordinates of the top-left corner of the tile in the source image and area of 48x48 pixels is copied into screen buffer.

Background creation

If you tried to add new or change existing content of Level_1.xml file, you most probably soon found out, that it is not as simple as you thought it would be. Even small changes are sometimes hard to make. You need to know which tiles you wish to change, you have to count the vertical and horizontal indexes of new tiles and enter them on the right place in XML document. Each time you make a change, you have to recompile and restart the game. And finally you have to play the game until the changes become visible. If you make one mistake, you have to repeat the whole process until you are satisfied.

There is not much you can do to improve the whole process, unless you use an editor. I am sure there are lots of free editors on the internet, if you already found a good one that has all that you need, use it. You will most probably have to make some adjustments in the code, but it will make your life much easier.

There is one thing we can do to improve the process of background creation. We can implement the special mode that will allow us to preview the stage before the game begins, by using mouse wheel. This mode should be available only during development process.

First, new event listener is needed for Canvas object. Assign mouseWheel event listener to mx:Canvas element in SnakesAdventure.mxml file:

mouseWheel="MouseWheelMoved(event)"

Whenever mouse cursor is position inside game frame and mouse wheel is moved, MouseWheelMoved function will be executed. MouseWheelMoved belongs into SnakesAdventure.as file:

private var prevDate:Date = null;

private function MouseWheelMoved(event:MouseEvent):void
{
  if (prevDate == null)
    prevDate = new Date();
  else
    if (new Date().time - prevDate.time < 50)
      return;
    else
      prevDate = new Date();

  switch (state)
  {
  case GAME:
    MouseWheelMoved_Game(event);
    break;
  }

  event.preventDefault();
}

Code at the beginning prevents the function to execute twice for one event. This sometimes happens when SWF file is embedded into HTML page, which is opened with Internet Explorer.

The rest of the code is quite simple: while GAME state is active, function MouseWheelMoved_Game is called. This function is implemented in Game.as file:

private function MouseWheelMoved_Game(event:MouseEvent):void
{
  switch (gameState)
  {
  case GAME_START:
    if (event.delta > 0)
      scrollPos += 48 * 2;
    else
      scrollPos -= 48 * 2;

    if (scrollPos < 0)
      scrollPos = 0;
    break;
  }
}

Mouse wheel event contains property delta. If its value is positive, mouse wheel was moved forward and value of scrollPos variable is increased for 96, which is exactly height of two tiles. When mouse wheel is moved backward, value of delta property is negative and value of scrollPos variable is decreased for the same amount.

Value of scrollPos variable must be reseted before the game begins. Add this statement at the end of ChangeState_Game function:

if (gameState == GAME_STARTED)
  scrollPos = 0;

From now on you can preview the stage before you test it. When game is embedded into web page, scrolling will most probably not work, because web browser scrolls the content of the page when mouse wheel is moved. Buy as I said, this mode should be used only during development process and removed in final version of the game.

No comments:

Post a Comment