Just read the RPG thing, but OOP solved a different kind of problem when it was first introduced:
What is the data, and what can you do with it?
Example: a vector.
In C, you would define a vector 2D as a struct like:
struct Vector2 {
float x;
float y;
};
So that’s the data: a “vector” is made up of “x” and “y”. But data alone is not useful, you may want to do things with this vector. Say you want to get the length of this vector, you would then write:
float vector_length(struct Vector2 *v) {
return sqrt(v->x * v->x + v->y * v->y)
}
But you can see how the data and methods to operate on it are “disconnected”.
In Python with OOP, you can create a “class” which essentially is data + functions to act upon said data.
class Vector2:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def length(self):
return sqrt(self.x * self.x + self.y * self.y)
# usage:
v = Vector2(3, 4)
v.length() # returns 5
v.x = 0
v.length() # returns 4
Then there’s the concept of inheritance, like how 2D vectors have things in common with 3D vectors:
# in Python you use parenthesis to express inheritance:
class Vector3(Vector2):
def __init__(self, x=0, y=0, z=0):
super().__init__(x, y) # delegate work to Vector2, it will set x and y
self.z = z
def length(self):
return cubic_root(self.x * self.x + self.y * self.y + self.z * self.z)
# usage:
v = Vector3(3, 4, 0)
v.length() # returns 5
v.x = 0
v.length() # returns 4
v.z = 3
v.length() # returns 5
Vectors are a bad OOP example though, let’s try something else.
Let’s take an RPG example:
class NPC:
def __init__(self, max_life, name):
'''
Data on a NPC: "max_life", "life", and "name"
'''
self.max_life = max_life
self.life = max_life
self.name = name
def deal_damage(self, damage):
'''
Removes "damage" from current "life". Make sure we dont go below 0.
'''
self.life = max(0, self.life - damage)
NPCs will track their “max_life” as well as their current “life”. We will also give them a name, but we won’t use this data in this example.
class Villager(NPC):
'''
Villagers are NPC.
Villagers with the role "priest" take twice as much damage as other NPCs!
'''
def __init__(self, name, role):
'''
All villagers have a "max_life" of 100.
Villagers can have a specific role, which is not used at the moment.
'''
super().__init__(100, name)
self.role = role
def deal_damage(self, damage):
# detect this instance is a priest:
if self.role == 'priest':
damage = damage * 2
# the NPC class already knows how to apply damage, let's use that:
super().deal_damage(damage)
Here you can see how inheritance helps us. Since the NPC class knows how to handle max_life, life and damages, we can then focus on specialties for villagers.
In this case, we add a role property to our data that defines NPCs, and we apply double damage based on the value stored in “role”.
Even better, all code that expects an instance of “NPC” should be able to handle instances of “Villager”, since villagers will share functions with NPCs.
npc1 = NPC(max_life=100, 'marc')
npc2 = Villager('henry', 'priest')
# same calls, but different instances:
npc1.deal_damage(10)
print(npc1.life) # 90
npc2.deal_damage(10)
print(npc2.life) # 80
OOP is all about data management. But you should also use it when different type of data must go through the same workflow. Example:
class Action:
def run(self):
raise NotImplementedError('subclasses must define this')
Let “Action” be our interface: something that has a “run” method.
class Attack(Action):
def __init__(self, damage):
self.damage = damage
def run(self):
print(self.damage)
The “Attack” class is the useful thing here: it actually defines how its internal data should look like, and implements what “run” should do. You could create a variety of actions, and as long as all actions have a “run” method like this, you will be able to use them all in the same way:
for action in actions:
action.run() # don't care what action is, as long as it is a subclass of "Action".
Although, interfaces are more important for statically typed languages. In Python, you should practice “duck typing”: even if instances don’t share a common class, as long as they have the same methods then its ok, it will work!
class Duck:
def quack(self):
print('quack quack')
class Bird:
def quack(self):
print('cui cui')
for thing in [Duck(), Bird()]:
thing.quack() # works, the bird knows how to quack!
Hope this helps. If not, just play around with it.