Movement and Player Statistics in a VB.NET Text-Based Game
Now that we've drawn the map and the player on the screen, we need to work on movement. Movement involves a number of things. We'll also need to give our player some statistics, such as health, attack, and defense. Keep reading for the lowdown as the text game we're creating to learn VB.Net really starts taking shape.
First, we need to allow the user to press a key. Second, we need to check to see if this movement is valid. For example, we don't want the player to walk on top of a wall. Checking to see if the movement is valid only involves checking the Passable property of the destination tile. Third, we need to actually make the movement. This involves drawing the player on the new tile, but it also involves erasing the player on the old tile.
Finally, this all needs to be contained within a loop. After all, we want the user to be able to continue making movements, and each time, the game should go through the same steps to respond to the movement.
The first thing we need to do is set up the loop. For now, an infinite While loop will work, but later we'll want to check for certain situations—such as when the player is dead. In this loop, we'll want to accept a keypress from the user. The keypress will be stored in a variable, so we'll need to define this variable. Also, we need to make sure that the cursor is not visible, and that the pressed key is not displayed in the console. Put the game loop at the bottom of Main:
Console.CursorVisible = False
Dim input As ConsoleKeyInfo
WhileTrue
input = Console.ReadKey(True)
EndWhile
When you run the game, there will now be no cursor visible, and the console will not display anything you type (recall that this is accomplished by passing True to ReadKey). Now that we have the key stored, we need to check it against valid inputs. If the input is valid, then we need to perform the associated action. Otherwise, we need to go back and let the user try again. The checking will be done in a Select statement. Right now, let's put in a quit action, which will be escape. Pressing this key will exit the game immediately. Put this at the bottom of the loop:
SelectCase input.Key
Case ConsoleKey.Escape
ExitWhile
CaseElse
ContinueWhile
EndSelect
Notice how, if the user has pressed the escape key, we exit the While loop. Otherwise, we use the Continue statement. The Continue statement immediately moves to the next iteration of the loop. In this case, we use it to allow the user to press another key.
Now it's time to add movement. We need to first set up a function, TryMove, that will try to move an Entity (not necessarily the player) a certain number of spaces. This function will accept the entity to be moved, the change in the entity's X value, and the change in the entity's Y value. It will then check to see if the move is valid. If it is valid, then the movement will be made, and True will be returned. If the movement cannot be made, then False will be returned. We need TryMove to return these values because we don't want to end the player's turn if he tries to make an invalid move. Rather, we want him to be able to try again before the other entities get their turns.
So far, we've never actually worked with functions before. They're very simple, however. A Func, as it is termed in the language, is similar to a Sub, only we must return a value, and we can explicitly specify what type that value will be.
We're also going to need to create yet another method called RedrawTile. DrawTile will draw a tile, but since it was originally built to be used to draw the entire map, it assumes that the cursor is already in the correct position. RedrawTile will manually set the cursor in the proper position and then call DrawTile. First, though, here is TryMove, which calls RedrawTile:
Function TryMove(ByVal toBeMoved As Entity, ByVal xChange AsInteger, ByVal ychange AsInteger) AsBoolean
Dim x AsInteger = toBeMoved.X + xChange
Dim y AsInteger = toBeMoved.Y + ychange
If map(x, y).Passable = TrueThen
RedrawTile(toBeMoved.X, toBeMoved.Y)
toBeMoved.X = x
toBeMoved.Y = y
DrawEntity(toBeMoved)
ReturnTrue
EndIf
ReturnFalse
EndFunction
The function accepts, as I said before, the change in the entity's X value and the change in the entity's Y value. So, if the entity is trying to move right, then the change in the X value is 1, and the change in the Y value is 0. If the entity is trying to move up, then the change in the X value is 0, and the change in the Y value is -1. The changes are added to the player's current location to determine the destination tile. The tile is then checked to see if the player can walk on it, and if the player can, then the player's old location is redrawn, and the player is drawn on the new location.
Now let's write RedrawTile, which is very simple, as was already noted:
Sub RedrawTile(ByVal x AsInteger, ByVal y AsInteger)
Console.SetCursorPosition(x, y)
DrawTile(x, y)
EndSub
Great. Now the methods required for movement are in place, and we can wire them up to our loop. Recall how I said that the player's turn will only be used up if the player has made a move. This is determined by what TryMove returned. We're going to need a Boolean variable to store the value returned by TryMove. Add this definition before the Select statement:
Dim playerMoved AsBoolean = False
We assume that the player has not made a move. If the player does indeed made a move, then we can change this. This also eliminates the need for the Case Else block, so you can go ahead and delete it. It will be replaced by a conditional that checks to see whether or not the player has moved. If the player has not moved, then we'll need to immediately move to the next iteration, just like we did in the Case Else block. Put the conditional below End Select:
IfNot playerMoved Then
ContinueWhile
EndIf
Now we can check for movement keys and respond accordingly. We'll use the numpad for movement. Here's what the entire Select block should now look like:
SelectCase input.Key
Case ConsoleKey.Escape
ExitWhile
Case ConsoleKey.NumPad8
playerMoved = TryMove(player, 0, -1)
Case ConsoleKey.NumPad2
playerMoved = TryMove(player, 0, 1)
Case ConsoleKey.NumPad4
playerMoved = TryMove(player, -1, 0)
Case ConsoleKey.NumPad6
playerMoved = TryMove(player, 1, 0)
EndSelect
Above, we check for movement keys and then call TryMove, passing the appropriate values. The result of the move is stored in playerMoved. Run the program and press the directional keys on the numpad. The player should now move around the map.
Now that the player can move, how about giving him some attributes such as health, attack and defense? Since these attributes are not specific to the player alone, let's add them to the Entity class as fields and properties. Attack and defense can be represented in a single field and a single property each, but health needs two fields and properties, one set for the current health, and another set for the maximum available health. Place all of this inside of the Entity class:
Private _health AsInteger
Private _maxHealth AsInteger
Private _attack AsInteger
Private _defense AsInteger
PublicProperty Health() AsInteger
Get
Return _health
EndGet
Set(ByVal value AsInteger)
_health = value
EndSet
EndProperty
PublicProperty MaxHealth() AsInteger
Get
Return _maxHealth
EndGet
Set(ByVal value AsInteger)
_maxHealth = value
EndSet
EndProperty
PublicProperty Attack() AsInteger
Get
Return _attack
EndGet
Set(ByVal value AsInteger)
_attack = value
EndSet
EndProperty
PublicProperty Defense() AsInteger
Get
Return _defense
EndGet
Set(ByVal value AsInteger)
_defense = value
EndSet
EndProperty
This also means that we have to modify Entity's constructor to accept values for each of the fields:
PublicSubNew(ByVal name AsString, ByVal symbol AsChar, _
ByVal color As ConsoleColor, _
ByVal x AsInteger, ByVal y AsInteger, _
ByVal health AsInteger, _
ByVal attack AsInteger, _
ByVal defense AsInteger)
_name = name
_symbol = symbol
_color = color
_x = x
_y = y
_health = health
_maxHealth = health
_attack = attack
_defense = defense
EndSub
And then we have to modify Adventurer's constructor to pass default values to Entity's new constructor:
PublicSubNew(ByVal name AsString, ByVal x AsInteger, _
ByVal y AsInteger)
MyBase.New(name, "@", ConsoleColor.Cyan, x, y, 10, 1, 1)
Now that we've added some properties to the player, it would be a good idea to display these somewhere on the screen so that the user can keep an eye on their values. We can wrap this user interface code into a new method, DrawStatBox. This method will, of course, go into the Game module. The player's statistics will be displayed to the right of the map, along with the player's name:
Sub DrawStatBox()
Console.SetCursorPosition(61, 1)
Console.Write(player.Name)
Console.SetCursorPosition(61, 3)
Console.Write("Health: {0}/{1}", player.Health, _
player.MaxHealth)
Console.SetCursorPosition(61, 4)
Console.Write("Attack: {0}", player.Attack)
Console.SetCursorPosition(61, 5)
Console.Write("Defense: {0}", player.Defense)
EndSub
Above the game's While loop, around the call to DrawMap, place a call to the new DrawStatBox method:
DrawStatBox()
When you start the game up, a the player's name and statistics should display to the right of the map.
Next, we need to work on other entities besides the player. We'll cover that next week, in the last article of this series. You won't want to miss it!