├── CommandLineFPS.cpp └── README.md /CommandLineFPS.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | OneLoneCoder.com - Command Line First Person Shooter (FPS) Engine 3 | "Why were games not done like this is 1990?" - @Javidx9 4 | 5 | License 6 | ~~~~~~~ 7 | Copyright (C) 2018 Javidx9 8 | This program comes with ABSOLUTELY NO WARRANTY. 9 | This is free software, and you are welcome to redistribute it 10 | under certain conditions; See license for details. 11 | Original works located at: 12 | https://www.github.com/onelonecoder 13 | https://www.onelonecoder.com 14 | https://www.youtube.com/javidx9 15 | 16 | GNU GPLv3 17 | https://github.com/OneLoneCoder/videos/blob/master/LICENSE 18 | 19 | From Javidx9 :) 20 | ~~~~~~~~~~~~~~~ 21 | Hello! Ultimately I don't care what you use this for. It's intended to be 22 | educational, and perhaps to the oddly minded - a little bit of fun. 23 | Please hack this, change it and use it in any way you see fit. You acknowledge 24 | that I am not responsible for anything bad that happens as a result of 25 | your actions. However this code is protected by GNU GPLv3, see the license in the 26 | github repo. This means you must attribute me if you use it. You can view this 27 | license here: https://github.com/OneLoneCoder/videos/blob/master/LICENSE 28 | Cheers! 29 | 30 | Background 31 | ~~~~~~~~~~ 32 | Whilst waiting for TheMexicanRunner to start the finale of his NesMania project, 33 | his Twitch stream had a counter counting down for a couple of hours until it started. 34 | With some time on my hands, I thought it might be fun to see what the graphical 35 | capabilities of the console are. Turns out, not very much, but hey, it's nice to think 36 | Wolfenstein could have existed a few years earlier, and in just ~200 lines of code. 37 | 38 | IMPORTANT!!!! 39 | ~~~~~~~~~~~~~ 40 | READ ME BEFORE RUNNING!!! This program expects the console dimensions to be set to 41 | 120 Columns by 40 Rows. I recommend a small font "Consolas" at size 16. You can do this 42 | by running the program, and right clicking on the console title bar, and specifying 43 | the properties. You can also choose to default to them in the future. 44 | 45 | Controls: A = Turn Left, D = Turn Right, W = Walk Forwards, S = Walk Backwards 46 | 47 | Future Modifications 48 | ~~~~~~~~~~~~~~~~~~~~ 49 | 1) Shade block segments based on angle from player, i.e. less light reflected off 50 | walls at side of player. Walls straight on are brightest. 51 | 2) Find an interesting and optimised ray-tracing method. I'm sure one must exist 52 | to more optimally search the map space 53 | 3) Add bullets! 54 | 4) Add bad guys! 55 | 56 | Author 57 | ~~~~~~ 58 | Twitter: @javidx9 59 | Blog: www.onelonecoder.com 60 | 61 | Video: 62 | ~~~~~~ 63 | https://youtu.be/xW8skO7MFYw 64 | 65 | Last Updated: 27/02/2017 66 | */ 67 | 68 | #include 69 | #include 70 | #include 71 | #include 72 | #include 73 | using namespace std; 74 | 75 | #include 76 | #include 77 | 78 | int nScreenWidth = 120; // Console Screen Size X (columns) 79 | int nScreenHeight = 40; // Console Screen Size Y (rows) 80 | int nMapWidth = 16; // World Dimensions 81 | int nMapHeight = 16; 82 | 83 | float fPlayerX = 14.7f; // Player Start Position 84 | float fPlayerY = 5.09f; 85 | float fPlayerA = 0.0f; // Player Start Rotation 86 | float fFOV = 3.14159f / 4.0f; // Field of View 87 | float fDepth = 16.0f; // Maximum rendering distance 88 | float fSpeed = 5.0f; // Walking Speed 89 | 90 | int main() 91 | { 92 | // Create Screen Buffer 93 | wchar_t *screen = new wchar_t[nScreenWidth*nScreenHeight]; 94 | HANDLE hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL); 95 | SetConsoleActiveScreenBuffer(hConsole); 96 | DWORD dwBytesWritten = 0; 97 | 98 | // Create Map of world space # = wall block, . = space 99 | wstring map; 100 | map += L"#########......."; 101 | map += L"#..............."; 102 | map += L"#.......########"; 103 | map += L"#..............#"; 104 | map += L"#......##......#"; 105 | map += L"#......##......#"; 106 | map += L"#..............#"; 107 | map += L"###............#"; 108 | map += L"##.............#"; 109 | map += L"#......####..###"; 110 | map += L"#......#.......#"; 111 | map += L"#......#.......#"; 112 | map += L"#..............#"; 113 | map += L"#......#########"; 114 | map += L"#..............#"; 115 | map += L"################"; 116 | 117 | auto tp1 = chrono::system_clock::now(); 118 | auto tp2 = chrono::system_clock::now(); 119 | 120 | while (1) 121 | { 122 | // We'll need time differential per frame to calculate modification 123 | // to movement speeds, to ensure consistant movement, as ray-tracing 124 | // is non-deterministic 125 | tp2 = chrono::system_clock::now(); 126 | chrono::duration elapsedTime = tp2 - tp1; 127 | tp1 = tp2; 128 | float fElapsedTime = elapsedTime.count(); 129 | 130 | 131 | // Handle CCW Rotation 132 | if (GetAsyncKeyState((unsigned short)'A') & 0x8000) 133 | fPlayerA -= (fSpeed * 0.75f) * fElapsedTime; 134 | 135 | // Handle CW Rotation 136 | if (GetAsyncKeyState((unsigned short)'D') & 0x8000) 137 | fPlayerA += (fSpeed * 0.75f) * fElapsedTime; 138 | 139 | // Handle Forwards movement & collision 140 | if (GetAsyncKeyState((unsigned short)'W') & 0x8000) 141 | { 142 | fPlayerX += sinf(fPlayerA) * fSpeed * fElapsedTime;; 143 | fPlayerY += cosf(fPlayerA) * fSpeed * fElapsedTime;; 144 | if (map.c_str()[(int)fPlayerX * nMapWidth + (int)fPlayerY] == '#') 145 | { 146 | fPlayerX -= sinf(fPlayerA) * fSpeed * fElapsedTime;; 147 | fPlayerY -= cosf(fPlayerA) * fSpeed * fElapsedTime;; 148 | } 149 | } 150 | 151 | // Handle backwards movement & collision 152 | if (GetAsyncKeyState((unsigned short)'S') & 0x8000) 153 | { 154 | fPlayerX -= sinf(fPlayerA) * fSpeed * fElapsedTime;; 155 | fPlayerY -= cosf(fPlayerA) * fSpeed * fElapsedTime;; 156 | if (map.c_str()[(int)fPlayerX * nMapWidth + (int)fPlayerY] == '#') 157 | { 158 | fPlayerX += sinf(fPlayerA) * fSpeed * fElapsedTime;; 159 | fPlayerY += cosf(fPlayerA) * fSpeed * fElapsedTime;; 160 | } 161 | } 162 | 163 | for (int x = 0; x < nScreenWidth; x++) 164 | { 165 | // For each column, calculate the projected ray angle into world space 166 | float fRayAngle = (fPlayerA - fFOV/2.0f) + ((float)x / (float)nScreenWidth) * fFOV; 167 | 168 | // Find distance to wall 169 | float fStepSize = 0.1f; // Increment size for ray casting, decrease to increase 170 | float fDistanceToWall = 0.0f; // resolution 171 | 172 | bool bHitWall = false; // Set when ray hits wall block 173 | bool bBoundary = false; // Set when ray hits boundary between two wall blocks 174 | 175 | float fEyeX = sinf(fRayAngle); // Unit vector for ray in player space 176 | float fEyeY = cosf(fRayAngle); 177 | 178 | // Incrementally cast ray from player, along ray angle, testing for 179 | // intersection with a block 180 | while (!bHitWall && fDistanceToWall < fDepth) 181 | { 182 | fDistanceToWall += fStepSize; 183 | int nTestX = (int)(fPlayerX + fEyeX * fDistanceToWall); 184 | int nTestY = (int)(fPlayerY + fEyeY * fDistanceToWall); 185 | 186 | // Test if ray is out of bounds 187 | if (nTestX < 0 || nTestX >= nMapWidth || nTestY < 0 || nTestY >= nMapHeight) 188 | { 189 | bHitWall = true; // Just set distance to maximum depth 190 | fDistanceToWall = fDepth; 191 | } 192 | else 193 | { 194 | // Ray is inbounds so test to see if the ray cell is a wall block 195 | if (map.c_str()[nTestX * nMapWidth + nTestY] == '#') 196 | { 197 | // Ray has hit wall 198 | bHitWall = true; 199 | 200 | // To highlight tile boundaries, cast a ray from each corner 201 | // of the tile, to the player. The more coincident this ray 202 | // is to the rendering ray, the closer we are to a tile 203 | // boundary, which we'll shade to add detail to the walls 204 | vector> p; 205 | 206 | // Test each corner of hit tile, storing the distance from 207 | // the player, and the calculated dot product of the two rays 208 | for (int tx = 0; tx < 2; tx++) 209 | for (int ty = 0; ty < 2; ty++) 210 | { 211 | // Angle of corner to eye 212 | float vy = (float)nTestY + ty - fPlayerY; 213 | float vx = (float)nTestX + tx - fPlayerX; 214 | float d = sqrt(vx*vx + vy*vy); 215 | float dot = (fEyeX * vx / d) + (fEyeY * vy / d); 216 | p.push_back(make_pair(d, dot)); 217 | } 218 | 219 | // Sort Pairs from closest to farthest 220 | sort(p.begin(), p.end(), [](const pair &left, const pair &right) {return left.first < right.first; }); 221 | 222 | // First two/three are closest (we will never see all four) 223 | float fBound = 0.01; 224 | if (acos(p.at(0).second) < fBound) bBoundary = true; 225 | if (acos(p.at(1).second) < fBound) bBoundary = true; 226 | if (acos(p.at(2).second) < fBound) bBoundary = true; 227 | } 228 | } 229 | } 230 | 231 | // Calculate distance to ceiling and floor 232 | int nCeiling = (float)(nScreenHeight/2.0) - nScreenHeight / ((float)fDistanceToWall); 233 | int nFloor = nScreenHeight - nCeiling; 234 | 235 | // Shader walls based on distance 236 | short nShade = ' '; 237 | if (fDistanceToWall <= fDepth / 4.0f) nShade = 0x2588; // Very close 238 | else if (fDistanceToWall < fDepth / 3.0f) nShade = 0x2593; 239 | else if (fDistanceToWall < fDepth / 2.0f) nShade = 0x2592; 240 | else if (fDistanceToWall < fDepth) nShade = 0x2591; 241 | else nShade = ' '; // Too far away 242 | 243 | if (bBoundary) nShade = ' '; // Black it out 244 | 245 | for (int y = 0; y < nScreenHeight; y++) 246 | { 247 | // Each Row 248 | if(y <= nCeiling) 249 | screen[y*nScreenWidth + x] = ' '; 250 | else if(y > nCeiling && y <= nFloor) 251 | screen[y*nScreenWidth + x] = nShade; 252 | else // Floor 253 | { 254 | // Shade floor based on distance 255 | float b = 1.0f - (((float)y -nScreenHeight/2.0f) / ((float)nScreenHeight / 2.0f)); 256 | if (b < 0.25) nShade = '#'; 257 | else if (b < 0.5) nShade = 'x'; 258 | else if (b < 0.75) nShade = '.'; 259 | else if (b < 0.9) nShade = '-'; 260 | else nShade = ' '; 261 | screen[y*nScreenWidth + x] = nShade; 262 | } 263 | } 264 | } 265 | 266 | // Display Stats 267 | swprintf_s(screen, 40, L"X=%3.2f, Y=%3.2f, A=%3.2f FPS=%3.2f ", fPlayerX, fPlayerY, fPlayerA, 1.0f/fElapsedTime); 268 | 269 | // Display Map 270 | for (int nx = 0; nx < nMapWidth; nx++) 271 | for (int ny = 0; ny < nMapWidth; ny++) 272 | { 273 | screen[(ny+1)*nScreenWidth + nx] = map[ny * nMapWidth + nx]; 274 | } 275 | screen[((int)fPlayerX+1) * nScreenWidth + (int)fPlayerY] = 'P'; 276 | 277 | // Display Frame 278 | screen[nScreenWidth * nScreenHeight - 1] = '\0'; 279 | WriteConsoleOutputCharacter(hConsole, screen, nScreenWidth * nScreenHeight, { 0,0 }, &dwBytesWritten); 280 | } 281 | 282 | return 0; 283 | } 284 | 285 | // That's It!! - Jx9 286 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CommandLineFPS 2 | A First Person Shooter at the command line? Yup... 3 | 4 | Please see the source file on how to configure your command line before running. 5 | 6 | This is designed for MS Windows 7 | --------------------------------------------------------------------------------