PDA

View Full Version : Object oriented programming for beginners



Harry Hunt
27-07-2004, 04:01 PM
Object oriented programming for beginners

Introduction
Object orientation is a very cool concept that can be extremely useful in game development and how object orientation is done in Object Pascal is both clever and straightforward. Obviously it?¢_Ts a big topic so I will have to restrict myself to the basics, but if you?¢_Tre new to object oriented programming, this little article will give you an idea of how it?¢_Ts done.

When using Delphi, you?¢_Tre usually dealing with objects whether you want to or not. The form is an object, all controls are objects and even if you write a formless application, chances are you?¢_Tll be using some kind of object. Most Delphi programmers have a rough idea of what object oriented programming is, but there are a lot of people who, when they?¢_Tre writing their own apps or games, rather not write their own objects. This is understandable because there are many ways of modularizing your code even without using objects: you can place your code into separate units, you can use records to structure data, you can use procedures and functions, etc.
Here's an analogy for you: objects are like chocolate cake: you don?¢_Tt need them, but they make your life a lot sweeter. So how exactly do you benefit from objects?


The benefits of object oriented programming
The following is merely an attempt at convincing you that using objects is a good idea. If you don't understand all of the example code in this passage, don't worry. I will explain how to actually write objects later in this article.

Imagine you have written a small game with something around ten thousand lines of code. This code consists of all kinds of stuff including graphics routines , sound routines, routines for loading data, routines that handle the game logic, the controls, the AI, whatever. Because you like to keep things neat and clean, everything is carefully separated into procedures, records, etc. Everything interacts with everything else following certain rules. Now imagine you compile your game and the sound effects don?¢_Tt work correctly. After nine hours of bug-hunting you finally realize that the sound procedure wasn?¢_Tt the problem?¢_¬¶ what caused the problem was that in the initialisation procedure of your game you set a global variable to an incorrect value which then caused seven other procedures to not do what they're supposed to do.
If you for a moment ignore the fact that global variables are ?¢_osloppy coding?¢__ you will realize that using just procedures can be a bit problematic sometimes and that debugging can be a big pain in the lower back region.

The solution for this and basically all the world?¢_Ts problems is object oriented programming. If you had encapsulated your sound routine into an object, that bug wouldn?¢_Tt have happened in the first place, and if there had been other bugs isntead, it would?¢_Tve been much easier to find them. To understand why this is the case, let met tell you a few things about objects:

An object consists essentially of two parts, the specification and the implementation. The specification is visible to any application using the object while the implementation is not. This means that as long as you don?¢_Tt touch the specification, you can change the implementation any way you want and no one will ever notice.

Here is an example


unit MyExampleObject;

interface

type
TMyObject = class(TObject)
public
function Multiply(X, Y: Integer): Integer;
end;


The above is the specification of an object, now look at these two different implementations:


function TMyObject.Multiply(X, Y: Integer): Integer;
begin
Result := X * Y;
end;

function TMyObject.Multiply(X, Y: Integer): Integer;
var
I: Integer;
begin
Result := Y;
for I := 1 to X - 1 do
Result := Result + Y;
end;


These are two completely different implementations of the same function. Imagine you're writing a game and design an object for it that does all the sound playback. Initiall you thought using MMSystem for the sound playback would be sufficent. If you later realize you want multi-channel audio and switch to DirectSound, all you'd have to do is to change the implementation of your sound object. You won't have to change the sepecification and therefore you also don't have to change your game code.

Once you got an object working, you can put in a separate unit and totally forget about it. Of course there could still be bugs in it but if there are, you'll most likely find them very quickly because you know where you have to search. And since there's only a strictly defined interface through which the main application can communicate with your object, you can be sure that no global variables can mess up things.

