Skip to content
Tags

85. A practical example showing the value of classes

June 14, 2013

This example takes you through the various options for programming a particular game, showing why classes work so well. If you’re not sure when to use classes, or what alternatives you have, this may make it clearer when to use each of them.
(If you don’t know what classes are, please look at my posts 7 and 8, first).

Suppose we’re creating a DD (dungeons and dragons) roleplay game, and we have different character types – humans, elves, goblins etc. They have various attributes like strength, and magic – which vary not only for each type, but also for each individual as they interact with the map and other players – and actions like walking, fighting, and talking – which are also different for each type of character.

So, just as in real life, there are differences between the different player types, and also between individuals in those types.

First, let’s think about how we’d do this in a physical board game, then in Lua. For the moment, we’ll just look at moving around the map.

One size fits all

We could have a big sheet of paper listing all the general rules, followed by all the differences for each player type, eg dwarves have trouble in long grass or whatever. Then, when each player, moved, they would have to look for themselves on this sheet, and see what they were supposed to do.

In Lua, this is the same as setting up a big Move function which has all the rules in it, and lots of “if tests”, ie if this is a dwarf, then do this, or if it’s an elf, do this, do that.

The problem with both of these is that you are continually looking things up and doing if tests, and it is a great big pile of stuff. There must be a better way.

Player type-specific

Now I don’t play DD, so I don’t know how it’s organised, but I imagine that rather than having one great big Move sheet that applies to all players, it makes sense to have one Move sheet that lists all the general rules that apply to everyone, and then a separate sheet for each player type that lists anything special about the way they move (it might have lots of other information about that type as well). Then the players can look at their own sheets when they make a move.

If you get what I’m talking about, I’ll now explain how we’d do this in Lua. Follow carefully, this is important.

First, we’d write some ordinary functions for the stuff that is the same for all classes, eg to check players aren’t falling off the edge of the map.

Next, we write type-specific functions to handle the differences between types. We could do this by writing functions called elf_Walk and dwarf_Walk, and choose the right one to use for each character. However, it is very inefficient using lots of if tests to choose the right function every time you want a character to do something (ie if thisChar.type==”dwarf” then dwarf_Walk() else …..). And it’s not much better than having everything in one big function.

A character table

In my earlier post 31, I showed how you can group functions together in a table, something I called “local functions”. (It may help you to go back and read that earlier post, but I won’t assume you have done so, below).

You could use this approach to manage different player types, like this.

First, you set up a table for each player type, containing information and functions for all the actions you will need for that type. I’ve given an example for elves below.

Elf={}  --define table

--store basic info about elves
Elf.walkSpeed=10
Elf.runSpeed=15
Elf.hitPoints=8  --damage
Elf.Eyesight=25 --distance seen in pixels

function Elf.Move(char)
   --code follows to calculate new position
    char.x=char.x+moveX --see explanation below
    char.y=char.y+moveY
end

function Elf.Fight(char1,char2) --char2 is opponent
   --code here
   return hitPoints
end

So we define a table called Elf, and store a few general things about elves that we will need, like how fast they walk and run.

Then we add some functions to move, fight, and all the other things we will need them to do. Functions like Move need to be given information about whichever elf we are moving, like current position, direction, whether we are walking and running, and our current health – so we pass through a table of values for the elf, which I’ve called char (more detail below). We calculate the new position, and update the x and y value in the char table.

So how does this help us?

There are (at least) two reasons you might put stuff like this, including functions, in a table. First, you are keeping everything about each type together, and separate from the other types.

Second, because now we can do this. I’ll explain below.

    elrond={}
    elrond.attrib={strength=5,magic=8, x=0,y=0,...}
    elrond.type=Elf
    arwen={}
    arwen.attrib={strength=3,magic=6, x=0, y=0,....}
    arwen.type=Elf
    gimli={}
    gimli.attrib={strength=8,magic=0,x=100,y=200,...}
    gimli.type=Dwarf

    ....later in the code, we want them to move
    elrond.type.Move(elrond.attrib)
    arwen.type.Move(arwen.attrib)
    gimli.type.Move(gimli.attrib)

I’ll just look at the first one, elrond. We define him as a table so we can put a lot of stuff in there. The first thing we define is his individual attributes like strength and magic, with the name attrib (for attributes).

Then we define his type as Elf. More precisely, we set his type equal to Elf. What does this do? It means that every time we write elrond.type, it is the same as writing Elf. Note we didn’t say elrond.type=”Elf”. We are literally setting elrond.type to be the same as the table called Elf, not a piece of text called “Elf”.

If you read the previous post about pointers, this will not seem too strange, as you will understand that elrond.type and arwen.type are both pointing at the same table, Elf. So if I make any changes to Elf, it will affect both elrond and arwen.

