Creating and Drawing a Game Map in VB.NET (FIXED PER REQUEST)
This is the seventh part of a nine-part article series that teaches you Visual Basic.Net through the development of a text-based PC game similar to Rogue or Nethack. In this installment, you'll learn how to build the map, starting with the creation of some basic tiles. We'll be dealing with arrays, entity classes and more. Let's get started.
Before we start working with the map as a whole, let's create a few basic tiles to work with by subclassing the Tile class. The first tile we'll create is a basic floor tile. It won't be flashy at all. It will have a gray foreground and a black background, it will be represented by a period, and the user will, of course, be able to walk over it. Create a new class in Visual Studio called BasicTile (in BasicTile.vb):
PublicClass BasicTile
EndClass
The first thing we need to do is set our class to inherit from the Tile class. This is very easy. All we have to do is use the Inherits keyword and then specify the parent class. As is characteristic of BASIC languages, the result is very straightforward and readable:
PublicClass BasicTile
Inherits Tile
EndClass
Notice how the Inherits statement is put on a new line. This is required.
Immediately, Visual Studio should complain that we have to create a constructor because the parent class does not have a parameterless constructor which can be called by default. This is easy to do. Let's create a parameterless constructor that sets up everything. In this constructor, we will have to call Tile's constructor, which will set the fields to the values we specify. Let's go ahead and create it:
One of the first things you should notice is the use of the MyBase keyword. The MyBase keyword simply provides access to the parent class. Above, we use MyBase to call the parent class's constructor. Also, notice how, when working with characters, we do not use single quotes as we might in other languages. Instead, we just use double quotes, which other languages often reserve for strings. Besides, the single quote mark in Visual Basic is taken to be a comment.
Now we have a basic floor tile. Next is a wall tile. It will be represented by the number sign, will have a gray foreground and a black background, and it will, of course, not be passable by the player. Create a class called WallTile (in WallTile.vb). The process is the same as before, except the name of the class is, obviously, different, and the values we pass to Tile's constructor are different:
The final basic tile we'll need is a blank tile. This tile will have a blank space associated with it, and it will be used to fill in if the physical map isn't as big as the available size. Make a class called BlankTile:
Great. We now have three simple tiles to work with, and we can begin to work with the map array. As stated before, the map will be represented by a two-dimensional array. In this way, we can treat the array as a coordinate system of sorts. One dimension's index will represent the x-coordinate of the tile, and the other dimension's index will represent the y-coordinate of the tile. This works out to be nice and neat.
As with player, our map array will be a field of the Game module. This is the easiest way to work with it. Let's go ahead and create a field for the map. Let's make the map 60x20. So, the upper bound for the x-coordinate will be 59, and the upper bound for the y-coordinate will be 19:
Dim map(59, 19) As Tile
Next, we need a map. Ordinarily, we may want to automatically generate a map, or else read an existing map from a file. However, let's keep this basic for now and create a map by hand. More ambitious projects will have to wait until later. Let's start with something simple. We'll make a large square room occupying the entire available space. We can create a room like this with a loop. We'll loop through every space in the map. This will require one loop for the x-coordinates and another embedded within that for the y-coordinates. If we're at the edge of the map, we'll place a WallTile. Otherwise, we need a BlankTile. Let's put this in a method called CreateBasicMap:
Sub CreateBasicMap()
' Get the x- and y- coordinates for the right and bottom
' edges of the map
Dim xLimit AsInteger = map.GetUpperBound(0)
Dim yLimit AsInteger = map.GetUpperBound(1)
For x AsInteger = 0 To xLimit
For y AsInteger = 0 To yLimit
If x = 0 Or x = xLimit Or y = 0 Or y = yLimit Then
map(x, y) = New WallTile()
Else
map(x, y) = New BasicTile()
EndIf
Next
Next
EndSub
We use a For loop because we need to be able to go tile-by-tile and also check to see if we're at an edge. The outer loop iterates over the x values, and the inner loop iterates over the y values. So, the method essentially creates the map column-by-column. Also, notice the use of the Or operator. We check multiple conditions, any of which, if true, indicate that we're at an edge of the map. Go ahead and call CreateBasicMap in Main:
Now we have a map. That didn't take much, did it? We need to draw it out to the screen now, one tile at a time. This will enable us to see what it looks like for the first time. To draw it out to the screen, we once again need two loops, but rather than going through the map column-by-column, we'll need to go through row-by-row. This way, we can just call Write for each tile, and we only have to manually adjust the cursor's position at the end of each row. Once again, we'll place this code in a separate method, so as not to clog up Main. The method will be called DrawMap:
Sub DrawMap()
For y AsInteger = 0 To map.GetUpperBound(1)
Console.SetCursorPosition(0, y)
For x AsInteger = 0 To map.GetUpperBound(0)
DrawTile(x, y)
Next
Next
EndSub
As noted, the outer loop goes by row, and the inner loop goes by column. At the start of each iteration of the outer loop, we set the cursor to the beginning of the appropriate row. As we draw the tiles, the cursor will automatically shift right. Notice how I've called a method called DrawTile. We'll be drawing tiles frequently, and so it's best to have a separate method for this. Inside of DrawTile, we apply our knowledge of console output:
Sub DrawTile(ByVal x AsInteger, ByVal y AsInteger)
The map is missing something very important, though-the player. Fortunately, displaying the player is easy enough, but first we need to consider something else first. Yes, we'll have the player walking around the screen, but we'll also have other entities (monsters, etc.) moving around the screen as well. Here, we have some common behavior shared between players and other entities. Both groups have a location, both groups are represented by a symbol, and both groups have names associated with them. This is a great opportunity for inheritance to play a role. Let's create a class called Entity that represents all the entities on the map, including the player. Then, let's rework Adventurer to inherit from this class.
The Entity class will have fields and properties for a name, a location, a symbol, and a foreground color. Let's go ahead and create the class-it's a little lengthy:
PublicClass Entity
Private _name AsString
Private _symbol AsChar
Private _color As ConsoleColor
Private _x AsInteger
Private _y AsInteger
PublicProperty Name() AsString
Get
Return _name
EndGet
Set(ByVal value AsString)
_name = value
EndSet
EndProperty
PublicProperty Symbol() AsChar
Get
Return _symbol
EndGet
Set(ByVal value AsChar)
_symbol = value
EndSet
EndProperty
PublicProperty Color() As ConsoleColor
Get
Return _color
EndGet
Set(ByVal value As ConsoleColor)
_color = value
EndSet
EndProperty
PublicProperty X() AsInteger
Get
Return _x
EndGet
Set(ByVal value AsInteger)
_x = value
EndSet
EndProperty
PublicProperty Y() AsInteger
Get
Return _y
EndGet
Set(ByVal value AsInteger)
_y = value
EndSet
EndProperty
EndClass
We also need a constructor to assign initial values to the fields:
PublicSubNew(ByVal name AsString, ByVal symbol AsChar, _
ByVal color As ConsoleColor, ByVal x AsInteger, _
ByVal y AsInteger)
_name = name
_symbol = symbol
_color = color
_x = x
_y = y
EndSub
The Entity class is long, but we can now drastically reduce the size of Adventurer, since the functionality we provided previously is now provided in Entity. All we need to do now is call the constructor. Replace the entire Adventurer class with this:
PublicClass Adventurer
Inherits Entity
PublicSubNew(ByVal name AsString, ByVal x AsInteger, _
ByVal y AsInteger)
MyBase.New(name, "@", ConsoleColor.Cyan, x, y)
EndSub
EndClass
There, that's shorter than before. However, since the constructor's parameters have changed, we need to modify Main a bit, where we create the Adventurer:
player = New Adventurer(name, 1, 1)
Now we can create a method that will draw not only the player, but every other entity as well. The method will accept an Entity object and will then draw it on the proper location on the screen:
The method is similar to DrawTile, except it specifies a parameter and sets the cursor to the proper location itself. In Main, after the call to DrawMap, call DrawEntity and pass player (since, after all, an Adventurer is an Entity):
DrawEntity(player)
This will draw the player on the screen. Run the program, and you should see the player in the top left-hand corner of the screen.
Now that we have drawing taken care of, it's time to move on to movement. The game is finally starting to take shape.