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.
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's 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.