Приятного просмотра

Code-It-Yourself! First Person Shooter (Quick and Simple C++)

Опубликовано: 2 года назад
210 015 просмотров
👎 84
Скопируйте и вставте на Ваш сайт


With a bit of time on my hands, I decided to have a go at making a 3D engine using nothing but ASCII at the Windows Command Prompt. I was surprised how sophisticated you can be with just 200 lines of hastily written C++ code.
This video shows it off, talks about how it works, and you can grab the code from github, or visit my onelonecoder.com blog.


Hello I found myself with a couple of hours to kill the other night so I thought I'd set myself a little programming challenge what can we do regarding graphics at the console people have been doing graphics in terminals and consoles for years and ASCII art is an established art form now but I thought would be a fun series of tutorials to put together a nice.

Complicated graphics engine anyway 200 lines of code this is what we go so it's a little first-person shooter engine and it's a it's very primitive because I mean it's rendering everything using ASCII characters you can see we can navigate it around I can turn the character we can walk down this corridor just rotators their face the other way now we.

Can walk up and down the map you can see a little map in the corner and the P indicates were and currently stood in the in the maze have a quick look outside here and have a look there we go go back in we've got some blocks so you can see it doesn't look that great but you know it's a bit of fun let's have a look how it's done I'm going to start by creating a new.

Console project and we'll call it con FPS first person shooter although there's not going to be any shooting we're just going to walk around the maze and I just want to do everything empty project don't do anything for me and we'll start with the basic in main I'm sure if you're watching this video you know how to output to a console but this this does have a problem because.

It's very slow and it Scrolls so if I just tell it to do this many times in Sai 0 I is less a big number I plus plus plus plus plus and we run this we can actually see it's drawing to the the console here and it's scrolling very slowly down the screen and to actually make that a bit more clear and we can put a number on here just to show how slow it Scrolls hi this is because.

Outputting to the console isn't very quick so we can't use our traditional see out method for doing this we're going to have to grab the console buffer and write to it directly and in this respect we're not doing anything very different to writing to the screen except instead of pixels we're going to be using characters now on windows to get hold of the console I actually do.

Need to call some windows functions and I'll need to setup the console to a known set of dimension so I'm going to call these n screen width and we'll have 120 columns and we'll have n screen height I'm going to have 40 rows so this bit of code here creates a screen I'll type the wheelchair so we're going to try and do everything with unicode and we can see here we've got the dimensions.

Of our array so we're creating just a regular two-dimensional array using our width and our height variables and we get a handle to the console with create console screen buffer you can see there's nothing special going on here it's just a regular text mode buffer and we tell this buffer that it is going to be our console the target of our console and when we want to write to the screen.

We use this bit of code here and cutting and pasting today because I don't want this video to go on for half an hour so this is our screen array and it's we're basically just setting the very last character of this array to this get character so it knows when to start pressing the string and we use the function right console output character we give it our handle we give it the.

Buffer we tell it how many bytes and this is an interesting function because it allows us to specify the coordinate of where the text is to be written in this case we always want to write to the top left hand corner and that starts the console from scrolling down and typical windows we have a variable here that isn't really useful but we've got have it anyway now with any game engine you.

Need a game loop now I got criticized for using wild one as a loop in some of my earlier videos but I don't care so what do we know that we're going to need so far well we're going to have to store where the player is now for things to run smoothly we can't use integers to store where the player is else is going to clunk about the maze on a tile by tile basis so we're going to use.

Floating points I'm going to store the player's exposition and the player's Y position and we also need to store the angle the player is looking at we'll call that player a what are the information do we need well we know we're going to need a map so let's put in some constants for our map the map like the screen is just going to be a 2d array and we're going to use the hash.

Symbol for a wall and we'll use the tool start to represent empty space the game doesn't really know anything about polygons so we're going to throw all of that from this two-dimensional map and we'll see some tricks for that later let's create the map and to use a type W string because it's for a Unicode map and just to make the map visually easier to draw if we use the append simple each.

Time we can do things line by line so if I want to do the boundary of my maze for example 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 is what we specified I can take that line and we can duplicate it 16 times this way and let's just create a big empty room to start with it's always a good way to be debugging I'm going to just hold down the Alt key here and select the bits I.

