/** General definitions common for Pokitto raycasting demos. The demos use mode 13: 1 byte per pixel = 256 colors. Bitmaps (textures, sprites, ...) are also in this format (use the provided python script to convert png images). author: Miloslav "drummyfish" Ciz license: CC0 1.0 */ #ifndef RAYCAST_DEMO_GENERAL_HPP #define RAYCAST_DEMO_GENERAL_HPP #include "stdio.h" // for debugging raycastlibg #define RCL_VERTICAL_FOV RCL_UNITS_PER_SQUARE /* redefine camera vertical FOV: RCL_UNITS_PER_SQUARE would normally mean 360 degrees, but it's not an actual angle, just linear approximation, so this is okay */ #define RCL_PIXEL_FUNCTION pixelFunc /* ^ This has to be defined to the name of the function that will render pixels. */ #include "raycastlib.h" #include "Pokitto.h" Pokitto::Core pokitto; #ifndef FPS #define FPS 30 #endif #ifndef PLAYER_SPEED #define PLAYER_SPEED (4 * RCL_UNITS_PER_SQUARE) #endif #ifndef PLAYER_ROTATION_SPEED #define PLAYER_ROTATION_SPEED (RCL_UNITS_PER_SQUARE / 2) #endif #ifndef PLAYER_JUMP_SPEED #define PLAYER_JUMP_SPEED 500 #endif #ifndef HEAD_BOB_HEIGHT #define HEAD_BOB_HEIGHT 100 #endif #ifndef HEAD_BOB_STEP #define HEAD_BOB_STEP 10 #endif #ifndef GRAVITY_ACCELERATION #define GRAVITY_ACCELERATION ((3 * RCL_UNITS_PER_SQUARE) / 2) #endif #define SCREEN_WIDTH 110 #define SCREEN_HEIGHT 88 #define MIDDLE_ROW (SCREEN_HEIGHT / 2) #define MIDDLE_COLUMN (SCREEN_WIDTH / 2) #ifndef SUBSAMPLE #define SUBSAMPLE 2 #endif #define SUBSAMPLED_WIDTH (SCREEN_WIDTH / SUBSAMPLE) #define TRANSPARENT_COLOR 0b00000111 /// Transparent color for sprites and GUI. RCL_Unit zBuffer[SUBSAMPLED_WIDTH]; ///< 1D z-buffer for visibility determination. RCL_RayConstraints defaultConstraints; unsigned short palette[256]; #ifdef POK_SIM inline void putSubsampledPixel(int32_t x, int32_t y, uint8_t color) { pokitto.display.drawPixel(x * SUBSAMPLE,y,color); pokitto.display.drawPixel(x * SUBSAMPLE + 1,y,color); } #else // This code breaks the simulator. inline void putSubsampledPixel(int32_t x, int32_t y, uint8_t color) { uint8_t *buf = pokitto.display.screenbuffer; buf += x * SUBSAMPLE; buf += y * SCREEN_WIDTH; for (uint8_t i = 0; i < SUBSAMPLE - 1; ++i) *buf++ = color; *buf = color; } #endif /** Gets (the index of) color by specified RGB components. @param r red, 3 bits (0 to 7) @param g green, 3 bits (0 to 7) @param b blue, 2 bits (0 to 3) @return palette index of the color */ inline uint8_t rgbToIndex(uint8_t r, uint8_t g, uint8_t b) { return (r & 0b00000111) | ((g & 0b00000111) << 3) | ((b & 0b00000011) << 6); } /** Inits and loads a general 256 color palette. */ void initPalette() { for (uint8_t r = 0; r < 8; ++r) for (uint8_t g = 0; g < 8; ++g) for (uint8_t b = 0; b < 4; ++b) palette[rgbToIndex(r,g,b)] = pokitto.display.RGBto565(36 * r, 36 * g, 85 * b); pokitto.display.load565Palette(palette); } /** Adds given intensity to a color. @param color input color @param intensity intensity to add, 3 bit (0 to 7) @return new color */ inline uint8_t addIntensity(uint8_t color, int16_t intensity) { uint8_t r = color & 0b00000111; uint8_t g = (color & 0b00111000) >> 3; uint8_t b = (color & 0b11000000) >> 6; if (intensity >= 0) { r += intensity; r = r > 7 ? 7 : r; g += intensity; g = g > 7 ? 7 : g; b += intensity / 2; b = b > 3 ? 3 : b; } else { intensity *= -1; r = (intensity > r) ? 0 : r - intensity; g = (intensity > g) ? 0 : g - intensity; intensity /= 2; b = intensity > b ? 0 : b - intensity; } return rgbToIndex(r,g,b); } inline uint8_t addRGB(uint8_t color, int16_t red, int16_t green, int16_t blue) { int8_t r = color & 0b00000111; int8_t g = (color & 0b00111000) >> 3; int8_t b = (color & 0b11000000) >> 6; r = RCL_clamp(r + red,0,7); g = RCL_clamp(g + green,0,7); b = RCL_clamp(b + blue,0,3); return rgbToIndex(r,g,b); } /** Inits a color gradient from darkest (0, -7 intensity) to brightest (14, +7 intensity). Index 7 will have the base color. */ void initGradient(unsigned char dest[15], unsigned char baseColor) { for (int8_t i = 0; i < 15; ++i) dest[i] = addIntensity(baseColor, i - 7); } /** Samples an image by normalized coordinates - each coordinate is in range 0 to RCL_UNITS_PER_SQUARE (from raycastlib). */ inline uint8_t sampleImage(const unsigned char *image, RCL_Unit x, RCL_Unit y) { // TODO: optimize x = RCL_wrap(x,RCL_UNITS_PER_SQUARE); y = RCL_wrap(y,RCL_UNITS_PER_SQUARE); int32_t index = (x / (RCL_UNITS_PER_SQUARE / TEXTURE_W)) * TEXTURE_H + (y / (RCL_UNITS_PER_SQUARE / TEXTURE_W)); return image[2 + index]; } /** Draws a scaled sprite on screen in an optimized way. The sprite has to be square in resolution. */ void inline drawSpriteSquare(const unsigned char *sprite, int16_t x, int16_t y, RCL_Unit depth, int16_t size) { if (size < 0 || size > 200 || // let's not mess up with the incoming array sprite[0] != sprite[1]) // only draw square sprites return; int16_t samplingIndices[size]; // optimization: precompute the indices for (RCL_Unit i = 0; i < size; ++i) samplingIndices[i] = (i * sprite[0]) / size; x -= size / 2; y -= size / 2; uint8_t c; int16_t jTo = size - max(0,y + size - 88); int16_t iTo = size - max(0,x + size - 110); for (RCL_Unit i = max(-1 * x,0); i < iTo; ++i) { int16_t xPos = x + i; if (zBuffer[xPos / SUBSAMPLE] <= depth) continue; int16_t columnLocation = 2 + samplingIndices[i] * sprite[0]; for (RCL_Unit j = max(-1 * y,0); j < jTo; ++j) { c = sprite[columnLocation + samplingIndices[j]]; if (c != TRANSPARENT_COLOR) pokitto.display.drawPixel(xPos,y + j,c); } } } /// Faster than drawSprite. void drawImage(const unsigned char *image, int16_t x, int16_t y) { // TODO: optimize for (int16_t i = 0; i < image[0]; ++i) { int16_t xPos = x + i; int16_t column = 2 + i * image[1]; for (int16_t j = 0; j < image[1]; ++j) { char c = image[column + j]; if (c != TRANSPARENT_COLOR) pokitto.display.drawPixel(xPos,y + j,image[column + j]); } } } class Player { public: RCL_Camera mCamera; RCL_Unit mVericalSpeed; bool mRunning; RCL_Unit mHeadBob; bool mHeadBobUp; Player() { RCL_initCamera(&mCamera); mCamera.position.x = 0; mCamera.position.y = 0; mCamera.direction = 0; mCamera.height = RCL_UNITS_PER_SQUARE * 3; mCamera.resolution.x = SCREEN_WIDTH / SUBSAMPLE; mCamera.resolution.y = SCREEN_HEIGHT; mCamera.shear = 0; mVericalSpeed = 0; mRunning = false; mHeadBob = 0; mHeadBobUp = true; } void setPosition(RCL_Unit x, RCL_Unit y) { mCamera.position.x = x; mCamera.position.y = y; } void setPosition(RCL_Unit x, RCL_Unit y, RCL_Unit z, RCL_Unit direction) { mCamera.position.x = x; mCamera.position.y = y; mCamera.height = z; mCamera.direction = direction; } void setPositionSquare(int16_t squareX, int16_t squareY) { setPosition( squareX * RCL_UNITS_PER_SQUARE + RCL_UNITS_PER_SQUARE / 2, squareY * RCL_UNITS_PER_SQUARE + RCL_UNITS_PER_SQUARE / 2); } void update(int16_t moveDirection, bool strafe, int16_t turnDirection, bool jump, int16_t shearDirection, RCL_ArrayFunction floorHeightFunction, RCL_ArrayFunction ceilingHeightFunction, bool computeHeight, uint32_t dt) { RCL_Vector2D moveOffset; moveOffset.x = 0; moveOffset.y = 0; if (moveDirection != 0) { int16_t horizontalStep = (dt * PLAYER_SPEED * (mRunning ? 2 : 1)) / 1000 * (moveDirection > 0 ? 1 : -1); moveOffset = RCL_angleToDirection(mCamera.direction + (strafe ? RCL_UNITS_PER_SQUARE / 4 : 0)); moveOffset.x = (moveOffset.x * horizontalStep) / RCL_UNITS_PER_SQUARE; moveOffset.y = (moveOffset.y * horizontalStep) / RCL_UNITS_PER_SQUARE; mHeadBob += mHeadBobUp ? HEAD_BOB_STEP : -HEAD_BOB_STEP; if (mHeadBob > HEAD_BOB_HEIGHT) mHeadBobUp = false; else if (mHeadBob < -HEAD_BOB_HEIGHT) mHeadBobUp = true; } else mHeadBob /= 2; if (turnDirection != 0) { int16_t rotationStep = (dt * PLAYER_ROTATION_SPEED) / 1000; mCamera.direction = RCL_wrap(mCamera.direction + turnDirection * rotationStep,RCL_UNITS_PER_SQUARE); } RCL_Unit prevHeight = mCamera.height; RCL_moveCameraWithCollision(&mCamera,moveOffset,mVericalSpeed, floorHeightFunction, ceilingHeightFunction, computeHeight ? 1 : 0, 0); RCL_Unit heightDiff = mCamera.height - prevHeight; if (heightDiff == 0) mVericalSpeed = 0; // hit floor/ceiling if (jump && mVericalSpeed == 0) { int16_t camX = RCL_divRoundDown(mCamera.position.x,RCL_UNITS_PER_SQUARE); int16_t camY = RCL_divRoundDown(mCamera.position.y,RCL_UNITS_PER_SQUARE); if (mCamera.height - RCL_CAMERA_COLL_HEIGHT_BELOW - floorHeightFunction(camX,camY) < 2) mVericalSpeed = PLAYER_JUMP_SPEED; // jump } if (shearDirection != 0) mCamera.shear = RCL_clamp(mCamera.shear + shearDirection * 10, -1 * mCamera.resolution.y, mCamera.resolution.y); else mCamera.shear /= 2; if (computeHeight) mVericalSpeed -= (dt * GRAVITY_ACCELERATION) / 1000; // gravity } }; class Sprite { public: const unsigned char *mImage; RCL_Vector2D mPosition; RCL_Unit mHeight; RCL_Unit mPixelSize; Sprite(const unsigned char *image, int16_t squareX, int16_t squareY, RCL_Unit z, RCL_Unit pixelSize): mImage(image), mPixelSize(pixelSize) { mPosition.x = squareX * RCL_UNITS_PER_SQUARE + RCL_UNITS_PER_SQUARE / 2; mPosition.y = squareY * RCL_UNITS_PER_SQUARE + RCL_UNITS_PER_SQUARE / 2; mHeight = z * RCL_UNITS_PER_SQUARE + RCL_UNITS_PER_SQUARE / 2; } Sprite(): mImage(0), mHeight(0), mPixelSize(1) { mPosition.x = 0; mPosition.y = 0; } }; void initGeneral() { pokitto.begin(); pokitto.setFrameRate(FPS); pokitto.display.setFont(fontTiny); pokitto.display.persistence = 1; RCL_initRayConstraints(&defaultConstraints); initPalette(); for (uint8_t i = 0; i < SUBSAMPLED_WIDTH; ++i) zBuffer[i] = 0; } /** Computes an average color of given texture. */ unsigned char computeAverageColor(const unsigned char *texture) { uint32_t sumR = 0; uint32_t sumG = 0; uint32_t sumB = 0; uint32_t pixels = texture[0] * texture[1]; for (uint16_t i = 0; i < pixels; ++i) { sumR += texture[2 + i] & 0b00000111; sumG += (texture[2 + i] & 0b00111000) >> 3; sumB += (texture[2 + i] & 0b11000000) >> 6; } return rgbToIndex(sumR / pixels,sumG / pixels,sumB / pixels); } #endif