An introduction to programming Ultimate Stunts

Contents

Introduction

This document is written for people who are interested in reading the Ultimate Stunts sourcecode, or who want to contribute to Ultimate Stunts by programming new features into it. The reason to write this is to attract new Ultimate Stunts developers, and to make it new developers as easy as possible to get started.

Before you start adding code

Remember that Ultimate Stunts is released under the GNU General Public License. You should also release your code changes under this license. Make sure that all terms of this license are acceptable to you, before you commit any code. It can be useful to read this license at least once. You can find this license in your Ultimate Stunts package as a text file with the name 'COPYING'. You can also get it from the website of the Free Software Foundation.

What you should already know

If you are only going to work on a limited part of Ultimate Stunts, then you may not need to have experience with all of these subjects. I'd suggest that you read this chapter entirely, and then decide for yourself how useful you are for the Ultimate Stunts project, and if necessary get some extra experience before joining the project.

C and C++

Almost all of the Ultimate Stunts sourcecode is written in the programming language C++, and the rest is written in C. If you want to edit the sourcecode, then you definitely need experience with these languages. There are enough online courses available for free on the internet, and there are also free compilers available for these languages. Make sure that you have experience (not only knowledge) with subjects like derived classes, virtual functions and overloading them, operator overloading, and C++ templates. If you need to choose between different compilers for training, then I'd suggest to use the same compiler as Ultimate Stunts: the GNU Compiler Collection (gcc). In windows, you can get it by installing Cygwin, but it is recommended to use Linux, or another free UNIX-like operating system.

UNIX/Linux

Ultimate Stunts is designed to work primarily on UNIX-like platforms, like Linux; the windows-version is compiled by using the UNIX emulation program Cygwin. Therefore, it is useful to know how to work with these systems. The build process of Ultimate Stunts is based on programs like autoconf and automake, so if you need to change anything there (e.g. for linking with a new library, or for fixing the build system for some platform), you need to know how to use them. Even when you add new source files to Ultimate Stunts, you need to know how to update the correct Makefile.am (though that is not really difficult).

For translation of the user interface to other languages, Ultimate Stunts uses the GNU gettext library. When configured properly, using gettext is not really difficult, but you should be aware that gettext is being used.

OpenGL

All graphics output in Ultimate Stunts (2D and 3D graphics, menus, texts, etc.) are based on OpenGL. So, if you want to modify anything in Ultimate Stunts that has something to do with graphics output, then you need OpenGL experience. Even when working on a higher level, where you don't have to give direct OpenGL calls, it can be useful to know the behavior of OpenGL. You need to have some basic theoretical knowledge of how 3D rendering works in OpenGL, what the different transformation matrices do, and of course how to draw some lines and triangles. Also it is very useful to have some experience writing your own OpenGL programs.

SDL

SDL is used for creating a window for the OpenGL context, for user input (keyboard/mouse/joystick) and for multithreading. If your work is not directly related to these subjects, then you don't need to learn SDL. Even when you need to do something with SDL in a later stage, this is not a big problem, because SDL is quite easy to learn.

The additional library SDL_image is also used. This library has an even simpler programming interface, but if you are not involved in the functions that load textures from files, then you do not need to know it.

FMOD / OpenAL

The libraries FMOD and OpenAL are both used for sound output. If you don't work on the soundsubsystem you don't need to know them, but if you do, you need knowledge of both of them. The reason is that on some platforms FMOD is used, and on others OpenAL is used, so if anything changes to the sound system, you need to be sure that both of them still work. It is also useful to be able to compile with both of them (and to know something about autoconf to be able to choose between them).

Math

Ultimate Stunts is a very threedimensional program, so in most parts of the sourcecode you will have to work with 3D vectors and their operations, and often also with 3*3 transformation matrices. So, make sure that your linear algebra knowledge is up-to-date, and that you understand the concept of a rotation matrix (you can search for that on the internet). For some parts of the sourcecode, especially the physics engine, you also need to know other math concepts, like calculus, or numerical integration.

(Car) Physics

You need detailed knowledge of physics, if and only if you are going to work on the physics engine. Ultimate Stunts uses a lot of tricks and shortcuts in the simulation to make it easier to play, but there is still a lot of real-world physics involved in the simulation. You need to know the basic laws of newtonian mechanics (you know, about velocities, acceleration, mass, forces etc.), and of rotational mechanics (torques, moments of inertia etc.). Also, it is useful to have some knowledge of how a car works, with things like engine torque curves, gear ratios, suspension, weight transfer, tire behavior, etc..