Don't want and we can fill those out with some spaces now I will deny that the rendering engine that I'm using is inspired by the Wolfenstein 3d game the Wolfenstein 3d game engine is far more capable than the one I'm doing in the console but what I'm curious about on this excellent Wikipedia article is this little gift down here which looks very similar to what we've got.

I've loomed in here so we can see what's happening and there's a 2d array of blocks and spaces and we have an icon indicating where the player is and as the player rotates we see Ray's being cast out from the player the result of this approaches are very compelling yet totally artificial 3d illusion of a room this animation is all we need to get started programming our own engine as.

I'm not bothered about being able to look up and down this is really just a two-dimensional problem and therefore we only really need to concern ourselves with one axis and that's the axis going across the screen so we're going to do a computation for each column on the screen let's just quickly sketch your Potter algorithm is going to be so here is our.

Player and we're going to have effectively some blocks I'm going to put a second block here just to make it a bit more interesting these represent our tiles now at any point the player has a limited field of view so I'm going to sketch that in with a red line here and we'll say the field of view is something like this and our player is predominantly looking in this.

Direction our algorithm is going to work by taking each column of the console and relating that to a ray cast within the space of the field of view so we've got 120 columns on our screen we're going to cast a hundred and twenty Ray's out into the scene and what we're interested in is how far does the Ray travel before it hits a surface so for example we take a array in this direction how far does it.

Go before we hit a wall and we do that 120 times across this whole field of view and what we should get is an array full of distances this is why I have a loop going through each column on the screen so if I add in a field of view variables call that fov and to begin with we'll set that to something like pi divided by 4 which is quite a narrow field of view so our field of view is.

Actually this angle here theta field of view the first that this code here takes the player angle and tries to find out what's the starting angle for the field of view so it takes the field of view and divide it by two and if we go back to our diagram here we can see that this is the player angle the red line in the middle here our field of view is bisected by the.

Players angle so we're looking for half the field of view this way to plus the field of view that way and the second part of this code is chopping it up into little bits so in this case 120 because that's the width of our screen we're going to need a variable to track what is the distance from the player to the wall for that given angle and start with we'll set that off to zero how do we.

Work out the distance to the wall Len well we take array and we're going to test it in little increments so we're going to test at that step and then we're going to test at that step and then we're going to test at that step and each time we keep incrementing until our increment lands in a Cell that's classified as a wall therefore we now know the approximate distance to the.

Wall it's not very efficient I'm sure there are better ways to do this ray tracing so we're going to need a loop and we'll have a flag which is a hit wall whilst we've not hit a wall we want to keep incrementing our distance to a wall by some small step and you'll see I'm working in the floating-point domain here just out this flag in to calculate the test point I'm going to need a unit.

Vector which represents the direction the player is looking in so here I'm just taking the sign-in cars of the Ray angle to give me a unit vector and from this unit vector I can create a line our very given distance in this case we want to extend our unit vector to the length that we're currently checking for so this distance the wall is going to grow and therefore our unit vector is going.

Grow in the right direction and we're really only interested in the integer value of this if we assume that all of the walls are on integer boundaries so the edges of the walls meet on integer boundaries everything else in between is wall and so we only need the integer part so if you get to a cell say at 1.5 we really know that that's 1 in our array we don't care about the point 5.

Now we have to be careful here because we could end up never hitting a wall particularly if it's not been drawn in so we should put the maximum limit on how far we check in this case I'm going to create a variable called f depth I'm going to set it to 16 it seems to make sense because our map size is 16 now we've got our integer location we can test in our map if we're out of town so.

If we're beyond the boundary of the map that we've hard-coded up before then we may as well just say yes you've hit the wall we've hit the maximum distance however if we're within bounds of our map we want to check the cells individually on the map so this bit of code here is just a quick check it's converting our 2d system into a 1d for the array so we've got our y coordinate.

Times the width plus X if that contains the hash symbol then we have hit a wall and our F distance to wall value will retain the last value that it had because this loop will now exit if we just have a quick look at this animation again really what we can see is that the further away the wall is the larger the ceiling and the floor is for that column and this gives the illusion of.

