Lesson 8 - Arena with a mage in VB.NET (inheritance and polymorphism)
In the previous lesson, Inheritance and polymorphism in VB.NET, we went over inheritance and polymorphism in VB.NET. In today's tutorial, we're going to make a sample program using these concepts, as promised. We'll get back to our arena and inherit a mage from the Warrior class. These next couple of lessons will be among the most challenging. Which is why you should be working on OOP on your own, solving our exercises and coming up with your own applications. Try putting all that you've learned to practice. Make programs that you would find useful, so it will be more engaging and fun.
Before we even get to coding, we'll think about what a mage should be capable of doing. The mage will work just like a warrior, but on top of health, he will also have mana. At first, the mana will be full. When it is, the mage can perform a magic attack which will have a higher damage than a normal attack, depending on how we set it. This attack will bring his mana down to 0. The mana will increase by 10 every round and the mage would only be able to perform regular attacks. Once the mana is full, he'll be able to use his magic attack again. The mana will be displayed using a graphical indicator just like the health bar.
Let's create a Mage.vb class, inherit it from the Warrior class and give it extra fields (that warriors don't have). I will look something like this (don't forget to add comments):
Public Class Mage Inherits Warrior Private mana As Integer Private maxMana As Integer Private magicDamage As Integer End Class
We don't have access to all the warrior variables in the mage class because we still have the fields in the Warrior class set to Private. We'll have to change the Private field modifiers to protected in the Warrior class. All we really need now is the die and the name fields. Either way, we'll set all of the warrior's fields to Protected because they might come in handy for future descendants. On second thought, it wouldn't be wise to set the message field as Protected since it is not related to the warrior directly. With all of that in mind, the class would look something like this:
Protected name As String Protected health As Integer Protected maxHealth As Integer Protected damage As Integer Protected defense As Integer Protected die As RollingDie Private message As String ...
Moving on to the constructor.
Descendant constructor
VB.NET does not inherit constructors! This is probably because it assumes the descendant will have extra fields and would make the original constructor irrelevant. Which is correct in our case, since the mage's constructor will have 2 extra parameters (mana and magic damage).
We'll define the constructor in the descendant, which will take both parameters needed to create a warrior and the extra ones needed for the mage.
The descendant constructor must always call the parent constructor. If you forget to do so, the instance may not be properly initialized. The only time we don't call the ancestor constructor is when there isn't one. Our constructor must have all needed parameters for an ancestor and the new ones that the descendant needs. The ancestor's constructor will be executed before the descendant's.
In VB.NET, there is a keyword known as MyBase which is similar to Me. Unlike Me which refers to the current instance, MyBase refers to the ancestor. Meaning, that we can call the ancestor constructor with the given parameters and initialize the mage as well.
Mage's constructor should look something like this:
Public Sub New(name As String, health As Integer, damage As Integer, defense As Integer, die As RollingDie, mana As Integer, magicDamage As Integer) MyBase.New(name, health, damage, defense, die) Me.mana = mana Me.maxMana = mana Me.magicDamage = magicDamage End Sub
Note: we can call another constructor of the same class, not from the ancestor, using Me instead of MyBase.
Now, let's switch to Module1.vb and change the second warrior to a mage. Like this:
Dim gandalf As Warrior = New Mage("Gandalf", 60, 15, 12, die, 30, 45)
We will also have to change the line where we put the warrior in the arena. Note that we are still able to store the mage into a variable of the Warrior type because it is its ancestor. We could also change the variable type to Mage. When you run the application now, it'll work exactly as it did before. Mage inherits everything from the warrior and behaves just like a warrior.
Polymorphism and overriding methods
It would be nice if the Arena could work with the mage in the same way as it does with the warrior. We already know that in order to do so we must apply the concept of polymorphism. The arena will call the Attack() method, passing an enemy as a parameter. It won't care whether the attack will be performed by a warrior or by a mage, the arena will work with them in the same way. We'll have to override the ancestor's Attack() method in the mage class. We'll rewrite its inherited method so the attack will use mana, but the header of the method will remain the same.
In order to override a method, it must be marked as Overridable in the ancestor (these methods can be also referred to as virtual in some other programming languages). It basically just tells VB.NET to allow descendants to override this method. Now, the method header in Warrior.vb should look like this:
Public Overridable Sub Attack(enemy As Warrior)
Talking about methods, we'll certainly need a SetMessage() method which is Private now. We have to make it Protected:
Protected Sub SetMessage(message As String)
Note. When you create a class you should always consider whether it would have descendants and therefore make appropriate Protected fields and Overridable methods. A method that can be overridden in the descendant must be marked with the 'Overridable' keyword. I didn't to overwhelm you with all of this information when we first made the Warrior class but now that we understand these modifiers, we should use them.
The Attack() method in the warrior will be Public Overridable. We would normally re-declare the method in Mage.vb. However, thanks to the Overrides keyword, we are able to override it. All it does is indicate that we're aware the method was inherited, and that we want to change its behavior.
Public Overrides Sub Attack(enemy As Warrior)
Similarly, we have overridden the ToString() method in our objects earlier,
each object in VB.NET is in fact implicitly inherited from the System.Object
class which contains four methods, one of them is ToString(). So we have to use
Overrides
to re-implement it.
Our descendant's Attack() method won't be all that different. Depending on the mana value, we'll either perform a normal attack or a magic attack. The mana value will be either increased by 10 each round or in the case where the mage uses a magic attack, it will be reduced to 0.
Public Overrides Sub Attack(enemy As Warrior) ' Mana isn't full If mana < maxMana Then mana += 10 If mana > maxMana Then mana = maxMana End If Dim hit As Integer = damage + die.Roll() SetMessage(String.Format("{0} attacks with a hit worth {1} hp", name, hit)) Else ' Magic damage Dim hit As Integer = magicDamage + die.Roll() SetMessage(String.Format("{0} used magic for {1} hp", name, hit)) mana = 0 End If enemy.Defend(hit) End Sub
Notice how we limit the mana to maxMana since it could be that it exceeds the maxim value when increasing it by 10 each round. When you think about it, the normal attack is already implemented in the ancestor's Attack() method. Certainly, it'd be better to just call the ancestor's Attack() instead of copying its behavior. We'll use the MyBase keyword to do just that:
Imports System.Threading
Public Class RollingDie
Private random As Random
Private sidesCount As Integer
Public Sub New()
sidesCount = 6
random = New Random()
End Sub
Public Sub New(sidesCount As Integer)
Me.sidesCount = sidesCount
random = New Random()
End Sub
Public Function GetSidesCount() As Integer
Return sidesCount
End Function
Public Function Roll() As Integer
Return random.Next(1, sidesCount + 1)
End Function
Public Overrides Function ToString() As String
Return String.Format("Rolling die with {0} sides", sidesCount)
End Function
End Class
Public Class Warrior
Protected name As String
Protected health As Integer
Protected maxHealth As Integer
Protected damage As Integer
Protected defense As Integer
Protected die As RollingDie
Private message As String
Public Sub New(name As String, health As Integer, damage As Integer, defense As Integer, die As RollingDie)
Me.name = name
Me.health = health
Me.maxHealth = health
Me.damage = damage
Me.defense = defense
Me.die = die
End Sub
Public Overrides Function ToString() As String
Return name
End Function
Public Function Alive() As Boolean
Return health > 0
End Function
Public Function HealthBar() As String
Dim s As String = "["
Dim total As Integer = 20
Dim count As Double = Math.Round((health / maxHealth) * total)
If count = 0 And Alive() Then
count = 1
End If
For i As Integer = 1 To count
s &= "#"
Next
s = s.PadRight(total + 1)
s &= "]"
Return s
End Function
Public Overridable Sub Attack(enemy As Warrior)
Dim hit As Integer = damage + die.Roll()
SetMessage(String.Format("{0} attacks with a hit worth {1} hp", name, hit))
enemy.Defend(hit)
End Sub
Public Sub Defend(hit As Integer)
Dim injury As Integer = hit - (defense + die.Roll())
If injury > 0 Then
health -= injury
message = String.Format("{0} defended against the attack but still lost {1} hp", name, injury)
If health <= 0 Then
health = 0
message &= " and died"
End If
Else
message = String.Format("{0} blocked the hit", name)
End If
SetMessage(message)
End Sub
Protected Sub SetMessage(message As String)
Me.message = message
End Sub
Public Function GetLastMessage() As String
Return message
End Function
End Class
Public Class Arena
private warrior1 As Warrior
private warrior2 As Warrior
private die As RollingDie
Public Sub New(warrior1 As Warrior, warrior2 As Warrior, die As RollingDie)
Me.warrior1 = warrior1
Me.warrior2 = warrior2
Me.die = die
End Sub
Private Sub Render()
Console.Clear()
Console.WriteLine("-------------- Arena -------------- " & vbCrLf)
Console.WriteLine("Warriors health: " & vbCrLf)
Console.WriteLine("{0} {1}", warrior1, warrior1.HealthBar())
Console.WriteLine("{0} {1}", warrior2, warrior2.HealthBar())
End Sub
Private Sub PrintMessage(message As String)
Console.WriteLine(message)
Thread.Sleep(500)
End Sub
Public Sub Fight()
' The original order
Dim w1 As Warrior = warrior1
Dim w2 As Warrior = warrior2
Console.WriteLine("Welcome to the Arena!")
Console.WriteLine("Today {0} will battle against {1}!{2}", warrior1, warrior2, vbCrLf)
' swapping the warriors
Dim warrior2Starts As Boolean = (die.Roll() <= die.GetSidesCount() / 2)
If warrior2Starts Then
w1 = warrior2
w2 = warrior1
End If
Console.WriteLine("{0} goes first!{1}Let the battle begin...", w1, vbCrLf)
Console.ReadKey()
' fight loop
While w1.Alive() And w2.Alive()
w1.Attack(w2)
Render()
PrintMessage(w1.GetLastMessage()) ' attack message
PrintMessage(w2.GetLastMessage()) ' defense message
If w2.Alive() Then
w2.Attack(w1)
Render()
PrintMessage(w2.GetLastMessage()) ' attack message
PrintMessage(w1.GetLastMessage()) ' defense message
End If
Console.WriteLine()
End While
End Sub
End Class
Public Class Mage
Inherits Warrior
Private mana As Integer
Private maxMana As Integer
Private magicDamage As Integer
Public Sub New(name As String, health As Integer, damage As Integer, defense As Integer, die As RollingDie, mana As Integer, magicDamage As Integer)
MyBase.New(name, health, damage, defense, die)
Me.mana = mana
Me.maxMana = mana
Me.magicDamage = magicDamage
End Sub
Public Overrides Sub Attack(enemy As Warrior)
' Mana isn't full
If mana < maxMana Then
mana += 10
If mana > maxMana Then
mana = maxMana
End If
MyBase.Attack(enemy)
Else ' Magic damage
Dim hit As Integer = magicDamage + die.Roll()
SetMessage(String.Format("{0} used magic and took {1} hp off", name, hit))
enemy.Defend(hit)
mana = 0
End If
End Sub
End Class
Module Module1
{VBNET_MAIN_BLOCK}
' creating objects
Dim die As RollingDie = New RollingDie(10)
Dim zalgoren As Warrior = New Warrior("Zalgoren", 100, 20, 10, die)
Dim gandalf As Warrior = New Mage("Gandalf", 60, 15, 12, die, 30, 45)
Dim arena As Arena = New Arena(zalgoren, gandalf, die)
' fight
arena.Fight()
Console.ReadKey()
{/VBNET_MAIN_BLOCK}
{/VBNET_OOP}
There are lots of time-saving techniques we can set up using inheritance. In this case, all it did is save us a few lines, but in a larger project, it would make a huge difference.
The application now works as expected.
Console application
-------------- Arena --------------
Warriors health:
Zalgoren [########### ]
Gandalf [############## ]
Gandalf attacks with a hit worth 23 hp
Zalgoren defended against the attack but still lost 9 hp
For completeness' sake, let's make the arena show us the mage's current mana state using a mana bar. We'll add a public method and call it ManaBar(). It will return a String with a graphical mana indicator.
We'll modify the HealthBar() method in Warrior.vb to avoid writing the same graphical bar logic twice. Let me remind us how the original method looks:
Public Function HealthBar() As String Dim s As String = "[" Dim total As Integer = 20 Dim count As Double = Math.Round((health / maxHealth) * total) If count = 0 And Alive() Then count = 1 End If For i As Integer = 1 To count s &= "#" Next s = s.PadRight(total + 1) s &= "]" Return s End Sub
The health method doesn't really depend on a character's health. All it needs is a current value and a maximum value. Let's rename the method to GraphicalBar() and give it two parameters: current value and maximum value. We'll rename the health and maxHealth variables to current and maximum. We will also make the method Protected so we could use it in descendants:
Public Function GraphicalBar(current As Integer, maximum As Integer) As String Dim s As String = "[" Dim total As Integer = 20 Dim count As Double = Math.Round((current / maximum) * total) If count = 0 And Alive() Then count = 1 End If For i As Integer = 1 To count s &= "#" Next s = s.PadRight(total + 1) s &= "]" Return s End Function
Let's implement the HealthBar() method in Warrior.vb again. It'll be a one-liner now. All we have to do now is call the GraphicalBar() method and fill the parameters accordingly:
Public Function HealthBar() As String Return GraphicalBar(health, maxHealth) End Function
Of course, I could add the GraphicalBar() method in the Warrior class like I did with Attack() before, but I wanted to show you how to deal with cases where we would need to accomplish similar functionality multiple times. You'll need to put this kind of parametrization in practice since you never know exactly what you'll need from your program at any given moment during the design stage.
Now, we can easily draw graphical bars as needed. Let's move to Mage.vb and implement the ManaBar() method:
Public Function ManaBar() As String Return GraphicalBar(mana, maxMana) End Function
Simple, isn't it? Our mage is done now, all that's left to do is tell the arena to show mana in case the warrior is a mage. Let's move to Arena.vb.
Recognizing the object type
We'll add a separate printing method for warriors, PrintWarrior(), to keep things nice and neat. Its parameter will be a Warrior instance:
Private Sub PrintWarrior(w As Warrior) Console.WriteLine(w) Console.Write("Health: ") Console.WriteLine(w.HealthBar()) End Sub
Now, let's tell it to show the mana bar if the warrior is a mage. We'll use the TypeOf ... Is operator to do just that:
Private Sub PrintWarrior(w As Warrior) Console.WriteLine(w) Console.Write("Health: ") Console.WriteLine(w.HealthBar()) If TypeOf w Is Mage Then Console.Write("Mana: ") Console.WriteLine(DirectCast(w, Mage).ManaBar()) End If End Sub
We had to cast our warrior to the mage type in order to access the ManaBar() method. The warrior class doesn't have it. This is it, PrintWarrior() will be called in the Render() method, which now looks like this:
Imports System.Threading
Public Class RollingDie
Private random As Random
Private sidesCount As Integer
Public Sub New()
sidesCount = 6
random = New Random()
End Sub
Public Sub New(sidesCount As Integer)
Me.sidesCount = sidesCount
random = New Random()
End Sub
Public Function GetSidesCount() As Integer
Return sidesCount
End Function
Public Function Roll() As Integer
Return random.Next(1, sidesCount + 1)
End Function
Public Overrides Function ToString() As String
Return String.Format("Rolling die with {0} sides", sidesCount)
End Function
End Class
Public Class Warrior
Protected name As String
Protected health As Integer
Protected maxHealth As Integer
Protected damage As Integer
Protected defense As Integer
Protected die As RollingDie
Private message As String
Public Sub New(name As String, health As Integer, damage As Integer, defense As Integer, die As RollingDie)
Me.name = name
Me.health = health
Me.maxHealth = health
Me.damage = damage
Me.defense = defense
Me.die = die
End Sub
Public Overrides Function ToString() As String
Return name
End Function
Public Function Alive() As Boolean
Return health > 0
End Function
Public Function GraphicalBar(current As Integer, maximum As Integer) As String
Dim s As String = "["
Dim total As Integer = 20
Dim count As Double = Math.Round((current / maximum) * total)
If count = 0 And Alive()
count = 1
End If
For i As Integer = 1 To count
s &= "#"
Next
s = s.PadRight(total + 1)
s &= "]"
Return s
End Function
Public Function HealthBar() As String
Return GraphicalBar(health, maxHealth)
End Function
Public Overridable Sub Attack(enemy As Warrior)
Dim hit As Integer = damage + die.Roll()
SetMessage(String.Format("{0} attacks with a hit worth {1} hp", name, hit))
enemy.Defend(hit)
End Sub
Public Sub Defend(hit As Integer)
Dim injury As Integer = hit - (defense + die.Roll())
If injury > 0 Then
health -= injury
message = String.Format("{0} defended against the attack but still lost {1} hp", name, injury)
If health <= 0 Then
health = 0
message &= " and died"
End If
Else
message = String.Format("{0} blocked the hit", name)
End If
SetMessage(message)
End Sub
Protected Sub SetMessage(message As String)
Me.message = message
End Sub
Public Function GetLastMessage() As String
Return message
End Function
End Class
Public Class Arena
Private warrior1 As Warrior
Private warrior2 As Warrior
Private die As RollingDie
Public Sub New(warrior1 As Warrior, warrior2 As Warrior, die As RollingDie)
Me.warrior1 = warrior1
Me.warrior2 = warrior2
Me.die = die
End Sub
Private Sub Render()
Console.Clear()
Console.WriteLine("-------------- Arena -------------- " & vbCrLf)
Console.WriteLine("Warriors: " & vbCrLf)
PrintWarrior(warrior1)
Console.WriteLine()
PrintWarrior(warrior2)
Console.WriteLine()
End Sub
Private Sub PrintMessage(message As String)
Console.WriteLine(message)
Thread.Sleep(500)
End Sub
Public Sub Fight()
' The original order
Dim w1 As Warrior = warrior1
Dim w2 As Warrior = warrior2
Console.WriteLine("Welcome to the Arena!")
Console.WriteLine("Today {0} will battle against {1}!{2}", warrior1, warrior2, vbCrLf)
' swapping the warriors
Dim warrior2Starts As Boolean = (die.Roll() <= die.GetSidesCount() / 2)
If warrior2Starts Then
w1 = warrior2
w2 = warrior1
End If
Console.WriteLine("{0} goes first!{1}Let the battle begin...", w1, vbCrLf)
Console.ReadKey()
' fight loop
While w1.Alive() And w2.Alive()
w1.Attack(w2)
Render()
PrintMessage(w1.GetLastMessage()) ' attack message
PrintMessage(w2.GetLastMessage()) ' defense message
If w2.Alive() Then
w2.Attack(w1)
Render()
PrintMessage(w2.GetLastMessage()) ' attack message
PrintMessage(w1.GetLastMessage()) ' defense message
End If
Console.WriteLine()
End While
End Sub
Private Sub PrintWarrior(w As Warrior)
Console.WriteLine(w)
Console.Write("Health: ")
Console.WriteLine(w.HealthBar())
If TypeOf w Is Mage Then
Console.Write("Mana: ")
Console.WriteLine((DirectCast(w, Mage)).ManaBar())
End If
End Sub
End Class
Public Class Mage
Inherits Warrior
Private mana As Integer
Private maxMana As Integer
Private magicDamage As Integer
Public Sub New(name As String, health As Integer, damage As Integer, defense As Integer, die As RollingDie, mana As Integer, magicDamage As Integer)
MyBase.New(name, health, damage, defense, die)
Me.mana = mana
Me.maxMana = mana
Me.magicDamage = magicDamage
End Sub
Public Function ManaBar() As String
Return GraphicalBar(mana, maxMana)
End Function
Public Overrides Sub Attack(enemy As Warrior)
' Mana isn't full
If mana < maxMana Then
mana += 10
If mana > maxMana Then
mana = maxMana
End If
MyBase.Attack(enemy)
Else ' Magic damage
Dim hit As Integer = magicDamage + die.Roll()
SetMessage(String.Format("{0} used magic and took {1} hp off", name, hit))
enemy.Defend(hit)
mana = 0
End If
End Sub
End Class
Module Module1
{VBNET_MAIN_BLOCK}
' creating objects
Dim die As RollingDie = New RollingDie(10)
Dim zalgoren As Warrior = New Warrior("Zalgoren", 100, 20, 10, die)
Dim gandalf As Warrior = New Mage("Gandalf", 60, 15, 12, die, 30, 45)
Dim arena As Arena = New Arena(zalgoren, gandalf, die)
' fight
arena.Fight()
Console.ReadKey()
{/VBNET_MAIN_BLOCK}
{/VBNET_OOP}
Done
Console application
-------------- Arena --------------
Warriors:
Zalgoren
Health: [#### ]
Gandalf
Health: [####### ]
Mana: [# ]
Gandalf used magic and took 48 hp off
Zalgoren defended against the attack but still lost 33 hp
I added an ASCIIart Arena heading that I made using this application, http://patorjk.com/software/taag. Also, I colored the graphical bars using foreground and background colors. Lastly, I modified the rendering method of the indicators so it prints filled rectangles instead of # (you could type full rectangles using Alt + 219). Mine looks like this now, but you could do whatever you want with yours:
Console application
__ ____ ____ _ _ __
/__\ ( _ \( ___)( \( ) /__\
/(__)\ ) / )__) ) ( /(__)\
(__)(__)(_)\_)(____)(_)\_)(__)(__)
Warriors:
Zalgoren
Health: <span style="color:red">████</span><span style="color:maroon">████████████████</span>
Gandalf
Health: <span style="color:red">███████</span><span style="color:maroon">█████████████</span>
Mana: <span style="color:blue">█</span><span style="color:navy">███████████████████</span>
Gandalf used magic and took 48 hp off
Zalgoren received and blow and lost 33 hp
You can download the code below. If there is something you don't quite understand, try reading the lesson several times, this content is extremely important for you to know. In the next lesson, Solved tasks for OOP in VB .NET lessons 5-8, we'll explain the concept of static class members.
In the following exercise, Solved tasks for OOP in VB .NET lessons 5-8, we're gonna practice our knowledge from previous lessons.