Lesson 8 - Arena with a mage in VB.NET (inheritance and polymorphism)

Visual Basic .NET OOP 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. :)

Mage

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:

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

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:

Private Sub Render()
        Console.Clear()
        Console.WriteLine("-------------- Arena -------------- " & vbCrLf)
        Console.WriteLine("Warriors: " & vbCrLf)
        PrintWarrior(warrior1)
        Console.WriteLine()
        PrintWarrior(warrior2)
        Console.WriteLine()
End Sub
Module Module1

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 in the indicators so it prints full 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: ████████████████████

Gandalf
Health: ████████████████████
Mana:   ███████████████████

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, Shared class members in VB.NET, we'll explain the concept of static class members.


 

 

Article has been written for you by Michal Zurek
Avatar
Do you like this article?
1 votes
Activities (11)

 

 

Comments

Avatar
Chen 琛
Member
Avatar
Chen 琛:24. April 9:48

Great tutorial of OOP with vb.net! Thanks, I think I will be finish reading in another 2 days

Edited 24. April 9:50
 
Reply 24. April 9:48
Avatar
Chen 琛
Member
Avatar
Chen 琛:2. May 9:17

Where is the downloadable source code :-? Also the online console application cannot be ran. It returned compiling error.still a great article! Read three times (v)

 
Reply 2. May 9:17
Avatar
David Capka
ICT.social team
Avatar
Replies to Chen 琛
David Capka:2. May 9:47

It looks like we haven't added the attachments to this course yet. The previous VB.NET basics course seems to be complete. I've put this course on the top of our TODO. There are also exercises for each lesson in C# which we haven't ported to VB.NET OOP yet. We'll start working on it next week. It's much more motivating when sb actually asks for it. There are also more VB.NET courses waiting to be ported (Files, Forms, Threading...). We'll do them with top priority. Stay tuned, it'll be all there very soon :)

Reply  +1 2. May 9:47
You can walk through a storm and feel the wind but you know you are not the wind.
Avatar
David Capka
ICT.social team
Avatar
Replies to Chen 琛
David Capka:2. May 11:07

The online compilers have been fixed :)

Reply  +1 2. May 11:07
You can walk through a storm and feel the wind but you know you are not the wind.
To maintain the quality of discussion, we only allow registered members to comment. Sign in. If you're new, Sign up, it's free.

4 messages from 4 displayed.