Thursday, October 24, 2013

Creating Object-Oriented Tools for Game Development


When possible I attend the local NSCoders night and CocoaHeads meetings.  At one of these meetings I was introduced to the book Game and Graphics Programming for iOS and Android with OpenGL ES 2.0 published by Wrox.  The author, Romain Marucchi-Foino, presents a series of lessons regarding different aspects of writing games.  There are several things I like about this book.
  • The first is that the author has created an abstraction layer which insulates the programmer from whether the game is being run on iOS or Android.  This abstraction layer allows the programmer to write in C++.
  • In addition to the abstraction layer the author has provided a number of modules for managing various tasks such as sound, threads, programming the GPU via OpenGL, etc.
  • The book also introduces a number of subjects which I had been curious about such as physics, path calculation, and various OpenGL shader techniques.
  • The topics are presented in an orderly fashion and I found the book really useful for learning how to use OpenGL for game development.
On the other hand there are several things about the code where I think there's room for improvement.  While the code is purported to be C++ code, to my way of thinking it's really C code which has been compiled using a C++ compiler.
  • The various modules could easily have been implemented as proper C++ classes
  • Most of the memory management is done using traditional C malloc(3) methods instead of new, delete, and the Standard Template Library.
  • C++ classes could have been used to define vector, matrix, and quaternion types to exploit operator overloading making the equations (IMHO) more readable.
  • No benefit is derived from C++ type checking.
Over the course of some number of posts I'm going to discuss the various modifications I've made to the author's original code to leverage the benefits of C++.  Along the way I'll point out some of the opportunities to optimize the code.

It's my assumption that the reader has the book and can get an explanation of the author's software tools from the author himself.  Also, these posts assume that the reader is familiar with OpenGL.  This series of posts will only focus on the changes I've made to the code.

I've created a source code repository at GitHub.  To pull a copy of the code as it would be written by the author use the tag baseline.  To get the code as I've rewritten it to use objects for the SHADER and PROGRAM data types use the tag chapter2-1.

The first sample program, chapter2-1, briefly introduces how to draw a simple polygon on the screen using the PROGRAM, and SHADER data types.  Since the SHADER type is simpler we'll start there.

Before getting into the details of the changes made to the SHADER data type a brief overview of the procedure for performing the rewrites is in order.  Generally, each of the author's header files thing.h declares the type THING as follows:

    typedef struct
    {
        ...
    } THING;

I'm going to change these various declarations to look like

    struct THING {
        ...
    };

Also, the thing.h header will usually declare THING_init() and/or THING_create() functions.  I use these functions to create one or more constructors for the THING class.

If there is a THING_free() function I remake it into the class destructor.  If this function is missing I'll have to build a destructor from scratch.

The remaining miscellaneous functions, such as THING_something(THING *, ...) will become class methods named something(...).

Look at the file SDK/common/shader.h.  There are four functions which all have names starting with "SHADER_" and most of them pass a "SHADER *" as the first argument.
The SHADER class declaration becomes

    struct SHADER {
        char            name[ MAX_CHAR ];
        unsigned int    type;
        unsigned int    sid;
    public:
        SHADER(char *name, unsigned int type);
        ~SHADER();
        unsigned char compile(const char *code,
                              unsigned char debug);
        void delete_id();
    };

So, for the SHADER data type the function SHADER_init() becomes the class constructor, retaining its original parameter list and SHADER_free() becomes the class destructor but we no longer explicitly pass in a pointer to the object we're going to free; inside the destructor we can reference the SHADER being destructed using this.

The functions SHADER_compile() and SHADER_delete_id() become the class methods compile() and delete_id(), respectively.  In addition, the first argument is removed from each of their argument lists because in our implementation code we'll be able to use this to reference the SHADER object, just like in the constructor/destructor methods.

We're not quite done with the changes to our header file.  The implementation file, SDK/common/shader.cpp, shows us that some of the data types we used in our modified header file don't accurately represent how the data is used.  For example, the author's SHADER_compile() function passes the type data member to glCreateShader().  If you look at the man page for glCreateShader() you'll see that the argument which we're supposed to pass has the type GLenum.  This implies that the type data member is more correctly declared to be type GLenum.  This also has implications for our constructor.  The type argument should also be declared to be of type GLenum.

When we look to see how the sid data member is used we can see that data member sid ought to be declared to be type GLuint.   This doesn't have any impact on the signatures of our other member functions because sid is never passed in an argument list.

