Author Topic: FB Graphics #1.2.2: The Object Code  (Read 3886 times)

0 Members and 1 Guest are viewing this topic.

Offline rdc

  • Pentium
  • *****
  • Posts: 1495
  • Karma: 140
  • Yes, it is me.
    • View Profile
    • Clark Productions
FB Graphics #1.2.2: The Object Code
« on: July 13, 2009 »
Introduction

In the last tutorial we briefly looked at some OOP concepts, the most important relating to FreeBasic being encapsulation and information hiding. In this tutorial we will examine the screen object code in detail, and see how encapsulation and information hiding are achieved through the use of the type structure. We will also examine in more detail the idea of an interface, and why it is such an important element in a well designed object.

The Screen Object Definition

Code: [Select]
'Create the screen object definition.
Type screenobject
Private:
_width As Integer       'The screen width.
_height As Integer      'The screen height.
_buffer As UInteger Ptr 'The screen update buffer.
_initok As sbool        'Flag indicates screen set successfully. True, set ok, False error.
Public:
Declare Constructor (ByVal scrwidth As Integer, ByVal scrheight As Integer) 'Sets up a graphics screen.
Declare Destructor  'Cleans up created objects.
Declare Property GetStatus () As sbool 'Returns command status.
Declare Property GetWidth () As Integer  'Returns the screen width.
Declare Property GetHeight () As Integer 'Returns the screen height.
Declare Sub DrawToBuffer (ByRef x As Integer, ByRef y As Integer, ByRef clr As UInteger) 'Sets the x and y location of buffer to color.
Declare Sub CopyBuffer ()'Copies buffer to screen.
Declare Sub ClearBuffer ()'Clears buffer. Fills to 0.
End Type

As you have already seen, we have defined a Private section and a Public section in the code. The private section is the hidden (information hiding) part of the object. The only code that can see the private variables is the code within the object itself. Why is it important to keep this data hidden from the rest of the program? To maintain the data integrity. The _width and _height describe the current screen dimensions. If these values were to suddenly change, any code that relies on the _width and _height would become invalid, such as the error checking code within the DrawToBuffer procedure. The object would cease to perform as expected, and the errors could potentially be very hard to find.

One of the most important variables in the object is the _initok variable. This is a flag that indicates that the object was initialized properly. Before we do anything in the object code, we check to make sure that the _initok flag is set to True. If the screen wasn't created properly, then there is no reason to execute any code related to the screen, as it is going to fail. This ensures that the object will behave as expected if the screen was created properly, and will just exit gracefully if it wasn't. To get the current status of the object, the GetStatus property can be queried and will return True if the object initialized properly and False if there was some problem. This should be checked immediately after attempting to create the screen object.

The _buffer Pointer

The _buffer variable represents the current screen, or more precisely, the colors (in RGB(A) format) you would see on the screen. The object writes an RGB color to the _buffer via the DrawToBuffer procedure. When the user is finished updating the screen buffer, the buffer is copied to the screen using the CopyBuffer procedure.

The _buffer variable is defined as a Uinteger Ptr, that is a pointer to a UInteger. An RGB color is contained within a Uinteger (unsigned integer) to prevent overflows, so the buffer needs to be setup with the same data type. There are three reasons to define this as a pointer rather than as an array of UIntegers. First, we don't know what the screen size is going to be before the object is initialized, so we can't define a static array since we don't know dimensions to use. Second, FreeBasic doesn't support dynamic arrays within type definitions, but we can simulate a dynamic array by using a pointer buffer.

The third reason is speed. Updating a pointer is much faster than updating an array, since we are dealing with variable addresses rather than variable data. However, poking data into the _buffer isn't where we really get our speed boost, it is in the update of the screen.

Code: [Select]
'Screen object CopyBuffer sub.
'Copies the buffer to the screen.
Sub screenobject.CopyBuffer ()

'Make sue the object was initialized.
If this._initok = TRUE Then
'Need to lock the screen before we copy the buffer.
ScreenLock
'Using the CRT memcpy to fast copy the buffer to the screen memory.
memcpy ScreenPtr, this._buffer, this._width * this._height * SizeOf(UInteger)
ScreenUnLock
End If
End Sub

