Working with Classes and Properties for Game Development in VB.NET
We've covered console input and output, variables, conditionals, loops, arrays and collections. With these tools, we're finally ready to begin constructing our game, whose basic plan we looked at in the first article of this series. It's been a while (this is the sixth of nine parts), so if you need a refresher, feel free to go back to the first article to take another look at the plan.
The first thing we need to do is set up the console window for our game. We need to give the window a proper size so that the interface fits on the screen, and we also need to give our game a title. I'm going to use the title "VB Quest," but feel free to use your own, more creative title. This code goes right at the top of Main:
Console.Title = "VB Quest"
Console.SetWindowSize(80, 35)
You can see what the window looks like by placing a call to ReadKey at the bottom. This will keep the window open until a key is pressed. If we set the window size to too small of a size, then the terminal window will scroll down, and the top lines of our game will not be visible.
When the user runs the game, he may have started execution by typing the name of the program into the console. If this is the case, there will be some text in the console already. We don't want anything to be in the console window already, so let's clear anything that might be there:
Console.Clear()
When the user opens the application, the first thing we need to do is welcome him to the game. This is easy. A short sentence will do, unless you feel artistic and want to decorate it somehow (maybe add some color?-you know how):
Console.WriteLine("Welcome to VB Quest.")
Then we need to ask him his name. This is also easy:
Where do we store the name though? We could just create a field to store it in, but there's a better approach. We can create a class to store the name in, along with other data about the player. For now, let's keep the class as simple as possible, but later, we'll make it more complex. In Visual Studio, go to Project on the menu, and click the Add Class option (or, right click the project in the Solution Explorer and, in the context menu, add the class). Let's name the class Adventurer and the class's file Adventurer.vb. The empty class looks like this:
PublicClass Adventurer
EndClass
In this new class, we need to create a private field for the name, and then we need to create a property to provide access to the field. Creating the field is not very hard. All we need to do is provide an access modifier (Private), an identifier and a type. So, let's create the field first:
Private _name AsString
This works just like normal variable declaration, except instead of the Dim keyword, we put an access modifier. We, of course, want our field to be Private, so in order for it to be accessed by another object, we'll need to provide a property. If you come from a language such as Java, this term may sound foreign to you, but it's not too complex. A property merely takes the place of accessor and mutator methods, but it can be accessed similarly to the way a public field is accessed. This way, there are no ugly method calls when the programmer only wants a small piece of data. (Note that any operation that is resource-intensive or takes a while to execute should still be placed in a method rather than a property).
Let's create a property to access the _name field. Here is a bare bones property:
PublicProperty Name() AsString
Get
EndGet
Set(ByVal value AsString)
EndSet
EndProperty
The property is divided into two parts: a Get procedure (the accessor half) and a Set procedure (the mutator half). The Get procedure expects us to return a valid value, or else the code won't compile. So, let's return the value of _name:
Get
Return _name
EndGet
Note the use of Return here. Of course, we're free to do any sort of manipulation and computation in the Get procedure, but this is unnecessary for our situation.
The Set procedure doesn't need anything in it for the code to compile, but it would be pointless to leave it blank (you can mark the property as read only by adding the ReadOnly modifier in front of Property, but in this case, you don't need to create Set at all). In this procedure, we need to set _name to whatever value is assigned to the property. The value that is assigned (or, more appropriately, the value that someone has attempted to assign to the property, since we can alter the value or ignore it altogether according to the situation-this is, of course, the beauty and safety of properties) is stored in the value parameter. Let's set _name to whatever is in value:
Now we have a functional object with one field and one property to access that field. We can return to the Game module now, where we've just collected the user's name and stored it in name. From here, we need to create an Adventurer object to store the name. This involves creating the object and then setting the property. Let's create the object first:
Dim player AsNew Adventurer()
This approach works, but since the object is created locally, only Main will have direct access to player. We can fix this either by passing player as an argument to each method, or we can just create player as a field. The latter approach is a lot simpler, so we'll use it. Erase the line creating player as a local variable and add this line at the module-level, up above Main:
Dim player As Adventurer
Now player is a field. Notice how, unlike before, we use Dim rather than an access modifier. If we don't provide an access modifier, then the default is automatically used. Now, back in Main, where we created player before, we instead need to assign a value to the player field:
player = New Adventurer()
Now we just need to specify the name somehow. There are a few ways we can do this. The first way is to assign it directly to the Name property:
player.Name = name
This approach certainly works, but property assignment is made a lot easier with the With operator, which can assign multiple property values when the object is instantiated. Let's instead assign a value to Name using the With operator:
player = New Adventurer() With {.Name = name}
This approach is much nicer, and it's the preferred method for assigning initial property values. The third way is to assign a value to the underlying field in the constructor. The constructor is, of course, called when the object is instantiated. If we don't specify any constructors, then a default one is created for us. Let's, however, create our own constructor. A constructor is created in the same way as a normal method, except it's called New:
SubNew(ByVal name AsString)
_name = name
EndSub
Notice, too, the parameter list. We've never talked about parameters before. For each parameter, we must specify an identifier, a type, and a keyword indicating how the parameter is to be passed. Here, we use the ByVal keyword, which is what Visual Studio puts in by default if we don't specify anything. This keyword indicates that the parameter is passed by value. We could also have it passed by reference using the ByRef keyword.
When we create a constructor, then a default, a parameterless constructor will no longer be created for us. So, if you enter the above constructor, then the previous instantiation of an Adventurer object is invalid since no arguments are passed. We need to modify it if it's going to be valid:
player = New Adventurer(name)
Or, we could leave it the same and create a parameterless constructor, though we would, of course, have to assign a value to the Name property manually, as before:
SubNew()
EndSub
That will work for now. We can add complexity to our class later as necessary.
The next thing we need to do is determine how to lay out the "tiles" (which are, remember, actually composed of single characters) for our maps. Since a map itself will be rectangular (though not all of the rectangle may be used, of course), the best way to represent it is through a two-dimensional array. Think of the map as a table of tiles. One dimension of the array contains the rows, and the other contains the columns. This is the most straightforward approach to it.
We need to have an array of something, though, and despite the tiles being represented as characters, an array of characters won't due because each tile has special properties, such as color or the ability to be walked on (as opposed to being a wall or some other obstruction). These properties will be best represented as part of a Tile class. Then, we can just create an array of Tile objects.
Create a new Tile class (inside Tile.vb) in Visual Studio, the same way we did with the Adventurer class:
PublicClass Tile
EndClass
We'll need fields and properties for the tile's character symbol, its foreground color, its background color and whether or not the user can pass over it. The symbol needs to be a Char, the two colors need to be of type ConsoleColor, and a Boolean can tell us whether or not the user can pass over the tile. Let's go ahead and create all of this at once (it's a bit lengthy, but it's very simple, and there's nothing new):
Private _symbol AsChar
Private _foregroundColor As ConsoleColor
Private _backgroundColor As ConsoleColor
Private _passable AsBoolean
PublicProperty Symbol() AsChar
Get
Return _symbol
EndGet
Set(ByVal value AsChar)
_symbol = value
EndSet
EndProperty
PublicProperty ForeGroundColor() As ConsoleColor
Get
Return _foregroundColor
EndGet
Set(ByVal value As ConsoleColor)
_foregroundColor = value
EndSet
EndProperty
PublicProperty BackgroundColor() As ConsoleColor
Get
Return _backgroundColor
EndGet
Set(ByVal value As ConsoleColor)
_backgroundColor = value
EndSet
EndProperty
PublicProperty Passable() AsBoolean
Get
Return _passable
EndGet
Set(ByVal value AsBoolean)
_passable = value
EndSet
EndProperty
As I said, it's lengthy, but we're just creating four fields and then creating four properties to access those fields. Now we need to create a constructor to set initial values for everything:
PublicSubNew(ByVal symbol AsChar, _
ByVal foregroundcolor As ConsoleColor, _
ByVal backgroundColor As ConsoleColor, _
ByVal passable AsBoolean)
_symbol = symbol
_foregroundColor = foregroundcolor
_backgroundColor = backgroundColor
_passable = passable
EndSub
We won't be using the Tile class like this, however. Rather, we're going to subclass it in order to easily create different types of tiles. And that is where we will pick up next week!