The interactive map is the key component of Power Grid. As the initial version I am writing is for the “Atolla Mudulis” expansion, I have to compile a map that deals with multiple “tiles” in variable combinations. Also, the map needs to be movable and zoomable both with the mouse and via a button interface.
I tried two approaches to creating the map, one which involved the TileContainer and another which created a single image.
The TileContainer Approach
The advantage of using a TileContainer is that it deals with all of the mechanics of placing the tiles in the correct alignment (providing you specified the appropriate properties). Each Tile in the container maintains its original properties and can be identified in a MouseEvent. This would have a lot of advantages in a game that uses tiles (such as Carcassonne or even Dominoes) but does not serve the purposes of a game board too well. If no aspect resizing (i.e. zooming in/out) is required then the TileCointer would be satisfactory, but of course my game board does require this functionality.
The Image Approach
After wrestling with the TileContainer for a day, I decided to scrap this approach to the game board implementation in favour of a single image with multiple “hit areas” (i.e. one hit area per city). The advantages this gave me were:
- the
localXandlocalYcoordinates were preserved, even after resizing - problems with the MOUSE_OUT event were resolved when dragging the image around
- more control over where each image tile was placed, i.e. no fighting with the TileContainer styles
I created a class for the game board image called MapTile which extends the Image class.
The next challenge I faced after deciding to drop the TileContainer approach was how to deal with each image as it loaded. Initially I attempted a simple for each loop which added each image to the map, but this didn’t work due to the unpredictable time it takes to load each tile. I could have embedded the images, but I am trying to keep the size of the .swf down so opted to use a COMPLETE event which signalled the need to load the next tile. It works nicely, and in the current version allows you to watch each tile load into the map.
Conclusion
This was ultimately a pretty simple problem. I needed to create an image that could be used by the main map component to zoom in and out of, as well as drag operations. This class takes in the array of tiles to be used, then creates one image at run time.
MapTile Code
All of the images used for the map board can be found here.
The full code for the MapTile class is listed below:
package com.gamecomponents
{
import mx.controls.Image;
import flash.events.Event;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Loader;
import flash.net.URLRequest;
import flash.geom.Rectangle;
import flash.geom.Point;
import flash.display.Sprite;
public class MapTile extends Image
{
private var mapBitmap:Bitmap;
private var mapBitmapData:BitmapData;
private var loader:Loader;
private var row:int;
private var column:int;
private var creationCounter:int;
private var tileChooser:Array;
private var sideLength:int;
// Location of tile images and file extension
private const sourcePath:String = "assets/board/";
private const filenameExt:String = ".png";
// All map tiles must be 500 pixels square
private const tileWidth:int = 500;
public function MapTile(inMapConfig:Array = null):void {
if (inMapConfig == null) {
tileChooser = [
"A7", "B7", "C6",
"D6", "E5", "F5",
"G4", "H4", "I3",
];
} else {
tileChooser = inMapConfig;
}
// Calculate how many tiles per side
sideLength = Math.sqrt(tileChooser.length);
row = 0;
column = 0;
// Tiles are all currently 500 pixels square
var mapSize:int = tileWidth * sideLength;
mapBitmapData = new BitmapData(mapSize, mapSize);
mapBitmap = new Bitmap();
// Create map image
creationCounter = 0;
getNextTile();
}
private function getNextTile():Boolean {
if (tileChooser.length <= 0) {
return false;
}
// Get the next tile in the array
var tileName:String = tileChooser.shift();
// Load new tile
loader = new Loader();
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, picLoaded);
loader.load(new URLRequest(sourcePath + tileName + filenameExt));
return true;
}
private function picLoaded(event:Event):void
{
// Define a BitmapData object with dimensions identical to the sprite
var loadedBitmapData:BitmapData;
loadedBitmapData = new BitmapData(loader.width, loader.height);
// Draw the image into the BitmapData object.
loadedBitmapData.draw(loader);
mapBitmapData.copyPixels(loadedBitmapData,
new Rectangle(0, 0, tileWidth, tileWidth),
new Point(column * tileWidth, row * tileWidth)
);
mapBitmap = new Bitmap(mapBitmapData);
this.source = mapBitmap;
if ((creationCounter+1)%sideLength == 0) {
column = 0;
row++;
} else {
column++;
}
creationCounter++;
if (!getNextTile()) {
// All tiles have been loaded
loader.contentLoaderInfo.removeEventListener(Event.COMPLETE, picLoaded);
}
}
}
}