/* OneLoneCoder.com - Upgraded Command Line First Person Shooter (FPS) Engine "Bricks and Lamps people, bricks and lamps..." - @Javidx9 Disclaimer ~~~~~~~~~~ I don't care what you use this for. It's intended to be educational, and perhaps to the oddly minded - a little bit of fun. Please hack this, change it and use it in any way you see fit. BUT, you acknowledge that I am not responsible for anything bad that happens as a result of your actions. However, if good stuff happens, I would appreciate a shout out, or at least give the blog some publicity for me. Cheers! Background ~~~~~~~~~~ The FPS video was one of my first youtube videos, and I feel one of my better ones, but its not had the popularity it deserves. So I'm upgrading the engine to make it more appealling. IMPORTANT!! ~~~~~~~~~~~ You'll need the FPSSprites folder too! Author ~~~~~~ Twitter: @javidx9 Blog: www.onelonecoder.com Video: ~~~~~~ https://youtu.be/HEb2akswCcw Last Updated: 23/10/2017 */ #include #include #include using namespace std; #include "olcConsoleGameEngine.h" class OneLoneCoder_UltimateFPS : public olcConsoleGameEngine { public: OneLoneCoder_UltimateFPS() { m_sAppName = L"Ultimate First Person Shooter"; } private: int nMapWidth = 32; // World Dimensions int nMapHeight = 32; float fPlayerX = 14.7f; // Player Start Position float fPlayerY = 8;// 5.09f; float fPlayerA = -3.14159f / 2.0f; // Player Start Rotation float fFOV = 3.14159f / 4.0f; // Field of View float fDepth = 16.0f; // Maximum rendering distance float fSpeed = 5.0f; // Walking Speed wstring map; olcSprite *spriteWall; olcSprite *spriteLamp; olcSprite *spriteFireBall; float *fDepthBuffer = nullptr; struct sObject { float x; float y; float vx; float vy; bool bRemove; olcSprite *sprite; }; list listObjects; protected: virtual bool OnUserCreate() { map += L"#########.......#########......."; map += L"#...............#..............."; map += L"#.......#########.......########"; map += L"#..............##..............#"; map += L"#......##......##......##......#"; map += L"#......##..............##......#"; map += L"#..............##..............#"; map += L"###............####............#"; map += L"##.............###.............#"; map += L"#............####............###"; map += L"#..............................#"; map += L"#..............##..............#"; map += L"#..............##..............#"; map += L"#...........#####...........####"; map += L"#..............................#"; map += L"###..####....########....#######"; map += L"####.####.......######.........."; map += L"#...............#..............."; map += L"#.......#########.......##..####"; map += L"#..............##..............#"; map += L"#......##......##.......#......#"; map += L"#......##......##......##......#"; map += L"#..............##..............#"; map += L"###............####............#"; map += L"##.............###.............#"; map += L"#............####............###"; map += L"#..............................#"; map += L"#..............................#"; map += L"#..............##..............#"; map += L"#...........##..............####"; map += L"#..............##..............#"; map += L"################################"; spriteWall = new olcSprite(L"FPSSprites/fps_wall1.spr"); spriteLamp = new olcSprite(L"FPSSprites/fps_lamp1.spr"); spriteFireBall = new olcSprite(L"FPSSprites/fps_fireball1.spr"); fDepthBuffer = new float[ScreenWidth()]; listObjects = { { 8.5f, 8.5f, 0.0f, 0.0f, false, spriteLamp }, { 7.5f, 7.5f, 0.0f, 0.0f, false, spriteLamp }, { 10.5f, 3.5f, 0.0f, 0.0f, false, spriteLamp }, }; return true; } virtual bool OnUserUpdate(float fElapsedTime) { // Handle CCW Rotation if (m_keys[L'A'].bHeld) fPlayerA -= (fSpeed * 0.5f) * fElapsedTime; // Handle CW Rotation if (m_keys[L'D'].bHeld) fPlayerA += (fSpeed * 0.5f) * fElapsedTime; // Handle Forwards movement & collision if (m_keys[L'W'].bHeld) { fPlayerX += sinf(fPlayerA) * fSpeed * fElapsedTime;; fPlayerY += cosf(fPlayerA) * fSpeed * fElapsedTime;; if (map.c_str()[(int)fPlayerX * nMapWidth + (int)fPlayerY] == '#') { fPlayerX -= sinf(fPlayerA) * fSpeed * fElapsedTime;; fPlayerY -= cosf(fPlayerA) * fSpeed * fElapsedTime;; } } // Handle backwards movement & collision if (m_keys[L'S'].bHeld) { fPlayerX -= sinf(fPlayerA) * fSpeed * fElapsedTime;; fPlayerY -= cosf(fPlayerA) * fSpeed * fElapsedTime;; if (map.c_str()[(int)fPlayerX * nMapWidth + (int)fPlayerY] == '#') { fPlayerX += sinf(fPlayerA) * fSpeed * fElapsedTime;; fPlayerY += cosf(fPlayerA) * fSpeed * fElapsedTime;; } } // Handle Strafe Right movement & collision if (m_keys[L'E'].bHeld) { fPlayerX += cosf(fPlayerA) * fSpeed * fElapsedTime; fPlayerY -= sinf(fPlayerA) * fSpeed * fElapsedTime; if (map.c_str()[(int)fPlayerX * nMapWidth + (int)fPlayerY] == '#') { fPlayerX -= cosf(fPlayerA) * fSpeed * fElapsedTime; fPlayerY += sinf(fPlayerA) * fSpeed * fElapsedTime; } } // Handle Strafe Left movement & collision if (m_keys[L'Q'].bHeld) { fPlayerX -= cosf(fPlayerA) * fSpeed * fElapsedTime; fPlayerY += sinf(fPlayerA) * fSpeed * fElapsedTime; if (map.c_str()[(int)fPlayerX * nMapWidth + (int)fPlayerY] == '#') { fPlayerX += cosf(fPlayerA) * fSpeed * fElapsedTime; fPlayerY -= sinf(fPlayerA) * fSpeed * fElapsedTime; } } // Fire Bullets if (m_keys[VK_SPACE].bReleased) { sObject o; o.x = fPlayerX; o.y = fPlayerY; float fNoise = (((float)rand() / (float)RAND_MAX) - 0.5f) * 0.1f; o.vx = sinf(fPlayerA + fNoise) * 8.0f; o.vy = cosf(fPlayerA + fNoise) * 8.0f; o.sprite = spriteFireBall; o.bRemove = false; listObjects.push_back(o); } for (int x = 0; x < ScreenWidth(); x++) { // For each column, calculate the projected ray angle into world space float fRayAngle = (fPlayerA - fFOV / 2.0f) + ((float)x / (float)ScreenWidth()) * fFOV; // Find distance to wall float fStepSize = 0.01f; // Increment size for ray casting, decrease to increase float fDistanceToWall = 0.0f; // resolution bool bHitWall = false; // Set when ray hits wall block bool bBoundary = false; // Set when ray hits boundary between two wall blocks float fEyeX = sinf(fRayAngle); // Unit vector for ray in player space float fEyeY = cosf(fRayAngle); float fSampleX = 0.0f; bool bLit = false; // Incrementally cast ray from player, along ray angle, testing for // intersection with a block while (!bHitWall && fDistanceToWall < fDepth) { fDistanceToWall += fStepSize; int nTestX = (int)(fPlayerX + fEyeX * fDistanceToWall); int nTestY = (int)(fPlayerY + fEyeY * fDistanceToWall); // Test if ray is out of bounds if (nTestX < 0 || nTestX >= nMapWidth || nTestY < 0 || nTestY >= nMapHeight) { bHitWall = true; // Just set distance to maximum depth fDistanceToWall = fDepth; } else { // Ray is inbounds so test to see if the ray cell is a wall block if (map.c_str()[nTestX * nMapWidth + nTestY] == '#') { // Ray has hit wall bHitWall = true; // Determine where ray has hit wall. Break Block boundary // int 4 line segments float fBlockMidX = (float)nTestX + 0.5f; float fBlockMidY = (float)nTestY + 0.5f; float fTestPointX = fPlayerX + fEyeX * fDistanceToWall; float fTestPointY = fPlayerY + fEyeY * fDistanceToWall; float fTestAngle = atan2f((fTestPointY - fBlockMidY), (fTestPointX - fBlockMidX)); if (fTestAngle >= -3.14159f * 0.25f && fTestAngle < 3.14159f * 0.25f) fSampleX = fTestPointY - (float)nTestY; if (fTestAngle >= 3.14159f * 0.25f && fTestAngle < 3.14159f * 0.75f) fSampleX = fTestPointX - (float)nTestX; if (fTestAngle < -3.14159f * 0.25f && fTestAngle >= -3.14159f * 0.75f) fSampleX = fTestPointX - (float)nTestX; if (fTestAngle >= 3.14159f * 0.75f || fTestAngle < -3.14159f * 0.75f) fSampleX = fTestPointY - (float)nTestY; } } } // Calculate distance to ceiling and floor int nCeiling = (float)(ScreenHeight() / 2.0) - ScreenHeight() / ((float)fDistanceToWall); int nFloor = ScreenHeight() - nCeiling; // Update Depth Buffer fDepthBuffer[x] = fDistanceToWall; for (int y = 0; y < ScreenHeight(); y++) { // Each Row if (y <= nCeiling) Draw(x, y, L' '); else if (y > nCeiling && y <= nFloor) { // Draw Wall if (fDistanceToWall < fDepth) { float fSampleY = ((float)y - (float)nCeiling) / ((float)nFloor - (float)nCeiling); Draw(x, y, spriteWall->SampleGlyph(fSampleX, fSampleY), spriteWall->SampleColour(fSampleX, fSampleY)); } else Draw(x, y, PIXEL_SOLID, 0); } else // Floor { Draw(x, y, PIXEL_SOLID, FG_DARK_GREEN); } } } // Update & Draw Objects for (auto &object : listObjects) { // Update Object Physics object.x += object.vx * fElapsedTime; object.y += object.vy * fElapsedTime; // Check if object is inside wall - set flag for removal if (map.c_str()[(int)object.x * nMapWidth + (int)object.y] == '#') object.bRemove = true; // Can object be seen? float fVecX = object.x - fPlayerX; float fVecY = object.y - fPlayerY; float fDistanceFromPlayer = sqrtf(fVecX*fVecX + fVecY*fVecY); float fEyeX = sinf(fPlayerA); float fEyeY = cosf(fPlayerA); // Calculate angle between lamp and players feet, and players looking direction // to determine if the lamp is in the players field of view float fObjectAngle = atan2f(fEyeY, fEyeX) - atan2f(fVecY, fVecX); if (fObjectAngle < -3.14159f) fObjectAngle += 2.0f * 3.14159f; if (fObjectAngle > 3.14159f) fObjectAngle -= 2.0f * 3.14159f; bool bInPlayerFOV = fabs(fObjectAngle) < fFOV / 2.0f; if (bInPlayerFOV && fDistanceFromPlayer >= 0.5f && fDistanceFromPlayer < fDepth && !object.bRemove) { float fObjectCeiling = (float)(ScreenHeight() / 2.0) - ScreenHeight() / ((float)fDistanceFromPlayer); float fObjectFloor = ScreenHeight() - fObjectCeiling; float fObjectHeight = fObjectFloor - fObjectCeiling; float fObjectAspectRatio = (float)object.sprite->nHeight / (float)object.sprite->nWidth; float fObjectWidth = fObjectHeight / fObjectAspectRatio; float fMiddleOfObject = (0.5f * (fObjectAngle / (fFOV / 2.0f)) + 0.5f) * (float)ScreenWidth(); // Draw Lamp for (float lx = 0; lx < fObjectWidth; lx++) { for (float ly = 0; ly < fObjectHeight; ly++) { float fSampleX = lx / fObjectWidth; float fSampleY = ly / fObjectHeight; wchar_t c = object.sprite->SampleGlyph(fSampleX, fSampleY); int nObjectColumn = (int)(fMiddleOfObject + lx - (fObjectWidth / 2.0f)); if (nObjectColumn >= 0 && nObjectColumn < ScreenWidth()) if (c != L' ' && fDepthBuffer[nObjectColumn] >= fDistanceFromPlayer) { Draw(nObjectColumn, fObjectCeiling + ly, c, object.sprite->SampleColour(fSampleX, fSampleY)); fDepthBuffer[nObjectColumn] = fDistanceFromPlayer; } } } } } // Remove dead objects from object list listObjects.remove_if([](sObject &o) {return o.bRemove; }); // Display Map & Player for (int nx = 0; nx < nMapWidth; nx++) for (int ny = 0; ny < nMapWidth; ny++) Draw(nx+1, ny+1, map[ny * nMapWidth + nx]); Draw(1 + (int)fPlayerY, 1 + (int)fPlayerX, L'P'); return true; } }; int main() { OneLoneCoder_UltimateFPS game; game.ConstructConsole(320, 240,4,4); game.Start(); return 0; }