Ultimate Stunts

It is essential to know what is already present in Ultimate Stunts. This document will help you find your way in the sourcecode, but it is not detailed enough to explain everything. Make sure that you played the game enough to know all its features, play a bit with the configuration options, maybe make your own track to get some feeling of how Ultimate Stunts tracks 'work', and read on the Ultimate Stunts website about its history and its planned features. The way how things are implemented will be clearer to you if you know what it has to do, and what it will have to do in the future.

Coding habits

When different programmers work on the same piece of code, it is often frustrating when other people's code does not follow you own habits. Therefore, this section describes some things you should do in order not to frustrate other programmers. The rules given below are meant to be general guidelines, not rules that should always be followed even when the result looks stupid. Examples are not given: the existing sourcecode should provide enough examples.

File format

All source-files should be UNIX-style text files, not DOS/windows-style or Macintosh-style. If you work in windows (not recommended), make sure that your text editor saves UNIX-style text files. Most text editors in windows don't. The character set should be ASCII as much as possible, so only the lower 128 characters of a character set should be used, and no unicode should be used. In the rare cases that other characters are needed, the ISO 8859-1 standard is followed. All variable names, function names, comments etc. should be written in english. An exception are of course short variable names like x, y or i. As a rule, don't use any other natural language in your sourcecode.

Indentation

Indentation is done with tabs, not with spaces. Be sure not to use a text editor that converts tabs to spaces. For every indentation level one extra tab is used. No assumption is made on the width of a tab. A possible exception to the rule of using tabs is for aligning expressions that are on lines next to each other, and which share a similarity. Aligning such expressions can make it easier to see the similarity, which makes the code easier to read. As no assumption is made on the width of a tab, such an alignment can often only be done with spaces.

The placing of the acollade characters ('{' and '}') should be done in the same way as the existing code does. So, the opening and closing acollade should each be placed on their own line, containing nothing but the acollade and possibly a comment. The indentation should be the same as the indentation of the statement of which the code block is a part; the code inside the acollades should have one indentation more. An exception is made for very small code blocks that fit on a single line. See the source code on how this is handled.

Identifier names

For identifier names, like function names, class names and variable names, the following conventions are used:

Re-using code

A lot of classes in Ultimate Stunts have been made with the purpose of being re-usable. Using these classes will make your code look cleaner, and your code will be easier to understand for Ultimate Stunts developers who are familiar with those classes. Also, it can save you time because you don't have to re-invent the wheel, and future improvements on these classes like bugfixes and performance improvements will also improve your code.

If you want to use a class that seems to be useful, but in the end it turns out that it misses some functionality that is essential to your code, then it may still be useful to use that class and to implement the missing functionality by making a derived class or by modifying the class itself. That last possibility is encouraged when there is a high possibility that the new functionality will also be useful to other parts of the program. When in doubt about where to place new functionality, please ask the project administrator. It is much better to ask too much about architecture decisions than to ask too few.

There are a number of general-purpose classes that should always be used whenever that is appropriate:

Adding new source files

For most parts of the Ultimate Stunts sourcecode, every class is placed in its own files. Every class has two files, a header file containing the class definition without member function implementations (except for the most trivial member functions), and a .cpp file containing the member function implementations. It is appreciated if you follow this habit. C++ source files have the .cpp extension, not .cxx, .c++, .cc or anything else. Header files (both C and C++) have the .h extension. When adding new files, give them the same format as the existing source files, with the filename, a short description, author name etc. (you can fill in your own name).

Whenever new source files are added, make sure that they are added to the Makefile.am file in the source file's directory, and to the ultimatestunts.kdevelop.filelist file in the top directory of the Ultimate Stunts sources. Adding them to Makefile.am will add them to the build process, so that they are actually compiled, and adding them to ultimatestunts.kdevelop.filelist will show users of the KDevelop environment that these files are part of the sources.

Keeping your code clean

There are some general guidelines for C++ programmers to keep their code clean. Using well-structured code will make it better readable and understandable to you and other programmers in the future, and it will help you not to make bugs. There are already lots of guidelines out there: I will not repeat every single one. These are some important ones:

Source code layout