Perspective so the walls are the further away you can see there's more ceiling and more floor and when the close up there's less flow and less ceiling so we can also emulate this projection these two lines do this calculation for the ceiling we want to take the midpoint and from that we're subtracting a proportion of the screen height relative to distance to wall so as distance to wall.

Gets larger this subtraction gets smaller and therefore we have a higher ceiling this is why the stars don't move in the night sky because you can assume that they're infinity away so this all disappears and they remain in a fixed position and the floor in our case it's just the mirror of the ceiling now we know where our floor begins our ceiling begins and.

Where our wall ends so we can draw this into the column so now if Y is less than R and ceiling count we know it must be the current cell being drawn to must be part of the ceiling so we'll shade in our array using the same little 2d fiddle before screen with plus X and we're going to just shade in our sky as being blank space else if we're greater than our current ceiling.

Level and we're less than our floor then we must be wall take the same thing within out for wall I'm going to use will we use the hash symbol because that's what we're using in our map and if I'm either ceiling or wall I must be floor and for now we'll shade in a floor blank as well before we run this let's set our player to actually be in the middle of our room.

We want to be in the middle of a wall that might just get confusing well that could be a wall just take this opportunity of having the console window up to say to make this to work you're going to need to change what the properties of your windows console is to do this I recommend you use the console as font set to size 16 and you choose the window size to be the dimensions of.

The screen size that we're using in this case 120 and 40 the other thing to do is you can set up the default so each time it pops up you don't have to keep setting these values so I've done this already here so 120 and 40 and set the font to something that I like how do we know that this is work it just looks like three lines well one thing to try will be to add the rotation so we need.

To be able to get the character to move now let's have some controls rotations are the easy one I'm using Lee get a sync key state function here which you'll have seen for my synthesizer videos it's a bit of a hack to use in a console but it works quite nicely and when the user presses in this case the a key I'm going to use the WASD layout standard in first-person shooters we.

Want to alter the players angle and we press the a key we want to rotate counterclockwise so we're decreasing the angle and we can put in a fixed amount to start with so let's try not point one and we need the opposite for rotating the other way but this time instead of decreasing we're going to increase and we'll change the key that we're using to a D let's see how that looks.

Well there's certainly something happening on the screen but I think it's all happening far too quickly so we can see there is a feature in there but it still doesn't really look like a wall why did it look so scrambled well we're turning at naught point 1 radians per frame update and the console framerate is actually going to be very quick one way to solve this would just be to use.

Very small values but that's not very tidy and something strange is going to happen because our computer is sharing resources with other programs we don't really know what speed it's going to run that so we need to time how fast one frame is and use that time to augment the variables for movement and this will give us then a consistent moving experience regardless.

Of what the computer is doing and to do this we're going to use our old friend and very wordy chrono library we're going to be measuring a duration so I need two time points and for each iteration of our game loop I want to grab the current system time calculate the duration between the current system time and the previous system time that's what I'm doing here updating the old.

Time point and I want to get the elapsed time as a floating point because all the rest our game engine is in floating point and we can take our standard movement speed and if we multiply that by the elapsed time for both of them this should give us a bit more consistency let's try that so I'm rotating and we can see it's a it's a much more smooth controlled movement.

This time still doesn't look like wall let's consult the animation again what's going on here well the walls are the further away are shaded darker than those that are close this is again a little illusion we'll need to do the same thing right now we're just shading anything that's a wall with this hash symbol that's not going to be any good for us I'm going to use a variable here.

Called end shade I'm setting it to space rather oddly because I don't know what the unicode number is for empty space and don't forget we're working with the console so rather than shading with the hash symbol we're going to need to change it depending on how far our rays from the wall so we're going to use this F distance to wall variable to work out a suitable shading character if we look.

At an ASCII table we can see there are actually some characters here that have shading already but we can't use these directly with Unicode so we're going to need the extended Unicode equivalence I know some of you'll be growing or it won't work on all computers and you won't work with every type of font but you know we're just having some fun so we can see.