By using a pointer we can take advantage of the C Runtime (CRT) memcpy function and blast the update buffer to the graphic screen buffer. Memcpy takes a destination pointer, in this case the FreeBasic function ScreenPtr, which returns a pointer to the graphic screen buffer, the source buffer, our _buffer variable, and the number of bytes to copy. Since our _buffer is a UInteger buffer, to get the number of bytes we simply multiply the width of the screen by the height of the screen by the size of a UInteger, which happens to be 4 bytes.

Here is a good example of where information hiding is so important to maintaining data integrity. If the _width and _height could be changed will-nilly by the program, you could get a nasty crash here. Memcpy doesn't care what you are pointing at or what the buffer size really is, it will simply copy bytes from one location to another location, regardless of where those bytes are coming from, even if they happen to be coming from some location other than the _buffer. Pointers are very powerful, but they also need to be handled carefully since pointer bugs are the most difficult to track down and fix.

The Constructor

Since one of the main elements of the screen object is the _buffer, we need to create this first, and this is done in the object's constructor. The constructor is called when the object is created, as shown in the following code snippet.

Code: [Select]
'Create the screen object which will initialize and create the graphics screen.
Dim As screenobject aScreen = screenobject(640, 480)

The Dim statement creates a variable aScreen which is an instance of our screen object. You can think of the type definition, the code between Type and End Type, along with the supporting member functions as the blueprint that the compiler uses in order to build the screen object. The actual running object, the thing you interact with as the program is running, is created in this Dim statement.

When the object is created, the compiler calls the constructor of the object which is passed the width and height of the desired screen. You see this in the portion of the code screenobject(640, 480). The constructor handles all the work of creating a graphics screen and creating our screen _buffer variable.

Code: [Select]
Screen object constructor.
'This sets up both the screen and the associated screen buffer.
Constructor screenobject (ByVal scrwidth As Integer, ByVal scrheight As Integer)
'Set the default status. An error will change to False.
this._initok = TRUE
'Check the parameters to make sure that they are in bounds.
If (scrwidth > 0) And (scrheight > 0) Then
'Try and set the screen.
ScreenRes scrwidth, scrheight, 32
'Check to see if the screen was created successfully.
If ScreenPtr Then
'Set the screen width and height.
this._width = scrwidth
this._height = scrheight
'Create the update buffer.
this._buffer = Callocate (scrwidth * scrheight, SizeOf(UInteger))
'Nake sure that the buffer was created.
If this._buffer = 0 Then
'Set status to False, error.
this._initok = FALSE
EndIf
Else
'Set status to False, error.
this._initok = FALSE
EndIf
Else
'Set status to False, error.
this._initok = FALSE
EndIf
End Constructor

The constructor (and destructor) use a different format than the rest of the object's member function definitions. The member functions are defined using Sub/Function objectname.membername, to inform the compiler that the definition is a member function, but the constructor is defined using the type definition variable name, screenobject. The constructor takes two parameters, the width and height of the screen.

The first thing we do is set the _initok flag to True, since at the start we are going to assume that everything will proceed according to plan. This makes our life a bit easier since instead of checking two conditions, success or failure and setting the appropriate value in _initok, we can just check for failures to update the _initok flag.

We then check to make sure that the screen width and height are not zero, since it does not make sense to create a screen 0 by 0. We can't really check the upper bounds since we don't know what the upper bounds will be at this point in the code. We will have to try and set the graphic screen and see if we can create it, which we do with ScreenRes scrwidth, scrheight, 32. This attempts to create a screen with the desired width and height at a color depth of 32 bits, or true color mode.

We can use the FreeBasic ScreenPtr function to see if the screen was created successfully. ScreenPtr will return 0 if the screen was not created or a value representing the address of the internal screen buffer. Once we have a good screen, we can create the _buffer using the FreeBasic Callocate function. Callocate not only creates a memory buffer but also clears the buffer to 0 so that it contains known values rather than just random data.

The last thing to do in the constructor is to check to make sure that the _buffer was created successfully. Like the ScreenPtr function, if the _buffer was successfully created, it will contain an address value. If the _buffer creation failed, it will contain NULL or 0.

Updating the Buffer