By now you should be ready to get an introduction on how the Ultimate Stunts sourcecode is organized, so that you can find your way in it.

Directories

The Ultimate Stunts sourcecode is placed into separate directories. The reason for this is that Ultimate Stunts consists of several programs (the main program, the server program, the track editor, the 3D editor and the AI client). While it would have been possible to put all their sources in a single directory, this would not have made it easy to find the right file you are looking for. Some of these directories contain code that is specific for one of these programs, and the orher directories contain code that is shared between programs. This shared code is compiled into static libraries, and the programs which use this code are linked against those libraries.

These are the source directories:

Classes

Ultimate Stunts contains a lot of classes, and during its development, new classes are created continuously, and sometimes classes are renamed or removed. Therefore, it is not possible to give a complete list of all classes in the latest Ultimate Stunts version. In this section, the most important classes will be mentioned.
CGameCore/CUSCore
CGameCore is probably the most important class to understand if you are going to work within the "game loop". This class provides an easy-to-use programming interface to other code for initializing a game, starting it, stopping it, unloading it, etc.. The CGameCore class itself only handles the simulation, and it is located in simulation/gamecore.*. The CUSCore class in ultimatestunts/uscore.*, which is derived from CGameCore, also handles sound and graphics.
CFile/CDataFile/CFileControl
CFile is a generic file-loading and -saving class. It can handle both text files and binary files. When reading text files with the readl() method, it is able to read both UNIX-style and windows-style text files correctly. The shared/cfile.h header also defines some filesystem utility functions.

CDataFile, derived from CFile, provides the same programming interface. The difference is that CDataFile does not take the absolute filename as a parameter. Instead, the programmer gives the path relative to the Ultimate Stunts datadir. Then, CDataFile automatically determines the real location of the file, and gets it from there. It is able to search on different locations on the local directory system, but it may also download the file from an Ultimate Stunts game server. The actual behavior may be quite complicated, so it is important that this is implemented only once. As almost every data file is located somewhere in the datadir, CDataFile should be involved somehow in the loading of all these files. When the loading of a file is done with a library call, so that a physical path is required instead of a CDataFile object, the useExtern() method is useful.

The behavior of CDataFile is controlled by an instance of the CFileControl class. There should be only one instance of this class. The behavior needs to be changed e.g. when a connection is made with an Ultimate Stunts gameserver, to enable the possibility of automatic file downloads.

CDataObject/CDataManager/CWorld/CGraphicWorld
Ultimate Stunts has to load a lot of different data objects: tracks, textures, tiles, cars etc., and there are a lot of dependencies between them. In order to manage these dependencies efficiently, and to make sure that objects are not loaded twice, all object types are derived from CDataObject (shared/dataobject.*). Classes like CTexture or CTrack are all derived from CDataObject.

A CDataObject object is identified by its type (an enum-type variable), its filename and a number of parameters. Usually, CDataObject objects are connected to a CDataManager object. The object is created by it when the CDataMamager object receives a request for the object, a pointer to the object is stored in the CDataManager object, and it is deleted by the CDataManager object when necessary. Also, a pointer to the CDataManager object is passed to the CDataObject constructor, so that the CDataObject object can ask the CDataManager to loaded dependency files. When the requested object is already loaded, a pointer to the already-loaded object is returned, so objects are never loaded twice.

There are different instances of CDataManager-derived classes in an Ultimate Stunts game-session. The most central one is the instance of CWorld (simulation/world.*). In CWorld::createObject you can see how the type enum is connected to class types in CWorld. Most of them are connected to some class, but for instance the eSample type is connected to a clean CDataObject. This is because, at this level, Ultimate Stunts does nothing with the sound file information, it only stores the file information so that the sound subsystem knows which files to load in a later stage.

Another CDataManager-derived instance is of the type CGraphicWorld (graphics/graphicworld.*). While CWorld only loads the data necessary for the simulation subsystem, CGraphicWorld loads all data necessary for the graphics subsystem. That is why the enum type of objects in CGraphicWorld is connected to different classes than in CWorld: in CWorld, eTileModel would be connected to a tile collision model, while in CGraphicsWorld it is connected to a CGraphObj, containing the graphic mesh data of the tile. Because of this separation, it is possible for the server program to load all simulation data without the need to load all graphic data.

