JavaScript week JavaScript week
This week up to 80% off on HTML/CSS and JavaScript courses.

Lesson 8 - Arena with a mage in Swift (inheritance and polymorphism)

In the previous lesson, Inheritance and polymorphism in Swift, we went over inheritance and polymorphism in Swift. 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 class in the Warrior.swift file, inherit it from the Warrior class, and give it extra properties (that warriors don't have). It'll look something like this:

class Mage: Warrior {
    private var mana : Double
    private var maxMana : Double
    private var magicDamage : Int
}

We don't have access to all the warrior variables in the mage class because we still have the properties in the Warrior class set to private. We'll have to change the private property modifiers to fileprivate in the Warrior class. All we really need now is the die and the name properties. Either way, we'll set all of the warrior's properties to fileprivate because they might come in handy for future descendants. On second thought, it wouldn't be wise to set the message property as fileprivate since it's not related to the warrior directly. With all of that in mind, the class would look something like this:

class Warrior {
    fileprivate var name : String
    fileprivate var health : Double
    fileprivate var maxHealth : Double
    fileprivate var damage : Int
    fileprivate var defense : Int
    fileprivate var die : RollingDie
    private var message : String = ""

    // ...

Moving on to the constructor.

Multiple constructors in Swift

Now we have the ideal opportunity to explain how multiple constructors work. Swift differentiate between designated and convenience constructors. A designated constructor is simply put the primary/default constructor. If we only have one init(), it's automatically designated.

If we want to have multiple constructors in a class, so that the class can be instantiated based on different parameters, we have to declare the others with the convenience keyword. Those constructors have to call the designated constructor using self. It's not very difficult, let's make a simple example. If we wanted to be able to create our warrior without any parameters, it'd look like this:

init(name: String, health: Int, damage: Int, defense: Int, die: RollingDie) {
    self.name = name
    self.health = health
    self.maxHealth = health
    self.damage = damage
    self.defense = defense
    self.die = die
}

convenience init() {
    self.init(name: "Default warrior", health: 100, damage: 20, defense: 10, die: RollingDie())
}

Now we can use the first constructor and enter all the parameters, or write just new Warrior() using the second constructor. By calling the designated constructor, it sets the default values.

Descendant constructor

Swift inherits constructors only in specific cases. A constructor is inherited if we assign default values to the new properties of the descendant (or if the properties are Optional). That way, the constructor isn't needed. We also can't create a designated constructor, the parent's constructor wouldn't be inherited in that case. It's possible to create convenience constructors.

In the case of our Mage, it'd be better to create our own constructor, because we have extra properties we want to set.

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. We don't call the parent constructor only if the parent doesn't have one. Our constructor, of course, must have all parameters needed to instantiate the parent and the new descendant ones as well. We'll pass some of them to the parent and process the rest by ourselves. It's necessary to call the parent constructor as the very last thing, otherwise Swift displays an error.

In Swift, there's the super keyword which is similar to self. Unlike self which refers to the particular class instance, super refers to the parent. This way, we can call the parent's constructor with the given parameters and then perform some additional initialization for the mage.

The mage constructor will look like this:

init(name: String, health: Int, damage: Int, defense: Int, die: RollingDie, mana: Int, magicDamage: Int) {
    self.mana = Double(mana)
    self.maxMana = self.mana
    self.magicDamage = magicDamage

    super.init(name: name, health: health, damage: damage, defense: defense, die: die)
}

we can call another constructor of the same class, not from the ancestor, using self instead of super.

Again, we internally converted mana to Double, you'll soon see why.

Let's move to main.swift and change the second warrior (Shadow) to mage, e.g. like this:

let gandalf : Warrior = Mage(name: "Gandalf", health: 60, damage: 15, defense: 12, die: die, mana: 30, magicDamage: 45)

We'll also have to change the line where we put the warrior in the arena. Note that we're still able to store the mage into a variable of the Warrior type because it's 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'd 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.

We'll override the parent method using the override keyword, see the example below.

Talking about methods, we'll certainly need a setMessage() method which is private now. Let's make it fileprivate:

fileprivate func setMessage(_ message: String)

When designing the class, we should have considered it might have descendants and therefore mark appropriate properties and methods as fileprivate.

Now let's go back to the descendant and override the attack() method. We'll declare the method in Mage.swift just as we're used to. However, we'll start its definition with the override keyword which tells Swift we're aware that the method is inherited but we want to change its behavior.

override func attack(enemy: Warrior)

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.

override func attack(enemy: Warrior) {
    var hit = 0

    // Mana isn't full
    if mana < maxMana {
        mana += 10
        if mana > maxMana {
            mana = maxMana
        }
        hit = damage + die.roll()
        setMessage("\(name) attacks with a hit worth \(hit) hp")
    } else { // Magic attack
        hit = magicDamage + die.roll()
        setMessage("\(name) used magic for \(hit) hp")
        mana = 0
    }
    enemy.defend(hit: hit)
}

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. It'd surely be efficient to call the parent method instead of rewriting the behavior. We'll use super for this:

override func attack(enemy: Warrior) {
    var hit = 0

    // Mana isn't full
    if mana < maxMana {
        mana += 10

        if mana > maxMana {
            mana = maxMana
        }
        super.attack(enemy: enemy)
        setMessage("\(name) attacks with a hit worth \(hit) hp")
    } else { // Magic attack
        hit = magicDamage + die.roll()
        setMessage("\(name) used magic and took \(hit) hp off")
        mana = 0
    }
    enemy.defend(hit: hit)
}

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.

-------------- 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.swift to avoid writing the same graphical bar logic twice. Let me remind us how the original method looks:

func healthBar() -> String {
    var s = "["
    val total : Double = 20

    var count : Double = round((health / maxHealth) * total)
    if (count == 0) && (alive()) {
        count = 1
    }

    for _ in 0..<Int(count) {
        s += "#"
    }

    s = s.padding(toLength: Int(total) + 1, withPad: " ", startingAt: 0)
    s += "]"
    return s
}

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 fileprivate so we could use it in descendants:

fileprivate func graphicalBar(current: Double, maximum: Double) -> String {
    var s = "["
    val total : Double = 20

    var count : Double = round((current / maximum) * total)
    if (count == 0) && (alive()) {
        count = 1
    }

    for _ in 0..<Int(count) {
        s += "#"
    }

    s = s.padding(toLength: Int(total) + 1, withPad: " ", startingAt: 0)
    s += "]"
    return s
}

Let's implement the healthBar() method in Warrior again. It'll be a one-liner now. All we have to do now is call the graphicalBar() method and fill the parameters accordingly:

func healthBar() -> String {
    return graphicalBar(current: health, maximum: maxHealth)
}

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 and implement the manaBar() method:

fun manaBar(): String {
    return graphicalBar(current: mana, maximum: maxMana)
}

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.swift.

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:

func printWarrior(_ w: Warrior) {
    print(w)
    print("Health: ", terminator: " ")
    print(w.healthBar())
}

Now, let's tell it to show the mana bar if the warrior is a mage. We'll use the is operator to do just that:

func printWarrior(_ w: Warrior) {
    print(w)
    print("Health: ", terminator: " ")
    print(w.healthBar())
    if w is Mage {
        print("Mana: ", terminator: " ")
        print((w as! Mage).manaBar())
    }
}

We had to cast our warrior to the mage type in order to access the manaBar() method. The warrior class doesn't have it. The exclamation mark we know from Optional has appeared once again. It works very similarly here. If the w variable wasn't of the Mage type internally, the program would crash. However, we first ask using is if its Mage and then force-cast it. We could use ? returning an Optional and allowing us to safely process the type casting result. However, it's not necessary nor appropriate here.

This is it, printWarrior() will be called in the render() method, which now looks like this:

func render() {
    print("\n \n \n \n \n \n \n \n")
    print("-------------- Arena -------------- \n")
    print("Health: \n")
    printWarrior(warrior1)

    print(" ")
    printWarrior(warrior2)
}

Done :)

-------------- 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

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


 

 

Activities (2)

 

 

Comments

To maintain the quality of discussion, we only allow registered members to comment. Sign in. If you're new, Sign up, it's free.

No one has commented yet - be the first!