In order to get something on the screen, it needs to be placed into the _buffer. This is done in the DrawToBuffer method. The first thing we need to do here is to make sure that we are working with a valid screen so the _initok flag is checked for a True value. The passed x and y coordinates are then checked to make sure that they are in the range of the defined screen. If these two tests are passed successfully, then the buffer is updated with the passed RGB color.

Code: [Select]
this._buffer[y * this._width + x] = clr

The odd format of the index code is used to translate a two-dimensional screen coordinate into a one-dimensional index. We normally think of the screen as two-dimensional, since we access the individual pixels using an x and y parameter. Internally though, the screen buffer is created much like our _buffer, as a one-dimensional buffer of contiguous values. This is why we can use the memcpy function to copy our _buffer to the internal screen buffer.

Clearing the Buffer
Code: [Select]
'Using the CRT memset to set buffer to 0.
memset this._buffer, 0, this._width * this._height * SizeOf(UInteger)

Memset takes the _buffer pointer, the value to place in the buffer, in this case 0 which represents the RGB color black,  and the number of bytes to write to the buffer. Just like the Callocate function, the width * height * SizeOf(UInteger) gives us the number of bytes to write to the buffer. Memset is extremely fast and is designed for situations like this.

Supporting Methods

Our screen object also has two properties, GetWidth and GetHeight that simply return the current screen width and height. Since you need to know the width and height of the screen when you create the screen object, why do you need these? There may be cases when a program tries to set a screen resolution and it isn't supported by the computer's graphic card, and will then attempt different resolutions until a supported resolution is found. Having the final screen width and height available will always ensure that the program will always know how much screen real estate it has to work with, and makes the program much more flexible.

The this Qualifier

You will notice that the internal object variables use a this qualifier. This is a hidden parameter that is passed to all of the object's methods that allow access to the object's private data. In fact, we don't really need to use it in the code, since the compiler uses it behind the scenes, but it was added in this version to illustrate the mechanism that the compiler uses to access the private data. In future versions of the code, we will drop the this qualifier, but keep the underscore as a reminder that we are working with private data.

The Interface

All of the properties, functions and subroutines contained within the Public section of our object comprises the object's interface. It is what the program uses to interact with the object. You will notice in most of the methods that we are usually validating the input, or taking other actions that the main program never sees because the code is hidden from view. This is important since it gives us the freedom to change the internals of these methods when needed, and yet not cause major problems in the main program. As long as we can maintain the interface as is, that is, keep the existing parameter and return set, we can improve our object with minimal impact on the user program.

This is called maintaining the interface contract. Imagine it as a contract between the object and the code that uses the object, and as long as the contract is honored, everyone is happy. There are times though, when you will need to change the interface, but this can be done in ways that will cause little to no change in the contract. For example, adding to the interface will not cause any problems to the user code, since new programs can use the new interface element, and old programs can use the old interface element. In order for the legacy code to take advantage of the improved object code, we can simply call the new interface element code within the old interface element (in the hidden portion of the code) and the legacy program will be none the wiser. We have improved our object and honored the contract at the same time.

However, there are situations that require us to break the contract, and these normally take place at the major version changes giving developers an opportunity to update the legacy code as needed. The goal though is to keep the interface intact as much as possible and a little extra work will go a long way in ensuring that changes to the object code will cause minimal disruption.

In the developing stages, like we are in now, this isn't as much of a concern. We don't really have any legacy code to worry about, and some things we will try will simply not work as expected, and better methods can always be found to do something. As long as we are in the development stage, we don't have a second party to deal with and have much more freedom to change things as we see fit. However, we should always make the interface as solid as can be, even at this early stage, as it will pay big dividends later down the road when the code gets more complicated and the interface elements grow. A good design at the start means less to change later, and less potential bugs will be introduced into the system.

Summary

In this tutorial we have examined the code of our screen object a bit more detail. Although the code is quite short, and relatively simple, we have already used many OOP techniques and have optimized portions of the code that will be heavily used by most programs. It is a good start, but we have quite a ways to go before we have a complete screen object.

In the version 2 tutorial series will examine a very important subject in graphics, color, and add some useful methods to our screen object to take advantage of all that we can do with color.
« Last Edit: July 13, 2009 by rdc »