CWinSystem
In Ultimate Stunts, the CWinSystem class (graphics/winsystem.*) encapsulates the graphics window. It initialises the window, and manages a number of events, like resize events, keyboard and mouse input events, and joystick input. It has two methods called runLoop; both of them start a loop which responds to events. One of them interacts with the rest of the program through a callback function: this one is used in the game itself. The other one is based on a CWidget object, and is used in the menu user interface.

CWinSystem also has two systems for determining the keyboard state (three, if we also count the widget-system, but that one should not be used at the same time with the other two systems). One method, the getKeyState(..) method, returns the actual state of a key (pressed down or released), which is useful for keys that are used as game input, like the arrow keys for controlling a car. The other method, wasPressed(..), returns whether the key has been pressed since the last call of wasPressed. This is useful for keys that have a more event-like nature, like the escape-key for leaving a game. And, to make things even more difficult, there is a derived class CGameWinSystem (ultimatestunts/gamewinsystem.*) which provides a number of functions for getting the state of user-defined keys.

CWidget/CGUIPage/CGUI
The menu user interface is based on an event-based widget system. Every user interface component, like a list of options, or a message box, is based on classes derived from CWidget. Such a widget is responsible for updating its part of the screen, and for handling input events in the right way. Events are passed to a widget by calling methods of the object, and a widget can handle those events by having overloaded versions of these methods. For instance, most widget types will have an overloaded onRedraw method, which will be called when the widget needs to be redrawn. When a widget contains sub-widgets, the widget is responsible for calling the event handling methods of the sub-widgets. All graphics output is done with openGL, all input parameters (e.g. keyboard key values) are done with SDL constants.

A commonly used widget-type is the CGUIPage class. It contains a background and a title, and a number of sub-widgets. The sub-widgets are ordered and drawn bottom-to-top, and the widget on the top has the keyboard and mouse input focus. Usually the top-level widget is of the CGUI type, and its only sub-widget is of the CGUIPage type.

CGUI is a widget-type, containing just a single, full-screen CGUIPage sub-widget. It has some features that make it useful as a top-level widget. For instance, it contains methods for switching between 2D and 3D mode in OpenGL, and it contains methods for the easy creation of message boxes and other commonl used user interface components.

CLConfig
The CLConfig class (shared/lconfig.*) is used for the .ini-style configuration files in Ultimate Stunts, like ultimatestunts.conf and the car configuration files. Because of the early development stage of Ultimate Stunts when it was implemented, it is not based on CFile. It is also not based on CDataFile because ultimatestunts.conf has to be loaded before it is known where the datadir locations are, as this information is stored in ultimatestunts.conf.

Following an Ultimate Stunts game in the source

You'll probably learn a lot about the Ultimate Stunts sourcecode by seeing which functions are called where in an Ultimate Stunts game session. The scenario followed in this section is as follows: a player starts Ultimate Stunts, clicks on "drive!" to start a race, races until he/she finishes, and then quits the program. It is strongly recommended to read the corresponding source files while reading this text.

ultimatestunts/main.cpp

Like in any C/C++ program, execution starts in the main function. For the ustunts program, it is located in ultimatestunts/main.cpp. You can see that it is actually quite short: The shared_main function (in shared/usmisc.cpp) sets up some generic things. For instance, it finds ultimatestunts.conf and loads it, and it sets up the gettext localization settings. Creating the CGameWinSystem object (derived from CWinSystem in graphics/winsystem.cpp) creates a window for Ultimate Stunts, according to the settings in the theMainConfig object, which is a CLConfig object loaded from ultimatestunts.conf. This window will be deleted when the CGameWinSystem object is deleted, at the end of the main function.

All the interesting stuff happens in the CGameGUI::start method. This function will only return when the player decides to leave Ultimate Stunts.

ultimatestunts/gamegui.cpp

The first thing we did with the CGameGUI object was to call its constructor. CGameGUI::CGameGUI(..) does different things, but most of it has to do with initializing the different pages of the game menu and the menu texts. Another important thing to see is that it creates an instance of the CUSCore class, which will only be deleted when the CGameGUI object is deleted.

Next, we called CGameGUI::start(). This method first puts OpenGL into 2D mode by calling CGUI::enter2DMode(). Then, it executes a while-loop. Inside this loop, various parts of the menus are identified by string values, and they are all implemented by their own CGameGUI method. Each menu method returns the string identifier of the next menu that should be displayed. In our example, section is initially "mainmenu", so viewMainMenu() is called. When the player selects the "Drive!" button, viewMainMenu() returns "playgame", so the playGame() method is called. When the game is finished, playGame() returns "hiscore", etc., until the player selects "exit" in the main menu and viewMainMenu() returns "exit". When the player confirms that he/she really wants to quit, the loop is left, leave2DMode() is called, and the start() method returns.

