# Visual Basic > Games and Graphics Programming > Game Demos >  [VB.Net] 2d Map Tile-Based Engine

## dday9

*Features*
Allows you to render images to simulate a map in a game.

*Drawbacks*
The LoadMap sub can take a while depending on how large your map file is.

*DLL*
MapEngine.zip

*Notes*
Thanks to Jacob Roman for showing a technique that allows the tiles to be rendered quickly.

*Source Code*
See Post #8. The amount of text was too long to fit in just this post, so I posted the source in #8 and kept the DLL in this post.

----------


## Siddharth Rout

Thread moved to  `Game Demos` as per OP's request.

----------


## dday9

Thank you sid!

----------


## mholmes_3038

Looks great man, do you have a video tut explaining how to build it? Video tuts on these topics are so hard to find and really good stuff for learning. Please share if you have the time. Thank you.

----------


## Jacob Roman

Im not sure he moved the map using my method. But my method has 0 slowdown regardless of the map size, even if the map is 10000x10000:


vb.net Code:
Private Sub Draw_Map()         Dim imageFile As Image        Dim g As Graphics        Dim newGraphics As Graphics        Dim Coordinates As Vector 'Tile Coordinates of the position on map such as 5,10 or 24, 8        Dim X1 As Integer, Y1 As Integer, X2 As Integer, Y2 As Integer        Dim X As Integer, Y As Integer        Dim R As RECT         R.Left = 0        R.Top = 0        R.Right = Me.Width        R.Bottom = Me.Height         Coordinates.X = Int(-(Map.Position.X) / TILE_WIDTH)        Coordinates.Y = Int(-(Map.Position.Y) / TILE_HEIGHT) 'converts your position into tile coordinates         'This here allows the world to be as gigantic as you want with zero slow down like 5000x5000 for example. Only draws what is on screen.         X1 = Coordinates.X        Y1 = Coordinates.Y        X2 = Coordinates.X + Map.Screen_Tile_Width        Y2 = Coordinates.Y + Map.Screen_Tile_Height         If X2 <= 0 Then X2 = 0        If Y2 <= 0 Then Y2 = 0        If Y2 >= Map.Height - 1 Then Y2 = Map.Height - 1        If X2 >= Map.Width - 1 Then X2 = Map.Width - 1        If X1 <= 0 Then X1 = 0        If Y1 <= 0 Then Y1 = 0        If X1 >= X2 Then X1 = X2        If Y1 >= Y2 Then Y1 = Y2         For Y = Y1 To Y2            For X = X1 To X2                If Running = True Then                    If Not ((Map.Position.X + ((TILE_WIDTH * X) + TILE_WIDTH) < R.Left) Or (Map.Position.X + ((TILE_WIDTH * X)) > R.Right) Or (Map.Position.Y + ((TILE_HEIGHT * Y) + TILE_HEIGHT) < R.Top) Or (Map.Position.Y + ((TILE_HEIGHT * Y)) > R.Bottom)) Then                        imageFile = Image.FromFile(Map.Texture_List(Map.Tile(X, Y)))                        g = Me.CreateGraphics()                        newGraphics = Graphics.FromImage(imageFile)                        g.DrawImage(imageFile, New RectangleF(Map.Position.X + (TILE_WIDTH * X), Map.Position.Y + (TILE_HEIGHT * Y), TILE_WIDTH + 3, TILE_HEIGHT + 3))                        newGraphics.Dispose()                        g.Dispose()                    End If                End If            Next X        Next Y     End Sub

Now do you see this area of the code?



```
        Coordinates.X = Int(-(Map.Position.X) / TILE_WIDTH)
        Coordinates.Y = Int(-(Map.Position.Y) / TILE_HEIGHT) 'converts your position into tile coordinates

        X1 = Coordinates.X
        Y1 = Coordinates.Y
        X2 = Coordinates.X + Map.Screen_Tile_Width
        Y2 = Coordinates.Y + Map.Screen_Tile_Height
```

This guarantees the For loops will never be bigger than the number of tiles shown on screen. Screen tile width is actually the number of tiles you are allowed to see on screen horizontally. Screen tile height is the number of tiles you are allowed to see on screen vertically. So you are only able to see 32 x 16 tiles on screen at once.

