Time for something light-hearted, I feel. Following on from last year’s Bing Maps tribute to Grand Theft Auto, I thought I’d do another videogame-themed map hack inspired by perhaps my most-loved game of all time – the Legend of Zelda.
Almost the entire year of my 10-year old life was spent playing the Legend of Zelda on the Nintendo Entertainment System. In those days, games rarely had their own in-game maps, and it was long before GameFAQs or Prima strategy guides existed, so the only way to keep track of where you’d been in an adventure game was to create your own maps. My mum would bring me home sheets of graph paper from work, and I would spend hours laboriously drawing the layout of each room in every dungeon, and how each of the screens in the overworld connected to each other. (If you’ve ever tried to do this yourself, you’ll appreciate the ingenuity of the “lost woods”, which are very hard to map on paper!).
As a result, I’ve always personally found the overworld maps of Zelda games to be a thing of beauty. Here’s the world map for the original Legend Of Zelda:
This one’s from Zelda II: The Adventure of Link:
and here’s The Legend of Zelda: A Link to the Past:
The world of Hyrule is undeniably beautiful – its deserts, rivers, and forests becoming ever more intricate in each successive game in the Zelda series. But wouldn’t it be cool to create an 8bit-style tileset of the real world that you navigate around?
Now, I’m no graphic artist and, even if I was, the thought of drawing the 275 billion tiles required to cover the world at zoom level 19 would probably put me off the idea anyway. So, I need some way to automatically determine whether a given area should be represented as a water tile, or a grass tile, say. And I wondered if it was possible to do this based on a simple image analysis of the corresponding aerial tile from Bing Maps.
Converting Bing Maps Aerial Imagery to 8bit tiles
So, here’s the plan: I build a Bing Maps application that uses a custom tile layer. The handler for the tile layer requests aerial imagery from Bing Maps. But, instead of returning the tile image direct to the client, it converts the features on that tile into appropriate 8bit sprites and returns a new tile composed of those sprites to the client instead.
To choose which tile images to use, I’ll divide each 256px x 256 tile retrieved from Bing Maps into smaller 32px x 32px subtiles. I’ll then do a simple RGB analysis to determine the “average” colour of each subtile.
You can obviously tweak these, or add more rules to fit the tileset as you see fit.
Here’s the code to implement these rules in a C# handler:
<%@ WebHandler Language="C#" Class="Handler" %> using System; using System.Web; using System.Drawing; using System.Data.SQLite; using System.IO; using System.Drawing.Imaging; public class Handler : IHttpHandler { public void ProcessRequest(HttpContext context) { string quadKey = context.Request.QueryString["q"]; // Create a random number generator to vary grass tiles Random random = new Random(); // Retrieve the original aerial tile image System.Net.WebRequest request = System.Net.WebRequest.Create("http://ecn.t" + quadKey[quadKey.Length - 1] + ".tiles.virtualearth.net/tiles/a" + quadKey + "?g=774&mkt=en-gb&lbl=l1&stl=h&n=z"); System.Net.WebResponse response = request.GetResponse(); System.IO.Stream responseStream = response.GetResponseStream(); Bitmap mapImage = new Bitmap(responseStream); // Set up a graphics class based on the tile bitmap using (Graphics graphics = System.Drawing.Graphics.FromImage(mapImage)) { int tileWidth = 32, tileHeight=32; int numRows = 8, numCols = 8; // Loop through the aerial tile in 16x16 squares for (int row = 0; row < numRows; row++) { for (int col = 0; col < numCols; col++) { // Get the bitmap data for this square Rectangle tileRect = new Rectangle(col * tileWidth, row * tileHeight, tileWidth, tileHeight); System.Drawing.Imaging.BitmapData bmData = mapImage.LockBits( tileRect, System.Drawing.Imaging.ImageLockMode.ReadOnly, mapImage.PixelFormat); // Get the average R,G,B values of pixels in this square long[] totals = new long[] { 0, 0, 0 }; unsafe { // 24bit image so 3 bytes per pixel (PNG + transparency would be 4) int PixelSize = 3; for (int y = 0; y < tileHeight; y++) { byte* p = (byte*)bmData.Scan0 + (y * bmData.Stride); for (int x = 0; x < tileWidth; x++) { totals[0] += p[x * PixelSize]; // Blue totals[1] += p[x * PixelSize + 1]; // Green totals[2] += p[x * PixelSize + 2]; // Red } } } mapImage.UnlockBits(bmData); // Work out the average RGB colour value int avgB = (int)(totals[0] / (bmData.Width * bmData.Height)); int avgG = (int)(totals[1] / (bmData.Width * bmData.Height)); int avgR = (int)(totals[2] / (bmData.Width * bmData.Height)); // Determine average colour of this tile Color fillColour = Color.FromArgb(avgR, avgG, avgB); // Snow if (fillColour.G >= 225 && fillColour.B >= 225 && fillColour.R >= 225) { graphics.FillRectangle(Brushes.White, tileRect); } // Grass else if (fillColour.G >= fillColour.B && fillColour.G >= fillColour.R) { // Random chance of being a flower tile if (random.Next(0, 50) < 1) { Bitmap grassImage = (Bitmap)Image.FromFile(@"C:\Users\Alastair\Documents\Visual Studio 2008\WebSites\BingMaps\v7\ZeldaMap\flowers.png"); graphics.DrawImage(grassImage, tileRect); } else { Bitmap grassImage = (Bitmap)Image.FromFile(@"C:\Users\Alastair\Documents\Visual Studio 2008\WebSites\BingMaps\v7\ZeldaMap\grass.png"); graphics.DrawImage(grassImage, tileRect); } } // Water else if (fillColour.B >= fillColour.G && fillColour.B >= fillColour.R) { graphics.FillRectangle(new SolidBrush(Color.FromArgb(255,48,89,166)), tileRect); } // Dirt/Sand else if (fillColour.R >= fillColour.G && fillColour.R >= fillColour.B && fillColour.B <= 90) { Bitmap dirtImage = (Bitmap)Image.FromFile(@"C:\Users\Alastair\Documents\Visual Studio 2008\WebSites\BingMaps\v7\ZeldaMap\rock.png"); graphics.DrawImage(dirtImage, tileRect); } // Rock else if (fillColour.R >= fillColour.G && fillColour.R >= fillColour.B && fillColour.B > 90) { Bitmap rockImage = (Bitmap)Image.FromFile(@"C:\Users\Alastair\Documents\Visual Studio 2008\WebSites\BingMaps\v7\ZeldaMap\dirt.png"); graphics.DrawImage(rockImage, tileRect); } // Default - just fill tile in single average colour else { graphics.FillRectangle(new SolidBrush(fillColour), tileRect); } } } } // Send the resulting image back to the client context.Response.ContentType = "image/png"; System.IO.MemoryStream memStream = new System.IO.MemoryStream(); mapImage.Save(memStream, System.Drawing.Imaging.ImageFormat.Png); memStream.WriteTo(context.Response.OutputStream); } public bool IsReusable { get { return false; } } }
(Note that, because I’m accessing aerial tiles directly from the Bing Maps tile servers rather than through the API, this method is unsupported and probably not allowed in a production environment. But then, why on earth would you ever want to do this in a production environment? It’s just for fun!)
Based on the rules above, let’s see how certain tiles are rendered:
![]() 0331 |
![]() 0331 |
![]() 1 |
![]() |
![]() 01 |
![]() 01 |
That’s pretty much the effect I was after, and here’s what it looks like then you create a custom tile layer based on the above handler displayed on a Bing Maps control at zoom level 1. Pretty neat, huh?:
Adding a Player Sprite
To finish this example off, I want to add a player to the world as well. He’ll stay in the middle of the map, and the tiles will scroll under his feet as you press the cursor keys for him to navigate around. I’ll create a few simple frames of animation to show him walking in different directions, as shown in the sprite strip to the left.
The player sprite will appear in a div placed in the centre of the map, and I’ll use javascript to change the background of the div to the appropriate frame depending on whether the player is moving up, right, down, or left. Since there are three frames of animation for each direction, I’ll create a simple frame matrix that will loop through the relevant frames.
var frameMatrix = [1, 2, 0, 4, 5, 3, 7, 8, 6, 10, 11, 9];
In addition to animating the background of the player sprite div, pressing a cursor key will also cause the map to scroll in the appropriate direction. For this, I’ve added a custom Pan() method that extends the Microsoft.Maps.Map prototype.
Here’s the code listing for the HTML page:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0"></script> <script type="text/javascript"> // Define some globals for handling key input var keys = { UP: 38, DOWN: 40, LEFT: 37, RIGHT: 39 }; var keysPressed = []; // These used to track player animation frames var frameMatrix = [1, 2, 0, 4, 5, 3, 7, 8, 6, 10, 11, 9]; var currentFrame = 0; // The map. Obviously. var map = null; // Add a convenient method to pan the map Microsoft.Maps.Map.prototype.Pan = function(x, y) { var pos = this.tryPixelToLocation(new Microsoft.Maps.Point(this.getWidth() / 2 + x, this.getHeight() / 2 - y), Microsoft.Maps.PixelReference.control); this.setView({ center: pos }); }; // Set the frame rate of how often the map moves/player animates var int = window.setInterval("animate()", 150); // This function gets called once per animation loop function animate() { // Only look at the first key pressed switch (keysPressed[0]) { case keys.UP: // If we've reached the end of the "up" animation frames, start again if (currentFrame >= 2) { currentFrame = 0; } // otherwise, go on to the next "up" animation frame else { currentFrame++ }; // Pan the map up map.Pan(0, 10); break; case keys.DOWN: // If we've reached the end of the "down" animation frames, start again if (currentFrame < 3 || currentFrame >= 5) { currentFrame = 3; } // otherwise, go on to the next "down" animation frame else { currentFrame++ }; // Pan the map down map.Pan(0, -10); break; case keys.LEFT: // If we've reached the end of the "left" animation frames, start again if (currentFrame < 6 || currentFrame >= 8) { currentFrame = 6; } // otherwise, go on to the next "left" animation frame else { currentFrame++ }; // Pan the map left map.Pan(-10, 0); break; case keys.RIGHT: // If we've reached the end of the "right" animation frames, start again if (currentFrame < 9 || currentFrame >= 11) { currentFrame = 9; } // otherwise, go on to the next "right" animation frame else { currentFrame++ }; // Pan the map right map.Pan(10, 0); break; } // Update the offset of the background of the player div document.getElementById("player").style.backgroundPosition = "0 " + -currentFrame * 32 + "px"; } // This function keeps track of the key(s) currently being pressed. // Uses an array so that, for example, can use diagonal movement when // two keys are pressed. function handleKeyDown(e) { switch (e.keyCode) { case keys.UP: if (!inArray(keys.UP, keysPressed) && !inArray(keys.DOWN, keysPressed)) { keysPressed.push(keys.UP); } break; case keys.DOWN: if (!inArray(keys.DOWN, keysPressed) && !inArray(keys.UP, keysPressed)) { keysPressed.push(keys.DOWN); } break; case keys.LEFT: if (!inArray(keys.LEFT, keysPressed) && !inArray(keys.RIGHT, keysPressed)) { keysPressed.push(keys.LEFT); } break; case keys.RIGHT: // Right: if (!inArray(keys.RIGHT, keysPressed) && !inArray(keys.LEFT, keysPressed)) { keysPressed.push(keys.RIGHT); } break; } e.handled = true; } // Remove key from the array when they are released function handleKeyUp(e) { keysPressed = removeFromArray(e.keyCode, keysPressed); } // Utility methods function inArray(element, arr) { for (var i = 0; i < arr.length; i++) { if (arr[i] === element) { return true; } } return false; } function removeFromArray(element, arr) { for (var i = 0; i < arr.length; i++) { if (arr[i] == element) arr.splice(i, 1); } return arr; } // Create the map function GetMap() { map = new Microsoft.Maps.Map(document.getElementById("mapDiv"), { credentials: "GETYEROWNBINGMAPSKEY!", center: new Microsoft.Maps.Location(50, 0), mapTypeId: Microsoft.Maps.MapTypeId.mercator, zoom: 5, inertiaIntensity: 0.00001, tileBuffer:3, disableKeyboardInput: true }); // Create a custom tile source that points to the .NET handler var tileSource = new Microsoft.Maps.TileSource({ uriConstructor: location.href.substring(0, location.href.lastIndexOf('/')) + "/" + "handler.ashx?q={quadkey}" }); // Construct the layer using the tile source var tilelayer = new Microsoft.Maps.TileLayer({ mercator: tileSource, opacity: 1 }); // Push the tile layer to the map map.entities.push(tilelayer); // Position the player sprite in the middle of the map document.getElementById("player").style.backgroundImage = "url('player.png')"; document.getElementById("player").style.left = map.getWidth() / 2 + "px"; document.getElementById("player").style.top = map.getHeight() / 2 + "px"; // Attach event handlers window.addEventListener('keydown', handleKeyDown, false); window.addEventListener('keyup', handleKeyUp, false); } </script> </head> <body onload="GetMap();"> <div id='mapDiv' style="position:relative; width:640px; height:480px;"></div> <div id='player' style="position: absolute; z-index:10; width:20px; height:32px; background-repeat:no-repeat;"></div> </body> </html>
Finally, here’s a video of the finished product. Watch me walk from somewhere near Paris to Valencia and then onto Naples, all in glorious dynamically-generated 8-bit graphics!
Great work and lots of fun. Congratulations once again Alastair.
One thing : the code post for the httpHandler is incomplete. There are missing parts between the unsafe code and the foreach (line 50).
I would love running it locally and show the results to NES lovers (with an Alastair credit, no thieving stuff here…)
Well spotted! WordPress has munged my code paste somewhere – I’ll try to correct that now. And you’re welcome to show/share/amend this code to anybody.
Please try again now – code listings hopefully should be fixed.
Thanks Alastair, handler works ok now (I have used WPF Bing Maps control).
Another mashup idea : why not creating a fractal tile layer (ie Sierpiński triangle, Von Koch snowflake) where you can zoom in like a fool ? Lots of fun ahead…
Awesome. Cool application.
Thanks Ricky!
Brilliant stuff. Great idea and event better implementation.
I love these “just for fun” posts 😀
Thankyou Pedro, and I’m glad you like it.
I really enjoy writing these kind of posts “just for fun”, but hopefully this example might also help show someone how to use a tile layer, or how to animate a sprite, or just make them think of a new idea for a mapping application, so it’s also sort of educational!
And if it makes you smile as well, that’s even better 🙂
Excellent, innovative an creative…….like it 🙂
Excellent idea! And love the simple implementation 😉

Oh btw, the original legend of zelda map makes me think about the old arabic/oriental maps which represents the world with local features and in a completely different conception of the world and the representation that was made.. like those:
http://farm4.staticflickr.com/3364/3283867809_cfc500287f_z.jpg?zz=1
I have one in my books that really looks like the first from Zelda.. I will try to catch it in this ‘Atlas des Atlas’.
Thanks for sharing your fun stuff 🙂
Hey Nicolas! Glad you like it 🙂
Yes – I love looking at old maps. I have a copy of “The Map Book” on the shelf right behind me, which you might like yourself (http://www.amazon.com/The-Map-Book-Peter-Barber/dp/0802714749/ref=sr_1_1?ie=UTF8&qid=1331742927&sr=8-1)
lol again you surpass yourself! Want some real work to do 😉
Thanks for the offer Brian, but no – I’ve decided that real work sucks, and I shan’t be doing any more of it…
Fiddling around with pointless stuff like this is more aligned with my skill set, motivation, and resource availability 🙂
Fair enough, can I work for u instead then 🙂
And now Google Maps has 8-bit map tiles.
“View 8-bit landmarks
Be a hero, explore the world
Find hidden monsters
8-bit Quest Maps is our Beta Maps technology and has certain system requirements. Your system may not meet the minumum requirements for 8-bit computations.”
Interesting – thanks for that… I’ll send them an invoice 😉
Pingback: Google’s April Fools’ Day 8-bit Map | Alastair Aitchison