CGameGUI::viewMainMenu() shows a simple implementation of a game menu. First, the right child widget is selected, then the CGameWinSystem object is intructed to start an event loop, and when that is done, the results are inspected and processed. The CWinSystem::runLoop(CWidget *widget) method that is used here, contains a loop which continuously polls for SDL events, and calls the event handler methods of the widget when they occur. The loop is terminated whenever an event handler returns a value containing the WIDGET_QUIT flag. This happens, for instance, when the player clicks on the "drive!" menu option. Then, runLoop returns and execution continues in viewMainMenu where this choice is processed.

CGameGUI::playGame() is a bit different from the other menus because it sets up the game, lets it run and then unloads the game. The method itself is quite simple:

CGameGUI::load() does the dirty work of setting up the game, based on the input of the player in the different sub-menus. It calls a lot of things in the m_GameCore object, and sometimes also in the m_Server object (this is not the case in our scenario, because we play a local game). CGameGUI::unload() does the less-dirty job of unloading everything.

We enter deeper and deeper into the core of Ultimate Stunts. The call of CWinSystem::runLoop in playGame() gets the small function game_mainloop() as a callback-parameter. game_mainloop() (in ultimatestunts/gamegui.cpp) does nothing else than call the update() method of the CUSCore object. As long as this function returns true, CWinSystem::runLoop continues polling for events and calling game_mainloop(). So, now all control is in the hands of the CUSCore object.

ultimatestunts/uscore.cpp

The loading of all files is done by a call of CUSCore::readyAndLoad() from CGameGUI::load(). First, CUSCore::readyAndLoad() calls the same method of the base class, CGameCore::readyAndLoad(). This method initializes things like network communication, and then loads all data by calling loadTrackData and loadMovObjData. These methods, also overloaded in CUSCore, load the track and moving object data. In the overloaded versions of CUSCore, the corresponding graphics and sound data is also loaded. Loading the track data in CGameCore::loadTrackData() is simply done by instructing m_World to load the track object. All dependencies, like tiles, are solved automatically because CWorld is derived from CDatamanager. Loading the moving objects is a bit more complicated, because in a multiplayer game, remote players can also add cars. Managing the loading of moving objects is the responsibility of CPlayerControl and its derived classes. In our scenario, only a local game is played, so m_PCtrl is of the type CPlayerControl, and the implementation of CPlayerControl::loadObjects() is relatively simple. Again, the loading of objects is based on the CDataManager::loadObject method of the CWorld class.

Loading of the graphics data is done when CUSCore::loadTrackData() and CUSCore::loadMovObjData() are finished calling their CGameCore equivalents, and call the methods loadTrackdata() and loadObjData() of the m_Renderer object. As you can see in ultimatestunts/gamerenderer.cpp, these call similar methods in a CGraphicWorld object. And in graphics/graphicworld.cpp you can see how these methods load all graphics data based on the already loaded data in the CWorld object.

When everything is loaded, CUSCore::update() will be called repeatedly. CUSCore::update() does all things that are necessary in a game cycle. It checks the state of some keys, it updates the game simulation by calling CGameCore::update(), and finally it updates all output elements, like graphics and sound. So, the core of the simulation is in CGameCore::update().

CGameCore::update() does a lot of things that are only relevant for network communication in multiplayer games, but its core activities are the calling of the update() methods of all objects in m_Players and m_Simulations. The method of the m_Players-objects causes different players to do some actions. For instance, this may involve the routines of an AI player, or checking the state of input devices for a human player. The update() methods of objects in m_Simulations control the behavior of the moving objects. In our scenario, there are three simulation objects: one CRuleControl object, which implements all the game rules, like penalty time or finishing, one CPhysics object which does all the physics calculations, and a CReplayer object which records the states of all objects into a replay file.

When the player finishes, this is detected by the CRuleControl object (simulation/rulecontrol.*), and some time after finishing its update method will return false. This causes CGameCore::update() and CUSCore::update() to return false. This causes the CWinSystem object to terminate the runLoop function, and the player will return to the menu interface.