As you move, all the X1 Y1 X2 Y2 values keep within the range of 32 x 16. So its like you are moving a rectangular shaped magnifying glass within your massively sized world map. For example. Lets say your Tile Coordinates are 10, 10. So your values are now this:



```
X1 = 10
Y1 = 10
X2 = 42
Y2 = 26
```

If you move to the right about 5 tiles and you stop, you are now here:



```
X1 = 15
Y1 = 10
X2 = 47
Y2 = 26
```

So if you for loop X1 to X2, Y1 to Y2, you still are only going through only 32 x 16 tiles. Your loop will never get bigger, even if the values are within the 1000s range. This is why this chunk of code is important:



```
        X1 = Coordinates.X
        Y1 = Coordinates.Y
        X2 = Coordinates.X + Map.Screen_Tile_Width
        Y2 = Coordinates.Y + Map.Screen_Tile_Height
```

Dday I don't think used this method at all according to his code. But he should. As it is fast, efficient, and he can create worlds the size of World of Warcraft...in 2D if course. I kinda wish programmers in the past found this method out. Lord knows the RPGs in the past would be gigantic if this were so. But I think its a method I personally invented  :Stick Out Tongue: 

Oh and one last thing, if you are confused about what to do with Map.Position, this can be changed to Player.Position because your player is moving through the map while the player appears to stay in the middle of the screen. And to move the map it was simply this:


vb.net Code:
Private Sub frmMain_KeyDown(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles Me.KeyDown         If e.KeyCode = Keys.Left Then Map.Position.X += 10        If e.KeyCode = Keys.Right Then Map.Position.X -= 10        If e.KeyCode = Keys.Up Then Map.Position.Y += 10        If e.KeyCode = Keys.Down Then Map.Position.Y -= 10     End Sub

----------


## dday9

> Im not sure he moved the map using my method. But my method has 0 slowdown regardless of the map size, even if the map is 10000x10000:
> This guarantees the For loops will never be bigger than the number of tiles shown on screen.


That's what I do in the move_map sub. The loop looks like this:


```
        For col As Integer = x - 4 To x + 5
            For row As Integer = y - 4 To y + 5
```

Where x and y are the current position's x/y coordinates. In the post I made, if x = 10 and y = 15 the loop will run:


```
 For col As Integer = 6 To 15
   For row As Integer = 11 To 20
```

The only difference is how I set the map size. I set it x - 4 to x + 5 which is 10 and then y - 4 to y + 5 which is 10. So the loop will always only loop a 10x10 grid.

So I still use your idea of only rendering on screen what's being viewed at that moment, only a bit differently. I still get 0 slow down even at 1000x1000 map sizes.

The only slow down that I have is how I load the map initially in the load_map sub, in which case using splash screen saying the game is loading and using a backgroundworker to actually load the map would be a bit more user friendly.

----------


## dday9

I've updated the code. Much more efficient now and also allows for comments and blank spaces in the map file. In the map file, if you wish to have a comment or a blank space then you must do so before the mapping begins. The comments are designated by the pound sign(#) at the beginning of the line.

----------


## dday9

Update!
*Source Code*


```
Public Class Tile

    ''' <summary>
    ''' Gets or sets the unique value of the tile.
    ''' </summary>
    ''' <value>System.Int32</value>
    ''' <returns>System.Int32</returns>
    ''' <remarks>This property is NOT required for the class to work properly.</remarks>
    <System.ComponentModel.Description("Gets or sets the unique value of the tile.")> _
    Public Property ID As System.Int32

    ''' <summary>
    ''' Gets or sets the path to the image the tile represents.
    ''' </summary>
    ''' <value>System.String</value>
    ''' <returns>System.String</returns>
    ''' <remarks>Image location can only end with a valid image extension.</remarks>
    <System.ComponentModel.Description("Gets or sets the path to the image the tile represents.")> _
    Public Property ImageLocation As System.String

    ''' <summary>
    ''' Gets or sets the depth level the tiles sits from lowest to highest.
    ''' </summary>
    ''' <value>System.Int32</value>
    ''' <returns>System.Int32</returns>
    ''' <remarks>The lowest level may have the least visible area because other tiles sitting on top of it may obstruct the view of the image.</remarks>
    <System.ComponentModel.Description("Gets or sets the depth level the tiles sits from lowest to highest.")> _
    Public Property Level As System.Int32

    ''' <summary>
    ''' Gets or sets if the user can pass through the tile.
    ''' </summary>
    ''' <value>System.Boolean</value>
    ''' <returns>System.Boolean</returns>
    ''' <remarks>If true, then the user cannot pass through the tile.</remarks>
    <System.ComponentModel.Description("Gets or sets the user can pass through the tile.")> _
    Public Property Obstruction As System.Boolean

    ''' <summary>
    ''' Gets or sets the position of the tile in relation to it's map.
    ''' </summary>
    ''' <value>System.Drawing.Point</value>
    ''' <returns>System.Drawing.Point</returns>
    <System.ComponentModel.Description("Gets or sets the position of the tile in relation to it's map.")> _
    Public Property RelativeLocation As System.Drawing.Point

    ''' <summary>
    ''' Saves the tile into an XML file.
    ''' </summary>
    ''' <param name="folder">Defines the folder in which the tile will be saved to</param>
    ''' <remarks>Uses XML serialization to serialize each individual tile</remarks>
    Protected Friend Sub SaveTile(ByVal folder As System.String)

        Using writer As System.IO.StreamWriter = New System.IO.StreamWriter(System.IO.Path.Combine(folder, Me.RelativeLocation.ToString & ".xml"))
            Dim x As System.Xml.Serialization.XmlSerializer = New System.Xml.Serialization.XmlSerializer(Me.GetType)
            x.Serialize(writer, Me)
        End Using

    End Sub

End Class

Public Class Map : Inherits System.Windows.Forms.Control

    ''' <summary>
    ''' Gets or sets the center tile of the map.
    ''' </summary>
    ''' <remarks>If map has an even height or width then the X/Y position will round down to determine the center.</remarks>
    Private cTile As Tile
    <System.ComponentModel.Description("Gets or sets the center tile of the map.")> _
    Public ReadOnly Property CenterTile() As Tile
        Get
            Return cTile
        End Get
    End Property

    ''' <summary>
    ''' Gets or sets the RelativeLocation of the tile in the upper-left hand corner of the map.
    ''' </summary>
    Private cPosition As System.Drawing.Point
    <System.ComponentModel.Description("Gets or sets the RelativeLocation of the tile in the upper-left hand corner of the map.")> _
    Public Property CurrentPosition() As System.Drawing.Point
        Get
            Return cPosition
        End Get
        Set(ByVal value As System.Drawing.Point)
            If cPosition <> value Then
                cPosition = value
                RaiseEvent CurrentPositionChanged(Me, System.EventArgs.Empty)
            End If
        End Set
    End Property

    ''' <summary>
    ''' Gets or sets the tiles that make up the content the map.
    ''' </summary>
    Private mTiles As Tile(,)
    <System.ComponentModel.Description("Gets or sets the tiles that make up the content the map.")> _
    Public Property Tiles() As Tile(,)
        Get
            Return mTiles
        End Get
        Set(ByVal value As Tile(,))
            If mTiles IsNot value Then
                mTiles = value
                RaiseEvent TilesChanged(Me, System.EventArgs.Empty)
            End If
        End Set
    End Property

    ''' <summary>
    ''' Gets or sets the size of all the tiles that make up the map.
    ''' </summary>
    ''' <remarks>The default size is 32x32.</remarks>
    Private tSize As System.Drawing.Size
    <System.ComponentModel.Description("Gets or sets the size of all the tiles that make up the map.")> _
    Public Property TileSize() As System.Drawing.Size
        Get
            Return tSize
        End Get
        Set(ByVal value As System.Drawing.Size)
            If tSize <> value Then
                tSize = value
                RaiseEvent TileSizeChanged(Me, System.EventArgs.Empty)
            End If
        End Set
    End Property

    ''' <summary>
    ''' Gets or sets the amount tiles by width and height that make up the map.
    ''' </summary>
    ''' <remarks>The default size is 10x10.</remarks>
    Private vArea As System.Drawing.Size
    <System.ComponentModel.Description("Gets or sets the amount tiles by width and height that make up the map.")> _
    Public Property VisibleArea() As System.Drawing.Size
        Get
            Return vArea
        End Get
        Set(ByVal value As System.Drawing.Size)
            If vArea <> value Then
                vArea = value
                RaiseEvent VisibleAreaChanged(Me, System.EventArgs.Empty)
            End If
        End Set
    End Property

    ''' <summary>
    ''' Occurs when the CurrentPosition property changes
    ''' </summary>
    ''' <param name="sender">The Map that changes the CurrentPosition property</param>
    ''' <param name="e">Empty</param>
    ''' <remarks></remarks>
    Public Event CurrentPositionChanged(ByVal sender As System.Object, ByVal e As System.EventArgs)

    ''' <summary>
    ''' Occurs after the Load method finishes
    ''' </summary>
    ''' <param name="sender">The Map that gets loaded</param>
    ''' <param name="e">Empty</param>
    Public Event Loaded(ByVal sender As System.Object, ByVal e As System.EventArgs)

    ''' <summary>
    ''' Occurs after the Save method finishes
    ''' </summary>
    ''' <param name="sender">The Map that gets saved</param>
    ''' <param name="e">Empty</param>
    Public Event Saved(ByVal sender As System.Object, ByVal e As System.EventArgs)

    ''' <summary>
    ''' Occurs when the Tiles property changes
    ''' </summary>
    ''' <param name="sender">The Map that changes the Tiles property</param>
    ''' <param name="e">Empty</param>
    Public Event TilesChanged(ByVal sender As System.Object, ByVal e As System.EventArgs)

    ''' <summary>
    ''' Occurs when the TileSize property changes 
    ''' </summary>
    ''' <param name="sender">The Map that changes the TileSize property</param>
    ''' <param name="e">Empty</param>
    Public Event TileSizeChanged(ByVal sender As System.Object, ByVal e As System.EventArgs)

    ''' <summary>
    ''' Occurs when the VisibleArea property changes
    ''' </summary>
    ''' <param name="sender">The Map that changes the VisibleArea property</param>
    ''' <param name="e">Empty</param>
    Public Event VisibleAreaChanged(ByVal sender As System.Object, ByVal e As System.EventArgs)

    Private Sub Map_CurrentPositionChanged(ByVal sender As Object, ByVal e As EventArgs) Handles Me.CurrentPositionChanged
        Me.Draw()
    End Sub

    Private Sub Map_Loaded(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Me.Loaded
        Me.CreateControls()
        Me.Draw()
    End Sub

    Private Sub Map_TilesChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Me.TilesChanged
        Me.CreateControls()
        Me.Draw()
    End Sub

    Private Sub Map_TileSizeChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Me.TileSizeChanged
        Me.CreateControls()
        Me.Draw()
    End Sub

    Private Sub Map_VisibleAreaChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Me.VisibleAreaChanged
        Me.CreateControls()
        Me.Draw()
    End Sub

    ''' <summary>
    ''' Creates the tiles that make up the map.
    ''' </summary>
    ''' <remarks>The tiles are represented as a System.Windows.Forms.PictureBox</remarks>
    Private Sub CreateControls()
        Me.Controls.Clear()

        Dim pb As System.Windows.Forms.PictureBox
        For x As System.Int32 = 0 To vArea.Width - 1
            For y As System.Int32 = 0 To vArea.Height - 1
                pb = New System.Windows.Forms.PictureBox

                With pb
                    .Location = New System.Drawing.Point(x * tSize.Width, y * tSize.Height)
                    .Size = tSize
                    .SizeMode = Windows.Forms.PictureBoxSizeMode.StretchImage
                    .Tag = New System.Drawing.Point(x, y)
                End With

                Me.Controls.Add(pb)
            Next
        Next

    End Sub

    ''' <summary>
    ''' Draws the images that represent the tiles.
    ''' </summary>
    ''' <remarks>The images are drawn by using the PictureBox.Load() method.</remarks>
    Public Sub Draw()

        If mTiles IsNot Nothing Then
            Dim pb As System.Windows.Forms.PictureBox
            Dim t As Tile = Nothing
            For x As System.Int32 = 0 To vArea.Width - 1
                For y As System.Int32 = 0 To vArea.Height - 1
                    pb = Me.Controls.OfType(Of System.Windows.Forms.PictureBox).FirstOrDefault(Function(p) p.Tag.ToString = New System.Drawing.Point(x, y).ToString)

                    If cPosition.X + x <= mTiles.GetUpperBound(0) AndAlso cPosition.Y + y <= mTiles.GetUpperBound(1) Then
                        t = mTiles(cPosition.X + x, cPosition.Y + y)
                    End If

                    If t IsNot Nothing Then
                        pb.Load(t.ImageLocation)

                        If CInt(Math.Floor((vArea.Width - 1) / 2)) = x AndAlso CInt(Math.Floor((vArea.Height - 1) / 2)) = y Then
                            cTile = t
                        End If
                    End If
                Next
            Next
        End If

    End Sub

    ''' <summary>
    ''' Loads a map from a previously saved state.
    ''' </summary>
    ''' <param name="folder">Defines the folder in which the tiles will be loaded from</param>
    ''' <remarks>Deserializes all the tiles from the folder into a List(Of Tile). Then the List(Of Tile) is converted to a 2d Tile array.</remarks>
    Public Sub Load(ByVal folder As System.String)
        Dim files() As System.String = New IO.DirectoryInfo(folder).GetFiles.Select(Function(f) f.FullName).ToArray

        Dim tempTiles As System.Collections.Generic.List(Of Tile) = New System.Collections.Generic.List(Of Tile)

        For i As System.Int32 = 0 To files.Length - 1

            Using reader As System.IO.StreamReader = New System.IO.StreamReader(files(i))
                Dim x As System.Xml.Serialization.XmlSerializer = New System.Xml.Serialization.XmlSerializer(GetType(Tile))
                tempTiles.Add(DirectCast(x.Deserialize(reader), Tile))
            End Using

        Next

        Dim maxX As System.Int32 = tempTiles.Max(Function(t) t.RelativeLocation.X)
        Dim maxY As System.Int32 = tempTiles.Max(Function(t) t.RelativeLocation.Y)

        ReDim mTiles(maxX, maxY)

        For Each item As Tile In tempTiles
            mTiles(item.RelativeLocation.X, item.RelativeLocation.Y) = item
        Next

        RaiseEvent Loaded(Me, System.EventArgs.Empty)
    End Sub

    ''' <summary>
    ''' Attempts to move the CurrentPosition property's Y position down by a specified amount.
    ''' </summary>
    ''' <param name="increment">The specified amount to move the CurrentPosition property's Y position down</param>
    ''' <remarks>Will not move the map if the increment falls out of the Tiles bounds</remarks>
    Public Sub MoveDown(ByVal increment As System.Int32)

        If cPosition.Y + increment <= mTiles.GetUpperBound(1) Then
            cPosition = New System.Drawing.Point(cPosition.X, cPosition.Y + increment)
            Me.Draw()
        End If

    End Sub

    ''' <summary>
    ''' Attempts to move the CurrentPosition property's X position left by a specified amount.
    ''' </summary>
    ''' <param name="increment">The specified amount to move the CurrentPosition property's X position left</param>
    ''' <remarks>Will not move the map if the increment falls out of the Tiles bounds</remarks>
    Public Sub MoveLeft(ByVal increment As System.Int32)

        If cPosition.X - increment >= 0 Then
            cPosition = New System.Drawing.Point(cPosition.X - increment, cPosition.Y)
            Me.Draw()
        End If

    End Sub

    ''' <summary>
    ''' Attempts to move the CurrentPosition property's X position right by a specified amount.
    ''' </summary>
    ''' <param name="increment">The specified amount to move the CurrentPosition property's X position right</param>
    ''' <remarks>Will not move the map if the increment falls out of the Tiles bounds</remarks>
    Public Sub MoveRight(ByVal increment As System.Int32)

        If cPosition.X + increment <= mTiles.GetUpperBound(0) Then
            cPosition = New System.Drawing.Point(cPosition.X + increment, cPosition.Y)
            Me.Draw()
        End If

    End Sub

    ''' <summary>
    ''' Attempts to move the CurrentPosition property's Y position up by a specified amount.
    ''' </summary>
    ''' <param name="increment">The specified amount to move the CurrentPosition property's Y position up</param>
    ''' <remarks>Will not move the map if the increment falls out of the Tiles bounds</remarks>
    Public Sub MoveUp(ByVal increment As System.Int32)

        If cPosition.Y - increment >= 0 Then
            cPosition = New System.Drawing.Point(cPosition.X, cPosition.Y - increment)
            Me.Draw()
        End If

    End Sub

    ''' <summary>
    ''' Saves all the tiles in the map.
    ''' </summary>
    ''' <param name="folder">Defines the folder in which the tiles will be saved to</param>
    Public Sub Save(ByVal folder As System.String)

        For Each item As Tile In mTiles
            item.SaveTile(folder)
        Next

        RaiseEvent Saved(Me, EventArgs.Empty)

    End Sub

    Sub New()
        cPosition = New System.Drawing.Point(0, 0)
        tSize = New System.Drawing.Size(32, 32)
        vArea = New System.Drawing.Size(10, 10)
        Me.CreateControls()
        Me.Draw()
    End Sub

End Class
```

----------


## passel

> ...
>  I kinda wish programmers in the past found this method out. Lord knows the RPGs in the past would be gigantic if this were so. But I think its a method I personally invented 
> ...


No new invention here. Been doing it for more than 30 years, and I'm sure others even longer.
Of course, when you had less than 64K of memory to work with, and slow 8-bit processors, you had to be efficient and of course only drew what was visible on the screen, and often less (only partial updates of only the parts that have changed).
Have you actually seen code that loops though all the tiles when drawing, other than possible amatures programming a game as their first programming exercise?

----------


## dday9

@Passel, do you have any tips on the initial loading of the tiles? I'm currently uses the Load method which deserializes a folder of XML files which represent a tile. This can take a good bit if the map is greater than 100 x 100.

----------


## passel

Well, I'm not sure how long your loading is taking per tile and how many fit on the screen (I guess you said 10x10), but probably the first thing to try would be to put the tile loading in a background thread, and choose to load the tiles around your starting position first, and work your way out from there.
That way you should be able to display the currently area of the map fairly quickly and if they choose to move, the near tiles should be available as tiles are being loaded in the background.
If you had a really large map, you might choose to never load all the tiles, but maintain a cache of 400 or so tiles and load 10 or 20 at a time when needed.
Also, part of the efficient of a tile based game is that you reuse a lot of tiles, so you shouldn't have 10,000 unique tiles in a 100x100 gaming area.
The Map data should be using indexes to the tiles so be much smaller, and quicker to load. 
I haven't done too many tile based applications over the last 30+ years, but have done tactical plots and scrolling maps, etc. from way back.
More recently I did do several versions of a test case of a large scrolling area using VB.Net, but it isn't technically tile based.
It just uses a textured background that scrolls so you can perceive motion, and you place a lot of objects around the map (which could be tiles), but can be placed at any point.  In my test case, I just grabbed a couple of bit maps that had a number of plant, bushes and trees, and scattered them around the area.
In my test case, the area is 256K by 256K pixels in size, and I limited myself to placing a million plants/bushes/trees around the area. I didn't use all one million slots, something like 600000+ I think. So, even though there are 600000+ items located all over the gaming area, there are probably less than 200 unique images that have to be loaded to draw them. And of course, only the items in a 2000 by 2000 pixels area around the center of the drawing area is considered when drawing. 
The area is divided into 1K by 1K sized logical areas, and the things that are located in those areas are linked to a linked list in that area, so you only have to look at a very small number of the one million items for things like drawing, hit testing and collision testing.
I have a bunch of versions of the thing, playing around with different aspects. None are really optimized and the drawing isn't done in the best way. I haven't messed with it for a year or two, but I could probably find some reasonable version of it to post somewhere on the forum. I have a lot of various test projects, none polished. All the polished stuff is for work, and none of that, until lately, has been any form of VB.
99% of my VB programming (since 1991) has been personal hobby stuff.

----------


## dday9

That does give me some idea's to rewrite the program. I'll work on it some more!

----------

