by Brent Orford



Project - X (c) 1999 Brent Orford.
This code is distributed freely as an example of a basic game shell using the Genesis 1.0 API.


To get the most up to date code for this tutorial go HERE.



This game shell has support for...
- Jumping
- Crouching
- Mouselook
- FPS independence
- KB Movement
- Gravity
- Wall sliding


***** UPDATE *****

The code in this tutorial has been updated. Also two new pages of info have been added as follows:

What's New
Setting it up




Directory structure:

All files contained in the .zip should be unpacked to a directory of your choice (i.e. C:\ProjectX\).

D3DDrv.dll GBSPLib.dll Genesis.dll GlideDrv.dll SoftDrv.dll Softdrv2.dll should be copied from the Genesis3d directory into the new directory (C:\ProjectX\).

A levels directory (C:\ProjectX\levels) should be created and the levels from the Genesis GTest levels directory should be copied over. (genvs.3dt and genvs.bsp should be copied.)

A lib directory (C:\ProjectX\lib) should be created and genesis.lib genesisd.lib genesisi.lib should be copied over from the Genesis3d\lib directory.

An include directory (C:\ProjectX\include) should also be created and the Genesis3d\include files should be copied over.


Download the .zip file containing the code HERE.


Code:


ProjectX.h:


static HWND CreateMainWindow (HINSTANCE hInstance, char *AppName, int32 Width, int32 Height);
LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam);
void MoveHead(void);
void ApplyGravity(void);
void LoadPrefs(int *CWidth, int *CHeight, char *OurDriver);
static geBoolean IsKeyDown(int KeyCode);
void Setup(void);
void Jump (void);
void Crouch (void);
void UnCrouch(void);
void Move (float speed);

int CWidth = 640;
int CHeight = 480;

geDriver_System *DrvSys = NULL;
geDriver *Driver = NULL;
geDriver_Mode *Mode = NULL;


char *modename = NULL,
*drvname = NULL;

char ourdriver = '(';

geWorld *World = NULL;
geCamera *Camera = NULL;
geRect Rect;

POINT pos;

geVFile *ActorFile, *MainFS, *Level = NULL;
HWND mainwindowhandle;
geEngine *Engine = NULL;

MSG Msg;

geXForm3d Xform;
geXForm3d ViewXForm;

geVec3d Angles;
geVec3d oldpos; //old and new pos are temporary values
geVec3d newpos;
geVec3d In;
geVec3d Mins; //this is for collision detect
geVec3d Maxs; //they set your minimum and maximum size.

GE_Collision Collision;

BOOL Result;

geBoolean Debuginfo;

float Bd;

typedef struct Player_Info_Struct
{
float CurrentHeight; //how tall we currently are
float NormalHeight; //how tall we normally are
int CurrentSpeed; //how fast we currently are
int NormalSpeed; //how fast we normally are
int TimeinAir;
float FallingSpeed;
} Player_Info;


Player_Info Player;

geXForm3d ActorXform;
geFloat ModelCounter = 0.0f;
geMotion *Motion = NULL;
geActor_Def *ActorDef;
geActor *Actor;



ProjectX.c:


#include <windows.h>
#include <stdio.h>
#include "include\Genesis.h"
#include "ProjectX.h"

/////////////////////////////////////////////////////////////////////////////////////
// ProjectX was written as an example of how to start a first person perspective
// game using Genesis 1.0. Use it however you want, if you have any problems with
// it that I should know about or you make improvements, I probably would like to
// know what they are so write me an e-mail. This code was based off the original
// 3rd person perspective Minapp, but is now 1st person, has mouselook as well as
// movement commands. FPS independence has been implemented. Gravity, Jumping and
// crouching have also been implemented.
/////////////////////////////////////////////////////////////////////////////////////


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow)

{
int run, Height, Width, tick;
long LastTickCount = GetTickCount();

geBitmap *Black = NULL;

pos.x = 0;
pos.y = 0;

LoadPrefs(&CWidth, &CHeight, &ourdriver);

mainwindowhandle = CreateMainWindow(hInstance, "Project - X", 640, 480);

Engine = geEngine_Create(mainwindowhandle, "Project - X", ".");

geEngine_EnableFrameRateCounter(Engine, GE_FALSE); // Disable the frame rate counter.
if (ourdriver == '(') ShowCursor( FALSE ); // If DX Turn the cursor off!

if (!Engine) MessageBox(NULL,"No Engine", "Error", MB_OK);

DrvSys = geEngine_GetDriverSystem(Engine);

if (!DrvSys) MessageBox(NULL,"No DrvSys", "Error", MB_OK);

Driver = geDriver_SystemGetNextDriver(DrvSys, NULL);

if (!Driver) MessageBox(NULL,"No Driver","Error", MB_OK);

while(1) {

geDriver_GetName(Driver, &drvname);
if (drvname[0] == ourdriver) break; // check to see if it is the correct driver

Driver = geDriver_SystemGetNextDriver(DrvSys, Driver);
if (!Driver) _exit(-1);
}


Mode = geDriver_GetNextMode(Driver, NULL);

while(1) {

if (!Mode) MessageBox(NULL,"No Mode","Error", MB_OK);

geDriver_ModeGetWidthHeight(Mode, &Width, &Height);
if (Width == CWidth && Height == CHeight) break;

Mode = geDriver_GetNextMode(Driver, Mode);
}

if (!geEngine_SetDriverAndMode(Engine, Driver, Mode))
{
MessageBox(NULL,"Set Driver/Mode failed","Error", MB_OK);
_exit(-1);
}


geEngine_SetGamma(Engine, 2.0f);

Rect.Left = 0;

Rect.Right = CWidth - 1;

Rect.Top = 0;

Rect.Bottom = CHeight - 1;

Camera = geCamera_Create(2.0f, &Rect);

if (!Camera) {
MessageBox(NULL,"No Camera","Error", MB_OK);
_exit(-1);
}


Level = geVFile_OpenNewSystem(NULL, GE_VFILE_TYPE_DOS, "levels\\genvs.bsp", NULL, GE_VFILE_OPEN_READONLY); // | GE_VFILE_OPEN_DIRECTORY);

World = geWorld_Create(Level);

geVFile_Close(Level);


if (!World) {
MessageBox(NULL,"No World","Error", MB_OK);
_exit(-1);
}

if (geEngine_AddWorld(Engine, World)== GE_FALSE)

MessageBox(NULL, "engine cannot use this level", "Error", MB_OK);

Setup();

geXForm3d_SetIdentity(&Xform);
geXForm3d_RotateZ(&Xform, (geFloat)0.0);
geXForm3d_RotateX(&Xform, (geFloat)0.0);
geXForm3d_RotateY(&Xform, (geFloat)0.0);
geXForm3d_Translate(&Xform, (geFloat)0.0, (geFloat)Player.NormalHeight, (geFloat)0.0);


geXForm3d_SetIdentity(&ActorXform);
geXForm3d_RotateZ(&ActorXform, (geFloat)0.0);
geXForm3d_RotateX(&ActorXform, (geFloat)-89.55); //stand the actor upright.
geXForm3d_RotateY(&ActorXform, (geFloat)2.8);
geXForm3d_Translate(&ActorXform, (geFloat)0.0, (geFloat)Player.NormalHeight, (geFloat)0.0);


//load the act file.
ActorFile = geVFile_OpenNewSystem(NULL, GE_VFILE_TYPE_DOS, "actors\\dema.act", NULL, GE_VFILE_OPEN_READONLY);

if(ActorFile)
{
//create a definition of the actor
ActorDef = geActor_DefCreateFromFile (ActorFile);
if(ActorDef)
{
Actor = geActor_Create (ActorDef);
//add that actor to the world
geWorld_AddActor (World, Actor, GE_ACTOR_RENDER_NORMAL | GE_ACTOR_COLLIDE, 0xffffffff);
//make the actor bigger.
geActor_SetScale(Actor, 2.3f,2.3f,2.3f);

//extract the motion saved in the actor as "Idle"
Motion = geActor_GetMotionByName(ActorDef, "Idle" );
//set the actors pose (this is called later repeatedly for
//simple animation
geActor_SetPose(Actor, Motion, ModelCounter, &ActorXform);
}
}

geVFile_Close(ActorFile); // Close our file.


geCamera_SetWorldSpaceXForm(Camera, &Xform);


//this loop is intended to be the main rendering and windows message pumping loop

run = 1;

Debuginfo = GE_FALSE;


while (run) {

oldpos = Xform.Translation; //old position
newpos = Xform.Translation; //new position


if (IsKeyDown(' ')) // Jump
{
Jump();
}

if (IsKeyDown('C')) // Crouch
{
Crouch();
}
else // See if we need to Uncrouch
{
if (Player.CurrentHeight < Player.NormalHeight)
UnCrouch();
}

if (IsKeyDown('E')) // Move Forward
{
geXForm3d_GetIn(&Xform, &In); // get forward vector
Move ((float)Player.CurrentSpeed);
}

if (IsKeyDown('D')) // Move Backward
{
geXForm3d_GetIn(&Xform, &In); // get forward vector

Move (-1 * (float)Player.CurrentSpeed); // Multiply the speed by -1 to go
// in reverse along that vector
// i.e. backwards.
}

if (IsKeyDown('S')) // Move Left
{
geXForm3d_GetLeft(&Xform, &In); // Get the left vector
Move ((float)Player.CurrentSpeed);
}

if (IsKeyDown('F')) // Move Right
{
geXForm3d_GetLeft(&Xform, &In); // Get the left vector
Move (-1 * (float)Player.CurrentSpeed); // Multiply the speed by -1 to go
// in reverse along that vector
// i.e. to the right.
}

if (IsKeyDown('P')) // Toggle Printing debug info on the screen.
{
if (Debuginfo == GE_FALSE)
Debuginfo = GE_TRUE;
else
Debuginfo = GE_FALSE;

geEngine_EnableFrameRateCounter(Engine, Debuginfo);
}



Result = geWorld_Collision(World, &Mins, &Maxs, &oldpos, &newpos, GE_CONTENTS_SOLID_CLIP, GE_COLLIDE_ALL, 0, NULL, NULL, &Collision);

if(Result == 1) //Your new position collides with a wall or object
newpos = oldpos;


ApplyGravity(); //Apply our gravity

MoveHead(); //gather mouse motion data, move camera


ActorXform.Translation = newpos; // Set our Xforms to the new world position.
Xform.Translation = newpos;


// We don't want to be clipping our actor's face so lets fix that here.
geXForm3d_GetUp(&ActorXform, &In);
geVec3d_AddScaled (&ActorXform.Translation, &In, 10.0f, &ActorXform.Translation);

ModelCounter += 0.1f; // Make the actor move.
if(ModelCounter > 5.0)
ModelCounter = 0.0;

geActor_SetPose(Actor, Motion, ModelCounter, &ActorXform); // Repose



tick = GetTickCount() - LastTickCount; // Num of ticks since last loop

if (Debuginfo)
geEngine_Printf (Engine, 1, 151, "TickCount: %d", tick);


while (tick < 70) // Loop until our tick count is approximately right.
tick = GetTickCount() - LastTickCount;

if (Debuginfo)
geEngine_Printf (Engine, 1, 165, "TickCount Compensation: %d", tick);

LastTickCount = GetTickCount();

if (GE_FALSE == geEngine_BeginFrame(Engine, Camera, GE_FALSE)) break;

//render to offscreen buffer

if (GE_FALSE == geEngine_RenderWorld(Engine, World, Camera, 0.0f)) break;
//end the frame, copy offscreen buffer to onscreen window

if (GE_FALSE == geEngine_EndFrame(Engine)) break;



while (PeekMessage( &Msg, NULL, 0, 0, PM_NOREMOVE))
{

if (!GetMessage(&Msg, NULL, 0, 0 ))
{
run = 0;
break;
}

TranslateMessage(&Msg);
DispatchMessage(&Msg);

}



}

//shut it all down and quit

geCamera_Destroy(&Camera);
geWorld_Free(World);
geEngine_ShutdownDriver(Engine);
geEngine_Free(Engine);

return (0);

}
//=====================================================================================
// CreateMainWindow
//=====================================================================================
static HWND CreateMainWindow(HINSTANCE hInstance, char *AppName, int32 Width, int32 Height)

{

WNDCLASS wc;

HWND hWnd;
RECT ScreenRect;

GetWindowRect(GetDesktopWindow(),&ScreenRect); // Get the size of the screen

//
// Set up and register application window class
//

wc.style = CS_HREDRAW | CS_VREDRAW;

wc.lpfnWndProc = (WNDPROC)WndProc;

wc.cbClsExtra = 0;

wc.cbWndExtra = 0;

wc.hInstance = hInstance;

wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);

wc.hCursor = LoadCursor(NULL, IDC_ARROW);

wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);

wc.lpszMenuName = (const char*)NULL;

wc.lpszClassName = AppName;

RegisterClass(&wc);

//
// Create application's main window
//

hWnd = CreateWindowEx(
0, AppName, AppName, 0, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, NULL, NULL, hInstance, NULL);

if (!hWnd)
{
MessageBox(0, "Could not create window.", "** ERROR **", MB_OK);
_exit(1);
}

UpdateWindow(hWnd);

SetFocus(hWnd);

SetWindowLong(hWnd, GWL_STYLE, GetWindowLong(hWnd, GWL_STYLE) & ~WS_POPUP);

SetWindowLong(hWnd, GWL_STYLE, GetWindowLong(hWnd, GWL_STYLE) | (WS_OVERLAPPED |
WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX));

SetWindowLong(hWnd, GWL_STYLE, GetWindowLong(hWnd, GWL_STYLE) | WS_THICKFRAME |
WS_MAXIMIZEBOX);

SetWindowLong(hWnd, GWL_EXSTYLE, GetWindowLong(hWnd, GWL_EXSTYLE) | WS_EX_TOPMOST);


SetWindowPos
(hWnd, HWND_TOP,
(ScreenRect.right + ScreenRect.left) /2 - 300,
(ScreenRect.bottom + ScreenRect.top) /2 - 250,
500, 400, SWP_NOCOPYBITS | SWP_NOZORDER);


ShowWindow(hWnd, SW_SHOWNORMAL); // Make window visible

return hWnd;
}

////////////////////////////////////////////////////////////////////////////////////////
void LoadPrefs(int *CWidth, int *CHeight, char *OurDriver)
{
FILE *stream;

if ((stream = fopen ("prefs.ini","r")) == NULL)
{
MessageBox(NULL, "Prefs.ini file not found, using defaults...", "No Prefs", MB_OK);
return;
}


fscanf(stream,"%s", OurDriver);
fscanf(stream,"%d", CWidth);
fscanf(stream,"%d", CHeight);

fclose(stream);

}

//=====================================================================================
// IsKeyDown
//=====================================================================================
static geBoolean IsKeyDown (int KeyCode)
{
if (GetAsyncKeyState(KeyCode) & 0x8000)
return GE_TRUE;

return GE_FALSE;
}

///////////////////////////////////////////////////////////////////////////////////////
LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{

switch(iMessage)

{


case WM_RBUTTONDOWN:

case WM_LBUTTONUP:

case WM_RBUTTONUP:

case WM_LBUTTONDOWN:

{

PostMessage(hWnd, WM_QUIT, 0, 0);



}

default:

return DefWindowProc(hWnd, iMessage, wParam, lParam);

}

return 0;

}


///////////////////////////////////////////////////////////////////////////////////////
void MoveHead(void) //Moves the camera base on our mouse position
{

POINT temppos;
geVec3d Pos; // this value will be used later when we translate
// our ViewXform.
geVec3d TempAngles;

GetCursorPos(&temppos);

oldpos = newpos;
TempAngles = Angles;

if (ourdriver == 'G') // Glide
SetCursorPos(CWidth/2, (CHeight/2 - 16));
else // DirectX
SetCursorPos(CWidth/2 + 4, (CHeight/2 + 24));

ScreenToClient(mainwindowhandle, &temppos);

if ((temppos.x != pos.x) || (temppos.y != pos.y))
{
if (temppos.x > pos.x) // is it to the left?
{
TempAngles.Y = TempAngles.Y-(geFloat)((temppos.x-(CWidth/2))*.0010); //if so spin left
geXForm3d_RotateY(&ActorXform, TempAngles.Y - Angles.Y);
}
else if (temppos.x < pos.x) // is it to the right?
{
TempAngles.Y = TempAngles.Y+(geFloat)(((CWidth/2)+temppos.x)*.0010); //if so spin right
geXForm3d_RotateY(&ActorXform, TempAngles.Y - Angles.Y);
}
if (temppos.y > pos.y) // is it to the top?
TempAngles.X = TempAngles.X-(geFloat)((temppos.y-(CHeight/2))*.0010); //if so look up

else if (temppos.y < pos.y) // is it to the bottom?
TempAngles.X = TempAngles.X+(geFloat)(((CHeight/2)+temppos.y)*.0010); //if so look down

//make sure we arent looking too far up or down. If we are then fix that!
if (TempAngles.X >0.9f)
TempAngles.X =0.9f;
if (TempAngles.X <-0.9f)
TempAngles.X =-0.9f;
}

Xform.Translation = newpos;
Angles = TempAngles;
// Copy our Xform into ViewXForm (so we can deform it
// to where our player is looking etc)
ViewXForm = Xform;

Pos = ViewXForm.Translation;

geXForm3d_SetIdentity(&ViewXForm); // Clear the matrix

geXForm3d_RotateZ(&ViewXForm, Angles.Z); // Rotate then translate
geXForm3d_RotateX(&ViewXForm, Angles.X);
geXForm3d_RotateY(&ViewXForm, Angles.Y);
geXForm3d_Translate(&ViewXForm, Pos.X, Pos.Y, Pos.Z);

// We give a +140 adjustment to simulate an eye-high view
ViewXForm.Translation.Y += Player.CurrentHeight;

geCamera_SetWorldSpaceXForm(Camera, &ViewXForm);
geCamera_SetAttributes(Camera, 2.0f, &Rect);
}

///////////////////////////////////////////////////////////////////////////////////////
void ApplyGravity(void)
{
geVec3d Up; //will contain the up/down vector for scale, it is the up/down counterpart of In

oldpos = newpos;

geXForm3d_SetYRotation(&Xform, Angles.Y); //which way are we facing?

geXForm3d_GetUp(&Xform, &Up); //get our upward vector

Player.TimeinAir++;
Player.FallingSpeed = -5.6f * (float)Player.TimeinAir;

geVec3d_AddScaled (&oldpos, &Up, Player.FallingSpeed, &newpos); // Move

Result = geWorld_Collision(World, &Mins, &Maxs, &oldpos, &newpos, GE_CONTENTS_SOLID_CLIP, GE_COLLIDE_ALL, 0, NULL, NULL, &Collision);
if (Result == 1) // Your new position collides with something
{

if (Collision.Plane.Normal.Y < 0.3f) // non-climbable slope
{
geVec3d_AddScaled (&newpos, &Up, -9.6f, &newpos); // Make it a faster slide
Bd = geVec3d_DotProduct (&newpos, &Collision.Plane.Normal) - Collision.Plane.Dist;

newpos.X -= Collision.Plane.Normal.X * Bd;
newpos.Y -= Collision.Plane.Normal.Y * Bd;
newpos.Z -= Collision.Plane.Normal.Z * Bd;

Result = geWorld_Collision(World, &Mins, &Maxs, &oldpos, &newpos, GE_CONTENTS_SOLID_CLIP, GE_COLLIDE_ALL, 0, NULL, NULL, &Collision);
if(Result == 1) // Your new position collides with something
newpos = Collision.Impact; // set new position to the point of collision.
}
else
{
Player.TimeinAir = 0;
newpos = Collision.Impact;
}
}

}

///////////////////////////////////////////////////////////////////////////////////////
void Setup(void)
{
Player.CurrentHeight = 140.0f; // Setup our Player defaults.
Player.CurrentSpeed = 35;
Player.NormalHeight = 140.0f;
Player.NormalSpeed = 35;
Player.TimeinAir = 0;
Player.FallingSpeed = 0.0f;


Mins.X = -20.0f; // Mins/Maxs set up the bounding box around our player which is
Mins.Y = 0.0f; // used for collision detect purposes. It tells it when we are
Mins.Z = -20.0f; // colliding.

Maxs.X = 20.0f;
Maxs.Y = Player.NormalHeight;
Maxs.Z = 20.0f;
}

///////////////////////////////////////////////////////////////////////////////////////
void Jump (void)
{
geVec3d Up; //will contain the up/down vector for scale, it is the up/down counterpart of In
float upspeed = 40.0f; //speed we are raising.

oldpos = Xform.Translation; //old position
newpos = Xform.Translation; //new position

geXForm3d_SetYRotation(&Xform, Angles.Y); //which way are we facing?

geXForm3d_GetUp(&Xform, &Up); //get our upward vector

geVec3d_AddScaled (&oldpos, &Up, upspeed, &newpos); // Move

Result = geWorld_Collision(World, &Mins, &Maxs, &oldpos, &newpos, GE_CONTENTS_SOLID_CLIP, GE_COLLIDE_ALL, 0, NULL, NULL, &Collision);
if(Result == 1) //Your new position collides with something
{
Xform.Translation = Collision.Impact; // We've hit something.
Player.TimeinAir = Player.TimeinAir + 3; // Force us down.
}
}

///////////////////////////////////////////////////////////////////////////////////////
void Crouch (void)
{
float upspeed = -40.0f; //speed we are lowering.

if (Player.CurrentHeight > 20) // If we can crouch down lower...
{
Player.CurrentHeight = Player.CurrentHeight + upspeed; // Lower our current height.
Maxs.Y = Player.CurrentHeight; // Update our bounding box.

Player.CurrentSpeed = Player.CurrentSpeed - 5; // Slow us down as you probably
// Wouldn't be able to crouch and walk as fast as you would normally walk.
}
}

///////////////////////////////////////////////////////////////////////////////////////
void UnCrouch(void)
{
geVec3d Up; //will contain the up/down vector for scale, it is the up/down counterpart of In
float upspeed = 40.0f; //speed we are raising.

oldpos = Xform.Translation; //old position
newpos = Xform.Translation; //new position

geXForm3d_SetYRotation(&Xform, Angles.Y); //which way are we facing?
geXForm3d_GetUp(&Xform, &Up); //get our upward vector

geVec3d_AddScaled (&oldpos, &Up, upspeed, &newpos); // Move

Result = geWorld_Collision(World, &Mins, &Maxs, &oldpos, &newpos, GE_CONTENTS_SOLID_CLIP, GE_COLLIDE_ALL, 0, NULL, NULL, &Collision);
if(Result == 1) //Your new position collides with something
{
Xform.Translation = oldpos; // We be hittin' the ceilin'
}

else // We can move up, so lets not do it all at once, nobody pops up that
{ // Quickly!!
Player.CurrentHeight = Player.CurrentHeight + 40; // Increase our current height variable.
Maxs.Y = Player.CurrentHeight; // Reset our bounding box.
Player.CurrentSpeed = Player.CurrentSpeed + 5; // Make us a little faster again.

newpos = oldpos; // Let our CurrentHeight variable change the Xform or else
// we'll bounce when we get to the top.
}

}

///////////////////////////////////////////////////////////////////////////////////////
void Move (float speed)
{
geVec3d_AddScaled (&newpos, &In, speed, &newpos); // Move

Result = geWorld_Collision(World, &Mins, &Maxs, &oldpos, &newpos, GE_CONTENTS_SOLID_CLIP, GE_COLLIDE_ALL, 0, NULL, NULL, &Collision);
if(Result == 1) //Your new position collides with something
{

geVec3d_AddScaled (&newpos, &In, speed, &newpos); // Move

if (Collision.Plane.Normal.Y < 0) // This is really crappy code here
Collision.Plane.Normal.Y = 0; // It allows sliding on reverse
// inclined walls but it's not that
// good. Just a quick hack.


// This is the sliding code used for wall slides. It works well and
// should remain in the program.
Bd = geVec3d_DotProduct (&newpos, &Collision.Plane.Normal) - Collision.Plane.Dist;

newpos.X -= Collision.Plane.Normal.X * Bd;
newpos.Y -= Collision.Plane.Normal.Y * Bd;
newpos.Z -= Collision.Plane.Normal.Z * Bd;
}
}




Prefs.ini File:


(
800
600



Project - X (c) 1999 Brent Orford.