Technical Design Document
Producer, Josh Hobbes
Designer, CJ Clark
Technical Director, Ed Pfent
Technical Writer, Jeff Keely
Art Director, Nathan Gray
Product Manager, Dan Brakeley
Lead Tester, Amadou Savadogo
Connection Methods and Protocols
Sound Engineering Instructions
Windows 9x/NT/2000/ME
CDROM
PII
300mhz CPU
64MB
RAM
8MB
OpenGL compatible 3D video card
100MB
free hard drive space
Direct
Sound compatible Sound Card
CDROM
TCP/IP
Internet connection required for online play
PII
600mhz CPU
64MB
RAM
32MB
OpenGL compatible 3D video card
100MB
free hard drive space
Direct
Sound compatible Sound Card
CDROM
DSL/Cable or faster connection to the Internet
The All-Purpose File I/O tool serves as a wrapper to abstract the actual work of opening, closing, reading, and writing files. This allows the game to be a little less platform dependent as only a few functions would need to be changed to alter the scheme by which files can be read. Only a few functions are actually even necessary to be wrapped at all.
None, though the primary data type is the NS_FILE, a
typedef’ed (FILE*)
OpenNSFile takes the pathname of the file that will
be opened/created. The flags are a combination
of both whether or not to open it as ASCII or binary and how much read/write
access to give it. It returns a handle
to the file that has been opened, which will be equal to the null define if it
has bad data. It serves as a wrapper
currently for the stdio function fopen.
NS_FILE OpenNSFile(char
*pathname, char flags);
CloseNSFile takes a handle to an open file and calls
the stdio function fclose to deallocate resources.
void CloseNSFile(NS_FILE
file);
IsEndOfNSFile takes a handle to an open file and
returns 1 if the handle is at the end of the file or a 0 if it is not. It is basically a wrapper for feof.
int IsEndOfNSFile(NS_FILE
file);
WriteString takes a handle to a file to be written
to and a string to write to that file.
It will write the string in ASCII.
This function serves as a sort of wrapper for the fprintf function.
void WriteString(NS_FILE
file, char *string);
ReadString takes a handle to a file and will grab
one string at a time out of that file until it finds one that has data that
does not begin with the “skip” character which is passed in along with the
function. This allows users to add
comments to their text files without it disrupting any reading the game will do
from that file. It grabs the string
with the stdio function fgets.
void ReadString(NS_FILE
file, char *string, char skipMe);
WriteData is designed to be used with a binary file,
specified by the handle parameter. It
can write out data of any size or type, but both need to be specified before it
knows how much data to write into the file.
As a final parameter it also needs how many elements of the data type
are in the array that is passed in to be used for writing. It returns the
amount of these “items” that it successfully writes to the file. It is a wrapper for fwrite.
int WriteData(NS_FILE
file, void *data, int itemSize, int numItems);
ReadData is designed to be used with a binary file,
specified by the handle parameter. It
reads data into the buffer passed as a parameter. The user must specify how big each element of the buffer is and
how many of these “items” that are to be read into the array. It returns the amount of items successfully
read. It is a wrapper for the stdio function
fread.
int ReadData(NS_FILE
file, void *data, int itemSize, int numItems);
The Game-specific portion of the File I/O tool is actually spread out among the other game-specific tools. For instance, the function to load in all ship models is a game-specific graphics engine function that accesses the all-purpose File I/O tool. Furthermore, there is no ability to load or save a game. Therefore there are no functions or data types specific to this section of File I/O.
We do not expect any security problems and therefore are including no secure file management. All game files will be easily accessible and modifiable by players.
·
NullSpace – the
executable file, documentation (readme.txt)
o
Art
§
2D
§
3D
·
Texture
·
Model
o
Map
o
Collision
o
Option – preference
o
Ship
Null Space will not be a memory intensive application. 95% of all dynamic allocations will occur at the initialization phase of run time. The only other time any dynamic allocation will occur is when a player enters or leaves during mid-game. For this reason, a memory tool is not necessary. We will use C ++’s new and delete memory operators for all dynamic memory allocations.
The OpenGL API will be used to supply vector-based drawing routines in order to create the in-game graphics. It also supplies the transformations necessary to draw objects in three dimensions. If available, OpenGL extensions will also be used to enhance the drawing speed of the game.
The sound engine will indirectly require use of DirectSound for its ease of use and supplied mixing ability. All music and sound objects will have data passed through DirectSound functions before reaching the sound card.
For the final version of the game, the input engine will be altered to make use of DirectInput functions to optimize the speed at which the game receives input and therefore is able to act upon that input.
Winsock will be used to supply all of the lowest level networking functions and support for the network itself. Using the API allows for the construction of a TCP/IP and UDP based messaging system across the LAN or Internet.
Xaudio is a freeware MP3 decoder written by Gilles Boccon-Gibod and is freely distributable so long as either the product it is involved in is non-profit or the user signs a freeware license agreement. For quick access to multiple audio file support including MP3 and WAV, the Xaudio library’s functions will provide the lowest level of the audio engine. Xaudio will interface with DirectSound to abstract the hardware level processes of the audio engine. For more information on Xaudio, see the Xaudio webpage at www.xaudio.com.
Win32 provides low-level interface with the OS including most of the menu functions prior to the actual game. During the menus, the Win32 API will be used to provide the drawing routines and handle the callback system for the buttons and dialog boxes.
Prior to the current project, Nathan Gray investigated OpenGL and found the NeHe Tutorials (http://nehe.gamedev.net) to be an excellent resource on the subject. These resources are supplied free of charge and any code may be reused however credit must be made to NeHe. Therefore, much of what has been already written of the graphics engine owes greatly to these tutorials.
Already written by Nathan Gray before the current semester but for the current semester’s project is the code to:
Specific credit is due to Jeff Molofee for his knowledge and functions to help play an .avi file in OpenGL. Also, Michael Lodge-Paolini wrote the app that converts model files from the exported 3D Studio Max Ascii file (.ASE) to the NullSpace Model File (.NSM), which is what the code that is already written can read in.
Much more graphics code is yet to be written. Most of it is in getting the existing code to work with the NullSpace engine. For example, there are 3 predetermined camera states that need to be added to the functionality of the camera, and instead of only allowing a different model per game object, multiple objects will need to use the same model file. Finally, code has to be written to draw the new map tiles to the screen.
The networking portion of the game is most essentially handling the messages sent to and from different computers. To speed up the process and allow the programmers to immediately get their feet wet in the networking, several functions are being ported over from a previous DigiPen Institute of Technology project.
These functions serve to abstract the users from the lowest level of network programming or, specifically, the sockets functions. They also provide functionality for maintaining a list of connections so that creating a new one does not replace the current one unless specified. And perhaps most importantly, the already written functions have been tested thoroughly and provide extra error checking and error handling that the basic WinSock functions do not. The full list of already written functionality is:
All of these functions were written by Michael Lodge-Paolini, and have been modified for use with the C++ STL List tool by Nathan Gray. The primary part of the networking remains to be written and involves which messages will be sent, how they will be constructed, to whom they will be sent, and when they will be sent.
The sound engine was written over the past summer by Nathan Gray to better encapsulate the functionality supplied by the Xaudio API. This includes abstracting out music objects and sound objects, including the differences between the two. While it is not completely finished, the current audio functions can:
Xaudio supplied a great encapsulation of its API already in the XaudioPlayer class, which creates an invisible window upon instantiation to handle all of the callback messages for the player. Beyond that, some of the Audio code still needs to be written, including anything game-specific. This includes the loading of the audio files and when/where they are played.
Typically, projects that need to use precise timing use the Windows Multimedia extensions library function timeGetTime() which returns the current time specific to the millisecond. However, code supplied in one NeHe tutorial supplied a method to be even more exact. By using the low-level command QueryPerformanceCounter(), the current time can be retrieved to the nano-second. Nathan Gray neatly wrapped up this functionality into a class with the help of the NeHe tutorial. Nothing left about this needs to be written as it is a tight, concise concept.
Dan Brakeley started the in-game UI in NullSpace last March. The code in its present condition can render text in any of the windows fonts in any size and color in an OpenGL graphics window. It also makes allowances for designs that were intended for one resolution, but are being rendered in another, allowing it to have the same size relative to total screen real estate regardless of the resolution.
Also, code to display a bmp file (windows bitmap) as a simple texture mapped quad in OpenGL has been written (the size of the bitmap is limited by the max texture size allowed by the graphics hardware). Using these textures, the code also can draw frames with transparency around text, as well as placing a background behind the text itself, giving functionality similar to the text boxes in Square RPGs like the Final Fantasy series.
The code does have holes current, and it still needs bug testing and some additional functionality added to it before it is ready to be used in NullSpace. Specifically, the functionality of easy loading from a file does not work in all cases. Also, there is no functionality to easily display columns (this would be useful for score lists that NullSpace will need).
Additionally, this code is useless in creating the front end for the NullSpace server and client, as those will be using standard Microsoft Windows controls in a typical 2D Windows style. The code written ONLY works in OpenGL.
For the purpose of readability and neatness, our project will require coding guidelines. These guidelines will give a standard way of writing function declarations, function comment headers, and file layout. The exact way described here is not necessarily the way that it must be done. This just provides an outline of what must be done. Each member on the team may a have a slightly different style.
Here is an example of what the beginning of a file would contain:
/*******************************************************
* FILE fileName.cpp
*
* AUTHOR name
* CREATED 10/1/01
* LASTMOD 10/7/01
*
* PURPOSE
* Description of what’s
going on in this file.
*
******************************************************/
Here is an example of what a function declaration would contain:
//-----------------------------------------------------
// FUNCTION
// functionName
//
// DESCRIPTION
// Description of what the function does.
//
// RETURN
// void
//-----------------------------------------------------
Void functionName( TYPE1
name1, TYPE2 name2 )
{
}
Other than above, programmers will be using advanced programming techniques to help the readability. Some examples of these are:
Naming Conventions
Members of a class with a prefix such as m_MemberName.
Class names should begin with a prefix c such as cClassName.
Pointers should have a prefix p such as pPointerName.
Comments
Comments are a big key in readable code. So for our project, we will require that there be comments on almost everything. Declaring a variable? What’s it used for? Calling a function? What for? You get the point.
The code objects that we will be using for Null Space are OpenGL, Window based GDIs, C++’s STL lib, Winsock DLLs, and custom tool libraries.
OpenGL is used for the actual game window. Any OpenGL objects are initialized in GameInit() and deleted in GameTerminate(). The Window’s GDIs are used for the front-end menu system. Any Window objects are initialized in MenuInit() and deleted in MenuTerminate().
The STL lib has a nice link list interface specific to C++. For more information see the STL lib section in External Code.
The networking will be using the Winsock DLLs. Any networking/Winsock objects will be created before running the menu system. The objects will be released/deleted after all other engines and windows are destroyed. For more information see client main in the Control Loop section.
Our custom tool libraries will make up most of our game engine. Each tool library will try to stay independent from the other libraries. There maybe some small exceptions where this cannot be avoid. Each tool library will have two types of functions, game specific and all-purpose. Game specific functions are functions that are mandatory functions that will only be used for this project. They cannot be ported over to another project because their parameters will have data structures and other Null Space related objects being passed in. All-purpose code is the exact opposite. All-purpose functions are general code that is portable and the game specific functions depend on them. They do not take any Null Space objects as parameters.

Null Space’s engine will be built using our modular tools. This makes function calls in the game loops very high level and easy on the eyes. Another purpose of making modular tools is for easier debugs. Being able to debug a tool separately away from the rest of the engine is very time saving.
The map editor will be used to create all the maps for NullSpace. All NullSpace maps will be saved in a file with a .map extension. The map editor will be a fully seperate executable than the client or server executables. It will not interact with or rely upon any other sections due to it being fully separate than the game and likewise no other sections will rely on the map editor. The File section will load the maps that were created and saved from the map editor. The map editor will be written using a pre-exsisting map editor's core engine.
The interface for the map editor will consist of very few buttons. The standard arrow keys will be used to move around the map and holding shift and an arrow key will jump the user one screen in the given direction. The mouse will be used to select options or buttons and place or remove tiles. Finally the “esc” button will be used to quit and will auto save the map as “NewMap.map”. The map will be shown with every tile being 16x16 in size on the screen and in the nine tile buttons at the bottom of the screen. On the right hand side there will be five option buttons that the user can select that will do a variety of things.
When the user begins a new map the editor will automatically load a empty level that is the size of the screen. They will have the option of using that map, loading a different map, or resizing the map to a given number of tiles. If they choose to load a map the screen will turn black and they will be shown a list of all the *.map files that are in the same folder as the map editor’s .exe file. Clicking on one of these will load that map. If the user decides to resize the map they will be asked what width and height they want. Entering a couple of numbers and pressing enter will set each one in turn. If the user resizes the map so that it is smaller than the current map they may destroy part of the map and lose that data. If the user decides to make the map larger then it will add to the lower edge and the right edge of the map.
The tile buttons in the map editor are as follows: (in order from left to right)
0 – Blank tile, no collision, no art
1 – Solid tile, collision with art
2 – 45 degree angle tile with collision and art
3 – 45 degree angle tile with collision and art
4 – 45 degree angle tile with collision and art
5 – 45 degree angle tile with collision and art
6 – Multiprize spawning point with collision and art
7 – Safe Zone with no collision and some art
8 – Black Hole with collision and some art
The option buttons in the map editor are as follows: (in order from top to bottom)
0 – Place Tile (must also click a tile button that the user is going to place)
1 – Resize Map
2 – Select All (selects entire map and user can move it around to center it with the mouse)
3 – Save Map
4 – Load Map
Width #
Height #
######…….
……..
……..###
Explanation: Each ‘#’ character represents a single digit. After Width and Height there can be any number of digits as necessary to represent the number of tiles wide and high that the map is. After Height there will follow a long stream of ascii values that represent the tile number and will be in the proper position on the map. There will be as many tile digits here as Width * Height.
There are no game specific functions in the map editor due to it being a seperate executable and the layout of the .map file is able to be used for anything.
struct MAP
{
unsigned int **pMapArray; // A
2D array that holds all the tile index's in their //
relative positions on the map.
int Height; // The height of the map in tiles
int Width; //
The width of the map in tiles
char *pMapName; // A
string containing the map name to save the map as, // defaults as NewMap.map
}MAP;
struct DATA
{
DDSurface *pTileImage[9]; // A
direct draw surface that holds all the images for //
the Tiles
DDSurface *pObjectImage[6]; // A
direct draw surface that holds all the images // for the Object buttons
DDSurface *pBackBuffer; // A
pointer to the direct draw backbuffer
DDSurface *pFrontBuffer; // A
pointer to the direct draw screen surface
MAP *pMap; // A
pointer to an instance of the MAP structure
RECT ViewportLoc; // A
rect in tile coordinates that represents the // viewport's location on the map allowing the
user to //
see that portion of the map.
POINT MouseLoc; //
The location of the mouse in window coordinates
int OptionSelected; //
What option the user is currently using
int TileSelected; // What
tile the user has selected.
bool ShiftHeld; //
Weather or not the user is holding the Shift key.
}DATA;
void DrawMap(DATA *pData)
{
Check location of viewport on map.
for(x = loc.left; x < loc.right; x++)
{
for(y = loc.top; y < loc.bottom; y++)
{
DrawImage()
}
}
}
DrawOptionButtons(DATA
*pData)
{
for(i = 0; i < 6; i++)
{
DrawImage(pData->OptionImage[i])
}
}
DrawTileButtons(DATA *pData)
{
for(i = 0; i < 9; i++)
{
DrawImage(pData->TileImage[i])
}
}
void MoveViewport(DATA
*pData, int Direction)
{
Check location of the viewport on map.
if(user is holding the Shift key)
{
Move map in the Direction indicated by an entire screen
if able.
}
otherwise
{
Move the map in the direction indicated by one tile if
able.
}
}
void SelectOption(DATA
*pData)
{
Check the location of the mouse.
Perform RECT check to see if the mouse is inside one of the
option buttons.
if so
{
Set pData->OptionSelected to that index.
}
}
void SelectTile(DATA *pData)
{
Check the location of the mouse.
Perform RECT check to see if the mouse is inside one of the
tile buttons.
if so
{
Set pData->TileSelected to that index.
}
}
void PlaceTile(DATA *pData)
{
Check the location of the mouse.
Offset that location according to the location of the viewport
on the map.
Find out what tile the user clicked on.
Set pData->pMap->Map[MouseX][MouseY] =
pData->TileSelected.
}
void DeleteTile(DATA *pData)
{
Check the location of the mouse.
Offset that location according to the location of the viewport
on the map.
Find out what tile the user clicked on.
Set pData->pMap->Map[MouseX][MouseY] = 0.
}
The input tool is code that is responsible for having some
type of action occur when the user presses a key. This tool will only be used for the client game loop. The server doesn’t take much input, and the
input it takes is handle by Window’s GDIs.
The menu’s input will also be using the Window’s GDIs. This tool will be mainly used for in game
input, e.g. holding the up arrow key to accelerate the user’s ship forward.
There are four states that a key can be in, pressed, just-pressed, just-released, and not pressed. Pressed indicates that the User has been holding down the key in question. Just-Pressed indicates that the User has started holding down the key. Remember that when a key on the keyboard is tapped, the input devices get hundreds, if not thousands, of “hits” for that key. So if we only wanted one thing to happen when a key is press, we would have to check for the just-pressed state. Just-Released indicates that the User stopped pressing a key and not press simply means the key in question has not be used.
The input tool does not rely on any of the other tools and other tools do not rely on the input tool. Only the game loop needs to check the input to see what the User wants to do in the game.
The input class will need two very important arrays. Both these arrays will be n array of bytes the size of 256. The first array, called keys, will be the current state of all the keys in the keyboard. The second array, called lastKeysState, will hold all previous key states. We need to know the last state a key was in, to decide if a key is being pressed, just-pressed, or just-released.
There will be two input classes. One input class for Direct Input and one class for the Windows’ Input. The only difference between these classes is the private data. The Direct Input class will need instances of Direct Input and a keyboard device.
We will be using direct input, however since it makes debugging difficult, we will be using the standard input/ output until the final version. Two input classes will be used, one for direct input and one for the standard i/o. A define will determine what class we’ll use, so a switching between the two classes will be easy. However, this means that the standard i/o class must have the same functionality as the direct input class. The means that the standard class will have two empty functions, which are init() and end().
We will define all the direct input and the entire window input with the same defines so that we it will be easier to work with one set of input defines.
Here is an example:
#define INPUT_UP DIK_UP //VK_UP
#define INPUT_DOWN DIK_DOWN //VK_DOWN
#define INPUT_LEFT DIK_LEFT //VK_LEFT
#define INPUT_RIGHT DIK_RIGHT //VK_RIGHT
#define INPUT_PGUP DIK_PRIOR //VK_PRIOR
#define INPUT_PGDN DIK_NEXT //VK_NEXT
#define INPUT_TAB DIK_TAB //VK_TAB
#define INPUT_F1 DIK_F1 //VK_F1
#define INPUT_F2 DIK_F2 //VK_F2
#define INPUT_F3 DIK_F3 //VK_F3
#define INPUT_F4 DIK_F4 //VK_F4
#define INPUT_F5 DIK_F5 //VK_F5
#define INPUT_F6 DIK_F6 //VK_F6
#define INPUT_F7 DIK_F7 //VK_F7
#define INPUT_F8 DIK_F8 //VK_F8
#define INPUT_F9 DIK_F9 //VK_F9
#define INPUT_F10 DIK_F10 //VK_F10
#define INPUT_F11 DIK_F11 //VK_F11
#define INPUT_F12 DIK_F12 //VK_F12
#define INPUT_ESC DIK_ESCAPE //VK_ESCAPE
#define INPUT_NUMPAD0 DIK_NUMPAD0 //VK_NUMPAD0
#define INPUT_NUMPAD1 DIK_NUMPAD1 //VK_NUMPAD1
#define INPUT_NUMPAD2 DIK_NUMPAD2 //VK_NUMPAD2
#define INPUT_NUMPAD3 DIK_NUMPAD3 //VK_NUMPAD3
#define INPUT_NUMPAD4 DIK_NUMPAD4 //VK_NUMPAD4
#define INPUT_NUMPAD5 DIK_NUMPAD5 //VK_NUMPAD5
#define INPUT_NUMPAD6 DIK_NUMPAD6 //VK_NUMPAD6
#define INPUT_NUMPAD7 DIK_NUMPAD7 //VK_NUMPAD7
#define INPUT_NUMPAD8 DIK_NUMPAD8 //VK_NUMPAD8
#define INPUT_NUMPAD9 DIK_NUMPAD9 //VK_NUMPAD9
#define INPUT_A DIK_A //'A'
#define INPUT_B DIK_B //'B'
#define INPUT_C DIK_C //'C'
#define INPUT_D DIK_D //'D'
#define INPUT_E DIK_E //'E'
#define INPUT_F DIK_F //'F'
#define INPUT_G DIK_G //'G'
#define INPUT_H DIK_H //'H'
#define INPUT_I DIK_I //'I'
#define INPUT_J DIK_J //'J'
#define INPUT_K DIK_K //'K'
#define INPUT_L DIK_L //'L'
#define INPUT_M DIK_M //'M'
#define INPUT_N DIK_N //'N'
#define INPUT_O DIK_O //'O'
#define INPUT_P DIK_P //'P'
#define INPUT_Q DIK_Q //'Q'
#define INPUT_R DIK_R //'R'
#define INPUT_S DIK_S //'S'
#define INPUT_T DIK_T //'T'
#define INPUT_U DIK_U //'U'
#define INPUT_V DIK_V //'V'
#define INPUT_W DIK_W //'W'
#define INPUT_X DIK_X //'X'
#define INPUT_Y DIK_Y //'Y'
#define INPUT_Z DIK_Z //'Z'
void UpdateInput(mainData
*theWorld);
UpdateInput checks all the keys and will send the appreciate
data to the networking window. The
networking window will take care of the data being send to the server. To see more on the networking window, see
the networking section of this document.
The states of the keys that should be checked are pressed, just-pressed,
and just-released. Since we do not want
to flood the server, we only need to send messages if a button is just-pressed
or just-released. For example, if the
User is holding do the up arrow key to accelerate, the server only needs to
know that the User has started to accelerate.
The server will keep accelerating the User until it receives a
just-release update. UpdateInput will
take a mainData pointer so it can have access to the networking window. UpdateInput will not return anything.
Void UpdateInput(mainData
*theWorld)
{
if(IsKeyPress(upArrow) = = Just-Pressed)
PostMessage(GetNetworkingWindow(np),
WM_FORWARDDOWN,0,0);
if(IsKeyPress(upArrow) = = Just-Released)
PostMessage(GetNetworkingWindow(np), WM_FORWARDUP,0,0);
:
:
:
}
BYTE IsKeyPressed(BYTE key);
IsKeyPressed is a function that checks to see what was the last input for the key in question and returns what state is it in. The function also updates the lastKeyState.
Although this function is very simple, it’s very important. This function will be the heart of the input tool. The only data that IsKeyPressed needs is a BYTE which represents a key on the keyboard. The array lastKeyState will be updated to pressed for that key if the last key state was just-pressed. The function will return one of the four states a key could be in, pressed, just-pressed, just-released, and not pressed.
void Update();
Update goes through the array of keys and checks the current state of each key. Depending on what the key’s last state was, will decide what the current state will be. For example, if a key’s last state was pressed, and updated checks the key and sees that it is not longer being pressed, it will change the last key state to being just released. The next time Update is called, it sees that the key is still not being pressed so it will change the last key state to being not pressed. Update does not need any data since everything it needs is internal due to C++.
int Init(HINSTANCE
hInstance, HWND hWnd);
Init sets up the direct input devices. It needs the hInstance and hWnd of the window to create the direct input EX and to set the cooperative level. Init will return false if any of the direct x objects cannot be create thus failing the initialization. Init will return true if everything gets create correctly. The objects that need to be created are, the direct input, keyboard device, keyboard data format, setting the cooperative level, and acquiring the keyboard device.
void End();
The end function will free up all the direct input objects that Init creates. No data needs to be passed in and nothing is returned.
The All-Purpose section of the Networking Engine has a very distinct and simple purpose. The objective is to supply low-level functions that help to abstract use of the Winsock API from the game-specific networking. There are three types of functions that help accomplish this: the general functions, the TCP/IP-specific functions, and the UDP-specific functions.
The general functions include WinSock initialization and various ways of getting a user’s IP address from either domain names or sockets. The TCP/IP and UDP sections are fairly similar in that they both provide sections to send and receive data, differing mostly in the initialization data passed to the sockets (one specifying TCP, the other UDP). However, the major difference is that with TCP/IP one is allowed to establish connections between the computers, so a number of functions are made available to help connect and disconnect two computers.
This section is wholly contained in and of itself. It only needs access directly to the Winsock API, the STL List class and no other section, game-specific or otherwise. It has a single internally defined structure called “Connection Data” whose purpose is to keep track of connections between users and any data currently being sent between the two of them. It is also important to note that the all-purpose networking is neither specific to the client nor the server and may be used freely in either one.
The CONNDATA structure is important because it lets
the user keep representations of the connections they’ve made in the past. This also allows them to disconnect by
providing a CONNDATA instead of a socket.
To properly keep all the data of a connection, a number of variables are
needed. The first and most important is
the SOCKET variable that WinSock uses when sending data across the
network. This SOCKET is created upon
connection with another user. It also
needs a WinSock-defined struct that keeps the IP address of the connection and
the time that the connection was made.
In addition to this information, if the user is
currently receiving information from a user, it is important to keep track of
how many bytes they currently are receiving so that if a different user’s
packet is received in the meantime, they can pick up where they left off. Besides these counting variables and the
buffer used to store them, the final key piece of information is the internal
representation of the user’s identification: an unsigned integer that is 1 +
(the ConnectionID of the most recent connection).
typedef struct _stConnData
{
SOCKET hSock; // connection Socket
SOCKADDR_IN stRmtName; // local address and Port
SYSTEMTIME lStartTime; // time of connect (by the system
time)
int iBytesRcvd, // data currently buffered
iBytesSent; //
data sent from buffer
LONG lBytesCount; // total byte recieved
char IObuffer[BUF_SIZE]; // network I/O buffer
unsigned int ConnectionID; // a unique ID given to a connection
} CONNDATA, *PCONNDATA,
FAR *LPCONNDATA;
General Network Functions
This function runs Winsock’s initialization
functions, abstracting them from the user.
It must be called before any other networking functions will work.
int ALL_InitWSAComponent();
This function returns a string containing the IP
address of the socket that is passed in.
It also adds it to the buffer variable that is passed in. This IP address is received by calling a
sockets function.
char * ALL_GetIPString(SOCKET
hSock, char *buf, int len);
ALL_GetHostAddress takes a domain name (or, if on a
LAN, a computer name) and calls some sockets functions to fill in the IP variable
with a string containing the IP address of the computer.
int ALL_GetHostAddress(char
*cHost, char *IP, int iplen);
ALL_GetHostName fills in the cHost string with the
name of the computer associated with the IP variable that is passed in. This name is received by calling a
sockets-specific function.
int ALL_GetHostName(char
*IP, char *cHost, u32 hostlen);
TCP Functions
TCP_AcceptConn takes a socket and grabs some
information on it (storing it in a variable) using sockets functions, then
associates it with the current host socket and returns the newly connected
SOCKET. The SOCKET will be a NULL value
if the function failed.
SOCKET TCP_AcceptConn(SOCKET
hLstnSock, PSOCKADDR_IN pstName);
TCP_CloseConn takes a socket that it is currently
connected to, grabs any remaining information from that socket (storing it in a
buffer), then closes the connection. It
returns failure (0) if it could not close the connection.
int TCP_CloseConn(SOCKET
hSock, LPSTR chInBuf, int len);
Sends a connection message to a specified IP over a
specified port. It also needs access to
the socket that the message is being sent from. It calls the sockets connect function.
BOOL TCP_Connect(SOCKET hSock, char *IP, unsigned short PORT);
Needs the list of all connection messages made by
the socket that is to be terminated so that it can run through each of them and
disconnect the sockets. It also needs
the HWND of the window that will be handling the WinSock messages so that it
can post a WM_SOCKET close message. It
returns the amount of bytes that were left to be received from the closed
connections.
int TCP_DestroyWSAComponent(SOCKET
hSock, list<LPCONNDATA> & m_list, HWND hwnd);
TCP_Disconnect takes a socket and closes it off,
freeing any resources allocated for it.
Returns 0 for a successful close.
int TCP_Disconnect(SOCKET
hSock);
Given a SOCKET, TCP_FindConn will look through the
supplied list of connections and, if it finds it, will fill in the iterator
variable with the iterator of the connection.
If it does not find it, the iterator will be uninitialized and the
function will return 0.
int TCP_FindConn(SOCKET
hSock, list<LPCONNDATA>
&m_list, list<LPCONNDATA>::iterator &it);
TCP_InitClientSock is for sockets on the client
side. Given a handle to the window that
is to receive a socket’s WinSock messages, it will create the socket, associate
it with the window using WinSock, and return it. If the function fails, the socket will be NULL.
SOCKET TCP_InitClientSock(HWND
hwnd);
TCP_InitHostSock is for sockets on the server
side. Given a handle to a window that
is to receive the socket’s WinSock messages, it will create the socket and
associate it with the window. It will
set up to listen on the specified port and will fill in its properties in the
passed in SOCKADDR_IN.
SOCKET TCP_InitHostSock(HWND
hwnd, SOCKADDR_IN *stLclName, unsigned short Port);
TCP_NewConn allocates a new connection structure and
fills it in with the information supplied by the variable passed in. It then takes the socket and associates it
with the particular connection and adds it to the list of connections. It also returns a pointer to the newly
allocated connection directly, which will be NULL if the function failed.
LPCONNDATA TCP_NewConn(SOCKET
hSock, PSOCKADDR_IN pstRmtName, list<LPCONNDATA> & m_list);
TCP_RecvData takes a socket and checks if there is
any data waiting to be received from it.
If there is, it will fill out the packet that was passed in with an
amount of information specifed as a parameter.
A handle to the WinSock window is also provided so that any errors can
be posted directly to the window as messages to close the socket. The function returns the total number of
bytes actually received.
int TCP_RecvData(SOCKET hSock, char
*lpPacket, int cbTotalToRecv, HWND hwnd);
TCP_RemoveConn takes a pointer to a connection and a
list of connections. It finds the
connection within the list and removes it.
If it cannot find the connection in the list, then that means it has
already been removed.
void TCP_RemoveConn(LPCONNDATA lpstConn,
list<LPCONNDATA> & m_list);
TCP_SendData sends data out on a particular socket
(to any sockets it may be connected with).
The data it sends is supplied in the packet, which is passed in by
pointer, followed by the sizeof(the packet) so that it knows how many bytes to
send. Finally, the handle to the
WinSock window is supplied in case there is an error and it needs to close the
socket. Returns the number of bytes
successfully sent.
int TCP_SendData(SOCKET hSock, char
*lpPacket, int cbTotalToSend, HWND hwnd);
UDP Functions
This function takes a socket and a list of
connections, making sure to eliminate any connections between the socket and
the sockets in the list. It then frees
the socket and any resources allocated with it. If it encounters an error, it posts the error to the WinSock
window. It returns 0 for success.
int UDP_DestroyWSAComponent(HWND
hwnd, SOCKET hSock, list<LPCONNDATA> & m_list);
UDP_InitSock creates a socket and associates it with
the supplied port. It then associates
it with a window (via the window handle) so that if it receives WinSock
messages they are sent to that window.
Next it polls the hardware for some information and fills out the
SOCKADDR_IN structure that was passed in.
SOCKET UDP_InitSock(HWND
hwnd, SOCKADDR_IN *stLclName, u16 Port);
UDP_RecvData takes a socket to start looking for
data with, a string to fill in with any data received, and a buffer length for
that data. It searches all known
networks for any data and if it finds any will fill in the string and the
SOCKADDR_IN with the information about the sender of the data, then return the
number of bytes successfully received.
int UDP_RecvData(SOCKET
hSock, char *lpInBuf, int BufLen, SOCKADDR_IN *stRmtName);
UDP_SendData takes a socket that has already been
initialized, a pointer to a SOCKADDR_IN struct contains information about the
data’s destination (if this parameter is NULL, the data will be broadcast
across the network), a port to send the data on, an array with the data and an
amount of bytes to send. It will
attempt to send the data and will return the amount of bytes successfully sent.
int UDP_SendData(SOCKET
hSock, SOCKADDR_IN *stRmtName, u16 Port, char *lpOutbuf, int cbTotalToSend);
The Networking Engine interfaces with the game in two different manners. The first is when the game needs to tell the engine to send some information. To that end, a system of Windows messages has been “user-defined” that are associated with specific actions, such as sending a chat string. Therefore, at any time during the game, the code can post a networking message to the network window using the GetNetworkWindow() access function. Depending on the message sent, extra parameters may need to be passed as WPARAM and LPARAM. The Network Proc will receive this message and, because it is given access to the game’s main data structure at the beginning, it will have the ability to grab any additional information it needs from the game to send across the network.
The second way that the Networking Engine interfaces with the game is a little more “hands-off.” This is when the game receives a message across the network. Because the networking runs in its own thread constantly checking for messages across the network, it will receive any data and automatically act upon it. Because the network engine receives access to the game’s main data structure at initialization, it will already have the data to manipulate based off of the given message. What it manipulates and how is determined by the message that it receives.
The multi-threading is accomplished by creating the thread (passing in the pointer to the MainData structure as the extra parameter) which then handles the creation of the invisible Networking window that will serve as a callback point for all of the WinSock messages. Right after the thread is created, the main thread will suspend itself until the network window is created and “sets an event,” which unsuspends the main thread and allows it to continue. If there is an error and the even doesn’t get set, the suspension will time out and will return an error. Once the network window is created, it will throw itself into a message loop to handle any messages and will quit when a WM_QUIT message is received.
The callback structure of WinSock is a simple one; every time a socket is created that information will be sent out through, it is associated with a window handle so that all messages will be redirected to it through the WinProc callback function. The messages that it can receive are the WinSock-defined FD_ACCEPT, FD_READ, FD_WRITE, and FD_CLOSE. Whenever one of these messages are received, the appropriate action is performed and, if it was a read, the packet received is acted upon.
There are four main types of messages that can be sent, and several subtypes that belong to each of the main ones. There are also messages that the main thread will need to send to the network thread to indicate what messages need to be sent.
Primary Messages:
NM_REQUEST //For trying
to get access (and connections) to a server
NM_INIT //For
telling clients to (and how to) add players
NM_PLYR_SYNC //For keeping
all clients synchronized
NM_PLYR_ACTN //Messages
clients send to server saying they’ve done something
Secondary Messages:
/* NM_REQUEST messages */
RQST_ACCESS //sent
to server to request connection
RQST_DENIAL //sent
from server to client if game is full or an error
RQST_CFRM_NEW_USR //a message
to confirm that the server need make a new user
/* NM_INIT messages */
INIT_MAP //which
map will get used
INIT_PLYR_ADD //lets
the clients know to add a player to their game
INIT_PLYR_NUM //send
the amount of players currently in game
INIT_PLYR_NAME //using
bit manipulation allow for a name to be sent
INIT_PLYR_IDS //setup
each clients player ID
INIT_PLYR_SHIP //send a
players Ship ID
INIT_PLYR_ID //send a
players Connection ID
INIT_GC //send
any other “game conditions”
/* NM_PLYR_ACTN messages */
PLYR_SPEC_PROJ //tell all
computers about a special projectile creation
PLYR_NORM_PROJ //tell all
computers about creating a normal projectile
PLYR_CHANGE //send
whenever a player changes motions
PLYR_STRING //used
to send a string of characters
/* NM_PLYR_SYNC messages */
PLYR_POSITION //update
of the players position (incase of errors) PLYR_ACCEL //update of the player’s
acceleration
PLYR_VEL //sync
the player’s velocity
PLYR_ANG_ACCEL //update
of the player’s angular acceleration
PLYR_ANG_VEL //sync
the player’s angular velocity
PLYR_HEALTH //sync
the players health
PLYR_DEATH //notify
players about a player death
PLYR_NUM //sent
from the server, confirming number of players
PLYR_SCORE //lets
the player know player scores
PLYR_BOUNTY //lets
the player know bounty values
PLYR_DISCONN //let
players know that a person is disconnecting
Windows Messages:
WM_CONNECT //attempts
to connect to IP address string in WPARAM
WM_DISCONNECT //disconnects
from IP address specified by WPARAM
WM_SENDSTRING //includes
message to be sent as WPARAM
WM_UPDATE_PLAYERS //server
message telling netProc to send update messages
WM_UPDATE_STATUS //client
message telling netProc to send updates
The network_params structure is a solely internal
representation of variables important to keep track of for the network’s
separate thread. It serves as a way to
communicate between the network window and the main game. First and foremost, there’s the handle to
the program’s instance, which will need to be filled in before the
network_params is passed off to the InitNetwork function because the network
will need direct access to that handle so it can create a window. The remaining variables are going to be set
inside of the network’s thread.
The window handle is the handle to the invisible network window that all messages sent there will need to be posted to. Therefore, there is an access function that will return this window handle since all postmessages will need it. The thread’s ID is set upon creation of the thread and is used in destroying the thread. Next, the handle to the thread itself is created in case any error detection handling needs it. Then there is a handle to an event that will allow us to suspend the main thread while waiting for the network thread to complete a task (such as create its window). And finally there is the void parameter which points to game’s main data structure so that it can be passed off to the network proc.
typedef struct _network_params
{
HINSTANCE hInstance;
HWND hwnd;
DWORD ThreadID;
HANDLE Thread;
HANDLE hEvent;
PVOID pvoid;
}*NETWORK_PARAMS;
The tPacketData structure contains the fields for a data-based packet. It contains three fields, two for signed integers and one for an unsigned integer, which are all different depending on which packet is being sent. Note that it is possible to store floating point values within these integers with a simple memcpy command, they just won’t look right until they are memcpy’ed back into a floating point number again.
typedef struct _packet_data
{
int sdata1; // depends on type of packet
int sdata2; // depends on type of packet
unsigned int udata; // depends on type of packet
} tPacketData,
*ptPacketData;
The tPacketString structure contains the fields for a string-based packet. The size will be roughtly 512 bytes in size to begin with, and if it proves to be too much or too little during testing it will be accounted for.
typedef struct _packet_string
{
char p_string[BUF_SIZE]; //a string to be sent
} tPacketString,
*ptPacketString;
The tPacket is the important structure that groups
data specifically so that when it is sent across the network it can be read out
in an orderly fashion. The constant
portion of the packet will be the initial variable containing the ID value of
the user who sent the data, a variable for the type of data contained within
the packet (see Network Messages), and a variable for the size of the data
portion of the packet so that we don’t read out too much or too little
data. Finally, we have a simple union
of the packet string and the packet data structures, of which only one would
ever be used at a time so we can treat the packet as being one or the other
(based on the type field).
typedef struct _packet
{
unsigned char source; //
who sent the data
unsigned short type; // data in packet
unsigned short size; // the size of the data portion of the
packet
union
{
tPacketString
string;
tPacketData
data;
};
} tPacket, *ptPacket;
Internal Networking Functions:
InitNetworkWindow is a wrapper for the RegisterClass so that it creates a completely blank window type and associates it with the string that is passed in.
int InitNetworkWindow(HINSTANCE
hInst, char *App);
CreateNetworkWindow is a wrapper for the Windows API CreateWindow function, where most of the parameters that are passed are set to NULL since the networking window is completely invisible and “hidden” to the user. The only parameters it needs are the instance of the program, the same string as the one used in the InitNetworkWindow, and the extra parameter which points to the game’s main data structure.
HWND CreateNetworkWindow(HINSTANCE
hInst, char *App, void *pvoid);
The NetworkThread is the thread handler that will be passed off to the WinAPI CreateThread function when the network’s thread needs to be created. The pvoid parameters will be the NETWORK_PARAMS so that the network can communicate with the with the main thread by resupplying the window handle through the structure.
DWORD WINAPI NetworkThread(PVOID
pvoid);
The NetProc is the callback for the WinSock window that all networking messages will be processed in. It is a standard Windows API callback function.
LRESULT CALLBACK NetProc(HWND
hwnd, UINT message, WPARAM wparam, LPARAM lparam);
Public Networking Functions:
InitializeNetwork is the very important function whose responsibility is to create the network thread, passing off all of the proper variables including the pointer to the game’s main data struct. First of all, it allocates the NETWORK_PARAMS variable and sets its instance handle variable. The function will suspend itself immediately after creating the thread for a “timeout period” until either the network thread sets the event to unsuspend it or it times out. In the event that it times out, the function returns FALSE for failure, however if it succeeds then it returns TRUE for success.
BOOL InitializeNetwork(HINSTANCE
hInstance, NETWORK_PARAMS *np, PVOID pvoid);
Terminate network closes the thread and frees any resources allocated to the NETWORK_PARAMS, setting it to NULL when it is finished.
void TerminateNetwork(NETWORK_PARAMS
*np);
GetNetworkWindow is a data access function to get the handle to the network window from the NETWORK_PARAMS variable.
HWND GetNetworkWindow(NETWORK_PARAMS
np);
The All-Purpose portion of the Audio Engine exists to provide a simple way to play Music files and Sound Files (which can be in either MP3 or WAV format). To facilitate this, the audio engine provides an interface to the Xaudio API. The development kit for Xaudio provided a class called the XaudioPlayer that encapsulates most of what Xaudio has to offer. In the XaudioPlayer, an invisible window to handle player callback messages is automatically created, placed in a separate asynchronous thread, and destroyed at the proper times, keeping that nasty bit of code hidden from the user. Instead it reveals a number of virtual functions that the user is allowed to define personally to handle callback messages.
This is where the AudioPlayer class steps in. It is designed to further abstract the XaudioPlayer by handling the callback message “notify player state” so that the song knows when it ends and therefore when to loop. This looping funcionality needs only to be available to Music files, a fact that gets enforced by later classes. The cornerstone of the Audio Engine is the AudioClass, which manages an array of these AudioPlayers so that multiple channels of audio may be played at once. Due to Xaudio’s inherent use of DirectSound, all of these channels are automatically mixed together.
Finally, at the highest level, there are the Music and SoundEffect classes themselves, which are each linked to an AudioClass then contain other pieces of information such as the pathnames to the audio files that will be played. Because the audio files will be played out of files, the Audio Engine needs access to the File I/O handler, but other than that it is completely indepenedent of all other tools.
The AudioPlayer class is the encapsulated XaudioPlayer
that adds the extra feature of automatically repeating songs and keeping track
of whether or not it is currently in use.
It needs variables to know if it is currently playing (set during the
Play function and cleared when it reaches the end of file or a Stop is called),
what the offset is (i.e. where to play from in the file once it reaches the end
and it loops) and whether or not it should repeat at the end of a file. To accomplish this, it defines a function
that directly applies to the base XaudioPlayer class, the HandleStateMessage,
which repeats a song from the repeat point on the state “end of file”. The AudioPlayer is what ultimately needs
access to the File I/O handler as it needs to confirm whether or not a file
exists.
class AudioPlayer : public
XaudioPlayer
{
bool repeat;
bool inuse;
int offset;
public:
//Function to handle PlayerState callback messages
//Function to play/stop an audio file
//Access function to check if a player is currently in use
};
The AudioClass contains an array of several
AudioPlayers and an integer to keep track of how many were dynamically
allocated. There are two allocations
that must be performed: one for setting how many players there are, and then
one for each player itself. The
AudioClass is self-terminating as all the free commands are located in its
destructor. Whenever a song is called
to be played, it is run through the AudioClass to find a player that doesn’t
currently have audio playing and plays it from that AudioPlayer (consequently
setting that player to “active” until the song/sound effect is finished). It is important to note that it is assumed
music files will never need to overlap, therefore to insure maxiumum
efficiency, the first channel of the ppPlayers is reserved for music. If any new music file is played, it will
replace whatever is playing in the first index, while sound effects are
relegated out to how ever many players remain (the number of which is specified
upon the AudioClass’s initialization).
class AudioClass
{
AudioPlayer **ppPlayer;
int numPlayers;
public:
//Functions to initialize, play and stop audio
//Access function to get access to a player that has nothing
playing
};
A Song class is an encapsulation of what a song
is. First of all it has a pathname of
the song file and a name to be associated with the song (if one ever needs to
display that). Next it has whether or
not the song is supposed to be repeated and, if it is, where to repeat it
from. Specifically, this repeat point is
a number out of 1000 representing a percentage forward into the song to “skip over.” Finally, it needs access to an initialized
AudioClass from which to actually play itself, which must be set during the
Song’s creation.
class Song
{
char name[SONG_NAME_LEN];
char path[SONG_PATH_LEN];
bool repeat;
int repeatPoint;
AudioClass *ac;
public:
//Access functions to change the song’s pathname
//Function to “create” the song
//Function to play/stop the song
};
A SoundEffect class is an encapsulation of what a
sound effect is. First of all it has a
pathname of the sound effect file. Next
it has a value indicating the priority of the sound effect. This is set up so that if there is a sound
effect that absolutely must be heard and there are no players available, it
will stop one of those players and play itself. Finally, it needs access to an initialized AudioClass from which
to actually play itself, which must be set during the SoundEffect’s creation.
class SoundEffect
{
char path[SONG_PATH_LEN];
int priority;
AudioClass *ac;
public:
//Access functions to change the sound effect’s pathname
//Function to “create” the sound effect
//Function to play/stop the sound effect
};
AudioPlayer Functions
In the AudioPlayer constructor, we need to pass a
handle to the current application instance to the constructor of the
XaudioPlayer, which will use it to create an invisible, internal window to
handle its own messages.
AudioPlayer::AudioPlayer(HINSTANCE
hInst);
The XaudioPlayer supplies many virtual void
functions, including OnNotifyPlayerState, so by defining the function we can
handle any of its callback messages.
So, if we get any important state messages, here is where they are
handled. We are, in fact, only
interested in one possible state, that of the “end of file,” which is when we
set the file to loop if its looping variable is set to true.
void AudioPlayer::OnNotifyPlayerState(XA_PlayerState
state);
PlayAudio takes a pathname of a file and, using the
File I/O, checks to make sure the file actually exists. If so, it will initialize the variables for
looping and the offset upon repeat.
Then it calls the XaudioPlayer class’s functions for loading input and
playing the audio. Also sets the
isPlaying variable to true if it begins to successfully play.
void AudioPlayer::PlayAudio(char
*path, bool loop=false, int off=0);
StopAudio halts any audio currently playing in the
AudioPlayer by calling the XaudioPlayer’s stop and closeaudio functions. Also sets the isPlaying variable to false.
void AudioPlayer::StopAudio();
This function serves as an access function to the isPlaying variable and returns whether or not the audio currently is in use.
bool AudioPlayer::IsPlaying();
AudioClass Functions
The Init function takes a handle to the program
instance that it will pass to the constructors of each AudioPlayer it
creates. It also needs a handle to a
window that gets priority so that it can call an Xaudio function to associate a
player’s priority with a window.
Finally, it needs a number of channels to dynamically allocate
AudioPlayers in an array. This number
must be at least one for music to play, and more for there to be sound.
void AudioClass::Init(HINSTANCE
hInst, HWND hwnd, int numChannels);
Play indexes into its array of AudioPlayers by the specified index variable, then calls the PlayAudio function of the specified player. The remaining parameters are the parameters that will also need to be passed into the play audio function, including the pathname of the music file, whether or not it loops, and the point to loop over from.
void AudioClass::Play(int
whichPlayer, char *path, bool loop=false, int off=0);
Stop indexes into the array of AudioPlayers and
calls the StopAudio function of that player.
void AudioClass::Stop(int
whichPlayer);
GetOpenPlayer returns the index of the first player it finds in the array that isn't playing anything. If the parameter is specified to true, then it will include the first AudioPlayer in the search for an open player, but this is not recommended as the first AudioPlayer is reserved for music only.
int AudioClass::GetOpenPlayer(bool
includeZero=false);
StopAll runs through the array of AudioPlayers calls
the StopAudio function for each player.
void AudioClass::StopAll();
Song Functions
The Create function of the Song class associates the
song with an instantiated AudioClass so that it can play sound when it is told
to later. It also gives a string for
the name, the pathname for the mp3 or wav file, whether or not the song will
repeat, and if so what point to repeat the song from (a value from 0 to
1000). This function must be called
before the Play will work.
void Song::Create(AudioClass
*audio, char *n, char *p, int off, bool loop=1);
If a song is already created and its properties are
set, ChangePath is available to redirect where the Song will look for its file.
void Song::ChangePath(char
*p);
Play will, if the audio class associated with the
song is valid, pass the song’s information off to the first player (player 0)
of the AudioClass to let it handle the playing from there.
void Song::Play();
Stop will, if the audio class associated with the
song is valid, stop the audio that is playing in the first player (player 0) of
the AudioClass.
void Song::Stop();
SoundEffect Functions
The Create function of the SoundEffect class
associates the sound effect with an instantiated AudioClass so that it can play
sound when it is told to later. It also
gives a pathname for the mp3 or wav file and a priority value which, if set to
1, will guarantee that the audio gets played even if all the audio players are
currently in use. This function must be
called before the Play will work.
void SoundEffect::Create(AudioClass
*aclass, char *p, int priorityvalue=0);
If a sound effect is already created and its
properties are set, ChangePath is available to redirect where the SoundEffect
will look for its file.
void SoundEffect::ChangePath(char
*p);
GetPath is a data access function that returns the
path currently associated with the sound effect.
char * SoundEffect::GetPath();
Play will, if the audio class associated with the
song is valid, pass the song’s information off to the first open player of the
AudioClass to let it handle the playing from there. If the sound effect’s
priority is set to true, then if it does not find an open player it will play
out of player 1 regardless. Also, since sound effects can pan to the left and
to the right there is a “panvalue” parameter that is from –1.0 to 1.0 and
represents the bias to left and right speakers respectively. This variable needs to be calculated
beforehand so the sound comes from the proper direction (or can just be set to
0.0 to play equally in both speakers).
void SoundEffect::Play(float
panvalue=0.0);
In the game, different tools will call into the sound effect functions when they need them. For instance, all ship objects and projectile objects are associated with various sound effects, i.e. explosion sounds for when they collide. These sounds are loaded in as strings from a file and are “built” during the load function, which includes associating them with the AudioClass. Therefore it is not the responsibility of the game-specific audio engine to further abstract the process. Along the same lines, the AudioClass’s initialization function already exposes enough functionality and does not need a wrapper that is game-specific.
The only game-specifc functionality that is necessary is the loading/creation of the game’s songs, and how they are played. By interfacing this tool with the File I/O tool, the function can read in an array of songs from a text file and play them by index value instead of needing to rely on the actual Song object.
The mainData struct contains the Song array, which is
loaded in from a file and can be indexed later to play the individual music
files. There also is a variable for how
many songs are loaded in so that the song array is never over-indexed. For more information on what else the
mainData structure contains, see the Game Object Data section of this document.
struct mainData
{
//...other variables...
Song *songArray; //the array of song files that play
music
int numSongs; //the number of songs to choose from
//...other variables...
}
LoadSongList takes a pathname to a configuration file
and, using the File I/O handler, loads in a number of pathnames equal to the
number specified at the top of the file.
It then allocates the proper number of Songs in the song array. Finally, it associates them with the
instantiated AudioClass pointer that was also passed in. The function returns the number of properly
allocated Song files.
int LoadSongList(char
* configPath, AudioClass *ac, Song **songArray);
EmptySongList takes a pointer to an allocated array of
Songs and the number of those songs. It
runs through each one in turn, makes sure the audio is not playing, and then
deallocates the array, setting the songArray to NULL when it is finished.
void EmptySongList(Song
**songArray, int numSongs);
PlaySong takes an array of songs and an index into those
songs to the one that the user wants to play.
This index must not be longer than the array, or it will cause
errors. This index value will typically
be associated with a define for either MENU_MUSIC or GAME_MUSIC so that the
proper music file can be played when it’s supposed to and it looks intuitive to
the programmer.
void PlaySong(Song
*songArray, int whichSong);
The Special Effects
will rely entirely on a particle engine.
All special effects will be done with this engine and allow everything
from thruster fire, explosions, smoke and radial explosions. All particles will be placed in a list with
data for location, direction and what graphic to draw.
The special effects tool will rely on two other tools to call it. Collision will call it when there is any form of collision. For example if one player shoots another there will be an explosion effect or if a player runs into a wall there may be an effect with sparks flying. Input will also call the tool to generate thruster fire to allow the ship to actually look like its flying. It will also handle other input particle generation such as the radial explosion that pushes nearby players away.
The special effects tool’s draw function will be called by the game specific graphics function to draw all images.
Struct PARTICLE
{
IMAGE *pImage[ ]; // A
pointer to an array for the graphics for this //particle
Int Frame; //
What frame of the graphics to draw
POINT Location; //
Where the particle is
VECTOR Direction; //
Where the particle is going
};
struct SPECIAL_EFFECT
{
PARTICLE *pParticleList;
// A list of all the particles
in this effect
Int
EffectNumber; // What effect this special effect is
simulating
};
Generate_Thruster_Effect(SPECIAL_EFFECT
*pSpecialEffect)
{
Creates a certain number of particles for each period of time
the button is pressed.
NewParticle =
CreateNewParticle(….);
Add the new particle to the
pSpecialEffect->pParticleList;
}
Generate_Explosion_Effect(SPECIAL_EFFECT
*pSpecialEffect)
{
Creates a certain number of particles.
NewParticle = CreateNewParticle(….);
Add the new particle to the pSpecialEffect->pParticleList;
}
Generate_Radial_Explosion_Effect(SPECIAL_EFFECT
*pSpecialEffect)
{
Creates a certain number of particles.
NewParticle = CreateNewParticle(….);
Add the new particle to the pSpecialEffect->pParticleList;
}
InitParticleList(SPECIAL_EFFECT
*pSpecialEffect)
{
Initialize memory for the pParticleList and set to 0.
}
CreateNewParticle(SPECIAL_EFFECT
*pSpecialEffect)
{
Initialize memory for the new particle
Set all data necessary
Return that particle
}
AddParticleToList(SPECIAL_EFFECT
*pParticleList, PARTICLE *pParticle)
{
Adds the pParticle to the end
of the pParticleList
}
UpdateParticles(SPECIAL_EFFECT
*pSpecialEffect)
{
Updates the positions of all the particles in the
pSpecialEffect->pParticleList and changes their image indexes if necessary.
}
DrawParticles(SPECIAL_EFFECT
*pParticleList)
{
Draws all the particles in a given list with the appropriate images.
}
There will be two control loops, which are basically the heart of the program. The client main will be the control loop for the user and the server main will be the control loop for the dedicated server. The main purpose of the control loops is to start the game, by calling all the proper initialization functions, run the game in a loop, and, when the user gives the command, exit the game, by calling all the proper closing functions. This will interact will all other major code sections by the virtue of that it will call all the highest level functions to start up all the other lower level functions. A good way to think of this is that when the game runs, it forms a tree, the control loop would be the trunk, whereby all other sections of the game are branching from, i.e. networking, input, A.I., graphics.
The control loops, by default, is game specific. The following function prototypes are the highest level, game specific code that will be mandatory for this project.
void clientMain()
The client main has three main sections to it.
1) Initialize
2) Main game loop
3) Termination
Initialization will be handle by a function called Initialize. This function will set up all necessary networking code. The main game loop will be an infinite loop that will call the starting menu loop function and then the starting game loop function. The loop will break if either function returns the GAME_EXIT value. If GAME_EXIT is not returned by either function the infinite loop will insure that the other function will be called. The termination will be handle by a function called Termination. This function will clean up the networking code that was set up by the Initialization function.
All the data that will be used for the client part of our code, will be instantiated in the client main. No data is needed for this function.
Client main()
{
//initiate main data here.
UI_ClientFrontEnd menu;
Initialize();
While (1)
{
if(menu.run(Hinstance window, mainData * theWorld) ==
GAME_EXIT)
break;
if(GameLoop(mainData * theWorld) == GAME_EXIT)
break;
}
Termination();
Return (0);
}
void
Initialize(mainData *theWorld);
The Initialize function has the job of setting up all engines needed before the menu window is called. At this point, the only engine that needs to be called is the network engine. Since the menu and the game are going to exist in different windows, the only thing that they both need is networking. Otherwise, setting up functionality for the two different windows when there is a possibility that one of those windows will not be used (users decides not to play once in the menu) is going to slow down initialization time.
The initalize function needs a pointer to the main data
structure. This is for networking. The networking needs to know everything
about the environment and any data changes.
void
Initialize(mainData *theWorld)
{
initNetworking(theWorld);
}
void Termination();
The Termination function closes all the engines that Initialize() calls. The only engine that is need to be closed is the Network engine. No data is need and none is returned.
void Termination()
{
closeNetwork();
}
menu.run(Hinstance window,
mainData *theWorld);
The function menu.run() will make the front end window and system. The basic idea of this function is to initialize the menu window, run in a loop, and terminate the window when the user decides to play the game or exit out of the program. To learn more on the menu system, please go to the front-end section of this document.
GameLoop(mainData *
theWorld);
The GameLoop function controls the flow for everything in-game. The function will first call GameInit() which will initialize everything for in-game use. A infinite loop will handle the drawing, updating input, updating physics, and updating collision. When the update for input receives the data for exiting the game, the loop will break and GameTerminate function is called. This function will close down the window and anything else game specific.
The data need for GameLoop is a pointer to the main data
structure. The main data structure was
design to work with the game loop since everything changed in game must be
stored and passed on to other engines.
GameLoop(mainData *
theWorld)
{
GameInit(theWorld);
While(1)
{
if(peekmessage(…))
{
~~~~~ (handle message)
}
else
{
if(!drawGame)
return error; //draw failed
else
{
//waste unnesccessary cycles here
UpdateInput();
ret = handleInput();
UpdatePhysics();
UpdateCollision();
//includes setting,current menu , iplists, ship, name, ect
if(ret == RET_MENU || ret == EXIT)
break;
}
}
GameTerminate();
}
GameInit(mainData *
theWorld);
This function sets up all the data in the mainData structure to be used for the game. GameInit will also set up the game window. It is here where all game related engines would get called to set up the client side of the game. There are two engines that need to be set up, audio and input. The server handles other engines, like physics and collision, and the client application will not have a need for them. Also the function makes calls to load in the ship models and the ship template.
GameInit(mainData *
theWorld)
{
initopenGl();
createGLwindow();
associateGLextensions();
InitAudio(theWorld.GLinstance);
InitInput();
LoadShipModels(theWorld);
LoadShipTemplates(theWorld);
}
GameTermination(mainData
*theWorld);
The GameTermination function closes and frees up any memory allocated by the functions in GameInit(). The main data is needed so that it can be properly freed.
After this function is done, no new data is returned. No data period.
GameTermination(mainData
*theWorld)
{
GlTermiate();
CloseAudio();
UnloadShipModels();
UnloadTemplates();
}
void serverMain();
The serverMain function is the control loop for the dedicated server. This function’s main purpose is to instantiate the main data structure and calls all the high-level functions while the server is running. Before the server goes into its game loop, it must do five things; create an empty window, prompt the user for some game related information, load the ship templates, load the user stats, and initialize the network. When the game loop is broken, meaning the server has been asked to quite, a close network function will be called to release any networking objects. After the network has been closed, a function will delete the ship templates.
Void serverMain()
{
mainData * theWorld
CreateBlankWindow(theWorld);
SetServer(theWorld);
LoadShipTemplates(theWorld);
LoadUserStats(theWorld);
InitNetwork(theWorld);
SeverGameLoop(theWorld);
CloseNetwork(theWorld);
UnloadShipTemplate(theWorld);
}
void ServerGameLoop();
The function ServerGameLoop controls the persistent environment created by the dedicated server. Done in an infinite while loop, the function will repeatedly check and handle all the game’s statistics, physics, collision, and A.I. After updating, a networking function call will send out all the updates to all the appropriate clients. ServerGameLoop will need a pointer to the main data structure, so that it has access to all the players, networking, and A.I. data. Since this function is of the highest level, it will return nothing and will not physically change any main data, just pass it along to different tools.
void ServerGameLoop(mainData
* theWorld)
{
CreateAIBots();
While(1)
{
if(peekmessage(…))
{
~~~~~ (handle message)
}
else
{
SaveUserStats(theWorld);
UpDateCollision(theWorld);
UpDatePhysics(theWorld);
UpDateAI(theWorld);
UpDate(theWorld);
SendUpDatesToClients(theWorld);
}
}
}
typedef GLfloat Point3D[3];
typedef unsigned int
Triangle[3];
typedef GLfloat
GLPoint2D[2];
typedef GLfloat
GLPoint3D[3];
typedef GLuint GLInt3D[3];
typedef FILE *NS_FILE;
struct MainData
{
//these are the game objects that change in-game data
list<PLAYER> playerList; //contains ship list
list<PROJECTILE> projectileList;
list<char *> chatstrings;
MAP theMap;
MAPOBJ *tileArray;
Model *shipModelArray;
Model *projectileModelArray;
Arena theArena;
//these are interface objects that let us directly interact
with the game
DInput input; //
Main Input Object, the "DirectInput Class"
GLClass glObject; // Main Graphics Object, the
"OpenGL Class"
MoviePlayer mPlayer; // Movie Frame Grabber to display
movies
AudioClass audio; // Object that plays music and sound
files
Camera camera; // Object to change where the view is
set
}
struct MAP
{
TILE
*ptile;
int
width, height;
};
struct LINE2D
{
float x1,y1,x2,y2;
};
Classes derived from other classes are depicted as containing that class.
SHIP derived from OBJ derived from GEOMOBJ
PROJECTILE derived from OBJ derived from GEOMOBJ

MAPOBJ derived from GEOMOBJ

class GEOMOBJ
{
private:
POINT3D
position;
POINT3D
rotation;
float scale;
Model
*pModel;
AnimData animation;
public:
//write functions here
};
class OBJ : public GEOMOBJ
{
private:
float mass;
float centerMass;
float acceleration;
float angAcceleration;
float velocity;
float angVelocity;
int ownerID;
int objectID;
float radius;
public:
//write functions here
};
class SHIP : public OBJ
{
private:
int armor;
int weapon;
float mainThrust;
float turning;
float topSpeed;
float firingRate;
float weaponDrain;
float weaponSpread;
public:
//write functions here
};
class PROJECTILE : public
OBJ
{
private:
float damage;
bool doesBounce;
float lifeSpan; // (time) (-1 == infinite)
int collisionType; // (Bullet,Bomb,Power-up)
public:
//write functions here
};
class PLAYER
{
private:
int handle;
int score;
int bounty;
int team;
int kills;
int deaths;
float avgBountyKilled;
int maxPersonalBounty;
int maxKilledBounty;
int rank;
SHIP theShip;
public:
//write functions here
};
Class MAPOBJ : public
GEOMOBJ
{
private:
int numCollisionLines;
LINE2D *pLine; //
pLine[numCollisionLines];
bool doesBounce;
float elasticity; // (=1.0) (multiply by velocity)
bool safeArea;
public:
//write functions here
};
There are two main phases of data flow in NullSpace, the loading of the font end window and loading of the OpenGL “game” window. The Front End window will be loaded into virtual memory first. This window will be using windows GDIs and will not need to have any other data loaded in with it except for a few options to be read in from a text file. The OpenGL window will be loaded into virtual memory after the Front End winow has been destroyed. Also loaded into virtual memory, at this point, are the Null Space Arena file (.nsa) and any Null Space Model files (.nsm). When the user quits the game back the menu system, the OpenGL window is destroyed and the Front End window is loaded back into virtual memory.
Physics is the lifeblood for any type of simulation game. Without it, a sim is simply not, for lack of a better word, a sim. Seeing a ship drift through space after accelerating to a high velocity is an act of physics at work. Another example would be seeing a ship move slightly in a certain direction after impact from a missile. There are many factors to be considered in writing a physics engine.
NullSpace will need several functions written that represent Newtonian physics that will be applied to all objects that contain certain variables. These variables are an object’s mass, center of mass, acceleration, angular acceleration, velocity, angular velocity, position, and elasticity.
· Mass: The weight of an object.
· Center of Mass: Point representing the mean position of the matter in an object.
· Acceleration: The rate of change of velocity with respect to time.
· Angular Acceleration: The rate of change of the angular velocity with respect to time.
· Velocity: The speed (magnitude) and direction of an object.
· Angular Velocity: The speed and direction of rotation of an object.
· Position: Object’s X and Y position in 3D space.
· Elasticity: The amount of “bounce” an object applies to another object. For example, a ship colliding into a wall with an elasticity of 0 would not bounce off the wall; it would come to a complete stop. If the wall had an elasticity of 1, then the ship would bounce of the wall at a new angle and continue with its current velocity. For a picture example, look at the Phy_CalcElasticityVelocity() function explanation.
Velocities and accelerations are manipulated through the actions of movement (thrusting) and collisions. Ships will have side, fore, and aft thrusters. Activating your side thrusters will make the ship turn (rotate), therefore affecting the angular velocity and angular acceleration. Using the fore or aft thrusters will change your forward and backward velocities and acceleration. All the other properties listed above will be handled within the physics functions.
There are two main sections within the game that will rely on the physics engine for its calculations: Input and Collision. Collision will rely heavily upon physics because it is used to resolve all collision types. Input will make calls to the two update functions: Phy_UpdateVelocity() and Phy_UpdatePosition(). Collision on the other hand will make calls to all the physics functions that are provided. The physics engine itself will not rely too heavily upon any one section in the game, just the information passed in through the function calls, which can be called from anywhere within the game.
void UpdatePhysics( list *pShips, list *pProjectiles );
This function will be used to call non-game specific physics functions. When objects need to be updated every frame, a call to this function will renew all objects’ velocities and positions. This function will need the list of ship and projectile objects.
float Phy_UpdateVelocity(
float accel, float vel, float time );
The purpose of this function will be to update an object’s velocity or angular velocity. The velocity of an object is its current speed and direction. An amount of acceleration is added to the object’s velocity depending on how much time has elapsed since the last call to velocity update function. For example, if exactly one second has passed, then the full acceleration would be added to the velocity. Otherwise, a percentage of the acceleration would be added. Five seconds passed, then 500% of the acceleration will be added. Only .3 seconds have passed then 30% of the acceleration will be added.
If this function is not implemented then an object will never be able to reach full velocity or come to a complete stop.
Only three pieces of information will be needed in order for this function to do its job: the object’s acceleration, velocity, and the amount of elapsed time since the last call to this function for that specific object. After performing the calculations described above, a new velocity will be returned.
void Phy_UpdatePosition(
float vel, long *x, long *y );
This function will be used to update an object’s X and Y position based on its current velocity. The reason this function is needed is because the object is being moved based on its velocity, which gives the user a sense of true movement throughout the environment. There’s really nothing else that needs to be known about this function except for the variables that are passed in.
The object’s current velocity, a pointer to its X position, and a pointer to its Y position need to be passed in. The two given pointers will be modified within the function and are replaced with the object’s new X and Y position.
void
Phy_CalcForceFromVelAndMass( float vel, float mass, float *force );
Calculations of forces and their magnitudes play an important part in physics. In this function a force is calculated and returned to the caller through the pointer that was passed in. We need this function so that we can find the force that an object would exert onto another object. For example, if missile struck a ship, we would use the mass and velocity characteristics of a missile to compute the amount force that would be applied on the ship. We can now figure out how much rotation needs to be added and how much the ship needs to be shifted because of the impact.
All this needs to know in order to compute an object’s applied force is the object’s velocity and mass. A float pointer also needs to passed into the function to store the result of the computation and returned to the caller.
float
Phy_CalcForceRotational( float angleOfIncidence,
float force,
float mass,
float accel,
float angAccel,
float angVel,
float *vel );
Phy_CalcForceRotational() will be used to calculate the amount of force needed to rotate an object, which will in turn, dictate how fast it rotates. For game purposes, when a ship is struck by something very powerful, it will cause the ship to spin uncontrollably until the player is able to regain control.
In order to use this function (we will use a missile and a ship as an example): we will need the missile’s force (power), the ship’s current mass, acceleration, angular acceleration, current angular velocity, and the angle of incidence. The angle of incidence is the angle at which the missile collides with the ship.
|
|
a < 90°, clockwise rotation |
a > 90°, counter-clockwise rotation |
|
|
|
|
If a = 90°, then a head collision has occurred and only the ships velocity will be modified. After impact, the ships angular velocity will be either increased, decreased, or remain the same along with a modification in the ship’s velocity. The ships new angular velocity will be a return value, while a pointer to the ship’s velocity is needed so it can be adjusted.
To perform a perfect turn left or turn right using this function a couple of defines will be provided. These are:
#define CCW_THRUST_ANGLE 180 // rotate ship counter-clockwise
#define CW_THRUST_ANGLE 0 // rotate ship clockwise
These two defines can be passed in as the angleOfIncidence parameter.
float
Phy_CalcElasticityVelocity( float vel, float elasticity );
The above function will calculate the new velocity of an object after colliding with another object with an elasticity factor. This is done by multiplying the two given variables passed in through the function call and returning the result. Elasticity will be a value from 0.0 to 1.0, with 0.0 being non-elastic (no bounce) and 1.0 having full elasticity (full bounce). For an example, look at the below picture.
|
|
Wall with elasticity of 0.0 |
Wall with elasticity of 1.0 |
|
|
|
|
This section will encompass all of the collision for ships, weapons, and power ups. The first priority in the collision, are the ships. Collision will be checked with all ships to see if they have collided with another ship, a weapon, a power-up, or a wall. Then, all weapons will be checked with the walls. This will be all the checking that needs to be done. For this task to be accomplished, it will need to be able to use the SHIP Data, Object Data, and the Level Data. In the OBJ data, the center point and radius are stored. Each ship and object has this and will be used for detecting collision.
The fact that you know exactly when and where a collision accrued, and with what objects, many other functions from other sections will need to be called from the collision functions. Some other sections that the collision section will communicate with are:
Physics
Physics functions will need to be called from the collision section because it will need to calculate forces, magnitudes, and the elasticity, so that it can appropriately assign the correct positions, velocities, and forces. The physics functions that will be used include: Phy_UpdateVelocity(), Phy_UpdatePosition(), Phy_ForceFromVelAndMass(), Phy_CalcForceRotational(), Phy_CalcElasticityVelocity().
Sound Effects
Different sound effects will by played for all types of collision to make a more realistic appeal. If a collision results in the destroying of a ship, a louder and longer sound effect would be played. The sound function to call that would play the sound effect would be something like SoundEfx_Play(“explosion”).
Special Effects
Collision between weapons and walls, weapons and ships, and ships to enemy ships, will trigger special effects to be displayed. If a collision results on a ship being destroyed, a bigger special effect would be displayed, in this case an explosion. The function that would be called in this case would be SpecialEfx_Start(location, EFFECT_NAME).
Scoring and Stats
If there were a collision that results in destroying an enemy ship, the player’s score and possibly ranking would increase. A collision between a ship and a power-up, and also the case of destroying an enemy ship, the player’s bounty points would increase. This will require that the collision function call CaculateNewBounty (PLAYER).
Energy
If there were a collision between a weapon and a ship, that ship’s energy would decrease. If there were a collision between two ships that are on different teams, both ship’s energy would decrease. The amount of energy that is decreased for the ship to ship collision depends on the speed at which the both ships are traveling. For example, a collision where both ships are traveling at high speeds would decrease their energy more than a collision where both are traveling at low speeds. For this the collision section will need to call EnergyShipCollide(PLAYER,FORCE1,FORCE2).
After there had been a collision, depending on the objects that collided, different things will happen. Possibilities include:
|
Ship to Ship This collision will be the most computational collision
routine. It will require computing the point of collision, finding the line
orthogonal to the collision circle where the point of collision lies, calling
physics functions to calculate both directional forces and angles, calling
another physics function to set the new positions and velocity vectors of the
ships, and calling the physics function to set the rotational forces. For a
better vision of this, take a look at figure 1. Where V is the Velocity
Vector of ship 1, R1 is the
Reflection Vector from collision (ship 1’s new vector), θ is the angle
of collision, R2 is the Resultant Direction Vector of ship 2 after collision,
and R3 is the Rotational Force that is applied to ship 2. R1 is calculated by
using the N, (the normal vector of the Line Orthogonal to the collision
point). R3 is calculated in a physics function, where the angle and force on
impact are sent. On collision, there would be a sound played. If ship 1 and
ship 2 were on different teams, then energy would be taken off both ships.
The amount of energy taken off would depend on the force of both ships and an
explosion would be displayed if the energy of a ship goes below the minimum
level. |
|
|
Ship to Wall & Weapon to Wall This collision routine will encompass a less computations than the previous one. All that is needed is the angle β and the Normal Vector to the wall (N), and then you can calculate the Reflection Vector (R). See figure 2. On collision, a sound would be played. For the weapon colliding into the wall, the same technique would be used. But, if the weapon property, DoesBounce, were not true, then on collision, there would be an explosion called and a sound played. |
|
|
Ship to Weapon & Ship to Power-Up This type of collision is trivial. When the ship collides with the weapon, an explosion function and sound function will be called. If the ship’s energy is depleted, then it will be a larger explosion, and the ship will be destroyed, the player who fired the weapon, their score and bounty points will be increased, which will be a function call. Ship to Power-Up is the same except no explosion would be called. A sound that you picked it up will be called, and your bounty points increased. And also the power-up added to your inventory. |
|
void doCollision( SHIPOBJ,
PROJECTILEOBJ, MAPOBJ );
This is the main collision function that is called from the main game loop. This function serves as a placeholder for all other collision functions that follow. This helps simplify things by having all the collision functions inside one main function. The Ship Object, Projectile Object, and Map Object need to be passed into this function, so that the other functions inside can have access to them.
void Collision_Player(
SHIPOBJ, PROJECTILEOBJ, MAPOBJ );
This function will check the collision of ship-to-ship, ship to weapon, ship to power-up, and ship to wall. It will check this for every ship currently in the game. As you see, it takes the Ship Object, Projectile Object, and the Map Object. These are necessary in order to know the information that will be needed for collision detection. Inside this function, the fallowing functions will be called. Named accordingly.
int ship_ship( SHIPOBJLIST
);
int ship_projectile(
SHIPOBJLIST, PROJECTILEOBJLIST );
int ship_wall( SHIPOBJLIST,
MAPOBJ );
void Collision_Weapon(
PROJECTILEOBJ, MAPOBJ );
With the collision of ships already complete, the only other collision that needs to be checked is with Projectiles and Map Objects. So, the only data that we would need would be the Projectile Objects and the Map Objects. Please see Figure 2 in the previous section for a collision of a Projectile and a Map Object (wall).
int coll_CircleVector(
CenterPt, Radius, Vector, *IntersectPt );
This is the first one to be called when checking for
collision. It calculates the intersection point of a vector and a circle. To do
this it needs the center point and radius of the circle, and the vector. It
will return a 0 for no collision and a 1 it there was a collision. If there was
a collision, then the *IntersectPt would be filled with the correct
intersection point.
int getOrthVector( CenterPt,
Radius, PtOnCircle, *OrthVector );
This function, once the intersection point is known, will create an orthogonal vector to the circle. This will be used later on to get the normal vector. To get the orthogonal vector, it will need the center point and radius of the circle, and the point on the circle, which would be the intersection point. When the function is done, it will put the orthogonal vector into the *OrthVector and return 1. See O1 is figure 1.
int getNormalVector( Vector,
*NormalVector );
This function will use the orthogonal vector created above to find the normal vector. The normal vector will be perpendicular to the orthogonal vector. All it needs to do this is the orthogonal vector and will return the normal vector. See N is figure 1 and 2.
int getAngle( NormalVector,
Vector, *Angle );
It is necessary to have an angle of collision so that the reflection off of the objects is realistic. To get the angle, it will need the normal vector and another vector, and it will compute the angle between them and put it into *Angle. See β and θ in figure 1 and 2.
int getReflectionVector( NormalVector,
Angle, *ReflectionVector );
This final step of calculations is the resultant vector. This is the new vector that will be assigned to the ship, (or projectile), after collision. To calculate this, it would require the already computed normal vector and angle to return the reflection vector. See the reflection vectors R1 and R in figures 1 and 2.
Null Space will depend on a very complicated system for creating statistics. There are many different stats a player will establish while playing. These stats are the players Max Bounty, Deaths, kills, the Death/Kill ratio, Avg. Kill Bounty, Highest bounty of an enemy the player killed. Using these statistics, harsh functions will be implement for a unique way of calculating a player’s score (rank). All the statistics will be game specific code. None of it can be used for any other project.
The statistics code for the game will be just a collection of functions. This subject is simply not large enough to be organized as a class or anything else.
int calculate rank();
Calculate rank is a function that uses all the player’s stats to decide a placement which is represented in a point value. A high rank means that the player is doing better then other players with low rank. A mathematical function has been cleverly devised to decide the rank. This function will be passed in all the player stats and then using that data, in the math function, yet to be announced, to return the players rank.
int calculateRank(player
stats){
return (C1(maxBountyYou) +
C2(maxBountyThem )+ C3(kills * l)/(C4 + deaths));
}
where: C1 = 4.3, C2 = 6.98, C3 = 0.25 ,C4 = 1.68
note: these variables might be change depending on game play issues.
void CaculateNewBounty ();
After the player gets a frag, the bounty on their head will increase. To calculate this increase will depend on the bounty of the player they just fragged. The player should be rewarded more when they frag someone with a higher bounty and receive a smaller bounty increase when the Player frags an opponent with a smaller bounty. The function will need the player’s stats and the opponent stats.
CaculateNewBounty()
{
Bounty = theirBounty – yourBounty;
if
Bounty < 0 then
yourBounty += (C9(Them Bounty/ Your Bounty +C8));
else
yourBounty += Bounty(C7);
}
where: C7= 1 C8= 1C9 = 5
note: Once again, these variables might be change because of game play issues.
int calculate damage done();
This function will calculate the damage done to the player based about the power of the attack and the strength of the Player’s ship. The function will need data that tells the strength of the attack and the strength of the Player’s armor. To calculate the damage is easy, it’s just the strength of the weapon minus the strength of the armor.
int
CalculateDamageDone(player stats, attacker stats)
{
return WeaponStrength – PlayersArmor;
}
void recharge()
The shield of a player’s ship determines if he’ll survive an attack or not. When the player is not being hit by anything and is not firing, the shields will slowly recharge. This function will add the recharge rate to the shields of the player’s ship. The function will need the player’s stats.
void recharge(player stats)
{
shieldpower += rechargeRate;
}
The AI in NullSpace is used primarily to create life-like bots for humans to face off against. The best way to do that given the time and resources will be a finite state machine and some A* pathfinding. At any given point in the game the bot AI will have a state. These states will be out of the following list:
Powering Up
Triggering Conditions: Medium to full energy, but not many power-ups.
Primary Goal: Find power-ups (aka multiprizes).
Secondarty Goal: Find targets with low energy for targeting.
Tertiary Goal: Avoid high-ranking targets.
Aggressive
Triggering Conditions: Near full energy, at least some power-ups.
Primary Goal: Find high-ranking targets.
Secondary Goal: Find power-ups.
Low Power
Triggering Conditions: Low energy.
Primary Goal: Avoid getting hit.
Secondary Goal: Avoid firing weapons.
Tertiary Goal: Find a Safe Area.
Chase:
Triggering Conditions: Good target aquired.
Primary Goal: Destroy target.
Secondary Goal: Avoid getting killed.
Evasive Manuever:
Triggering Conditions: Shots land within a certain radius.
Primary Goal: Evade getting hit
Secondary Goal: Return fire.
Tertiary Goal: Resume last state.
The current state dictates the priority of actions according to the above table. In order for those actions to take place, the AI will need certain information about enemy locations and velocities. It can get this straight out of the MainData, since all the AI bots will be running off the server, which also contains all the data on all ships in the game. The AI can be made easier and harder by multiplying various random error variables against various stages of the vector math necessary to predict where the enemy ships are heading.
The math needed would be straight prediction routines that would multiply current velocity vectors by time and adding to current positions to get future positions. The AI can guage how fast it can get up to a position and look for the inersection with the projected enemy position, and try to fire shots ahead of the player.
To make the firing patterns more realistic, when the AI bot thinks it has a good shot, it can fire a spread of shots to cover more area, in case the enemy ship tries to turn or avoid the shot.
Also, the AI bots can have special routines for the different power-ups. The following is a list of actions the AI bots can take depending on their state and the power-ups the ships have:
1. Repel – The AI can use these if it enters the Evasive Maneuver state and it detects enemies close by.
2. Brick – The AI can use these if it is in the Evasive Maneuver state and detects a ship directly behind it.
3. Warp Gate – The AI can use these if it is in the Evasive Maneuver state and trying to get away from a chaser.
4. Warp Disabler – The AI can use these if it is in a Chase state and doesn’t want to lose its target (thinks it has a shot).
The basic AI class would look like this:
#define AI_STATE_POWERUP
#define AI_STATE_AGGRESSIVE
#define AI_STATE_LOWPOWER
#define AI_STATE_CHASE
#define
AI_STATE_EVASIVEMANEUVER
class AI_Bot {
int m_current_state;
int m_previous_state;
int m_current_target;
public:
AI_Bot();
~AI_Bot();
Update(MainData * theWorld);
};
It would contain all functionality and state information internally, and multiple bots can be created by making multiple instances of the class, 1 for each bot. Each iteration of the main game loop, each AI’s update function should be called with a pointer to the main data.
Game Specific
Functions
int pathFinding(TILE *tile)
Our pathfing algorithm will be based off of A *. The basic idea is to use the tile based maps as a way for the A.I. bot to find it’s opponent. When nothing is between the A.I. bot and it’s opponent, a straight line can be used to find out the angle at which the bot must turn to head towards/away from an opponent. However, there is a good chance that an obsticle will be in the way. This is where the pathfinding will be most important.
The A * algorithm checks the surrounding tiles of the bot. If the tile being check is a legal tile, meaning the bot can move there and it hasn’t been checked yet, then the algorithm will recursively call itself using that tile. The recursion will stop when one of three things occur, the oppenont has been found, a dead end was found, or if the algorithm is recursively called to its limit. The limit will be a constant.
Every time the function is called on a tile farther away from the bot, the constant will be decreased. When the constant becomes zero, it indicates that this tile is way too far from the A.I. bot to consider. This constant will be defined later after some play testing, but for now it can be 10. The function will take a TILE and use it as a base to check all the tiles around it. The function returns the tile of the best choice of where the bot should move.
void ChoseAction()
ChoseAction will call pathfinding to see where it’s opponent is. Once the location of the opponent is found, this function will take into consideration all the stats of the bot and make a choice. To see the list of choices a bot can make, see the beginning of the Artifical Intellegnce section. ChoseAction will need the bot data and a list of players, so it can choose a target. This function will not return anything.
Multiplayer is the predominant part of the game; a network connection must be established for a person to be able to play at all. The surest way to give the user reliable connection at the sacrifice of a little speed is by using the TCP/IP protocol. This is accomplished in the code using the windows sockets API WinSock.
The game is broken up into two individual executables: the server executable and the client executable. Once a server is run, client executables can connect to that server by the user inputting the IP address of the machine the server is running on. The client executables contain the majority of the game’s graphics, input, and sound routines, as that is what each player will run to play the game. Information is sent in the form of network packets between the two computers to establish first a connection and then to send the game’s data.
When the player first runs the client executable, they will be presented with a menu allowing them to input an IP address of a known server. All IP addresses in the list are periodically checked for latency, so that the player gets a good idea of how their connection fares to each server executable. They are also presented with other game options such as selecting a name and ship, and then they submit the information to the server of their choice. When a person first creates a server they are presented with a menu of options to select server settings (max number of players, max number of ai bots, limited camera angle, etc.) and then when the select “start,” these options are no longer made available to them because the game has begun.
The Multiplayer environment of NullSpace is persistent. That is to say, clients can connect and disconnect as they please and the server will continue running the world without them. Up to a server-specified number of players, the server will supply AI bots to fill in however many players are missing. If enough players join that this limit is exceeded, the AI bots would no longer respawn when destroyed. Therefore it is imperative in the construction process to properly connect and disconnect players, thereby freeing memory, as well as keeping track of players that may have timed out.
If, for whatever reason, the server becomes disconnected, all clients must recognize that the server timed out and then properly close off all the data and report an error, bringing the player back to the menu.
There is one general type of packet with several variables contained inside of it in the form of unions. These unions allow the packet to contain a variety of data types in different orders, so long as there is a header byte specifying what type of packet there is to follow. Another important feature for each packet to have is the ID of the player/computer that sent it. This allows for great ease of updating player data depending on which packet was received.
Again, depending on the packet type, some packets are sent nearly every game loop to keep all players updated. If a packet needs to be sent often like this, it will contain information about as many players as possible so that it doesn’t need to be sent quite as often. The reason for this is that internally a lot of data is added to each packet by the network protocol to assure that it gets to the proper destination. Therefore, by sending more packets when one could cram them all into one adds a staggering amount of unecessary headers and effectively slows the entire process down.
As the multiplayer needs to be available over the Internet, which is highly unstable, it is a genuine concern that packets will be lost. By using the TCP/IP protocol, any packets that are lost are internally sent again until they are confirmed received. This, while it slightly slows down the overall process, guarantees that all important packets will be received if there is something on the other end to receive them.
One major consideration is whether or not to use broadcast messages to find servers. Because this game is designed to be run over the Internet, it was felt unecessary to broadcast messages (which is limited to LANs) in order to be able to find servers. If this was a more mainstream game, an executable would be set up on a static server to receive any requests for NullSpace games, of which it would have a list because every time a NullSpace server was hosted it would let this executable know. It suffices for now to just display the current server’s IP address when it is run so that players know what IP address to connect to.
Any specifics on what sort of packets are to be sent and the general implementation of the Networking Engine are included in the Networking API section of the document.
The User Interface can be broken down into 2 sub-sections. The first is the front end. The front end is what the user first sees when he/she starts NullSpace. The front end will be contained in a window that pops up on the user’s desktop (as opposed to immediately going full screen). It will utilize Windows Controls and Win32 APIs in order to create a window that is recognizable and easy to use, in the standard Microsoft Windows style.
The second sub-section is the in-game UI, which will be quite different. Once the user has setup the game in the manner he/she wants, then the user will press a button to start the game. At this point, the main game window will open and the game will go full screen. All the graphics from this point on use the OpenGL APIs. This means that no longer can NullSpace rely on Windows controls to display UI information. All code from this point on will have to be written from scratch.
The front end is everything the user needs to get an actual gameplay session up and running. There are two Front ends, one for the server app and one for the client app. They both share from the same pool of basic UI elements, which are considered non-game specific.
This is the full list of available UI elements. Each of these will be entirely sefl contained in separate classes:
1.
class UI_TrackBar
2.
class UI_ListView
3.
class UI_IPAddress
4.
class UI_TextBox
5.
class UI_RadioButton
6.
class UI_Button
7.
class UI_CheckBox
8.
class UI_Bitmap
9.
class UI_Image
Each class, except UI_Bitmap and UI_Image, will be inherited from the following pure virtual class:
class UI_FrontEndBase {
private:
HWND m_hwnd;
int m_left;
int m_top;
int m_width;
int m_height;
public:
UI_FrontEndBase();
~UI_FrontEndBase();
virtual bool Create(
HWND
parenthwnd,
HINSTANCE
parenthinst,
int
left,
int
top,
int
width,
int
height) = 0;
virtual void Enable() = 0;
virtual void Disable() = 0;
};
UI_FrontEndBase::UI_FrontEndBase();
This is the constructor for this class. It will set all the internal variables to 0 or NULL.
UI_FrontEndBase::~UI_FrontEndBase();
This is the deconstructor for this class. It does nothing.
bool UI_FrontEndBase::Create(HWND
parenthwnd, HINSTANCE parenthinst,
int left, int top, int
width, int height);
This function will create the window associated with the control. It will also establish the controls position and size on the screen and draw it. The parenthwnd is the HWND of the window that owns this control. The parenthinst is the hInstance of the window that owns this control. The left and top define the upperleft corner’s x and y position in the client coordinates of the parent window. The width and height specify the x and y size of the control, respectively. This function returns true on a successful window creation, and false on an unsuccessful one.
void UI_FrontEndBase::Enable();
This function will enable a control. It takes in nothing and returns nothing. When a control is enabled, it will act normally. See Disable() for further clarification.
void UI_FrontEndBase::Disable();
This function will disable a control. It takes nothing and returns nothing. When a control is disables, it will be greyed out and the user will be unable to interact with it. To re-enable the control, call UI_Enable()
Each inherited class will also have the appropriate access functions to get at the values that that control holds, as well as any functions needed to update the state or status of the control (like any WndProc message handlers).
class UI_TrackBar : public
UI_FrontEndBase {
private:
int m_min;
int m_max;
int m_pos;
public:
UI_TrackBar();
~UI_TrackBar();
void SetAll(int min, int max, int pos);
void GetAll(int &min, int &max, int &pos);
void SetPos(int pos);
int GetPos();
};
UI_TrackBar::UI_TrackBar();
This is the constructor for this class. Its only purpose will be to set all the internal variables to 0.
UI_TrackBar::~UI_TrackBar();
This is the deconstructor for this class. It will be empty, as it will not be needed for anything.
void UI_TrackBar::SetAll(int min, int
max, int pos);
This function will set all the variables that define a track bar. The min is the minimum value the track bar can be set to, which visually is the left extreme of the track bar. The max is the maximum value the track bar can be set to, which visually is the extreme right side of the track bar. The pos is the current position of the track bar, which visually is the tab that you can drag around that indicates what the track bar is currently set to. It returns nothing.
void UI_TrackBar::GetAll(int &min,
int &max, int &pos);
This function will get all the variables that the track bar currently has (what you can currently see drawn). The variables have the same meaning as the above function, and since they are all reference variables, they will be filled in by the function and nothing needs to be returned.
void UI_TrackBar::SetPos(int pos);
This function will set only the position of the track bar. As stated above, the position is what the track bar’s tab currently points to. It must be between min and max. It returns nothing.
int UI_TrackBar::GetPos();
This function will retreives the current posititon of a trackbar and returns it.
class UI_ListView : public
UI_FrontEndBase {
private:
int m_num_cols;
int m_num_items;
public:
UI_ListView();
~UI_ListView();
void AddColumn(char * name, int width, int order);
void AddItem(char * buffer, int item, int subitem);
void GetItem(char * buffer, int buffsize, itn item, int
subitem);
void RemoveRow(int item);
int GetSelection();
int GetCols();
int GetRows();
};
UI_ListView::UI_ListView ();
This is the constructor for this class. Its only purpose will be to set all the internal variables to 0.
UI_ListView::~UI_ListView ();
This is the deconstructor for this class. It will be empty, as it will not be needed for anything.
void UI_ListView::AddColumn(char *
name, int order);
This function will insert a column into a list view control. It will use the string in name as the column header. The width specifies the width of the column in pixels, and the oder specifies where it should try to insert this column. If order is set to 0, it will become the far left column, and all other columns will be moved to the right. If it is set higher, it will wedge between order-th column and the column after (ie if order was 1, it will wedge between the first and second columns, or after the first if there was no second, or first if there were no columns). Nothing is returned.
void UI_ListView::AddItem(char * name,
int item, int subitem);
This function will insert a new item into a list view control. An item can be defined as one entry in a given row for a given column. The row is given by item, and the column given by subitem. For example, an item of 0 and a subitem of 0 means the first row and the first column. To fill the other columns in for a given row, make additional calls to AddItem with the subitem variable increasing by 1 each time. This function returns nothing.
void UI_ListView::GetItem(char *
buffer, int buffsize, int item, int subitem);
This function will retreive a given entry in a list view control. The entry is defined by the item, subitem pair in a similar fashion to the above AddItem() function. The item designates the item, and the subitem designates the column. This function fills in buffer with the string contained in the table at the specified location. There is no return value.
void UI_ListView::RemoveRow(int item);
This function will remove an entire row. The row is specified by item. There is no return value.
int UI_ListView::GetSelection();
This function will retreive the currently selected row in the table and return it. If there is no row selected (or no rows present) it will return –1.
int UI_ListView::GetRows();
This function returns the number of rows in the list view.
int UI_ListView::GetCols();
This function returns the number of columns in the list view.
class UI_IPAddress : public
UI_FrontEndBase {
private:
public:
UI_IPAddress ();
~UI_IPAddress ();
void GetIPString(char * buffer, int buffsize);
DWORD GetIPDWORD();
};
UI_IPAddress::UI_IPAddress();
This is the constructor for this class. It is empty.
UI_IPAddress::~UI_IPAddress();
This is the deconstructor for this class. It is emtpy.
void UI_IPAddress::GetIPString(char *
buffer, int bufsize);
This function will retreive the IP address from the IP Address control (all automated by Windows Common Controls) and place it into a string in the common xxx.xxx.xxx.xxx format. The maximum size of this string is therefore 15 + \0 = 16 bytes. The minimum size is 7 + \0 = 8 bytes. The size of the buffer needs to be passed in as bufsize. Nothing is returned.
DWORD UI_IPAddress::GetIPDWORD();
This function will retreive the IP address from the IP Address control (all automated by Windows Common Controls) and place it into a DWORD. This is the IP Common Control’s deafult format where you can use the FIRST_IPADDRESS(DWORD), SECOND_IPADDRESS(DWORD), THIRD_ IPADDRESS(DWORD), and FOURTH_ IPADDRESS(DWORD) macros to get the individual IP bytes out of it. Nothing is returned.
class UI_TextBox : public
UI_FrontEndBase {
private:
public:
UI_TextBox();
~UI_TextBox();
void GetText(char * buffer, int bufsize);
void SetText(char * buffer);
};
UI_TextBox::UI_TextBox();
This is the constructor for this class. It is empty.
UI_TextBox::~UI_TextBox();
This is the deconstructor for this class. It is emtpy.
void UI_TextBox::GetText(char * buffer,
int bufsize);
This function will retreive the IP address from the IP Address control (all automated by Windows Common Controls) and place it into a string in the common xxx.xxx.xxx.xxx format. The maximum size of this string is therefore 15 + \0 = 16 bytes. The minimum size is 7 + \0 = 8 bytes. The size of the buffer needs to be passed in as bufsize. Nothing is returned.
void UI_TextBox::SetTest(char *
buffer);
This function will retreive set the text in a text box. It copies out the NULL-terminated string in buffer. It returns nothing.
class UI_RadioButton :
public UI_FrontEndBase {
private:
public:
UI_RadioButton();
~UI_RadioButton();
bool Get();
void Set(bool state);
};
UI_RadioButton::UI_RadioButton();
This is the constructor for this class. It is empty.
UI_RadioButton::~UI_RadioButton();
This is the deconstructor for this class. It is emtpy.
bool UI_RadioButton::Get();
This function will retreive the setting of the radio button and return it. A return value of true indicates a filled in radio button. A return value of false indicates a cleared radio button.
void UI_RadioButton::Set(bool state);
This function will set the value of a radio button to the passed in state. A value of true lights up the radio button, while a value of false clears it. It returns nothing.
class UI_Button : public
UI_Button {
private:
public:
UI_Button();
~UI_Button();
bool IsPressed(LPARAM lParam, WPARAM wParam);
void SetButtonText(char * text);
};
UI_Button::UI_Button();
This is the constructor for this class. It is empty.
UI_Button::~UI_Button();
This is the deconstructor for this class. It is emtpy.
bool UI_Button::IsPressed(LPARAM
lParam, WPARAM wParam);
This function will return weather or not the button has been pressed. This function should be called within the WM_COMMAND message in the parent window’s WndProc, and the WM_COMMAND’s LPARAM and WPARAM should be passed in as parameters. It will return true when the button has been pressed, and false when it has not.
bool UI_Button::SetButtonText(char *
text);
This function will set the text in the button to the passed in Null-terminated string text. Nothing is returned.
class UI_CheckBox : public
UI_CheckBox{
private:
public:
UI_CheckBox();
~UI_CheckBox();
bool Get();
void Set(bool state);
};
UI_CheckBox::UI_CheckBox();
This is the constructor for this class. It is empty.
UI_CheckBox::~UI_CheckBox();
This is the deconstructor for this class. It is emtpy.
bool UI_CheckBox::Get();
This function will return true when the check box is checked, and false otherwise.
void UI_CheckBox::Set(bool state);
This function will set the check box to checked (true) or unchecked (false) depending on the value of state. It returns nothing.
class UI_Bitmap {
int m_width, m_height;
int m_pitch;
uchar * m_bits;
uchar * m_mask;
void DeleteBuffers();
public:
UI_Bitmap();
~UI_Bitmap();
void Clear();
void SetSize(int width, int height, bool mask);
void GetSize(int &width, int &height);
uchar * GetBitPointer();
uchar * GetLinePointer(int line);
uchar * GetMaskBitPointer();
uchar * GetMaskLinePointer(int line);
bool LoadBMPFromFile(char * filename,
uchar mode = LOAD_BMP_COLORKEY_NONE,
int ck_x = -1,
int ck_y = -1);
bool LoadBMPFromBuffer(uchar * buffer,
uchar mode = LOAD_BMP_COLORKEY_NONE,
int ck_x = -1,
int ck_y = -1);
int GetPitch();
bool hasMask();
};
This class will be the only class usable by both the front end and in-game UIs. It is a generic BMP file loader. Once loaded, the front end and the in-game will each have to have their own wrapper class to deal with the bitmaps. It has the following defines (for the loading mode):
#define
LOAD_BMP_COLORKEY_NONE 0
#define LOAD_BMP_COLORKEY_UPPERLEFT 1
#define
LOAD_BMP_COLORKEY_LOWERLEFT 2
#define
LOAD_BMP_COLORKEY_UPPERRIGHT 3
#define
LOAD_BMP_COLORKEY_LOWERRIGHT 4
#define
LOAD_BMP_COLORKEY_CENTER 5
#define
LOAD_BMP_COLORKEY_ARBITRARY 6
These defines allow the user to define a color to use as the colorkey, allowing a mask image to be generated concurrently that allows transparency against any single color in the image. The class itself uses default values of no masking, so the masking parameters need only be included if you want to define a mask. The ck_x and ck_y values specify which pixel contains the color key value, and are only used if the LOAD_BMP_COLORKEY_ARBITRARY flag is passed in.
UI_Bitmap::UI_Bitmap();
The constructor sets up the variables to starting values to 0 or NULL.
UI_Bitmap::~UI_Bitmap();
The deconstructor calls DeleteBuffers().
void UI_Bitmap::DeleteBuffers();
This checks for the existence of buffers, and if they exist, it frees them. This should be called only destroy all data (like when the class is being destroyed, hence why it is private and not public).
void UI_Bitmap::Clear();
This function clears out the bitmap to 0.