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