Here the Unicode hex numbers for the different characters of the extended ASCII set just realize you wouldn't be able to see that on the video so I've zoomed in on it here so we can see there's a light shade a medium shade in a dark shade and there's also I believe which way I got to go there's also a fully shaded character and of course we've got space for a non shaded.

Character so this code here takes the distance of the wall and if it's very close we're using the nice and bright fully shaded character and as we get further and further away we're going less and less shaded and eventually we just have space if there's a if it's so far away we can't see the wall let's give this a go well we can see there's definitely a change in character but.

It's still not very interesting I think we need to be able to now move forward and backwards so we can get right up close to the wall and see if the shading is working I've added this code for when the character is walking forwards with the W key it effectively takes the unit vector that we calculated for the array before multiplies it to give it a magnitude only by the elapsed time so.

We're only walking forward consistently so it's just updating where the player position is and we can do exactly the same but instead of an addition we add a subtraction and we'll apply that to the S key no collision detection yet so I'm rotating the character and I'm walking forwards and as you can see as we approach forwards the walls get brighter but we're rotating very slowly here I'm.

Going to speed this up that feels a bit more fluid so even though it doesn't look like it just yet I can infer this must be the corner of the room because the furthest away and this bright section here must be the middle wall and that must be a far corner of the room and that's an even further corner of the room so we're starting to get some 3d perspective and.

This is quite nice because we stood near a wall and we can see the wall changes in gradients as it gets further away at this point we might want to add a few extra features to our map so we can see what's going on so I'm going to write a line down here and I'm going to put in a couple of blocks there so there's definitely a block right in front of me here now we can see it's got some.

Perspective and we can see around the edge of this block there's a wall in the background so it's starting to get a bit of a wolfenstein feel we'll walk to this corner of the room and look around and see the block is now shaded out again consulting the little animation we can see that the floor is also shaded I think this helps you brain work out what the perspective is if it tells it well.

That's going further away so we need to do the same let's look at where our floor is being shaded then we can see here it's actually just being shaded with a space at the moment we can make this a bit more sophisticated and we'll use very similar to code to what we had appreciating the walls in fact we'll use the same variable but we're going to give it a different value and so I'm.

Shading constantly I'm not bothered about what the distance to the wall is because it doesn't change when you're shading the floor the floor is always going to be the same distance away so I'm taking a proportion of how far the floor can be seen and as we get further and further away I'm shading it differently but rather than using the same shading blocks as the walls because.

I think that would just get confusing I've decided to use these symbols and I think this one is almost full this one's a little less full this one seems a little less vibrant and so forth let's give that a go okay so our floor is now shaded I think it does help egg with the illusion so take the character a bit more those our block that we put in the middle now.

She that stands out a lot more now this is our little tunnel that we put into the map and it's nice there we can see the wall that gradually comes towards the player nice as the graphical fidelity is quite low there's a good chance as at play we're going to get horribly lost and not work out where we are so I think we need to have some collision detection now the player.

Rotating can't walk into a wall so we only need to add collision detection when we're walking forwards and backwards adding collision detection is quite simple when we project the player forwards we want to test how we hit the wall so the easiest way to do this is to convert the players current coordinates into integer space and test this on our map okay because a 1.0 floating point.

Where our player is is the same as being in cell one so when he hits the wall if we take if we check that the cell contains a hash symbol we simply just undo what we've just done therefore the player ends up in the same place so whereas before walking forward progressed the vector we've hit the cell we just undo it we go back and we can do exactly the same for when we're walking.

Backwards but instead of correcting backwards this time we correct forwards so if I just plow forwards now I fit a wall I can't go through it I can rotate out if it's another wall can't go through it but of course it's just a big blank screen and that's why I think it's important that we have some collision detection if we add strafing then all we would need to do is instead of our unit.

Vector we want to find the inverse of that we want to find the tangent so we just flip the X's and Y's around now we can move the characters side-to-side I'll leave that as an exercise up for you guys but now I can't go into the cells can't get lost they also can't go out of bounds so we're getting there however it's still something's not quite right.

