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:
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 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.
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):
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.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.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")
.
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:
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:
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 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); }
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)); } }
3.144.9.147