Chapter 2. Resource Management and 2D Graphics Rendering

Unless you happen to be writing an old school text adventure game (and perhaps even if you are), chances are that you will want more than just text in a simple debug font to appear on screen. Drawing nice-looking graphics demands that we should also be able to load those graphics into memory in order to display them; so in this chapter we will be looking at the following:

  • Using Marmalade's resource manager to load games resources
  • Extending the resource management system with our own custom classes
  • The programming choices we have available to us for rendering purposes
  • How to display a bitmapped image on screen using the IwGx API

The Marmalade ITX file format

An ITX file is Marmalade's built-in file format that can be used for loading all kinds of data into our program. The extension ITX is short for Ideaworks TeXt; Ideaworks being the original name of the company that created the SDK before they rebranded themselves as Marmalade.

ITX files have a simple text format and are used as the basis for resource loading. While it is possible to load resources ourselves, it is a bit like reinventing the wheel when Marmalade already provides a great deal of support for this truly tedious aspect of coding.

Marmalade has an API called IwUtil that contains a wide range of useful utility functions ranging from memory management and debugging through to the serialization of objects and random number generation. It also contains a class called CIwTextParserITX, which allows us to load and process an ITX file.

To add this functionality to our own project, we just need to add iwutil to the subprojects list of the MKB file and then add a call to IwUtilInit at the start of our program, and IwUtilTerminate in our shutdown code.

Before we can use the text parser, we will need to create an instance of it by using new CIwTextParserITX. This class is a singleton class, so we can create an instance of it at the start of our program and then reuse it as much as we like in the rest of our code (don't forget to release it on shutdown!). The instance can be accessed using the IwGetTextParserITX function, and we can then load and parse an ITX file using the following code:

IwGetTextParserITX()->ParseFile("myfile.itx");

An ITX file is little more than a big collection of class definitions. An instance of a class is defined by first putting the name of the class followed by a list of parameters for that instance enclosed in curly braces. Let's say we had a class called WidgetClass that was defined as follows (don't worry about the CIwManaged class and the IW_MANAGED_DECLARE macro for now, we'll come to these in a bit):

class WidgetClass : public CIwManaged
{
public:
  IW_MANAGED_DECLARE(WidgetClass)
  WidgetClass();
private:
  uint8         mColor[3];
  int32         mSize;
  bool          mSparkly;
  WidgetClass*  mpNextWidget;
  uint32        mNextWidgetHash;
};

Here is an example of how we might instantiate this class from within an ITX file:

WidgetClass
{
  name     "red_widget"
  color    { 255 0 0 }
  size     10
  sparkly  true
}

WidgetClass
{
  name     "green_widget"
  color    { 0 255 0 }
  size    20
  sparkly  false
  next    "red_widget"
}

This sample declares two instances of WidgetClass, and initializes those instances with a name, color value, size, and a flag indicating whether the widget in question is sparkly or not. Each of these settings is called an attribute, and they can be of any type we desire—string, integer, floating point, boolean, or an array of values (the color attribute provides an example of this).

Hopefully, you are looking at this and thinking how exactly this format can be magically loaded and instanced by the Marmalade text parser, since it obviously knows nothing about WidgetClass. A good question! The answer is that any class that you wish to parse from an ITX file must first be derived from the Marmalade class CIwManaged.

The CIwManaged class

The CIwManaged class is the base class used throughout the Marmalade SDK and by our own classes whenever we want to be able to create instances of them by loading from a file.

The class provides some virtual methods that we can override to allow the parser to recognize our own custom classes, and also to serialize them into a binary format and resolve any references to other classes or resources. It also provides the coding glue required to instantiate copies of our class at runtime.

This facility is really useful for us as it allows us to make our code more data-driven. Say we have a class that describes an item that the player can collect. We might have lots of different item types in our game, so rather than creating instances of them all in the source code, which only a programmer can then change, we could instead instantiate them from an ITX file, which a game designer with no coding knowledge can then edit.

Instantiating a class with the class factory

The first thing CIwTextParserITX will encounter in the ITX file is the class name, which it will use to create a brand new instance of our class. It achieves this by using the class factory, which is another part of the IwUtil API.

A class factory is a programming pattern that allows us to generate new instances of objects at runtime by asking another class (the so-called factory) to create a relevant class instance for us.

The Marmalade class factory system allows us to add our own classes to those provided by the SDK itself by registering a unique hash value identifying the class and a method that creates a new instance of it.

The hash value is normally derived by converting the name of the class into a number by passing its name as a string to the IwUtil API's function IwHashString. While this isn't guaranteed to produce a unique number, it is usually good enough for our purposes and clashes with hash values from other class names are rare.

To add our own custom CIwManaged derived class to the class factory, we just need to do the following (if you want to see a full example of this and indeed the things we'll be covering in the next few sections, take a look at the source code for the ITX project that accompanies this chapter):

  1. Add the IW_MANAGED_DECLARE(CustomClassName) macro to the public section of the class. This declares a method called GetClassName, which will return the name of the class as a string, and also adds a couple of type definitions to allow the class to be used more easily with the CIwArray class, which is yet another piece of functionality provided by IwUtil.
  2. Add the macro IW_MANAGED_IMPLEMENT_FACTORY(CustomClassName) to the source file for the class. This macro implements the GetClassName method and also creates the necessary class factory function that will be used to create a new instance of our class.
  3. Finally, we have to register our class with the class factory itself by adding the macro IW_CLASS_REGISTER(CustomClassName) somewhere in our initialization code.

With this done, we can now include our class in an ITX file. The CIwTextParserITX class can now create a brand new instance of it with a call to the class factory function IwClassFactoryCreate("CustomClassName").

Parsing a class

With the creation of a new instance of our class taken care of, the next step is to allow CIwTextParserITX to configure that instance by modifying its members. This is done with the following CIwManaged class' virtual methods:

Method

Description

ParseOpen

This method gets called when the text parser reaches the open curly brace of the class definition. It can be used to initialize anything that might be needed internally during the process of parsing an object.

It is important that you do not use this method to initialize all the member variables of your class to some default values. The class constructor is a far better place to do this, as it is guaranteed to be called however the instance ends up being created.

ParseAttribute

This method is called whenever an attribute is encountered in the object definition. The attribute is passed as a standard C-style string to this method, which can then process it as needed.

The text parser can be used within this method to extract any data elements in a variety of different ways, including strings, integers, and Boolean values.

ParseClose

This method is called when the closing curly brace of the class definition is encountered.

ParseCloseChild

It is possible to embed class definitions inside other class definitions in an ITX file. If a class does not implement the ParseClose method then when its closing curly brace is encountered, the ParseCloseChild method will be called on the parent class with a pointer to the child class.

In this case parent and child do not refer to class inheritance hierarchies, but rather to how the classes have been defined in the ITX file. For example:

ParentClass
{
    name "parent"

    ChildClass
    {
        name "child"
    }
}

When overriding any of these methods, you should normally call the version of the method from the superclass, be that CIwManaged or some other class derived from it. For example, the name attribute is parsed by CIwManaged::ParseAttribute, which not only reads the name for the class but also generates a hash value of the name. The hash value is very important when it comes to serializing and resolving class instances later.

The following diagram shows an example of how an instance of WidgetClass defined earlier in this chapter would be processed by the ITX parser:

Parsing a class

For WidgetClass the only method we would definitely need to implement is the ParseAttribute method, which might look like the following code:

bool WidgetClass::ParseAttribute(CIwTextParserITX* apParser,
const char* apAttribute)
{
  if (!stricmp(apAttribute, "color"))
  {
    apParser->ReadUInt8Array(mColor, 3);
  }
else if (!stricmp(apAttribute, "size"))
  {
    apParser->ReadInt32(&mSize);
  }
else if (!stricmp(apAttribute, "sparkly"))
  {
    apParser->ReadBool(&mSparkly);
  }
else if (!stricmp(apAttribute, "next"))
  {
  CIwStringL lNextWidget;
  apParser->ReadString(lNextWidget);
  mNextWidgetHash = IwHashString(lNextWidget.c_str());
  }
else
    return CIwManaged::ParseAttribute(apParser, apAttribute);
return true;
}

Serializing a class

Serializing an object instance is the process of converting the current state of the object into (or from) a binary format.

While not strictly necessary when parsing an ITX file, it is still very much a useful part of the functionality provided by CIwManaged, and forms an integral part of the resource handling process that we will be seeing later in this chapter.

The serialization functionality can also be useful when it comes to saving out things such as current game progress or high score tables, though of course we can still use normal file handling operations to do this if we prefer.

Serialization of our class is handled by overriding the virtual method Serialise. This method can then use the serialization functions provided by IwUtil, which all start with the prefix IwSerialise.

For example, IwSerialiseInt32 will serialize an int32 value. All these functions make use of the Marmalade type definitions for the basic variable types, as these are far more explicit when it comes to the memory footprint of a variable. Take a look at the header files IwSerialise.h and s3eTypes.h in the Marmalade SDK installation for more information on the IwSerialise functions and the variable types respectively.

We must make sure to call our superclass implementation of Serialise as well to ensure every part of the object is serialized. Normally this would be the first thing we do in our implementation of Serialise, but it does not have to be so as long as it is called at some point.

We can serialize our objects to a file of our choosing by calling IwSerialiseOpen. This allows us to specify the filename and a Boolean flag that indicates whether we are reading or writing the file. We then call the Serialise method of each object we want to serialize, and finally call IwSerialiseClose to finish the process.

One nice feature of the IwSerialise functions is that, in most cases, we do not have to worry about whether the Serialise method has been called to write data to a file or if it has been called to read data from a file. We just call the function and it will read or write the value, as appropriate.

There are times that we will care about reading or writing values to a file; for example, if we need to allocate a block of memory to read some values into. The functions IwSerialiseIsReading and IwSerialiseIsWriting allow us to make the appropriate decisions.

The following code snippet illustrates how the serialization functions are used by showing what the Serialise method might look like for WidgetClass:

void WidgetClass::Serialise()
{
  CIwManaged::Serialise();
  IwSerialiseUInt8(mColor[0], 3);
  IwSerialiseInt32(mSize);
  IwSerialiseBool(mSparkly);
}

Resolving a class

The act of resolving a class instance is to fix up any parts of our class that are not initialized correctly when parsing the object from an ITX file or having created it from the serialization process.

When might this happen? The most frequent reason for needing to resolve our instances is when the instance requires a pointer to another class that may not exist when it is first created.

This is best illustrated by an example. Let's say our class contains a pointer to another instance of our class in order to implement a linked list. When we read in our instances, it is possible we might refer to an instance that has not yet been created and so we can't create the linked list yet.

To solve this problem we instead store a value in our data that will allow us to look up the required instance later. This might be a string representing the name of the instance or perhaps a unique identifier number.

Once all the instances have been read in, we can then call the CIwManaged class' virtual method Resolve on each instance in turn and obtain the required pointer to the correct instance using whatever methodology we see fit. For example, we might maintain a list of all instances of our class that gets added to whenever a new instance is created. We can then use this list to look up the required instance.

It is not always necessary to create our own implementation of Resolve, but if we do we must be sure to call the inherited version of the method from our superclass.

We'll take one more look at WidgetClass to wrap this all up. You may remember that it had a member mpNextWidget that points to another instance of WidgetClass. In the ITX file, we supplied a value for this member by specifying the name of another WidgetClass instance. In the ParseAttribute method, we read in this name and calculated a hash value from it which was stored in the mNextWidgetHash member variable.

We can implement the Resolve method and look up a pointer to the correct instance but we'll also need to maintain a list of all WidgetClass instances in order to do this. One way of doing this is to implement ParseClose and store each instance in a list. The following code shows how this could be achieved:

void WidgetClass::ParseClose(CIwTextParserITX* apParser)
{
  // Add this instance to a list.  gpWidgetList is an instance of a
  // Marmalade class called CIwManagedList which is very useful
  // for storing lists of objects derived from CIwManaged!
  gpWidgetList->Add(this);
}

void WidgetClass::Resolve()
{
  // Look up an instance of WidgetClass with the given hash
  if (mNextWidgetHash)
  {
    mpNextWidget = static_cast<WidgetClass*>
                   (gpWidgetList->GetObjHashed(mNextWidgetHash));
  }
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.9.147