It seems that it's still difficult to work out where the walls are so I think we need to start adding boundaries this is going to be a bit complicated when we're doing our ray casting we detect if we've hit a wall or not we're also now going to need to detect if we've hit a boundary so I'm going to add another flag and what I mean by boundary is is it the edge of the cell now we don't.

Have any polygon information we just have a map with basically a 0 or 1 in it am i a wall or not and we assume with all of the cells at the same size so here I have a scenario where the player is looking at a wall and we've got our field of view as before and what I want is basically to delineate the walls by highlighting that that's a boundary and that's a boundary and that's a boundary.

This will give us some texture to the walls and we'll help we'll add to the 3d illusion if we go back to our little animation we can actually see that the shading here is perhaps based on the angle of the wall for the player and I think having the the blocks give you an indication of where in the room you can go now we're doing everything on a per ray basis so we have to do this.

Calculation on an individual array and our rays don't have knowledge of what's happened to their neighbors so this becomes an exercise on how do we detect whether our array has hit the corners of a block well I propose that we do it like this as the Rays being cast out from the the player as it's being searched for we know if we've got a hit that's how we know our distance to the.

Wall from this hit we can identify the coordinates of the cell because we converse our floating points into integer space once we know the cell if we trace a line the precise corner back to the player for all of the corners so the before rays drawn from each corner of the cell we observe that the Ray that's being cast out is actually a.

Very tight angle is very very acute here so let's make an assumption that if the Ray from a perfect corner of the cell is very close to the Ray angle being cast for the player the perhaps we're looking at the corner and this is where things now get a little bit tricky because what we can see here quite clearly is if we cast from this corner back to the player and we cast from this corner back to the.

Player for those two cells we calculate that this is also very tight angle so this corner gets seen which is clearly impossible because it's behind the front of the cell so we need to calculate what are the two closest points so just to reiterate we're going to cast array from each perfect corner of the cell back to the player we're going to look at the angle between the Ray that's been cast.

Out and that perfect ray back and we want the two closest ie the two smallest angles to represent the boundary of our cell and these are the ones we're going to draw differently solely this was complicated we know that we've hit the wall so we need to do a test and we're going to create a vector that's going to accumulate the four corners and in this vector we're going to store two things.

As a pair we're going to store the distance to the perfect corner and we're also going to store what is going to be the dot product so basically the angle between the two vectors because we need to sort this vector later on to tell us which are the two closest so we're going to sort it based on distance we've got four corners to try so I'm going to have two tight loops one inside the other.

Just to give us our offsets and the easiest way to do this is just to create a vector from the perfect corners remember this is an integer so r1 plus in our loop so we're getting the perfect integer corners offset from our player position this will give us just a vector from the player to the perfect corner then I want to calculate the magnitude of that vector so I know how far away.

The corner is from the player and at the same time I may as well calculate the dot product so this is going to be a representation of the angle between the very being cast at the moment which is remember our unit vector from before s IX and f iy it's the dot product between our unit vector and basically the unit vector of our perfect corner and I want to assemble that into a pair and push it.

Into our vector now thanks to modern C the easiest way to source a vector is to use a big nasty lambda function and if you look at my eight bits of C++ code you should know video there's quite a detailed explanation of lambda functions but I'll talk you through this one so we take our vector and we're calling the algorithm utility sort which takes a starting point ie the whole vector in.

This case from beginning to end and here we've got our little lambda function and the lambda function takes a type of pair because we want to compare our purse based on distance we're going to sort this from closest point to the furthest point away and our lambda function takes in the two pairs as arguments and does a simple boolean test is one smaller than the other and it's based on the first.

Element of our pair which is the distance in this case D up here now this angle that we're going to be testing is a little bit subjective so I'm going to make it a variable that we can play with naught point naught 5 radians and the calculation that we want to do is want to take the inverse cosine of the second part of our pair for our vector remember our vectors are sorted I know I know.

This is getting worse and worse isn't it now vectors are now sorted so the second element of the vector was the dot product we calculated and if you take the inverse cosine of the dot product you get the angle between the two rays and what we're looking for is now very small angles and if that angle is less than what we specified here then we can assume the the Rays hit the boundary of.