I'm sure you've noticed how in the specification (or interface) of every Delphi form there's an area for "public declarations" and one for "private declarations" and I'm also sure (tell me if I'm wrong) that you've noticed how you cannot access private-declared variables from outside your form's implementation. This a very important concept of object oriented programming called information hiding. The idea of this is to hide everything that is not directly required for a program to communicate with your object. This includes the actual implementation but also any variable that is only used internally.

An example:

unit MyExampleObject;

interface

type
TMyObject = class(TObject)
public
MyPi: Single;
procedure Init;
function CircleArea(Radius: Single): Single;
end;

implementation

procedure TMyObject.Init;
begin
MyPi := Arctan(1) * 4;
end;

function TMyObject.CircleArea(Radius: Single): Single;
begin
Result := (Radius * Radius) * MyPi;
end;


Why does it always have to be math? Quite frankly, I don't know. So the CirclerArea function given the radius returns the area of a circle. Let's see how you'd use that object in a program:

An example:

procedure MyProcedure;
var
MyObject: TMyObject;
begin
MyObject := TMyObject.Create;
MyObject.Init;
ShowMessage(FloatToStr(MyObject.CircleArea(7.2453) ));
MyObject.Free;
end;


This works fine, but what if I do this:


procedure MyProcedure;
var
MyObject: TMyObject;
begin
MyObject := TMyObject.Create;
MyObject.Init;
MyObject.MyPi := 17; // Evil!
ShowMessage(FloatToStr(MyObject.CircleArea(7.2453) ));
MyObject.Free;
end;


Whoops! All of a sudden you get a completely different (incorrect) result. In this example MyPi shouldn't be accessible from outside the object because changing it will cause the program to not function properly.

The fixed specification:

unit MyExampleObject;

interface

type
TMyObject = class(TObject)
private
MyPi: Single;
public
procedure Init;
function CircleArea(Radius: Single): Single;
end;



So let's say you need an object that can calculate both the area of a circle and the perimeter. How would you do that? You could just copy the code from your old object and add a new function or you could do this:

The fixed specification:


unit MyExampleObject;

interface

type
TMyObjectEx = class(TMyObject)
public
function CirclePerimeter(Radius: Single): Single;
end;

implementation

function TMyObjectEx.CirclePerimeter(Radius: Single): Single;
begin
Result := 2 * Radius * MyPi;
end;


If you now update your program like this:


procedure MyProcedure;
var
MyObjectEx: TMyObjectEx;
begin
MyObjectEx := TMyObjectEx.Create;
MyObjectEx.Init;
ShowMessage(FloatToStr(MyObjectEx.CircleArea(7.245 3)));
ShowMessage(FloatToStr(MyObjectEx.CirclePerimeter( 7.2453)));
MyObjectEx.Free;
end;


you will see that it works. This is called inheritance. Your new object TMyObjectEx inherits the CircleArea procedure and the MyPi variable from TMyObject. How is this useful? Here's an example: when writing games you're often dealing with three types of characters: players, enemies and NPCs. You could of course create three records, one for each of them or you could do this (which is infinitely cooler):


type
TCharacter = class
public
Name: string;
X, Y, Z: Single;
Health: Byte;
Mana: Byte;
end;

TPlayer = class(TCharacter)
public
Inventory: array of TInventoryItem;
Experience: Integer;
Strength: Byte;
{...}
procedure DrinkPotion(Potion: TPotion);
end;

TEnemy = class(TCharacter)
public
TimeSinceBreakfast: TDateTime;
Evilness: Integer;
{...}
procedure AttackPlayer;
procedure Die;
end;

TNPC = class(TCharacter)
private
LastDialogLine: Integer;
public
DialogLines: TStringList;
Friendliness: Integer;
Merchant: Boolean;
{...}
procedure BeginConversation;
end;

TSlimePrince = class(TEnemy) // Slime Prince is a registed trade-mark of Blizzard North
public
Slimyness: Integer;
end;

TBarbarian = class(TPlayer)
public
{...}
end;


Nice, eh?



Let's do it!
Okay, enough with the chit-chat, let's do it! You're going to write an object that maintains a high-score list and then you're going to write a new object that will not only maintain a high-score list but also save this high-score list in an encrypted format.

Create a new Delphi application, then click "File -> New -> Unit".
Your new object will be called THighScoreList. Why the "T" you ask? To pay homage to Mr. T of course (I appologize for this very lame joke). The T stands for "Type" and it's a naming convention that helps you distinguish between abstract or complex data types and elementary data types. After choosing a name for your object, you need to pick an ancestor class for it. Your object will inherit that classes properties and methods so you should think about what kind of basic functionality you need. The "mother" of all classes is TObject which doesn't have any real functionality at all. However, since you will need to maintain a list of all scores, it would be nice to inherit that kind of functionality from an ancestor and what could be more suitable for that than a TStringList? So here goes, change the code in the new unit you've just created to this:


unit Unit2;

interface

uses Classes;

type
THighScoreList = class(TStringList)
private

public

end;

implementation

end.


Nice. Since you chose TStringList as ancestor for your object, you need to include the Classes unit with the uses command. Also include SysUtils - you're gonna need it in a sec. The original TStringList stores strings, you however will have to store names and scores so basically strings and integers. You will do that by converting the score to a string, then adding it to the string list along with the name and a separator character between the two. Your object inherited a method "Add" from TStringList, this method however only takes one parameter and you need two. So let's write our a new Add procedure:


unit Unit2;

interface

uses Classes, SysUtils;
type
THighScoreList = class(TStringList)
private

public
procedure Add(Name: string; Score: Integer);
end;

implementation

procedure THighScoreList.Add(Name: string; Score: Integer);
var
I: Integer;
S: string;
Done: Boolean;
begin
Done := False;
for I := 0 to Count - 1 do
begin
S := Copy(Strings[I], 1, Pos(';', Strings[I]) - 1);
if Score > StrToInt(S) then
begin
Insert(I, IntToStr(Score) + ';' + Name);
Done := True;
Break;
end;
end;
if not Done then
inherited Add(IntToStr(Score) + ';' + Name);
end;

end.


What happened here? The new method Add hides the old inherited method Add (because it has the same name) meaning the old method is no longer accessible from outside your object. The new method has two parameters, name and score. When the Add method is called, it tries to find the proper place for the score (The object maintains a sorted list with the highest scores first) and then inserts it accordingly. If the score isn't bigger than anything that's already in the list, the new score is added to the end of the list. This can't be done just like that because the old Add method is no longer visible and if you'd simply use the new Add method, you'd create an infinite recursion. This is where the little word "inherited" comes in. Using inherited you can access the methods of the object's ancestor class even if they're hidden by your new methods. And guess what, this is already it. Your object inherited the SaveToFile and LoadFromFile methods from its ancestor, the individual scores can be accessed like this: MyHighScoreList[Index], there's a "Count" property, etc.

So let's get a little crazy. You will now create a new class that inherits the Add method from your old class, but this new class will save the score to an encrypted file.


unit Unit3;

interface

uses Classes, SysUtils;

type

THighScoreListEx = class(THighScoreList)
private
FEncrypt: Boolean;
FKey: string;
public
constructor Create;
procedure SaveToFile(const FileName: string); override;
procedure LoadFromFile(const FileName: string); override;
property Encrypt: Boolean read FEncrypt write FEncrypt;
property Key: string read FKey write FKey;
end;

implementation

end;




Woah! Loads of new things. First of all, your new object THighScoreListEx has the ancestor THighScoreList so it inherits your extended Add method. Then there are two new procedures. THighScoreListEx inherits SaveToFile and LoadFromFile from TStrings, however, the original methods don't support encryption so we "override" or replace them with two procedures with identical parameters. This allows you to change the implementation without changing the interface.
It might be a good idea to allow anyone who uses your object to decide whether they want the files to be encrypted or not, so there's also a new property Encrypt. So why a property and not just a public variable? In this case it wouldn't make a difference if you'd simply use a variable, however, properties will generally give you more control than a variable. For example you can create properties that are read-only simply by leaving out the "write" part or you can create properties that whenever changed run a procedure allowing you to react instantly to any changes made. There's also a new property "Key" which allows the user to choose an encryption key. The last thing that's new is the constructor. A constructor is used to create a new instance of an object. An example:


MyBitmap := TBitmap.Create;


This will call the constructor of the Bitmap. The constructor is basically a procedure and it's a good place to initialize variables and stuff like that. It also has an antagonist, the destructor which is called whenever an instance of an object is freed.

So let's implement everything:


unit Unit3;

interface

uses Classes, SysUtils;

type

THighScoreListEx = class(THighScoreList)
private
FEncrypt: Boolean;
FKey: string;
public
constructor Create;
procedure SaveToFile(const FileName: string); override;
procedure LoadFromFile(const FileName: string); override;
property Encrypt: Boolean read FEncrypt write FEncrypt;
property Key: string read FKey write FKey;
end;

implementation

constructor THighScoreListEx.Create;
begin
inherited;
FKey := 'dslhldg6sd896g896sd8g6sd6g86sdgsdgsdgsdg6sd8gsdg8 7sd5gsd85g';
end;

procedure THighScoreListEx.LoadFromFile(const FileName: string);
var
TempList: TStringList;
S, T: string;
I, J, K: Integer;
begin
if not FEncrypt then
inherited LoadFromFile(FileName)
else
begin
if not FileExists(FileName) then
Exit;
Clear;
TempList := TStringList.Create;
TempList.LoadFromFile(FileName);
for I := 0 to TempList.Count - 1 do
begin
S := TempList[I];
T := '';
K := 1;
for J := 1 to Length(S) do
begin
T := T + Char(Ord(S[I]) xor Ord(FKey[K]));
K := K + 1;
if K > Length(FKey) then
K := 1;
end;
TempList[I] := T;
end;
AddStrings(TempList);
TempList.Free;
end;
end;

procedure THighScoreListEx.SaveToFile(const FileName: string);
var
TempList: TStringList;
S, T: string;
I, J, K: Integer;
begin
if not FEncrypt then
inherited SaveToFile(FileName)
else
begin
TempList := TStringList.Create;
for I := 0 to Count - 1 do
begin
S := Strings[I];
T := '';
K := 1;
for J := 1 to Length(S) do
begin
T := T + Char(Ord(S[I]) xor Ord(FKey[K]));
K := K + 1;
if K > Length(FKey) then
K := 1;
end;
TempList.Add(T);
end;
TempList.SaveToFile(FileName);
TempList.Free;
end;
end;


end;



In the constructor a string is assigned to the Key property thereby making it optional for the user to choose their own key. The SaveToFile and LoadToFile both work the same way: first check if Encrypt is set to true, if not, simply call the inherited SaveToFile or LoadFromFile method which will then save the content of your list to an unencrypted file. If Encrypt is set to true, create a new string list, go through the high score list line by line, encrypt each line and then add it to a temporary string list which we then save to the hard disk.

And that's it for now! I hope you enjoyed reading this and I definitely hope you're going to use your own objects in your next project. If you have any questions, feel free to ask and I'll be happy to answer them.

Gadget
28-07-2004, 09:38 AM
Nice intro to OOP ;)

Even I learnt something there =D Didn't realise that a method using an exisiting name would hide the inherited version.

Harry Hunt
28-07-2004, 09:44 AM
Glad you liked it. I might write another article about polymorphism and stuff like that if I find the time.

Ultra
29-07-2004, 01:35 AM
I liked it! (But for some reason I feel like I also should read it without five beers in me... :? ) I feel like there are to few newbie tutorials on the net on OOP (or any other part of the delphi language for that matter).

The things I missed however (though they may be a bit to complicated) was how to use OOP without the VCL. Sure the VCL is great and all that, but one of the reasons it took so long for me to learn proper programming was that I used the VCL for everything and never got down and dirty with more low level stuff (so that I could learn how all this came together). Console applications are great... The second thing was some design patterns like singletons (I like singletons :wink: ) or such, but I guess people should learn polymorphism first. :wink: