//////////////////////////////////////////////////////////// // edrive.cpp // // Main program logic for EDrive, a multiplatform driving simulator // // The main program logic and frame-by-frame drawing functions // are in this file. // // Version 0.1 Release notes summary: // This release should be considered a very preliminary proof of concept. // For example, there is no car - just four wheels - and there are no // realistic physics - just some quick-and-dirty fake physics. // // This release supports the following platforms: // Windows (whatever version as long as DirectX 9 or later is installed) // Linux (as long as OpenGL or Mesa (under XFree86), GLUT (v 3.7 or later) // and OpenAL (v 1.0 or later) are installed) // // Copyright 2004 by Evan Alexander Weaver // // This initial release, including the source code is provided free of // charge, but WITHOUT WARRANTEE OF ANY KIND. Use at your own risk. While // future versions of this software will be released under some sort of // Open Source license (likely GPL), this version is FREE for use, // modification or distribution without any obligations. Well, there is // one obligation - you may not claim that you wrote or own this source // code. Enough copies of this have been distributed that you won't be // able to establish copyright anyway, but I just want to be clear that // you may not steal my right to release derived works under some other // license (a right which you also have, by the way). Furthermore, note // that software that EDrive requires in order to run (such as Windows, // DirectX, Linux, XFree86, OpenGL/Mesa, OpenAL) have more restrictive // licenses for distribution. // // Date last Modified: 31 May 2004 //////////////////// Header files //////////////////////////////////// // Identify which platform. In this release, there is just a two way choice, // Windows (which means DirectX 9 for graphics, input and sound) or Linux // (which means OpenGL or Mesa with GLUT for graphics and keyboard input, // OpenAL for sound, and the generic Linux joystick driver for joystick // input). // An attempt has been made to granularize the choices more finely than // a simple Windows-or-not decision, to aid in future porting (e.g. to // OpenGL for Windows graphics) #include "platform.h" #if ED_WINDOWS //*********************************************************** #include #include #else //******************************************************************** #include #endif //******************************************************************* #include #include #include // EDrive components #include "error.h" #include "edmath.h" #include "graphics.h" #include "input.h" #include "car.h" #include "terrain.h" //////////////////// Global variables //////////////////////////////// #if ED_DIRECTX_9 //******************************************************** // In Windows, this global variable is used as a trick to initialize // COM once before the program starts, and shut it down after // everything else is gone. A counter is used just in case some // smarty-pants makes multiple instances of this object, so that // COM won't be shut down until the very end. // // This must be created before any other globals which might involve // COM use somehow. (COM is Microsoft's Component Object Model, the // architecture underneath DirectX). // class appsetup { static int count; public: appsetup() { if (count++ == 0) CoInitialize(NULL); } appsetup(const appsetup &x) { count++; } ~appsetup() { if (--count == 0) CoUninitialize(); } } Theapp; int appsetup::count = 0; #endif //******************************************************************* // While I generally hate to use global variables to share data between // different functions, I couldn't avoid the following because of the // "least common denominator" approach I took to accomodate both DirectX // and OpenGL. I don't consider having a class populated by static data // members only to be any more "object oriented" than simply making a // global instance like this, and at least this way I have a chance at // minimizing the amount of code that depends on this specific global // variable name. // // You can think of the "context" class as the collection of hardware- // related objects on which all gameplay operations take place. // class context { public: // configuration data saved to, and restored from, file // joystickid joyid; // which joystick int joyaxis[3]; // which axes for steering, acceleration and braking int dispdev; // which display device (-1: run in a window) int dispmode; // which display mode (ignored in OpenGL) // lists of user selectable things // #if ED_DIRECTX_9_GRAPHICS //******************************************** graphicslist glist; // available display devices (DirectX only) #endif //*************************************************************** joysticklist joylist; // available joysticks // I/O devices // graphics g; // current display device keyboard kb; // system keyboard joystick joy; // current joystick // Drawable objects // car thecar; // user's car terrain ground; // the track environment // Various state flags // bool active; // true if we are in foreground bool ok; // true if it is OK to draw things bool redrawanyway; // true if we MUST redraw screen at next frame bool resetclock; // true if clock must be restarted at next frame context() { active = true; ok = false; // wait until setup is done before drawing redrawanyway = true; // draw the first frame regardless of conditions resetclock = true; // reset the timing clock at first frame dispdev = -1; // run in a window dispmode = 0; // irrelevant in a window joyaxis[0] = joyaxis[1] = joyaxis[2] = 0; // 0 means "use default" } // Warning: Lazy programmer trick ahead! // // In many places throughout EDrive, there are objects which are // difficult to duplicate properly because they tie up external // resources. Rather than try to make sure they can be duplicated // properly, we simply declare a copy constructor and = operator without // defining them. If we ever do try to duplicate one of these objects, // we'll get a linker error. On purpose. // context(const context &); // Intentionally not defined... context& operator=(const context &); // ... to prevent copies // Most EDrive objects that consume resources have a destroy() function // which frees up those resources. These destroy() functions should // work OK even if the instance has already been destroy()ed. // void destroy() { ground.destroy(); thecar.destroy(); g.destroy(); joy.destroy(); kb.destroy(); } // Free up resources when this instance disappears // ~context() { destroy(); } // Read saved configuration data from a file named in "file". // Configuration data is: // // - chosen joystick // - which joystick axes to use for steering, acceleration and braking // - chosen display device (-1 for a window) // - which display mode to use (only used in DirectX) // Returns true if successful. // bool readconfig(const char *file) { FILE *fp; bool rc = false; if ((fp = fopen(file, "rb"))) { char id[10]; joystickid jid; int n[5]; // File must begin with the string "EDrive001". The remaining // pieces of data are stored directly in binary format. Note // that all remaining items are int except for joystick ID, // which has a system dependent size // if (1 == fread(id, 10, 1, fp) && 0 == strcmp(id, "EDrive001") && jid.fread(fp) && 5 == fread(n, sizeof(int), 5, fp)) { joyid = jid; joyaxis[0] = n[0]; joyaxis[1] = n[1]; joyaxis[2] = n[2]; dispdev = n[3]; dispmode = n[4]; rc = true; } else errorf("Invalid configuration file %s", file); fclose(fp); } return rc; } // Save configuration data to the file named in "file". See readconfig() // comments for details. // Returns true if successful. // bool saveconfig(const char *file) { FILE *fp; bool rc = false; if ((fp = fopen(file, "wb"))) { if (1 == fwrite("EDrive001", 10, 1, fp) && joyid.fwrite(fp) && 3 == fwrite(joyaxis, sizeof(int), 3, fp) && 1 == fwrite(&dispdev, sizeof(int), 1, fp) && 1 == fwrite(&dispmode, sizeof(int), 1, fp)) rc = true; else errorf("Error writing configuration file %s", file); fclose(fp); } return rc; } } Globals; /////////////////// Global functions ///////////////////////////////// // The various ways to draw one frame, depending on what we are doing. // Since they make use of one another, we'll just declare them now and // define them further below. // void Drawframemainmenu(); void Drawframedrive(); void Drawframecontrolmenu(); void Drawframedisplaymenu(); // Return the time in milliseconds // unsigned int getms() { #if ED_WINDOWS //******************************************************* // Windows already has a function that does what we want // return timeGetTime(); #else //**************************************************************** // Return milliseconds elapsed since the first call to this function // static struct timeval start; // time when this was first called static bool firsttime = true; // is this the first time? struct timeval now; unsigned int sec, microsec; gettimeofday(&now, 0); if (firsttime) { start = now; firsttime = false; } sec = now.tv_sec - start.tv_sec; if (now.tv_usec >= start.tv_usec) microsec = now.tv_usec - start.tv_usec; else { sec--; microsec = 1000000 + now.tv_usec - start.tv_usec; }; return (sec % 1000000) * 1000 + microsec / 1000; #endif //*************************************************************** } #if ED_DIRECTX_9 //******************************************************** // In Windows, we need a WinMain to direct the action, as well as a // "window procedure" to handle messages sent to the main window. // Main window's message handler. // LRESULT CALLBACK mainwindowproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { switch (msg) { case WM_CREATE: // turn off cursor when window is created SetCursor(NULL); break; case WM_SETCURSOR: // turn off cursor when SetCursor(NULL); break; // Called when user switches applications. Note that // losing then later regaining focus may cause some // resources, such as input and graphics devices, to be // lost, so we have to make sure to put everything back // in working order. // case WM_ACTIVATEAPP: Globals.active = wp; if (wp) { // if we just got (re)activated if (Globals.ok) { // if Globals has already been set up OK // input resources may have been lost Globals.kb.restore(); Globals.joy.restore(); // losing then regaining fullscreen focus may cause // screen-related resources to be lost if (Globals.g.fullscreen()) Globals.g.restore(); // update the screen Globals.g.nextframe(true); } } else { // we just got deactivated // stop the noise Globals.thecar.hush(); // if we weren't on a menu, then go to main (pause) menu if (Globals.g.isdrawframe(Drawframedrive)) { Globals.thecar.hush(); Globals.g.chdrawframe(Drawframemainmenu); } } break; case WM_DESTROY: PostQuitMessage(0); break; } return DefWindowProc(hwnd, msg, wp, lp); } // The Windows main-line logic // int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hprevinst, LPSTR cmdline, int cmdshow) { int rc = 0, done = FALSE, mode = 0, w = 640, h = 480, hz = 0; bool ok; MSG msg; D3DFORMAT fmt = (D3DFORMAT)0; // Read saved configuration data, and try to get video mode // details about the saved configuration (defaults to windowed // mode at 640x480 if unsuccessful). // Globals.readconfig("edrive.cfg"); if (Globals.dispdev != -1 && Globals.glist.select(Globals.dispdev)) Globals.glist.getdetails(Globals.dispmode, mode, w, h, fmt, hz); // make main window // if (!(ok = Globals.g.create(hinst, mainwindowproc, Globals.dispdev, w, h, fmt, hz, Drawframedrive)) && Globals.dispdev == -1) if ((ok = Globals.g.change(-1, 640, 480, (D3DFORMAT)0, 0))) { Globals.dispdev = -1; Globals.dispmode = 0; } // set up input and graphics objects // if (ok) { Globals.kb.create(hinst, Globals.g.gethwnd()); Globals.ground.create(Globals.g, "track.txt"); Globals.thecar.create(Globals.g, "wheel.bmp", "engine.wav", vector(-1, 0, 0), vector(30, 0, Z(9)), 1); // The joystick is a bit trickier to set up. If it can't be // set up according to the saved configuration information // then default to no joystick. // if (!Globals.joyid.empty()) { if (Globals.joy.create(Globals.joyid, Globals.g.gethwnd(), hinst)) { if (Globals.joyaxis[0] != 0 || Globals.joyaxis[1] != 0 || Globals.joyaxis[2] != 0) Globals.joy.setaxes(Globals.joyaxis[0], Globals.joyaxis[1], Globals.joyaxis[2]); } else { Globals.joyid = joystickid(); Globals.joyaxis[0]=Globals.joyaxis[1]=Globals.joyaxis[2]=0; } } Globals.ok = true; // Now we can draw things // Main processing loop. Basically, we just keep drawing // the next frame, over and over again. On the other hand, // if we get any Windows messages, we'll handle them between // frames. do { if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) done = TRUE; else { TranslateMessage(&msg); DispatchMessage(&msg); } } else if (Globals.active) Globals.g.nextframe(true); else // don't waste cyles if we aren't in the foreground WaitMessage(); } while (!done); } else rc = 1; return rc; } #else //******************************************************************** // The Linux main-line logic // int main(int argc, char **argv) { // read saved configuration and create the graphics object // accordingly. Globals.readconfig("edrive.cfg"); if (Globals.g.create(&argc, argv, 640, 480, Drawframedrive)) { if (Globals.dispdev == 0) Globals.g.gofullscreen(); // create input devices. Note that joystick defaults to // "no joystick" if it can't be created as saved. Globals.kb.create(); if (!Globals.joyid.empty()) { Globals.joy.create(Globals.joyid); if (Globals.joyaxis[0] != 0 || Globals.joyaxis[1] != 0 || Globals.joyaxis[2] != 0) Globals.joy.setaxes(Globals.joyaxis[0], Globals.joyaxis[1], Globals.joyaxis[2]); } // Create graphics objects Globals.ground.create(Globals.g, "track.txt"); Globals.thecar.create(Globals.g, "wheel.bmp", "engine.wav", vector(-1, 0, 0), vector(30, 0, Z(9)), 1); Globals.ok = true; // Now we can draw things glutMainLoop(); // GLUT handles the event processing loop } return 0; } // Handler for when screen size is changed. This function is installed // when Globals.g.create() is done. // void Screensizechange(int w, int h) { Globals.g.newsize(w, h); Globals.redrawanyway = true; Globals.g.nextframe(true); } // Handler for when the window's visibility changes. This function is // installed when Globals.g.create() is done. // void Visibilitychange(int visible) { if (visible) Globals.redrawanyway = true; else { if (Globals.g.isdrawframe(Drawframedrive)) { Globals.thecar.hush(); Globals.g.chdrawframe(Drawframemainmenu); } } } #endif //******************************************************************* // Stop the program. // void shutdown() { #if ED_DIRECTX_9 //***************************************************** // In windows, posting a WM_CLOSE to the main window is sufficient // PostMessage(Globals.g.gethwnd(), WM_CLOSE, 0, 0); #else //**************************************************************** // exit(0) seems to be an approved way to get out of a GLUT program // exit(0); #endif //*************************************************************** } // The code for the various framedrawing functions follows. Note that the // menu functions are a mess. They were written as a way of getting // familiar with what we need for the menus. Now that what is needed is // clear, they could be rewritten much more cleanly. But they work, and // there are higher priority rewrites to be done.... // // You will note that each Drawframe function has a local variable, // context &c = Globals. This is because originally the context was // passed as a parameter (still what should be done), but there wasn't // a way to pass a parameter to the frame drawing function in GLUT, so // the context was hardcoded. But maybe someday I'll figure out a clever // way to write the frame drawing logic for a generic context, and this // way it'll be quick to change it back without having to replace // "Globals." everywhere. Hmmm, I have this policy that if you have to // overexplain something in the comments, then you should just change it // instead. I guess the benefit of being the one to set the policies is // that you can break them when you feel like it... // // All the frame drawing functions end with c.g.nextframe(), since some // platforms (e.g. GLUT) keep the frame loop going by having each frame // start the next frame. // // First comes the main (pause) menu, reached by pressing Esc on the // driving screen. All the menu drawing functions track the navigation // keys (Up, Down, Enter, Escape and sometimes left and right) with a // *keyup flag (e.g. esckeyup). If the flag is true and the function // determines the key is now pressed down, then the key hit is processed. // But if the flag is false, then the key was already pressed down and // so we've already processed it. These flags are reset to true whenever // we detect that the key has been released. // // Also, in general the menu screens do not redraw the screen unless there // is a need to, e.g. if something visible changes. // void Drawframemainmenu() { static bool esckeyup = false, // maybe we got here with Esc key upkeyup = true, downkeyup = true, enterkeyup = false, // maybe we got here with Enter key redraw = true; // do we need to redraw the screen? static char *choices[4] = { "Resume driving", "Exit EDrive", "Controller selection", "Display selection" }; static int choice = 0; // index of currently highlighted choice bool exitmenu = false; context &c = Globals; if (c.ok) { // only do anything if the context is ready to work // see if we need to redraw the screen regardless of whether // this function decides we need to redraw or not. if (c.redrawanyway) { c.redrawanyway = false; redraw = true; } // check keyboard unsigned int keys = c.kb.read(); // bail out on Esc key if (EDKEY_ESC & keys) { if (esckeyup) { esckeyup = false; exitmenu = true; } } else { esckeyup = true; // Process the Enter key, which "selects" the current choice if (EDKEY_ENTER & keys) { if (enterkeyup) { enterkeyup = false; switch (choice) { case 0: // return to driving exitmenu = true; break; case 1: // stop the program shutdown(); break; case 2: // go to controls menu c.g.chdrawframe(Drawframecontrolmenu); break; case 3: // go to display menu c.g.chdrawframe(Drawframedisplaymenu); break; } } } else enterkeyup = true; // process navigation keys if (EDKEY_DOWN & keys) { if(downkeyup && choice < 3) { choice++; redraw = true; downkeyup = false; } } else downkeyup = true; if (EDKEY_UP & keys) { if (upkeyup && choice > 0) { choice--; redraw = true; upkeyup = false; } } else upkeyup = true; } if (exitmenu) { // Return to driving. Remember to turn sounds back on. // c.g.chdrawframe(Drawframedrive); c.thecar.saywhat(); c.resetclock = true; // so that "menu time" is ignored } else if (redraw) { // Need to redraw the menu screen // c.g.startframe(mkcolor(.3, .3, .6)); c.ground.draw(c.g); c.g.drawtext(34, 15, "EDrive Main Menu", mkcolor(1, 1, .8)); c.g.drawtext(15, 85, "(Use Up/Down, then Enter, to select menu items)", mkcolor(.7, .7, .7)); for (int i = 0; i < 4; i++) c.g.drawtext(35, 35 + 10*i, choices[i], i == choice ? mkcolor(1, 1, 1) : mkcolor(0.5, 0.5, 0.5)); c.g.endframe(); redraw = false; } // if we are leaving this menu, get ready for next time we come back // if (!c.g.isdrawframe(Drawframemainmenu)) { choice = 0; esckeyup = false; // we could come back via the Esc key enterkeyup = false; // we could come back via the Enter key redraw = true; } } c.g.nextframe(); } // The display selection menu. Note that in the DirectX9 version we can // choose different display devices and different full screen resolutions, // but with the GLUT version we just use the X server, and simply have // a choice of full screen (at the X server resolution) or not. // void Drawframedisplaymenu() { static bool esckeyup = false, // maybe we got here with Esc key upkeyup = true, downkeyup = true, leftkeyup = true, rightkeyup = true, enterkeyup = false, // maybe we got here with Enter key redraw = true, // do we need to redraw the screen? firsttime = true; // some setup required the first time static const char *choices[3] = { "Back to main menu", "Run in a window", "" /* ignored in GLUT version */ }; static int choice = 0, // index of currently highlighted choice idev = -1, // index of display device (-1 means "window") imode = 0; // index of video mode (ignored in GLUT version) bool exitmenu = false; context &c = Globals; if (c.ok) { // only do anything if the context is ready to work // first time setup aligns current choice with actual settings // if (firsttime) { firsttime = false; idev = c.dispdev; imode = c.dispmode; #if ED_DIRECTX_9_GRAPHICS //************************************ if (idev != -1) { choices[1] = c.glist.displayname(idev); c.glist.select(idev); choices[2] = c.glist.modedesc(imode); } #else //******************************************************** if (idev == 0) choices[1] = "Run full screen"; #endif //******************************************************* } // see if we need to redraw the screen regardless of whether // this function decides we need to redraw or not. if (c.redrawanyway) { c.redrawanyway = false; redraw = true; } // check keyboard unsigned int keys = c.kb.read(); // bail out on Esc key if (EDKEY_ESC & keys) { if (esckeyup) { esckeyup = false; exitmenu = true; } } else { esckeyup = true; // process the Enter key, which may change video modes if (EDKEY_ENTER & keys) { if (enterkeyup) { enterkeyup = false; // choice 0 returns to main menu if (choice == 0) exitmenu = true; // otherwise, see if we need to change video modes #if ED_DIRECTX_9_GRAPHICS //**************************** // In DirectX 9, we change video modes if we are // on the resolution choice (2), or if we are on // the display device choice (1) switching back to // windowed mode. We'll need to get the new video // mode details from the graphicslist object. // else if (choice == 2 || (choice == 1 && idev == -1)) { if (imode != c.dispmode || idev != c.dispdev) { int mode = 0, wid = 640, hi = 480, hz = 0; D3DFORMAT fmt = (D3DFORMAT)0; if (idev == -1 || c.glist.getdetails(imode, mode, wid, hi, fmt, hz)) { // We are ready to change modes, so get // rid of all graphics data that may // have been downloaded to the video // card, change modes, then reload the // graphics and save new mode in startup // configuration file. // c.ok = false; // context is temporarily bad c.thecar.destroygraphics(); c.ground.destroy(); c.g.change(idev, wid, hi, fmt, hz); c.thecar.loadgraphics(c.g, "wheel.bmp"); c.ground.create(c.g, "track.txt"); c.dispdev = idev; c.dispmode = imode; c.saveconfig("edrive.cfg"); c.ok = true; // context is ok again choice = 0; // jump to "Back to main menu" redraw = true; } } } #else //************************************************ // In GLUT we only switch between windowed mode // and fullscreen mode. Note that the keyboard is // tied to the current active window, which changes // when we switch. // else if (choice == 1 && idev != c.dispdev) { // We are ready to change modes, so get // rid of all graphics data that may // have been downloaded to the video // card, unhook the keyboard, change modes, // then reload the graphics, hook up the // keyboard and save new mode in startup // configuration file. // c.ok = false; // context is temporarily bad c.kb.destroy(); c.thecar.destroygraphics(); c.ground.destroy(); if (idev == -1) c.g.gowindow(); else c.g.gofullscreen(); c.thecar.loadgraphics(c.g, "wheel.bmp"); c.ground.create(c.g, "track.txt"); c.kb.create(); // recalculate the car's position, so that // the current view behind the menu is where // it was (the view matrix is part of the // OpenGL context that just changed). c.thecar.move(0, 0, 0, 0, c.ground, c.g); c.dispdev = idev; c.saveconfig("edrive.cfg"); c.ok = true; // context is OK again choice = 0; // jump to "Back to main menu" redraw = true; } #endif //*********************************************** } } else enterkeyup = true; // process menu navigation keys if (EDKEY_DOWN & keys) { #if ED_DIRECTX_9_GRAPHICS //******************************** // In DirectX 9, only allow going to choice 2 if a // resolution is displayed there. // if(downkeyup && (choice < 1 || (choice == 1 && idev > -1 && c.glist.modecount() > 0))) { #else //**************************************************** // In GLUT, never let them go to choice 2 // if (downkeyup && choice < 1) { #endif //*************************************************** choice++; downkeyup = false; redraw = true; } } else downkeyup = true; if (EDKEY_UP & keys) { if (upkeyup && choice > 0) { choice--; upkeyup = false; redraw = true; } } else upkeyup = true; // process choice subselection keys if (EDKEY_LEFT & keys) { if (leftkeyup) #if ED_DIRECTX_9_GRAPHICS //**************************** // In DirectX 9, device and resolution can change // if (choice == 1 && idev > -1) { // switching devices. Note that resolution // also changes. idev--; if (idev == -1) { choices[1] = "Run in a window"; choices[2] = ""; redraw = true; } else { c.glist.select(idev); choices[1] = c.glist.displayname(idev); imode = 0; // go to first available res choices[2] = c.glist.modedesc(imode); redraw = true; } leftkeyup = false; } else if (choice == 2 && imode > 0) { // switching resolution imode--; choices[2] = c.glist.modedesc(imode); leftkeyup = false; redraw = true; } #else //************************************************ // In GLUT, only device changes if (choice == 1 && idev > -1) { idev--; if (idev == -1) choices[1] = "Run in a window"; leftkeyup = false; redraw = true; } #endif //*********************************************** } else leftkeyup = true; if (EDKEY_RIGHT & keys) { if (rightkeyup) { #if ED_DIRECTX_9_GRAPHICS //**************************** // In DirectX 9, device and resolution can change // if (choice == 1 && idev < c.glist.displaycount() - 1) { // switching devives. Note that resolution // also changes. idev++; choices[1] = c.glist.displayname(idev); c.glist.select(idev); imode = 0; choices[2] = c.glist.modedesc(imode); rightkeyup = false; redraw = true; } else if (rightkeyup && choice == 2 && imode < c.glist.modecount() - 1) { // switching resolution imode++; choices[2] = c.glist.modedesc(imode); rightkeyup = false; redraw = true; } #else //************************************************ // In GLUT, only device changes if (choice == 1 && idev < 0) { idev++; choices[1] = "Run full screen"; rightkeyup = false; redraw = true; } #endif //*********************************************** } } else rightkeyup = true; } if (exitmenu) { // Return to main menu c.g.chdrawframe(Drawframemainmenu); } else if (redraw) { // Need to redraw the menu screen // c.g.startframe(mkcolor(.3, .3, .6)); c.ground.draw(c.g); c.g.drawtext(28, 15, "EDrive Display Selection Menu", mkcolor(1, 1, .8)); #if ED_DIRECTX_9_GRAPHICS //************************************ if (choice == 1) if (idev == -1) c.g.drawtext(11, 75, "Use Left/Right to pick display device, Enter to " "activate", mkcolor(1, .8, .8)); else c.g.drawtext(25, 75, "Use Left/Right to pick display device", mkcolor(1, .8, .8)); else if (choice == 2) c.g.drawtext(14, 75, "Use Left/Right to pick resolution, Enter to " "activate", mkcolor(1, .8, .8)); #else //******************************************************** if (choice == 1) c.g.drawtext(11, 75, "Use Left/Right to pick display device, Enter to " "activate", mkcolor(1, .8, .8)); #endif //******************************************************* c.g.drawtext(15, 85, "(Use Up/Down, then Enter, to select menu items)", mkcolor(.6, .6, .6)); #if ED_DIRECTX_9_GRAPHICS //************************************ for (int i = 0; i < (idev == -1 ? 2 : 3) ; i++) #else //******************************************************** for (int i = 0; i < 2 ; i++) #endif //******************************************************* c.g.drawtext(30, 35 + 12*i, choices[i], i == choice ? mkcolor(1, 1, 1) : mkcolor(0.5, 0.5, 0.5)); c.g.endframe(); redraw = false; } // if we are leaving this menu, get ready for next time we come back // if (!c.g.isdrawframe(Drawframedisplaymenu)) { choice = 0; esckeyup = false; enterkeyup = false; redraw = true; } } c.g.nextframe(); } // The control selection menu. Note that the "configure" option allows the // user to select the control axes for steering, accelerating and braking by // using them, but it doesn't actually calibrate those axes. // // Each time the user comes to this menu, the list of joysticks is refreshed, // so that the user can plug in a joystick after the game has been started, // yet still be able to see it. // // There currently is no choice for the keys used to drive. // void Drawframecontrolmenu() { static bool esckeyup = false, // maybe we got here with Esc key upkeyup = true, downkeyup = true, leftkeyup = true, rightkeyup = true, enterkeyup = false, // maybe we got here with Enter key refreshlist = true, // should joystick list be refreshed? redraw = true, // do we need to redraw the screen? firsttime = true; // some setup required the first time static const char *choices[3] = { "Back to main menu", "Keyboard", "Configure" }, *configmsg[7] = { "First move the wheel/joystick around", "Now centre it and press Enter", "Now steer to the right", "Now centre it and press Enter", "Now accelerate", "Now centre it and press Enter", "Now brake" }; static int choice = 0, // index of currently highlighted choice idev = -1, // index of input device (-1 means keyboard) lastidev = -1, // index of last activated input device configstate = 0, // state of configuration process (0: not!) stax, acax, brax; // selected control axes bool exitmenu = false; context &c = Globals; if (c.ok) { // only do anything if the context is ready to work // first time setup aligns current choice with actual setting if (firsttime) { firsttime = false; #if ED_DIRECTX_9_INPUT //*************************************** c.joylist.refresh(c.g.gethinstance()); #else //******************************************************** c.joylist.refresh(); #endif //******************************************************* if (!c.joyid.empty()) { idev = -1; int i = 0; while (idev == -1 && i < c.joylist.count()) if (c.joyid == c.joylist.getid(i)) idev = lastidev = i; else i++; if (idev == -1) c.joyid = joystickid(); else choices[1] = c.joylist.getdesc(i); } } // see if we need to redraw the screen regardless of whether // this function decides we need to redraw or not. if (c.redrawanyway) { c.redrawanyway = false; redraw = true; } // get a fresh list of available joysticks if required // if (refreshlist) { refreshlist = false; #if ED_DIRECTX_9_INPUT //*************************************** c.joylist.refresh(c.g.gethinstance()); #else //******************************************************** c.joylist.refresh(); #endif //******************************************************* redraw = true; } // check keyboard unsigned int keys = c.kb.read(); // what we do depends on what stage of the joystick configuration // process (if any) we are in right now // switch (configstate) { case 0: // not configuring right now // bail out on Esc key if (EDKEY_ESC & keys) { if (esckeyup) { esckeyup = false; exitmenu = true; } } else { esckeyup = true; // process the Enter key, which may start configuration if (EDKEY_ENTER & keys) { if (enterkeyup) { enterkeyup = false; // choice 0 returns to main menu if (choice == 0) exitmenu = true; else if (choice == 2) { // start configuration. Note that we // may need to create or change the joystick // if (idev != lastidev) { c.joy.destroy(); if (idev >= 0) #if ED_DIRECTX_9_INPUT //*************** c.joy.create(c.joylist.getid(idev), c.g.gethwnd(), c.g.gethinstance()); #else //******************************** c.joy.create(c.joylist.getid(idev)); #endif //******************************* lastidev = idev; } configstate = 1; c.joy.pickaxis(true); // prime pickaxis() stax = acax = brax = 0; } redraw = true; } } else enterkeyup = true; // process menu navigation keys if (EDKEY_DOWN & keys) { if (downkeyup && (choice<1 || (choice==1 && idev>-1))) { downkeyup = false; choice++; redraw = true; } } else downkeyup = true; if (EDKEY_UP & keys) { if (upkeyup && choice > 0) { upkeyup = false; choice--; redraw = true; } } else upkeyup = true; // process choice subselection keys if (EDKEY_LEFT & keys) { if (leftkeyup && choice == 1 && idev > -1) { leftkeyup = false; idev--; choices[1] = (idev == -1 ? "Keyboard" : c.joylist.getdesc(idev)); redraw = true; } } else leftkeyup = true; if (EDKEY_RIGHT & keys) { if (rightkeyup && choice==1 && idev= 0) { c.joyid = c.joylist.getid(idev); #if ED_DIRECTX_9_INPUT //******************************* c.joy.create(c.joyid, c.g.gethwnd(), c.g.gethinstance()); #else //************************************************ c.joy.create(c.joyid); #endif //*********************************************** } else c.joyid = joystickid(); c.joyaxis[0] = c.joyaxis[1] = c.joyaxis[2] = 0; // default axes c.saveconfig("edrive.cfg"); lastidev = idev; } // return to main menu c.g.chdrawframe(Drawframemainmenu); } else if (redraw) { c.g.startframe(mkcolor(.3, .3, .6)); c.ground.draw(c.g); c.g.drawtext(26, 15, "EDrive Controller Selection Menu", mkcolor(1, 1, .8)); if (configstate != 0) c.g.drawtext(25, 75, configmsg[configstate - 1], mkcolor(1, .8, .8)); else if (choice == 1) c.g.drawtext(25, 75, "Use Left/Right to change input device", mkcolor(1, .8, .8)); else if (choice == 2) c.g.drawtext(25, 75, "Press Enter to begin configuration", mkcolor(1, .8, .8)); c.g.drawtext(15, 85, "(Use Up/Down, then Enter, to select menu items)", mkcolor(.6, .6, .6)); for (int i = 0; i < (idev == -1 ? 2 : 3) ; i++) c.g.drawtext(30, 35 + 12*i, choices[i], i == choice ? mkcolor(1, 1, 1) : mkcolor(0.5, 0.5, 0.5)); c.g.endframe(); redraw = false; } // if we are leaving this menu, get ready for next time we come back // if (!c.g.isdrawframe(Drawframecontrolmenu)) { refreshlist = true; choice = 0; esckeyup = false; enterkeyup = false; redraw = true; } } c.g.nextframe(); } // Finally the main frame drawing function, which moves the car accordingly // void Drawframedrive(void) { static unsigned int last = 0, // last time we moved fpscnt = 0, // frame counter for FPS calcuation fpslast = 0, // last time FPS was calculated fps = 0; // frames per second static char fpsstr[200] = ""; // string with FPS, etc. to be displayed static bool camkeyup = true, // is the camera selection key released? esckeyup = true; // is the Esc key released? static float accel = 0, // amount to accelerate (static so that....) brake = 0, // amount to brake (...keyboard use...) turn = 0; // amount to turn (...can be damped. ) context &c=Globals; unsigned int now = getms(), // the time right now delta; // change in time since last move if (c.ok) { // reset the timing clock if necessary (e.g. the first time // and on return from a menu) // if (c.resetclock) { last = fpslast = now; fpscnt = 0; c.resetclock = false; } // check keyboard unsigned int keys = c.kb.read(); // go to the main (pause) menu if Esc is pressed. Note that // Esc may momentarily still be down if returning from a menu if ((EDKEY_ESC & keys) && esckeyup) { esckeyup = false; // Esc may get us back from menu c.thecar.hush(); c.g.chdrawframe(Drawframemainmenu); } else if (c.ok) { // Esc may be momentarily still down if returning from menu, // but we still want to draw frames. if (!(EDKEY_ESC & keys)) esckeyup = true; // Note that since delta, now and last are unsigned, if // last > now, time has wrapped around the largest unsigned // and now-last will still be correctly calculated. // // Ideally, we should probably check that delta >= 5 (which // limits frames per second to 200) for more accurate movement // calculations (less rounding error). However, it is fun to // see how many FPS we can really get by checking delta >= 0. // Just remember to change it back to 5 before releasing a // production version. // if ((delta = now - last) >= 5 /* s.b. 5 */) { // change the camera if user presses C if (keys & EDKEY_C) { if (camkeyup) { c.thecar.camera(-1); c.thecar.move(0, 0, 0, 0, c.ground, c.g); camkeyup = false; } } else camkeyup = true; // Check joystick. Note that joy will set str, acl or // brk to 20000 if a particular axis is non-functional. // otherwise, we should get something between -100 and 100 // for str, and between 0 and 200 for acl and brk. // int str, acl, brk; c.joy.read(str, acl, brk); // calculate "turn", which is -1 for full left, 1 for // full right. If using keys, we want to damp the turning, // and provide "return to centre" when no steering key is // pressed. // if (str != 20000) turn = str/100.0; else { if ((keys & EDKEY_LEFT) && !(keys & EDKEY_RIGHT)) { if (turn > -1) { turn -= (delta * 0.0015); if (turn < -1) turn = -1; } } else if ((keys & EDKEY_RIGHT) && !(keys & EDKEY_LEFT)) { if (turn < 1) { turn += (delta * 0.0015); if (turn > 1) turn = 1; } } else if (turn > 0) { turn -= (delta * 0.002); if (turn < 0) turn = 0; } else if (turn < 0) { turn += (delta * 0.002); if (turn > 0) turn = 0; } } // calculate accel, which is 0 for no throttle, and 1 // for full throttle. If using keys, damp it. // if (acl != 20000) accel = acl/200.0; else { if (keys & EDKEY_A) { if (accel < 1) { accel += (delta * 0.002); if (accel > 1) accel = 1; } } else if (accel > 0) { accel -= (delta * 0.002); if (accel < 0) accel = 0; } } // calculate brake, which is 0 for no brake, and 1 // for full brake. If using keys, damp it. // if (brk != 20000) brake = brk/200.0; else { if (keys & EDKEY_Z) { if (brake < 1) { brake += (delta * 0.002); if (brake > 1) brake = 1; } } else if (brake > 0) { brake -= (delta * 0.002); if (brake < 0) brake = 0; } } // now move the car based on control input // c.thecar.move(delta, accel, brake, turn, c.ground, c.g); // every second or so, recalculate frames per second // if (now - fpslast > 1000) { double fpstime = (now - fpslast)/1000.0; fps = int(0.5 + fpscnt/fpstime); fpscnt = 0; fpslast = now; } else fpscnt++; // Useful information to display sprintf(fpsstr, "%4dfps %s", fps, c.thecar.info()); // Now draw the frame // c.g.startframe(mkcolor(.4, .4, 1)); c.thecar.draw(c.g); c.ground.draw(c.g); c.g.drawtext(1, 1, fpsstr, mkcolor(1, 1, 1)); c.g.endframe(); // reset timer data for next frame last = now; } } } c.g.nextframe(); }