Now look what we can do. When the game is running, I can say elrond.type.Move without doing any testing to see what kind of creature he is, because elrond.type points at the correct character table, and will run Elf.Move. I can do the same for all the other creatures, eg gimli.type.Move will run Dwarf.Move.

So using character tables like this means I can assign each character to one of them, and if I want to move, fight, eat, or whatever, I can just say things like elrond.type.Fight. As long as each character class has all the same functions (Move, Fight etc), this will work fine, and it means I can program all the differences between classes into their individual tables of functions.

So tables of functions are great for handling differences between character classes, and that would be all you needed, if individuals were identical. In some games, this will be the case, so a table of functions is all you need.

But in our DD case, individuals are different, and their attributes change as the game goes on. So a “type table” handles the differences between character types, but not between individuals. For example, when I say elrond.type.Move, I am going to have to pass through information like his current x,y position, his direction, maybe his health, etc. I can do this by passing elrond.attrib to the elf.Move function, because it contains all elrond’s personal information.

But you can see it’s a little awkward, because I have elrond’s personal information in one place, and the generic Elf functions in another. Is there a way to combine them?

Yes, classes.

In what follows, I’m going to assume you have read my earlier posts on classes, even though I will cover some of the explanations again below.

So let’s look at the code first, then think about what difference it makes.

Elf=class  --define class

--store basic info about elves
Elf.walkSpeed=10
Elf.runSpeed=15
Elf.hitPoints=8  --damage
Elf.Eyesight=25 --distance seen in pixels

--set up an individual elf
function Elf:init(a)  --expects table of attributes
    self.strength=a.strength
    self.magic=a.magic
    self.x, self.y = a.x, a.y
    --etc etc
end

function Elf:Move()
   --code follows to calculate distance moved, say moveX, moveY
    self.x=self.x+moveX --see explanation below
    self.y=self.y+moveY
end

function Elf:Fight() --char is opponent
   --code here
   return hitPoints
end

and this is how we use the class

    elrond=Elf({strength=5,magic=8, x=0,y=0,...})
    arwen=Elf({strength=3,magic=6, x=0, y=0,....})
    gimli=Dwarf({strength=8,magic=0,x=100,y=200,...})

    ....later in the code, we want them to move
    elrond.Move()
    arwen.Move()
    gimli.Move()

Now, apart from defining it as a class, what are the differences? You’ll see we now store elrond’s personal attributes in “self” variables within the class. I think of “self” variables as things which are different for each individual using the class, like x,y position, whereas things like runSpeed are the same for all elves. So think of the self prefix as saying “this information is just for me, nobody else”.

So to set up elrond, all we do is pass through all his information, and the Elf:init function stores it all away in “self” variables. When we set arwen up, her information will be stored in separate self variables specifically for her.

Now whenever we use any of the class functions, we don’t have to pass through any attributes for elrond, because the class already has them stored away. However, we do need to tell the class which elf is talking to it. And we do this by using colons. You’ll notice all the functions in our class have a colon between Elf and the function name. And this is a special Codea thing. It tells Codea that it needs to pass through the ID of the elf that is calling the function.

Codea does this really simply, by including an extra parameter which is the ID of your elf (or dwarf or whatever). So when Elf:Move is run, it is given an ID as well, which allows it to look up the right set of self variables.

You can test this by calling Elf.Move with a dot instead of a colon. It will work unless the Move function uses a “self” variable, in which case Codea throws an error, because it doesn’t know which elf you’re talking about. So that’s why you use colons with classes and dots with everything else. Colons simply say “please pass my ID to the function along with everything else”.

As an aside, this means that if you have any functions in your class which don’t use any “self” information, you can call them with either a dot or a colon, and they will work fine. The colon is only needed where you have “self” variables, to identify the individual.

But how did Codea know which ID to pass to Elf? Quite simple. You’ll notice we defined elrond as

     elrond=Elf({strength=5,magic=8, x=0,y=0,...})

What do you think got put in the variable elrond? What Codea did was to store elrond’s “self” variables somewhere safe under a pointer (ie memory address), and pass back just this pointer (address), which was stored in elrond. So any time we say elrond:Move(), Code passes this pointer (address) back to Elf again as an extra parameter.

And if we define arwen as Elf as well, she will get a different ID back, to be stored in arwen, and when we say arwen:Move(), that ID will be passed.

And that is how one class can manage

  • different player classes AND
  • individual attributes

So classes are truly awesome.

From → Programming

One Comment

Trackbacks & Pingbacks

  1. 169. Why tables and classes are so useful | coolcodea

Leave a comment