Skip to content

Commit 98fb858

Browse files
committed
First Upload
1 parent 13ac331 commit 98fb858

File tree

2 files changed

+269
-0
lines changed

2 files changed

+269
-0
lines changed

CommandLineFPS.cpp

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
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+
}

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
# CommandLineFPS
22
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

0 commit comments

Comments
 (0)