Terrain Tutorial Part 1 – Creating a Scrolling Map

For this tutorial we will be generating a 2D scrolling map. The view is a straight down birds eye view. As shown in Figure 1 below, each terrain tile is represented by a combine upper-left and lower-right polygon. A map can be any width and height of these tiles as shown in Figure 2. Figure 3 and 4 show a section of the map textured, one with a grid overlap toggled on. In Figure 4 you will notice a diagonal “seam”. This is an artifact caused by the way Silverlight deals with anti-aliasing. To avoid the seam, numbers must be integer values with no decimal points. This works fine with straight lines against the X and Y axis, but a diagonal line will have decimal points causing the seam to appear. When we deal with matrix transforms in part 2 of these tutorials we will get rid of the seam by scaling the textures a pixel to overlap with each other, thus hiding the seam.

Here is a demo of what we are creating. Place focus on the control than use the cursor keys to scroll it around.

 image  image

image 

Each tile will be represented by a custom control contained in a class called TerrainTile. Let’s take a look at a few points on this class:

  • Each tile is made up of two polygons.
  • The class constructor takes as parameters the X-position, Y-position, width and height of each tile.
  • Each polygon has one image brush associated with it.

TerrainTile.xaml:

 <UserControl x:Class="ScrollingMap.TerrainTile"
     xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" 
     Width="400" Height="300">
     <Canvas>
         <Polygon x:Name="UpperLeftPoly">
             <Polygon.Fill>
                 <ImageBrush x:Name="UpperImage"></ImageBrush>
             </Polygon.Fill>
         </Polygon>
         <Polygon x:Name="LowerRightPoly">
             <Polygon.Fill>
                 <ImageBrush x:Name="LowerImage"></ImageBrush>
             </Polygon.Fill>
         </Polygon>
     </Canvas>
 </UserControl>

In the code behind, we create the points for each polygon based upon its location. We also have a function to set the image for the tile.

TerrainTile.xaml.cs

 public partial class TerrainTile : UserControl
 {
     public TerrainTile(int xPos, int yPos, int width, int height)
     {
         InitializeComponent();
  
         UpperLeftPoly.Points = new PointCollection();
         UpperLeftPoly.Points.Add(new Point(xPos, yPos));
         UpperLeftPoly.Points.Add(new Point(xPos+width, yPos));
         UpperLeftPoly.Points.Add(new Point(xPos, yPos+height));
  
         LowerRightPoly.Points = new PointCollection();
         LowerRightPoly.Points.Add(new Point(xPos+width, yPos));
         LowerRightPoly.Points.Add(new Point(xPos + width, yPos+height));
         LowerRightPoly.Points.Add(new Point(xPos, yPos + height));
         
     }
  
     public void SetImage(string imgResource)
     {
         Uri uri = new Uri(imgResource, UriKind.Relative);
         ImageSource imgSrc = new System.Windows.Media.Imaging.BitmapImage(uri);
         UpperImage.ImageSource = imgSrc;
  
         imgSrc = new System.Windows.Media.Imaging.BitmapImage(uri);
         LowerImage.ImageSource = imgSrc;
     }
 }

To manage the different layers of the terrain I have created a class called TerrainManager. This class we:

  • We declare a two dimensional array of tiles to represent the ground layer.
  • Add a function called CreateGroundLayer() to create the ground layer.

TerrainManager.cs

 public class TerrainManager
 {
     const int TILE_WIDTH = 90;
  
     Canvas _mapCanvas;
     private TerrainTile[,] _groundLayer;
  
     public TerrainManager(Canvas mapCanvas)
     {
         _mapCanvas = mapCanvas;
     }
  
     public void CreateGroundLayer(int width, int height)
     {
         _groundLayer = new TerrainTile[height, width];
  
         for (int y = 0; y < height; y++)
         {
             for (int x = 0; x < width; x++)
             {
                 _groundLayer[y, x] = new TerrainTile(x * TILE_WIDTH, y * TILE_WIDTH, TILE_WIDTH, TILE_WIDTH);
                 _groundLayer[y, x].SetImage("grass.png");
                 _mapCanvas.Children.Add(_groundLayer[y, x]);
             }
         }
     }
 }

In our Page.xaml we create a ContentControl that will contain our map Canvas. The map Canvas is what we add the tiles to. The ContentControl is what we will scroll.

Page.xaml

 <UserControl x:Class="ScrollingMap.Page"
     xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" 
     Width="400" Height="300">
     <Canvas>
         <ContentControl x:Name="MapContent">
             <Canvas x:Name="Map"></Canvas>
         </ContentControl>
     </Canvas>
 </UserControl>

In the constructor of Page.xaml.cs we add two event handlers:

  • this.Loaded: We create the terrain manager and initialize a terrain layer once the page is fully loaded.
  • this.KeyDown: We monitor for keyboard events to scroll the map around. In this demo to move you can use the arrow keys, numpad keys or Q, W, E, D, C, X, Z, and A to go NW, North, NE, East, SE, South, SW and West respectively.

Page.xaml.cs

 public partial class Page : UserControl
 {
     TerrainManager _terrainMgr;
     double _mapOffsetX = 0;
     double _mapOffsetY = 0;
     int _scrollSpeed = 20;
  
     public Page()
     {
         InitializeComponent();
  
         this.Loaded += new RoutedEventHandler(Page_Loaded);
         this.KeyDown += new KeyEventHandler(Page_KeyDown);
     }
  
     void Page_KeyDown(object sender, KeyEventArgs e)
     {
         switch (e.Key)
         {
             case Key.NumPad8:  //North
             case Key.W:
             case Key.Up:
                 _mapOffsetY += _scrollSpeed;
                 break;
             case Key.NumPad2: // South
             case Key.X:
             case Key.Down:
                 _mapOffsetY -= _scrollSpeed;
                 break;
             case Key.NumPad6: // East
             case Key.D:
             case Key.Right:
                 _mapOffsetX -= _scrollSpeed;
                 break;
             case Key.NumPad4: // West
             case Key.A:
             case Key.Left:
                 _mapOffsetX += _scrollSpeed;
                 break;
             case Key.NumPad7: // NW
             case Key.Q:
                 _mapOffsetX += _scrollSpeed;
                 _mapOffsetY += _scrollSpeed;
                 break;
             case Key.NumPad9: // NE
             case Key.E:
                 _mapOffsetX -= _scrollSpeed;
                 _mapOffsetY += _scrollSpeed;
                 break;
             case Key.NumPad3: // SE
             case Key.C:
                 _mapOffsetX -= _scrollSpeed;
                 _mapOffsetY -= _scrollSpeed;
                 break;
             case Key.NumPad1: // SW
             case Key.Z:
                 _mapOffsetX += _scrollSpeed;
                 _mapOffsetY -= _scrollSpeed;
                 break;
         }
         MapContent.SetValue(Canvas.LeftProperty, _mapOffsetX);
         MapContent.SetValue(Canvas.TopProperty, _mapOffsetY);
     }
  
     void Page_Loaded(object sender, RoutedEventArgs e)
     {
          _terrainMgr = new TerrainManager(Map);
          _terrainMgr.CreateGroundLayer(10, 10);
     }
 }

Thank you,

--Mike Snow

 Subscribe in a reader

Comments