/** game: this file implements the backend of a complete, actually playable game, and is meant to be included and used by specific frontends (which will handle each platform's hardware details and I/O). TODO: more documentation UNITS: There are various kinds of units used to ensure independence of the game modules. Here is a summary: - LCR_GameUnit: data type, abstract unit of the game (racing module). One map square is LCR_GAME_UNITs long, a full angle is also LCR_GAME_UNITs. - LCR_GAME_UNIT: Size of one game square and full angle in LCR_GameUnits. Square height is only half of this. - S3L_Unit: data type, small3dlib's unit. May change with renderer change. - S3L_F: small3dlib's value representing 1.0. - LCR_RENDERER_UNIT: for the renderer one map square is this many S3L_Units. - TPE_Unit: tinyphysicsengine's unit. May change with phys. engine change. - TPE_F: tinyphysicsengine's value representing value 1.0. - LCR_PHYSICS_UNIT: for the phys. eng. one map square is this many TPE_Units. COORDINATE SYSTEM AND ROTATIONS: The game itself (racing module) is independent of rendering and physics libraries, but out of convenient adopts their coordinate system (X right, Y up, Z forward) and rotations (Euler angles, by Z, then by X, then Y). RESOURCE FILE: The game uses so called resource file to store various resources, mainly maps and replays. There is considered to be one abstract global file which is just a long text string. Internally the global file is composed of a hardcoded internal resource file string (stored in assets) with very basic maps, and an optional user file (appended to the internal file) that the frontend may provide, allowing adding more resources without recompiling the game. The user file may be disabled on platforms that e.g. don't have file systems, but the internal file will be always present. The format of the resource file is following: it consists of resource strings separated by '#' character (resource strings cannot contain this character). Each resource string starts with a magic number: a single character identifying the type of the resource (map, replay, ...), then the name of the resource follows, then character ';' and then the resource string itself (up until next '#' or end of file). The format of the string depends on the type of resource, i.e. the format of map string has a special format (described in the map module) etc. */ #ifndef _LCR_GAME_H #define _LCR_GAME_H #define LCR_KEY_UP 0x00 #define LCR_KEY_RIGHT 0x01 #define LCR_KEY_DOWN 0x02 #define LCR_KEY_LEFT 0x03 #define LCR_KEY_A 0x04 ///< confirm, restart race #define LCR_KEY_B 0x05 ///< cancel, open menu #define LCR_KEYS_TOTAL 6 /* FOR FRONTENDS: - Implement the below described functions according to their description. - Implement the main program and game loop. - Call the below described functions as described. - If you want to support music, make your frontend play music from the "music" file in assets. It is in raw format, storing 8bit unsigned samples at 8000 Hz. Use the LCR_gameGetMusicVolume to check what the music volume is. If you don't support music, set LCR_SETTING_MUSIC to 0 in your frontend code so that the game knows music is disabled. */ /** Implement this in your frontend. Returns 1 if given key is pressed or 0 otherwise. */ uint8_t LCR_keyPressed(uint8_t key); /** Implement this in your frontend. This function pauses program execution for given amount of milliseconds and relieves the CPU usage. On platforms that don't support this the function may simply do nothing. */ void LCR_sleep(uint16_t timeMs); /** Implement this in your frontend. This function draws a pixel of given color to the screen back buffer (i.e. NOT directly to screen, back buffer shall only be copied to front buffer once the LCR_gameStep function finishes all rendering). This function should NOT check for out-of-screen coordinates, this is handled by the game internals and out-of-screen pixels will never be drawn. The color value depends on game settings but is normally an RGB565 value. */ void LCR_drawPixel(unsigned long index, uint16_t color); /** Implement this in your frontend. This function will be called to log what the program is doing. If you want to ignore logging, simply make the function do nothing. */ void LCR_log(const char *str); /** Implement this in your frontend. This function serves for loading optional resource file that allows to add more maps, replays etc. If your frontend won't support this, just make the function return 0. Otherwise it must return characters from the resource file one by one; after reaching the end of file 0 must be returned and the reading position will be reset to start again. */ char LCR_getNextResourceFileChar(void); /** Implement this in your frontend. This serves to store data in the optional resource file, e.g. replays. If your frontend doesn't support this (e.g. because the file is read only), the function may ignore the append, but if the file is otherwise supported, a rewind of the read position must still be done. If appending is supported, the function must append the provided string to the resource file AND then reset the resource file reading position back to the start. */ void LCR_appendResourceStr(const char *str); /** Call this function in your frontend at the start of the program. */ void LCR_gameInit(void); /** Call this function in your frontend right before program end. */ void LCR_gameEnd(void); /** Call this function in your frontend repeatedly inside the main loop, pass the current time as the number of milliseconds since program start. This function will perform the game step AND other things such as checking the input states, rendering or sleeping (all using the above functions you should implement). Returns 0 if program should end, otherwise 1. */ uint8_t LCR_gameStep(uint32_t timeMs); /** Gets the current music volume; */ uint8_t LCR_gameGetMusicVolume(void); /** Gets next audio sample (unsigned 8bit samples, 8 KHz). */ uint8_t LCR_gameGetNextAudioSample(void); //------------------------------------------------------------------------------ #define LCR_LOG0(s) ; #define LCR_LOG1(s) ; #define LCR_LOG2(s) ; #if LCR_SETTING_LOG_LEVEL > 0 #undef LCR_LOG0 #define LCR_LOG0(s) LCR_log(s); #if LCR_SETTING_LOG_LEVEL > 1 #undef LCR_LOG1 #define LCR_LOG1(s) LCR_log(s); #if LCR_SETTING_LOG_LEVEL > 2 #undef LCR_LOG2 #define LCR_LOG2(s) LCR_log(s); #endif #endif #endif #define LCR_CONTROL_MODE_FREECAM 0x00 #define LCR_CONTROL_MODE_DRIVE 0x01 #define LCR_GAME_STATE_MENU 0x00 #define LCR_GAME_STATE_RUN_STARTING 0x01 #define LCR_GAME_STATE_RUN 0x02 #define LCR_GAME_STATE_RUN_FINISHED 0x03 // TODO: move to consts? #define LCR_MENU_MAX_ITEMS 9 #define LCR_RESOURCE_ITEM_CHUNK (LCR_MENU_MAX_ITEMS - 1) #define LCR_MENU_TABS 4 #define LCR_MENU_STRING_SIZE 16 struct { uint8_t state; uint32_t stateStartTime; uint32_t time; uint32_t frame; uint32_t nextRenderFrameTime; uint32_t nextRacingTickTime; uint8_t controlMode; uint8_t debugDraw; uint8_t musicVolume; uint8_t keyStates[LCR_KEYS_TOTAL]; /**< Assures unchanging key states during a single frame, hold number of frames for which the key has been continuously held. */ uint8_t menuSelectedTab; uint8_t menuSelectedItem; struct { int state; ///< -1 if reading external res. f., else pos. unsigned char loadedItemCount; char loadedItemNames[LCR_RESOURCE_ITEM_CHUNK * LCR_MENU_STRING_SIZE]; unsigned int loadedItemsIndices[LCR_RESOURCE_ITEM_CHUNK]; // absolute unsigned int itemsTotal; // total of items of this type } resourceFile; } LCR_game; uint8_t LCR_gameGetMusicVolume(void) { #if LCR_SETTING_MUSIC return LCR_game.musicVolume; #else return 0; #endif } void LCR_drawPixelXYUnsafe(unsigned int x, unsigned int y, uint16_t color); /** Internal pixel drawing function that checks for out-of-screen coordinates. Use this if the pixel can potentially lie of screen (however if you know it won't, use the normal unsafe function in sake of performance). */ static inline void LCR_drawPixelXYSafe(unsigned int x, unsigned int y, uint_fast16_t color); #include "racing.h" #include "assets.h" #include "renderer.h" #include "audio.h" void LCR_drawPixelXYUnsafe(unsigned int x, unsigned int y, uint16_t color) { #if LCR_SETTING_RESOLUTION_SUBDIVIDE == 1 LCR_drawPixel(y * LCR_SETTING_RESOLUTION_X + x,color); #else // TODO #endif } static inline void LCR_drawPixelXYSafe(unsigned int x, unsigned int y, uint_fast16_t color) { if (x < LCR_EFFECTIVE_RESOLUTION_X && y < LCR_EFFECTIVE_RESOLUTION_Y) LCR_drawPixelXYUnsafe(x,y,color); } void LCR_gameSetState(uint8_t state) { LCR_LOG1("changing state"); LCR_game.state = state; LCR_game.stateStartTime = LCR_game.time; } LCR_GameUnit LCR_carSpeedKMH(void) { return // we use 28/8 as an approximation of 3.6 to convers MPS to KMH (28 * LCR_SETTING_METERS_PER_BLOCK * LCR_racingGetCarSpeedUnsigned() * LCR_RACING_FPS) / (8 * LCR_GAME_UNIT); } void LCR_gameResetRun(void) { LCR_GameUnit carTransform[6]; LCR_LOG0("resetting run"); LCR_mapReset(); LCR_racingRestart(); LCR_rendererUnmarkCPs(); LCR_racingGetCarTransform(carTransform,carTransform + 3,0); LCR_rendererSetCarTransform(carTransform,carTransform + 3); LCR_rendererCameraReset(); LCR_gameSetState(LCR_GAME_STATE_RUN_STARTING); } void LCR_gameRewindResourceFile(void) { LCR_appendResourceStr(""); LCR_game.resourceFile.state = 0; } /** Reads the next resource file character while merging the internal resource file with the optional user file. First the internal file will be read, immediately followed by the user file, then zero char will return and reading will start over. */ char LCR_gameGetNextResourceFileChar(void) { #if LCR_SETTING_ENABLE_RESOURCE_FILE char c; if (LCR_game.resourceFile.state < 0) // external file? { c = LCR_getNextResourceFileChar(); if (c == 0) LCR_game.resourceFile.state = 0; // move to internal file next } else // internal file { c = LCR_internalResourceFile[LCR_game.resourceFile.state]; LCR_game.resourceFile.state++; if (c == 0) { c = LCR_getNextResourceFileChar(); LCR_game.resourceFile.state = c ? -1 : 0; // trust this } } return c; #else if (LCR_internalResourceFile[LCR_game.resourceFile.state] == 0) { LCR_game.resourceFile.state = 0; return 0; } return LCR_internalResourceFile[LCR_game.resourceFile.state++]; #endif } /** Similar to LCR_gameGetNextResourceFileChar, but returns 0 instead of the resource string separator character. This function is means to be used by functions that load something from a string while expecting a zero terminated string. */ char LCR_gameGetNextResourceStrChar(void) { char c = LCR_gameGetNextResourceFileChar(); return c != LCR_RESOURCE_FILE_SEPARATOR ? c : 0; } /** Seeks to the Nth resource string in the global resource file, after its name, so that the pure resource string will now be available for reading. */ void LCR_seekResourceByIndex(unsigned int index) { char c; LCR_LOG0("seeking resource string"); LCR_gameRewindResourceFile(); while (index) { do c = LCR_gameGetNextResourceFileChar(); while (c != LCR_RESOURCE_FILE_SEPARATOR && c != 0); index--; } do // skip magic number and name c = LCR_gameGetNextResourceFileChar(); while (c != LCR_RESOURCE_FILE_SEPARATOR2 && c != LCR_RESOURCE_FILE_SEPARATOR && c != 0); } void LCR_gameStartRun(void) { LCR_seekResourceByIndex(0); // TODO LCR_mapLoadFromStr(LCR_gameGetNextResourceStrChar); LCR_rendererLoadMap(); LCR_gameResetRun(); } void LCR_gameInit(void) { LCR_LOG0("initializing"); for (int i = 0; i < LCR_KEYS_TOTAL; ++i) LCR_game.keyStates[i] = 0; LCR_rendererInit(); LCR_racingInit(); LCR_audioInit(); LCR_game.resourceFile.state = 0; LCR_game.resourceFile.loadedItemCount = 0; LCR_game.resourceFile.itemsTotal = 0; for (int i = 0; i < LCR_RESOURCE_ITEM_CHUNK; ++i) { for (int j = 0; j < LCR_MENU_STRING_SIZE; ++j) LCR_game.resourceFile.loadedItemNames[i * LCR_MENU_STRING_SIZE + j] = 0; LCR_game.resourceFile.loadedItemsIndices[i] = 0; } LCR_game.menuSelectedTab = 0; LCR_game.menuSelectedItem = 0; LCR_game.frame = 0; LCR_game.musicVolume = 255; LCR_game.nextRenderFrameTime = 0; LCR_game.nextRacingTickTime = 0; LCR_game.controlMode = LCR_CONTROL_MODE_DRIVE; LCR_gameStartRun(); } /** Loads up to LCR_RESOURCE_ITEM_CHUNK items of given type (0 = map, 1 = replay) into RAM, starting at given index (among items of the same type). */ void LCR_gameLoadResourceFileChunk(uint8_t type, unsigned int startIndex) { // TODO } void LCR_gameEnd(void) { LCR_LOG0("ending"); } void LCR_gameDraw3DView(void) { LCR_GameUnit carTransform[6]; LCR_GameUnit physicsInterpolationParam = LCR_GAME_UNIT - ((LCR_game.nextRacingTickTime - LCR_game.time) * LCR_GAME_UNIT) / LCR_RACING_TICK_MS; LCR_racingGetCarTransform(carTransform,carTransform + 3, physicsInterpolationParam); LCR_rendererSetCarTransform(carTransform,carTransform + 3); if (LCR_game.controlMode != LCR_CONTROL_MODE_FREECAM) LCR_rendererCameraFollow(); #if LCR_ANIMATE_CAR LCR_rendererSetWheelState(LCR_racingGetWheelRotation(), LCR_racingGetWheelSteer() * 2); #endif LCR_rendererDraw(); #if LCR_SETTING_DEBUG_PHYSICS_DRAW LCR_GameUnit camTr[7]; LCR_rendererGetCameraTransform(camTr,camTr + 3,camTr + 6); LCR_physicsDebugDraw(camTr,camTr + 3,camTr[6]); #endif // GUI/HUD: char str[6]; switch (LCR_game.state) { case LCR_GAME_STATE_RUN_STARTING: str[0] = '0' + LCR_SETTING_COUNTDOWN_SECONDS - (LCR_game.time - LCR_game.stateStartTime) / 1000; str[1] = 0; LCR_rendererDrawText(str, (LCR_EFFECTIVE_RESOLUTION_X - LCR_rendererComputeTextWidth(str,8)) / 2, LCR_EFFECTIVE_RESOLUTION_Y / 2,0x0707,8); break; default: { int val = LCR_carSpeedKMH(); if (val < 5) // don't show tiny oscillations when still val = 0; str[0] = val >= 100 ? '0' + (val / 100) % 10 : ' '; str[1] = val >= 10 ? '0' + (val / 10) % 10 : ' '; str[2] = '0' + val % 10; str[3] = 0; LCR_rendererDrawText(str, LCR_EFFECTIVE_RESOLUTION_X - LCR_rendererComputeTextWidth(str,2) - 20, LCR_EFFECTIVE_RESOLUTION_Y - LCR_rendererComputeTextHeight(2) - 20,0,2); val = LCR_racingGetRunTimeMS() / 1000; // seconds str[3] = '0' + (val % 60) / 10; str[4] = '0' + val % 10; val = (val / 60) % 100; // minutes str[0] = '0' + val / 10; str[1] = '0' + val % 10; str[2] = '\''; str[5] = 0; LCR_rendererDrawText(str,20,LCR_EFFECTIVE_RESOLUTION_Y - LCR_rendererComputeTextHeight(2) - 20,0,2); break; } } } uint8_t LCR_gameStep(uint32_t time) { uint32_t sleep = 0; int paused = 0; LCR_LOG2("game step start"); LCR_game.time = time; for (int i = 0; i < LCR_KEYS_TOTAL; ++i) LCR_game.keyStates[i] = LCR_keyPressed(i) ? (LCR_game.keyStates[i] < 255 ? LCR_game.keyStates[i] + 1 : 255) : 0; switch (LCR_game.state) { case LCR_GAME_STATE_MENU: paused = 1; if (LCR_game.keyStates[LCR_KEY_RIGHT] == 1) { LCR_LOG1("menu tab right"); LCR_game.menuSelectedTab = (LCR_game.menuSelectedTab + 1) % LCR_MENU_TABS; LCR_game.menuSelectedItem = 0; } else if (LCR_game.keyStates[LCR_KEY_LEFT] == 1) { LCR_LOG1("menu tab left"); LCR_game.menuSelectedTab = (LCR_game.menuSelectedTab + LCR_MENU_TABS - 1) % LCR_MENU_TABS; LCR_game.menuSelectedItem = 0; } else if (LCR_game.keyStates[LCR_KEY_UP] == 1) { LCR_LOG1("menu item up"); LCR_game.menuSelectedItem -= LCR_game.menuSelectedItem ? 1 : 0; } else if (LCR_game.keyStates[LCR_KEY_DOWN] == 1) { LCR_LOG1("menu item down"); LCR_game.menuSelectedItem += (LCR_game.menuSelectedItem < (LCR_game.menuSelectedTab ? LCR_game.resourceFile.loadedItemCount : 5) - 1) ? 1 : 0; } else if (LCR_game.keyStates[LCR_KEY_B] == 1) { LCR_LOG1("menu quit"); LCR_gameSetState(LCR_GAME_STATE_RUN); } break; case LCR_GAME_STATE_RUN_STARTING: paused = 1; if (LCR_game.time - LCR_game.stateStartTime >= 1000 * LCR_SETTING_COUNTDOWN_SECONDS) LCR_gameSetState(LCR_GAME_STATE_RUN); // fall through default: if (LCR_game.keyStates[LCR_KEY_B] == 1) LCR_gameSetState(LCR_GAME_STATE_MENU); else if (LCR_game.keyStates[LCR_KEY_A] == 1) LCR_gameResetRun(); break; } /* if (LCR_game.keyStates[LCR_KEY_B] == 1) LCR_game.controlMode = LCR_game.controlMode == LCR_CONTROL_MODE_FREECAM ? LCR_CONTROL_MODE_DRIVE : LCR_CONTROL_MODE_FREECAM; else if (LCR_game.keyStates[LCR_KEY_B] == 30) LCR_gameResetRun(); */ LCR_GameUnit offsets[5]; for (int i = 0; i < 5; ++i) offsets[i] = 0; /* if (LCR_game.controlMode == LCR_CONTROL_MODE_FREECAM) { if (LCR_game.keyStates[LCR_KEY_A]) { if (LCR_game.keyStates[LCR_KEY_UP]) offsets[4] = LCR_FREE_CAMERA_TURN_STEP; else if (LCR_game.keyStates[LCR_KEY_DOWN]) offsets[4] -= LCR_FREE_CAMERA_TURN_STEP; if (LCR_game.keyStates[LCR_KEY_RIGHT]) offsets[3] -= LCR_FREE_CAMERA_TURN_STEP; else if (LCR_game.keyStates[LCR_KEY_LEFT]) offsets[3] = LCR_FREE_CAMERA_TURN_STEP; } else { if (LCR_game.keyStates[LCR_KEY_UP]) offsets[0] = LCR_FREE_CAMERA_STEP; else if (LCR_game.keyStates[LCR_KEY_DOWN]) offsets[0] -= LCR_FREE_CAMERA_STEP; if (LCR_game.keyStates[LCR_KEY_RIGHT]) offsets[1] = LCR_FREE_CAMERA_STEP; else if (LCR_game.keyStates[LCR_KEY_LEFT]) offsets[1] -= LCR_FREE_CAMERA_STEP; } LCR_rendererMoveCamera(offsets,offsets + 3); } */ // handle simulation: while (time >= LCR_game.nextRacingTickTime) { LCR_LOG2("gonna step racing engine"); unsigned int input = 0; if (LCR_game.controlMode != LCR_CONTROL_MODE_FREECAM) input = (LCR_game.keyStates[LCR_KEY_UP] ? LCR_RACING_INPUT_FORW : 0) | (LCR_game.keyStates[LCR_KEY_RIGHT] ? LCR_RACING_INPUT_RIGHT : 0) | (LCR_game.keyStates[LCR_KEY_DOWN] ? LCR_RACING_INPUT_BACK : 0) | (LCR_game.keyStates[LCR_KEY_LEFT] ? LCR_RACING_INPUT_LEFT : 0); uint32_t events = paused ? 0 : LCR_racingStep(input); if (events & LCR_RACING_EVENT_CP_TAKEN) { int carBlock[3]; LCR_racingGetCarBlockCoords(carBlock); LCR_LOG1("CP taken"); LCR_rendererMarkTakenCP(carBlock[0],carBlock[1],carBlock[2]); } else if (events & LCR_RACING_EVENT_FINISHED) { LCR_LOG1("finished"); } if (events & LCR_RACING_EVENT_CRASH_SMALL) { LCR_audioPlaySound(LCR_SOUND_CRASH_SMALL); LCR_LOG1("crash (small)"); } else if (events & LCR_RACING_EVENT_CRASH_BIG) { LCR_audioPlaySound(LCR_SOUND_CRASH_BIG); LCR_LOG1("crash (big)"); } int engineIntensity = LCR_carSpeedKMH() * 2; LCR_audioSetEngineIntensity(engineIntensity < 256 ? engineIntensity : 255); LCR_game.nextRacingTickTime += LCR_RACING_TICK_MS; } sleep = (3 * (LCR_game.nextRacingTickTime - time)) / 4; // handle rendering: if (time >= LCR_game.nextRenderFrameTime) { LCR_LOG2("gonna render next frame"); while (time >= LCR_game.nextRenderFrameTime) LCR_game.nextRenderFrameTime += 1000 / LCR_SETTING_FPS; if (LCR_game.state == LCR_GAME_STATE_MENU) { const char *items[LCR_MENU_MAX_ITEMS]; uint8_t itemCount = 1; items[0] = LCR_texts[LCR_game.menuSelectedTab]; switch (LCR_game.menuSelectedTab) { case 0: for (int i = 0; i < 5; ++i) items[1 + i] = LCR_texts[4 + i]; itemCount = 6; break; default: break; } LCR_rendererDrawMenu(items,itemCount,LCR_game.menuSelectedItem); } else LCR_gameDraw3DView(); } else { uint32_t tmp = (3 * (LCR_game.nextRenderFrameTime - time)) / 4; sleep = tmp < sleep ? tmp : sleep; } if (sleep) LCR_sleep(sleep); else { LCR_LOG1("can't sleep, frames take too long!"); } LCR_game.frame++; LCR_LOG2("game step end"); return 1; } uint8_t LCR_gameGetNextAudioSample(void) { return LCR_audioGetNextSample(); } #endif // guard