Unuk

Procedural Generation.

Procedural generation is the act of producing content algorithmically. In games, content may often be generated procedurally to give the game more variation or depth. Such algorithms have also been used in motion picture films for procedurally modeling buildings, or to give more variation to special affects.

While there were no references or third party resources used in this procedural generation algorithm, there has been some research carried out on many ways that I could approach this task. One such method was Perlin Noise, developed by Ken Perlin in late 1990's. This algorithm used a seeded random number generator that takes an integer and a parameter, then returns a random number based on the given parameter. Perlin Noise is generally used to generate textures on the fly and is very useful in situations where memory may be short. Noise is mainly implemented in the second, third or fourth dimensions, (x, y) (x, y, z) or (x, y, z, w).

I decided to implement procedural generation in a less complex manner, as algorithms like Perlin Noise would have required some hacking just to have it accomplish the task I wanted. Like all procedural generators I needed a way to generate random numbers while also trying to keep it looking natural and meeting a parameter given by the user.

We deploy a very simple level generator within Unuk by placing object, entities and NPC's at random locations on the map, while avoiding collision with other entities.

In /Unuk/src/LevelGen/LevelGen.*

GenerateEntities(const std::string&, int) takes in the name of the sprites file minus the file extension, and the frequency in which entities will be drawn. The higher the frequency, the less densely populated that particular object will become, giving the user the ability to make the level less crowded with the same type of objects.

The nextEntityGen variable is used to calculate entity frequency on a map. Once we place an entity on the map, the random number gets recalculated with respect to the entity's current frequency in order to determine where another of the same entity will be placed. The value of nextEntityGen is in the range of [1 ... frequency - 1], non-inclusive. It is calculated with the code:

int nextEntityGen = 1 + (rand() % frequency);

Afterwards, we iterate through the map's tile array in order to calculate the position of the next entity.

Data Serialization.

Data Serialization is of utmost importance in most software projects. Data structures often need to be stored in a file format that can be moved between other machines, streamed over a network or be easily configurable.

My choice to use XML came mainly down to prior experience in using the markup language with C++ and while it does not store the data in a binary format, encrypt or obfuscate the data, it offers a lot of freedom and flexibility in structuring data.

Generally, I would not have chosen XML format for human-created levels due to the amount of time it takes to write out data in an XML file, not to mention trying to give some form of structure to the world. However, with procedural generation working hand-in-hand with data serialization, the creation and management of the game's map files became much more eloquent .

Data Serialization for Unuk can be found in:

/Unuk/src/libUnuk/System/Serialiser.* and /Unuk/src/libUnuk/System/Serialisable.h

Our first two methods, StepIn(const string&) and StepOut(void) steps in and out of elements in an XML document. When we step in we are effectively:

Creating a new child element.

Pushing the current element onto the list of parent elements.

And setting the current element to the child node.

The next method we have is FirstElement(const string& name). This gives us a way to jump to the first element within a given block. Before we do anything we declare a variable of type TiXmlElement and store the memory address of our current element into it for safe keeping. If we then find that the current element is equal to NULL we go to the first element in that particular document, otherwise we go to the first element within the current elements block. We then check to see if we have a current element again, and if we do not we restore the old element we assigned earlier and return false. If we have a value other than NULL in oldElement we push it onto the list of parent elements.

To find the next element along we have a method conveniently declared NextElement(const string& name). It searches for the next element of a given 'name' provided that it is not the same as the current elements. Within the function, we check to see if the names match by ensuring that name is not equal to the current elements value. If they are not, then we pass name into the FirstElement() method and return the result.

We then go on to store the current element into a temporary pointer named oldElement and search for the next sibling of _currentElement. If none are found, we simply restore the old element into _currentElement and return false, as there are no other elements.

Rollback is quite a straight forward method. First, we have to make sure there is an element to go back to. If the size of _parentElements is greater than zero, then we can set the current element to the last element on the parent list. And then to clear that off the list of parents we pop it off of the stack.

Next up is Load. This method simply takes in an XML filename, and calls TinyXml's Loadfile method on the document. The save method is similar to Load, as the only parameter in Save is the filename and it wraps a TinyXml call. However, before the call to TinyXml's SaveFile method, LinkEndChild is passed the document's _rootElement and LinkEndChild adds a new node related to this.

RegisterMember(const string& name, int type, void* data) will allow us to serialize the data we intend to store in our XML file. We create a SerialiserMember object named member. This takes in the elements name as a string, then we insert the type of data that we would like to store, the types are stored conveniently in an enumerator. Lastly we pass in a pointer to the data we are storing and push the data onto the member list.

Once we have the data, we can write it to the XML file. The method WriteMembers(void) will allow us to do this. First, we declare a stringstream named sstream in order to manipulate strings. We iterate through the list of nodes stored in the _members list and each members data type is checked. After every check we will call stringstreams clear() method on sstream to remove any error flags that may have occurred.

Once we have found the data type, we cast sstream into the data type found. Finally we create a new TiXmlText pointer and convert the data into text.

With the casted data, we will create a new element. To create that element, we need to pass in the name and value, with the value being the text.

Once all the data in the list has been iterated through, we clear the list.

ReadMembers is the final method we need to complete data serialization. It is very much like WriteMembers, however once we know what data type we have, we store the information in _data using TiXmlElement 's GetText() method.

2012-03-07T23:41:32