The cell now we only need to really test for the first two or three because you'll never see all four in a normal Cartesian space projected normally so let's have a look at what this looks like oops really may have gone too far ahead we've not done anything if it is a boundary so let's shade in the wall differently if it's a boundary I'm going.

To do this very soon if we have hit a boundary for this column I'm going to black it out but I think just for now I'm going to make it stand out more so I'm going to set it with an eye so we can see where does it think the boundaries are if we run this Wow lots of eyes all over the place so it thinks there's boundaries everywhere it's kind of working so we can see that.

There's an individual cell here but there's far too many columns being attributed to being a boundary so it just looks a bit of a mess let's adjust our bounce parameter here let's get this nice and narrow so we've not given it much scope for for error really the Ray has to hit the perfect corner for it to be seen that looks a bit better so it's on on average.

Attributing three boundary columns to each cell let's go and try and find our features in the room there we are so we can see our two cell feature here it's nice it's found the corner quite sharply there and if we look at it from a slightly odd perspective it's found the corner but it's not found this corner here let's see if we can do something about.

That and that's because we're really only displaying the first two that comes back and we could technically see three edges to a cube in 3d space stow that in I look at the same feature and now we can see it's highlighting that corner that corner and that corner of this one cell in the middle let's change you from the eye that's a bit distracting we'll put that back as a.

Space and now it looks like it's part of the texture of the room very nice Oh even even better it kind of gives this bonus effect that it looks like shadow and that's because we're almost parallel with the plane of this row here and so a lot of those the boundary calculations will come back as being a hit it's hit the boundary so we make the angle a little bit more obtuse you can see they.

Don't bunch up quite as much and that's a really nice little 3d touch I think and we can also get a feeling for the amount of space so we can see that this corridor is two cells wide but the wall is only one cell wide ah there's interesting thing because we've got it showing the three closest points this but what looks like a narrowness black line is actually the other side of the.

Cube so that's something to think about I perhaps just set it back to drawing the first two I'll leave that up to you still quite easy to get lost in our maze so I think 11 that will not just add a map will also add some stat telling us where our player is what the angle is currently facing and also the frame rating it's a first-person shooter can it run Crysis yes it can and to get the.

Framerate of course we're measuring the elapsed time per frame but if we remember that the F equals one over T so frequency equals one over the elapsed time that's the equivalent their frame rate and drawing a map is nice and simple we're just going to go through the coordinates of our map and directly put them into the the screen buffer I've offset it here by one because I.

Don't want to overwrite the stats that we've just put out at the top because I've not specified a coordinate here our stats are just displayed at 0 0 I just want to add one final touch I'm going to add a marker to show where the player is so I'm going to draw the letter P somewhere in our map based on the integer versions of our player coordinates just drawn in on top have a.

Look so as I'm approaching the wall can turn around I can see my little player here if I put in near the edge of this column we see lines up nicely that's good and we can navigate the maze and it's running at a nice 300 frames per second and that don't forget I'm also running my screen capture software and we're running in debug mode let's.

Crank this out in release and see what happens so Wow release mode has come up to almost 7,000 frames per second and we'll see that it fluctuates as the as the Rays have further to go before they hit anything more calculations have to happen per column this is why we added the elapsed time because even though these frame rate is varying the perceived movement of the player doesn't.

Change very nice all I need to do now is add some bad guys and some bullets but I'm not going to do that in this video I've gone on long enough so there you have it the first-person shooter at the command line I know it's dark in a bit silly and it looks a bit crude but it got me thinking when Wolfenstein came out it revolutionized how people thought about doing game engines but I believe.

That it's quite clearly possible to do this approach on on old computers it must be that nobody is thought about doing it released not to my knowledge most dungeon crawlers are very static and quite boring at first where everything was at 90 degrees and you took one tile at the time as you crawl through the dungeon I'd be very interested to see if anybody out there.

Could port this over to an old machine and see how well it runs perhaps we have far more advanced first person shooters today how they had the 5 year head start using a very similar technology anyway if you like this video please give me a thumbs up subscribe spread the word and the code is all available on github from the link below take it do what you want with it hack it have fun.


Нет комментариев!