OOP might be a really nice convention for programmers to adopt when writing their big projects that they're being paid to write, but why should a game programmer bother with it? This is essentially the question asked by someone recently rather off-topically in a discussion. Rather than start a heated debate on the pros and cons of OOP (though there were plenty of replies and it started getting there...) I'm going to try to answer that question here.
We start with the origin of procedural programming. In the beginning, you didn't have procedures. You basically had spaghetti code - along with capital letters, line numbers, and all that ugly stuff:
Code: Select all
100 PRINT "I AM A DOOF."
200 GOTO 100
So programmers somewhere, one day, decided to do this:
Code: Select all
Do
Print "I am a Doof."
Loop
Code: Select all
Sub printDoof
Print "I am a Doof."
End Sub
Do
printDoof
Loop
With structured programming, someone eventually figured out a rather neat idea. You see, your Subs and Functions might tend to get kind of long:
Code: Select all
Sub moveSprite (sprite As FB.Image Ptr, x As Integer, y As Integer, vx As Integer, vy As Integer, suchandsuch As uInteger, thisthatandtheother As Byte)
'...
End Sub
Code: Select all
Type myUDT
As Integer a, b, c, d, e, f, g, h, i, j
End Type
'Which do you like better?
Sub mySub1 (param1 As myUDT)
End Sub
Sub mySub2 (a As Integer, b As Integer, c As Integer, d As Integer ...)
End Sub
you would create a variable for your program
Code: Select all
110 DIM A AS STRING = "I AM A DOOF."
120 PRINT A
130 GOTO 120
Code: Select all
Dim Shared As Integer mynumber
Sub mySub
Print mynumber
End Sub
mynumber = 3
mySub
mynumber = 5
mySub
But there's just a little problem. What if you have a global variable and you have twenty different procedures that access it, maybe even modify it, and who knows what happens then? Maybe a bank program storing the balance of an account as a global variable. A bug is bound to happen sometime somewhere, with all that code, and you suddenly realize that the balance is off - way off. So which one of those twenty different procedures messed it up? And remember, this is a bank program, so it's probably made up of hundreds of lines of code, each of which might potentially access the account balance. That means hours of debugging for you. And in case you think I'm only talking about business programs - what about games? What if you store the sprite position x and y in global variables, you have twenty different procedures moving the sprite around and drawing it on the screen, and you suddenly notice the sprite is going all over the place on the screen and it's not where it's supposed to be? Then what? So which of those procedures did it?
Scope solves this. Only certain procedures can modify certain things, so you know which procedures to check and see if they modified it - and most of them don't, because you only let certain ones do it. So what if those procedures need to modify it? Then you pass things ByRef, and at least now you know which one did it. But that's not always safe, so we could run into problems there. Enter the UDT. It solves the problem of too many parameters, and since you pass it as a parameter to the procedures, you don't have the problems of global variables. Everything is in its proper scope, so you have one less thing to worry about. Unfortunately, if the procedures want to modify the variables inside the UDT, you have to pass it ByRef. And then you can have problems that way just like with global variables, because if you pass it ByRef to everything, you'll still have twenty procedures potentially modifying variables and messing things up. Now what? We need more scope rules!
This is what encapsulation is all about. You put the variables inside a UDT, but make some of them Private. What's that mean? That means nobody else can access those variables! They're hidden inside the UDT, so even if I pass the UDT to a procedure, the procedure can't access those variables. In fact, even the procedure (or main program) that creates the UDT can't access the variables:
Code: Select all
Type myUDT
Public:
As Integer a
Private:
As Integer b
End Type
Dim something As myUDT
'This is OK
something.a = 3
Print something.a
'This is NOT OK
something.b = 5
Print something.b
This is why we add things called methods to the UDT. It's the beginning of OOP. It may seem a little strange, but it really solves a lot of problems. Now only ONE procedure can modify the sprite's position, and if there's any bugs you know who did it!
Code: Select all
Type mySprite
Public:
Declare Sub move (newx As Integer, newy As Integer)
Declare Sub draw ()
Private:
As Integer x, y
End Type
Sub mySprite.move (newx As Integer, newy As Integer
x = newx
y = newy
End Sub
Sub mySprite.draw ()
PSet (x, y), &hffffff
End Sub
Code: Select all
Type mySprite
As Integer x, y
End Type
Sub moveSprite (tsprite As mySprite, newx As Integer, newy As Integer
tsprite.x = newx
tsprite.y = newy
End Sub
Sub drawSprite (tsprite As mySprite)
PSet (tsprite.x, tsprite.y), &hffffff
End Sub
You might not think it matters, but it does. For example, although indentation doesn't really have any effect on what a program does, it's still a lot easier to read
Code: Select all
If something = 1 Then
Do
Print "Hi"
Loop While something = 1
End If
Code: Select all
If something = 1 Then
Do
Print "Hi"
Loop While something = 1
End If
Code: Select all
Type mapType
As Integer x, y
As Any Ptr tiles
End Type
Declare Sub loadMap (filename As String, map As mapType)
Declare Sub drawMap (map As mapType)
Declare Sub moveMap (map As mapType, x As Integer, y As Integer)
Hopefully you're getting an idea of why OOP makes sense. It's really just another abstraction which takes some ideas to the next level. UDTs don't exist in real life; neither do procedures for that matter. At the low level, the variables are separate, the procedures in the UDTs are not "part of" anything at all, and all procedures are really just Goto statements (actually, it's a little better than that but I won't go into the details). But OOP saves you from scoping problems, allows you to work much more easily, and makes your code look nicer and more logical too!
But OOP goes beyond making code nicer and more logical. It makes a lot of things simpler, and generally lets you do some very interesting things you otherwise couldn't do. For example, strings. Normally the + is used to add two numbers, but what if you want to use it to concatenate two strings? That's what operator overloading is for! If you make a string object, you can use the + operator for the object and it does something totally different! You can do this with pretty much all the other standard operators too.
And then there's RAII.
cha0s wrote a tutorial about RAII too, but some people might not understand it so well. What it basically means is, when you create an object the object keeps track of all the resources it needs. Take for example a buffer in memory. If you create a buffer (say, to store your map data) then you need to use a pointer, and you need to Allocate/ReAllocate/DeAllocate. Now what if you forget to DeAllocate? You may eventually get memory leaks and problems! But if you put it inside an object, the object will automatically allocate the memory it needs when it needs it, and as soon as the object is destroyed (which will be at the end of the program or at the end of the procedure it's created in, depending on the scope) it will automatically deallocate the memory, thus ensuring you won't get any leaks (or wasted memory) by forgetting to deallocate a buffer when you're done with it.
This is done with Constructors and Destructors, which are two special kinds of procedures that are called automatically as soon as the object is created or destroyed. The other nice thing about them is that they let you set the object up, make sure the object is valid before you call any other methods. For example, you might want to draw your map, but what if you forgot to load it? Then the pointer to the buffer containing the map data is invalid and when you try to draw the map your program will likely crash. But if you have a Constructor, which gets called automatically no matter what, then it can set a special flag inside the object explaining that the map has not been loaded so it shouldn't be used. You can also allocate the pointer, so when the map loading routine needs to reallocate there's something to reallocate (and of course the destructor will deallocate it):
Code: Select all
Type mapType
Public:
Declare Constructor ()
Declare Destructor ()
Declare Sub load (filename As String)
Declare Sub draw ()
Declare Sub move (newx As Integer, newy As Integer)
Private:
As uByte Ptr _map_data
As uInteger _map_width, map_height
As Integer _x, _y
End Type
Now of course in practice, you would be very careful not to call draw() until you call load(). But if you forget, or make a mistake, setting things up this way can save you a lot of trouble.
Another thing OOP lets you do is Properties. Now as I said earlier we want to hide all the variables inside the object where nobody else can access them, because otherwise we can have problems. But always using a sub to access the variables may seem rather uncomforable. This is why we use properties. They seem like variables, and they act just like variables - for example, if x is a property of myUDT, then you can do
Code: Select all
myUDT.x = 3
Print myUDT.x
Of course, how you do things is entirely up to you. It might be easier at first not to use properties, operators, or even constructors and destructors. These things are just special extras that come with the OOP paradigm. You may want to start out just using methods and keeping all variables Private. It's still much safer and smarter than using UDTs with everything public. And you'll start to get a feel for OOP, and maybe even learn to enjoy it and prefer it to just plain procedural programming.
OOP is not the be-all and end-all solution to everything. I don't use OOP everywhere, by any means - but I do use it whenever I can, because it can save me a lot of trouble and make sure I do things right. And by the way, OOP and procedural programming can be done side-by-side. As I said, I don't use OOP for everything - for some things, a plain old-fashioned function or sub will work just fine. But when it's possible, when it's useful, when it makes things easier, safer, and cleaner, I use OOP.