There is, however, one other argument list that needs (IMHO) to change.  We ought to declare the argument debug for the member function compile() to be of type bool.  The author's code which calls the original function SHADER_compile() just passes either 0 (zero) or 1 (one).  I don't like this programming style because the zero or the one doesn't really tell you what it means, i.e., the zero/one could be used by the function as a numeric value or as a true/false (Boolean) value.  We don't know without looking at the implementation of SHADER_compile().  This defeats the data hiding we would normally want to do when writing object oriented code.  When I modify the various places which call the compile() method I'll explicitly pass in either true or false as the debug parameter making it clear to the reader how the value passed to the debug parameter is being used, numerically vs. logically.  Also note that the return value for compile() should be bool; its return statements have been changed to return true/false, accordingly.

[Note:  Yes, I know that C/C++ can use zero/non-zero idiomatically for true/false; I still think this is easier to read.]

Our final declaration of the SHADER class is

    struct SHADER {
        char      name[ MAX_CHAR ];
        GLenum    type;
        GLuint    sid;
    public:
        SHADER(char *name, GLenum type);
        ~SHADER();
        bool compile(const char *code, bool debug);
        void delete_id();
    };

We can finally begin working on the implementation file, SDK/common/shader.cpp.  The code in SHADER_init() needed to allocate space for the new SHADER object and the allocation was done using calloc(3) which automatically sets all of the memory to zero.  We no longer need to allocate the space.  We'll be creating SHADER objects using the new operator.  When our constructor is invoked the space will already have been allocated.  The old SHADER_init() implementation took two arguments: name and type. We can initialize the type and sid data members outside of the body of the function by using an initializer list.  The data member type can/will be initialized from the constructor argument type but what value do we use to initialize sid?  Remember that in the original SHADER_init() function the code allocated space for the SHADER data structure using calloc(3).  The calloc() call sets all of the data fields to zero so that is the value we'll use to initialize sid.  The last thing we need to initialize in the constructor is the name data member.  name is a fixed sized data field.  To protect ourselves against buffer overflow errors I'll use assert(3) to make sure that the name argument passed into the constructor will fit into the name data field in the SHADER object.  We could also use assert(3)(but I don't) to verify that the type value passed into the constructor was GL_VERTEX_SHADER or GL_FRAGMENT_SHADER; in the future we could also choose to allow GL_GEOMETRY_SHADER but none of the author's sample programs use geometry shaders.

In the destructor method we release the shader ID using the delete_id() method.  We should keep track of the delete_id() method.  Once we've implemented all of the example code we may find that we can hide the method from users, that is, we may be able to redeclare the method to be private; I probably wouldn't choose to make the method protected because the SHADER class is never subclassed.  Since this is a destructor we no longer need to free(3)the memory; the compiler takes care of that for us.

Since we've already talked about the delete_id() method we might as well cover it next.  You'll see that it's pretty trivial.  If a shader ID has been allocated we release it using glDeleteShader().  Note that since we're conditionally releasing the shader ID I've already removed the guard logic that used to exist in the old SHADER_free() function from the destructor method. The guard logic in the SHADER_free() kept us from calling SHADER_delete_id() if the data member sid was zero; we don't need to test for this condition (sid == 0) in both places, i.e., in the destructor and in the delete_id() method.

The compile() method should be straight forward to readers who are familiar with programmable shaders in OpenGL.  Basically the method
  • generates a shader ID,
  • associates the shader's source code with its ID,
  • attempts to compile the shader code,
  • optionally prints any failure messages which are generated,
  • if compilation fails the shader ID is released, and
  • the method returns whether or not the attempt to compile the shader code was successful.
If you're unsure what any of this means you'll need to brush up on your OpenGL knowledge.

The truly compulsive can define the loglen, and status local variables to be of type Glint, and the local variable log to be of type "GLchar *" then cast:
  • loglen to type size_t when it's passed into malloc(),
  • the return value of malloc() to be "GLchar *",
  • loglen to type GLsizei when it's passed into glGetShaderInfoLog(), and
  • &loglen to type "GLsizei *" when it's passed into glGetShaderInfoLog().
[Note:  I may or may not continue to be so compulsive in future code examples.]

That's enough for now.  I'll discuss the changes needed to implement PROGRAM as a full C++ class in the next post.

No comments:

Post a Comment