Introduction

This tutorial was originally the result of a question posted on the XNA forums during the pre-XNA 1.0 days. The goal was to utilize the mouse to select a country from a strategy-game map, with the countries being of potentially any shape. The concept behind this tutorial is to create a secondary map, which is never displayed to the player, but which allows us to easily determine which country on the "real" map the mouse cursor is located over.

The Map

I searched on the web for a random map generator to use to get a map to work from (since I can't draw stick figures properly) and found This One. Using this site, I generated the following image:



The Color-Key Concept

As you can see, it would be impossible to tell anything about the area under the mouse based on the colors in this image. Instead, we create a second version of the map image that we never display to the user, but that has each "country" colored as a single, distinct, solid color. When we look up where the mouse is, instead of getting the color under the cursor on the normal map image, we look up the same spot on our "color key" image instead.

I took the image above into Photoshop and, after some selecting and recoloring, came up with this:



So if we look up the location of the mouse cursor on our hidden color-key image, and we get a red pixel, we know we are in "Country 1". The same is true for any of the other colors we define as countries. If we aren't over one of our pre-defined colors, we know we must be between countries.

There are a couple of other benefits here too... If we needed to overlay player information, map resources, text, etc, we don't have to worry about it interfering with our color key lookup because we really don't care what is displayed on the screen.

Creating the Project

I'm going to create a very basic project to demonstrate how to do the lookup here. After we get the basics working, I'll show another technique of overlaying a transparent image to hilight the selected country on the displayed map.

Open up Visual Studio 2010 and create a new Windows Game (XNA 4.0) project. I called mine ColorKeyMap.

We'll need to set up a few things so we can use content manager to load our images. In Solution Explorer, right click on the ColorKeyMapContent project and add a new folder called Textures to it. Copy your map and color key images into the textures folder and add them to the project through solution explorer.

Next we'll need to set the screen size and enable the mouse. We can do this during the game's Initialize method:

protected override void Initialize()
{
    // TODO: Add your initialization logic here
    this.IsMouseVisible = true;
    graphics.PreferredBackBufferHeight = 400;
    graphics.PreferredBackBufferWidth = 640;
    graphics.ApplyChanges();
    base.Initialize();
}

We also need a could of Texture2D objects to hold the images we will be using at runtime. Add these to the Game1 class' declarations area (where the GraphicsDeviceManager is declared):

Texture2D t2dMap, t2dColorKey;

And finally, we'll need to update LoadContent so we use Content Manager to load the textures:

t2dMap = Content.Load<Texture2D>(@"textures\map_display");
t2dColorKey = Content.Load<Texture2D>(@"textures\map_colorkey");

Drawing the Map

Drawing the actual map to the display is very straightforward. We just need to update the default Draw() method to start a SpriteBatch call set and issue a SpriteBatch.Draw() call. Add the following to your Draw method right before the call to base.Initialize():

spriteBatch.Begin();
spriteBatch.Draw(t2dMap,new Rectangle(0,0,t2dMap.Width, t2dMap.Height), Color.White);
spriteBatch.End();

Determining the Color Data

The next step is to figure out how the colors we used on our map are represented in the texture data. To do this, I put this in as my temporary Update method. It shows the coordinates and the color value of the pixel on the colorkey as the window title:

        protected override void Update(GameTime gameTime)
        {
            // Allows the default game to exit on Xbox 360 and Windows
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();
            // TODO: Add your update logic here
            MouseState ms = Mouse.GetState();
            string sColorval = "";
            uint[] myUint = new uint[1];
            if (ms.X >= 0 && ms.X < t2dColorKey.Width && ms.Y >= 0 && ms.Y < t2dColorKey.Height)
            {
                t2dColorKey.GetData(0, new Rectangle(ms.X, ms.Y, 1, 1), myUint, 0, 1);
                sColorval = myUint[0].ToString();
            }
            Window.Title = ms.X.ToString() + "," + ms.Y.ToString() + " - " + sColorval;

            base.Update(gameTime);
        }

This is temporary code, but it does contain the basics of what we are going to be doing, so lets run thru them:

We create a MouseState object called "ms" and get the current mouse state so that we can get the X,Y location of the mouse relative to the upper left corner of our window (0,0).

We also create an array of uints (unsigned integers) since that is the data format the texture data will be stored in inside the Texture2D object. We check to make sure that the mouse is within the texture area (if we aren't drawing at 0,0 we would need to offset that appropriately) and then we get the data from the texture. The GetData command returns raw data from the Texture and, in this usage, takes the following parameters:

  • the part tells GetData that we want the data returned as unsigned integers
  • The 0 indicates that we are reading from Mip Map layer 0. Mip Maps levels are different sized versions of the texture that can be stored within the same texture. They are used in 3D graphics as a replacement for large textures as you get further from the object.
  • The rectangle parameter specifies what region of the texture we want returned. In this case, it is a 1x1 pixel area at the mouse coordinates.
  • The "myUint" is the array we are using to store the returned results. Unlike most functions, GetData doesn't return anything (it's a void) but it updates the array passed to it instead.
  • The next 0 indicates that we want to start filling data at element 0 in our array. Since we are using an array with only 1 value in it, 0 is the only value in our array.
  • Finally, the last parameter (1) indicates how much data we want copied to our array. In this case, 1 data item (a single unsigned integer).
  • We don't actually have to run this to get the values, as we *could* calculate them ahead of time in hex. In hex, the values are AABBGGRR (Alpha Channel, Blue, Green, Red). But since pink (rgb 255,0,255) is interpreted as transparent by the content processor, it comes out as just plain 0, instead of 4294902015 (FFFF00FF).

Anyway, a fully non-transparent red pixel will be FF0000FF in hex, or 4278190335 in decimal. A half-transparent red pixel would be 7F0000FF in hex, or 2130706687 in decimal. Since I used completely non-transparent values to create the color key map above, the colors work out pretty simply.

I ran my project and placed the mouse over each of my "countries" and wrote down the reported color value. I used these to create a set of constants in my code. Right up under my texture and spritebatch declarations, I added:

        const uint color_red = 4278190335;
        const uint color_green = 4278255360;
        const uint color_pink = 0;
        const uint color_cyan = 4294967040;
        const uint color_blue = 4294901760;
        const uint color_yellow = 4278255615;
        const uint color_black = 4278190080;

Now I simply use those constants in a switch statement to determine what country the mouse is above. I replaced my temporary update routine with this one:

        protected override void Update(GameTime gameTime)
        {
            // Allows the default game to exit on Xbox 360 and Windows
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();
            // TODO: Add your update logic here
            MouseState ms = Mouse.GetState();
            uint uCountry=color_black;
            string sCountry = "";
            uint[] myUint = new uint[1];
            if (ms.X >= 0 && ms.X < t2dColorKey.Width && ms.Y >= 0 && ms.Y < t2dColorKey.Height)
            {
                t2dColorKey.GetData(0, new Rectangle(ms.X, ms.Y, 1, 1), myUint, 0, 1);
                uCountry = myUint[0];
            }
            switch (uCountry) 
            {
                case color_red: sCountry = "Reddistan"; break;
                case color_green: sCountry = "Greenburg"; break;
                case color_pink: sCountry = "Pinkshire"; break;
                case color_cyan: sCountry = "Cyanville"; break;
                case color_blue: sCountry = "United Blues"; break;
                case color_yellow: sCountry = "Republic of Yellow"; break;
            }
            Window.Title = sCountry;
            base.Update(gameTime);
        }

And now the window title will display the name of the country the mouse is over, or blank if the mouse cursor is over the water.

There area couple of things to note here. I'm displaying the map image at 0,0. If it was somewhere else, I'd have to compensate for that by offsetting the lookup into the color-key image by the appropriate number of pixels (ie, if the map was displayed at 50,25 instead of 0,0, I would have to subtract 50 from all of the X values above, and 25 from all of the Y values so that 50,25 on the screen is 0,0 in my lookup onto the color key.

Some Extra Fun

With a little more poking around in Photoshop, I was able to produce the following images that have the countries outlined and a little bit of an outer glow set to them:









Save all of these to your content\textures directory and add them to your project with Solution Explorer.

Next, update the game's declarations area to add Texture2Ds to hold the images and a variable to track what country should be hilighted (if any) We'll default it to -1 (for none):

Texture2D[] t2dCountries = new Texture2D[6];
int iCountryToHilight = -1;

And update our LoadContent method to load these new textures:

                t2dCountries[0] = Content.Load<Texture2D>(@"textures\map_country1");
                t2dCountries[1] = Content.Load<Texture2D>(@"textures\map_country2");
                t2dCountries[2] = Content.Load<Texture2D>(@"textures\map_country3");
                t2dCountries[3] = Content.Load<Texture2D>(@"textures\map_country4");
                t2dCountries[4] = Content.Load<Texture2D>(@"textures\map_country5");
                t2dCountries[5] = Content.Load<Texture2D>(@"textures\map_country6");

Next, we need two modifications to our Update method. The first sets the iCountryToHilight variable to -1 at the beginning of every update loop. Next, in the Switch statement where we determine what country is selected we set it to the appropriate country value:

        protected override void Update(GameTime gameTime)
        {
            // Allows the default game to exit on Xbox 360 and Windows
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();
            // TODO: Add your update logic here
            MouseState ms = Mouse.GetState();
            uint uCountry=color_black;
            string sCountry = "";
            iCountryToHilight = -1;
            uint[] myUint = new uint[1];
            if (ms.X >= 0 && ms.X < t2dColorKey.Width && ms.Y >= 0 && ms.Y < t2dColorKey.Height)
            {
                t2dColorKey.GetData
	
	(0, new Rectangle(ms.X, ms.Y, 1, 1), myUint, 0, 1);
                uCountry = myUint[0];
            }
            switch (uCountry) 
            {
                case color_red: sCountry = "Reddistan"; iCountryToHilight = 0; break;
                case color_green: sCountry = "Greenburg"; iCountryToHilight = 1; break;
                case color_pink: sCountry = "Pinkshire"; iCountryToHilight = 3; break;
                case color_cyan: sCountry = "Cyanville"; iCountryToHilight = 4; break;
                case color_blue: sCountry = "United Blues"; iCountryToHilight = 2; break;
                case color_yellow: sCountry = "Republic of Yellow"; iCountryToHilight = 5; break;
            }
            Window.Title = sCountry;
            base.Update(gameTime);
        }

And finally, we add a if clause to our Draw routine. If iCountryToHilight isn't -1 then we draw the appropriate overlay texture. Add this just prior to SpriteBatch.End():

if (iCountryToHilight != -1)
{
    spriteBatch.Draw(
        t2dCountries[iCountryToHilight], 
        new Rectangle(0, 0, t2dMap.Width, t2dMap.Height), 
        Color.White);
}

That's it! A pretty simple but effective means to determine exactly what the user is mousing over in a situation like this. It isn't limited to just maps though, or even 2D games. If you have an interface that you overlay onto your game with buttons and widgets the user can click on, you can make a color-keyed image and use the technique above to determine what "controls" the player has clicked on instead of checking dozens of little rectangles in code. It also makes it very easy to make non-rectangle shaped controls, since the color pattern on the keyed image is what you are using to determine what was clicked on.




































 

 
 
Site Contents Copyright 2006 Full Revolution, Inc. All rights reserved.
This site is in no way affiliated with Microsoft or any other company.
All logos and trademarks are copyright their respective companies.
RSS FEED