/** 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). */ #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. */ /** 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); /** 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); //------------------------------------------------------------------------------ #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 struct { uint32_t nextRenderFrameTime; uint32_t nextRacingTickTime; uint8_t controlMode; uint8_t debugDraw; } LCR_game; 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" uint8_t LCR_keyStates[LCR_KEYS_TOTAL]; /**< Assures unchanging key states during a single frame, holds number of frames for which the key has been continuously held. */ 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_gameInit(void) { LCR_LOG0("initializing"); for (int i = 0; i < LCR_KEYS_TOTAL; ++i) LCR_keyStates[i] = 0; LCR_mapLoad(map1); LCR_rendererInit(); LCR_racingInit(); LCR_game.nextRenderFrameTime = 0; LCR_game.nextRacingTickTime = 0; LCR_game.controlMode = LCR_CONTROL_MODE_FREECAM; LCR_game.debugDraw = 0; } void LCR_gameEnd(void) { LCR_LOG0("ending"); } uint8_t LCR_gameStep(uint32_t time) { LCR_LOG2("game step start"); LCR_GameUnit carTransform[6]; for (int i = 0; i < LCR_KEYS_TOTAL; ++i) LCR_keyStates[i] = LCR_keyPressed(i) ? (LCR_keyStates[i] < 255 ? LCR_keyStates[i] + 1 : 255) : 0; //S3L_logTransform3D(LCR_renderer.scene.camera.transform); if ((LCR_racing.tick % 32) == 0) { printf("speed: %d\n",LCR_racingGetCarSpeed()); } uint32_t sleep = 0; if (LCR_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_keyStates[LCR_KEY_B] == 30) LCR_game.debugDraw = !LCR_game.debugDraw; 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_keyStates[LCR_KEY_UP] ? LCR_RACING_INPUT_FORW : 0) | (LCR_keyStates[LCR_KEY_RIGHT] ? LCR_RACING_INPUT_RIGHT : 0) | (LCR_keyStates[LCR_KEY_DOWN] ? LCR_RACING_INPUT_BACK : 0) | (LCR_keyStates[LCR_KEY_LEFT] ? LCR_RACING_INPUT_LEFT : 0); LCR_racingStep(input); LCR_racingGetCarTransform(carTransform,carTransform + 3,0); carTransform[0] = (carTransform[0] + (LCR_GAME_UNIT * LCR_MAP_SIZE_BLOCKS) / 2) / LCR_GAME_UNIT; carTransform[1] = (carTransform[1] + (LCR_GAME_UNIT * LCR_MAP_SIZE_BLOCKS) / 4) / (LCR_GAME_UNIT / 2); carTransform[2] = (carTransform[2] + (LCR_GAME_UNIT * LCR_MAP_SIZE_BLOCKS) / 2) / LCR_GAME_UNIT; int blockIndex = LCR_mapGetBlockAt(carTransform[0],carTransform[1],carTransform[2]); if (blockIndex >= 0 && LCR_currentMap.blocks[blockIndex * LCR_BLOCK_SIZE] == LCR_BLOCK_CHECKPOINT_0) { LCR_currentMap.blocks[blockIndex * LCR_BLOCK_SIZE] = LCR_BLOCK_CHECKPOINT_1; LCR_rendererMarkTakenCP( carTransform[0],carTransform[1],carTransform[2]); } LCR_game.nextRacingTickTime += LCR_RACING_TICK_MS; } sleep = (3 * (LCR_game.nextRacingTickTime - time)) / 4; if (time >= LCR_game.nextRenderFrameTime) { LCR_LOG2("gonna render next frame"); LCR_GameUnit physicsInterpolationParam = LCR_GAME_UNIT - ((LCR_game.nextRacingTickTime - time) * LCR_GAME_UNIT) / LCR_RACING_TICK_MS; LCR_racingGetCarTransform(carTransform,carTransform + 3, physicsInterpolationParam); LCR_rendererSetCarTransform(carTransform,carTransform + 3); while (time >= LCR_game.nextRenderFrameTime) LCR_game.nextRenderFrameTime += 1000 / LCR_SETTING_FPS; LCR_GameUnit offsets[5]; for (int i = 0; i < 5; ++i) offsets[i] = 0; if (LCR_game.controlMode == LCR_CONTROL_MODE_FREECAM) { if (LCR_keyStates[LCR_KEY_A]) { if (LCR_keyStates[LCR_KEY_UP]) offsets[4] = LCR_FREE_CAMERA_TURN_STEP; else if (LCR_keyStates[LCR_KEY_DOWN]) offsets[4] -= LCR_FREE_CAMERA_TURN_STEP; if (LCR_keyStates[LCR_KEY_RIGHT]) offsets[3] -= LCR_FREE_CAMERA_TURN_STEP; else if (LCR_keyStates[LCR_KEY_LEFT]) offsets[3] = LCR_FREE_CAMERA_TURN_STEP; } else { if (LCR_keyStates[LCR_KEY_UP]) offsets[0] = LCR_FREE_CAMERA_STEP; else if (LCR_keyStates[LCR_KEY_DOWN]) offsets[0] -= LCR_FREE_CAMERA_STEP; if (LCR_keyStates[LCR_KEY_RIGHT]) offsets[1] = LCR_FREE_CAMERA_STEP; else if (LCR_keyStates[LCR_KEY_LEFT]) offsets[1] -= LCR_FREE_CAMERA_STEP; } LCR_rendererMoveCamera(offsets,offsets + 3); } else LCR_rendererCameraFollow(); #if LCR_ANIMATE_CAR LCR_rendererSetWheelState(LCR_racingGetWheelRotation(), LCR_racingGetWheelSteer() * 2); #endif LCR_rendererDraw(); if (LCR_game.debugDraw) { LCR_GameUnit camTr[7]; LCR_rendererGetCameraTransform(camTr,camTr + 3,camTr + 6); LCR_physicsDebugDraw(camTr,camTr + 3,camTr[6]); } } else { uint32_t tmp = (3 * (LCR_game.nextRenderFrameTime - time)) / 4; sleep = tmp < sleep ? tmp : sleep; } if (sleep) LCR_sleep(sleep); LCR_LOG2("game step end"); return 1; } #endif // guard