Introduction to programming: a simple 3D shooter from scratch over the weekend, part 2

• Tutorial
We continue to talk about the 3D shooter over the weekend. If anything, I remind you that this is the second half:

As I said, I strongly support the desire in the students to do something with their own hands. In particular, when I read a course of lectures on introduction to programming, then as practical classes I leave them almost complete freedom. There are only two limitations: a programming language (C ++) and a project theme; this should be a video game. Here is an example of one of the hundreds of games that my freshman students have done:

Unfortunately, most students choose simple games like 2D platformers. I am writing this article in order to show that creating the illusion of a three-dimensional world is no more difficult than cloning Mario Broz.

I remind you that we stopped at the stage that allows you to texture the walls:

Stage 13: draw monsters on the map

What is a monster in our game? These are its coordinates and texture number:

``````structSprite {float x, y;
size_t tex_id;
};
[..]
std::vector<Sprite> sprites{ {1.834, 8.765, 0}, {5.323, 5.365, 1}, {4.123, 10.265, 1} };
``````

Having identified several monsters, for a start, simply draw them on the map: You can see the

Stage 14: black squares instead of monsters in 3D

Now we will draw the sprites in the 3D window. To do this, we need to determine two things: the position of the sprite on the screen and its size. Here is the function that draws a black square in place of each sprite:

``````voiddraw_sprite(Sprite &sprite, FrameBuffer &fb, Player &player, Texture &tex_sprites){
// absolute direction from the player to the sprite (in radians)float sprite_dir = atan2(sprite.y - player.y, sprite.x - player.x);
// remove unnecessary periods from the relative directionwhile (sprite_dir - player.a >  M_PI) sprite_dir -= 2*M_PI;
while (sprite_dir - player.a < -M_PI) sprite_dir += 2*M_PI;
// distance from the player to the spritefloat sprite_dist = std::sqrt(pow(player.x - sprite.x, 2) + pow(player.y - sprite.y, 2));
size_t sprite_screen_size = std::min(2000, static_cast<int>(fb.h/sprite_dist));
// do not forget the 3D view takes only a half of the framebuffer, thus fb.w/2 for the screen widthint h_offset = (sprite_dir - player.a)*(fb.w/2)/(player.fov) + (fb.w/2)/2 - sprite_screen_size/2;
int v_offset = fb.h/2 - sprite_screen_size/2;
for (size_t i=0; i<sprite_screen_size; i++) {
if (h_offset+int(i)<0 || h_offset+i>=fb.w/2) continue;
for (size_t j=0; j<sprite_screen_size; j++) {
if (v_offset+int(j)<0 || v_offset+j>=fb.h) continue;
fb.set_pixel(fb.w/2 + h_offset+i, v_offset+j, pack_color(0,0,0));
}
}
}
``````

Let's understand how it works. Here is the diagram:

In the first line we consider the absolute angle sprite_dir (the angle between the direction from the player to the sprite and the x-axis). The relative angle between the sprite and the direction of gaze is obviously obtained by simply subtracting two absolute angles: sprite_dir - player.a. The distance from the player to the sprite is trivial to count, and the size of the sprite is a simple division of the screen size by distance. Well, just in case, I cut off two thousand from above, so as not to get giant squares (by the way, this code can easily be divided by zero). h_offset and v_offset give the coordinates of the upper left corner of the sprite on the screen; then a simple double loop fills our square with black. Check with a pen and a piece of paper for the correctness of the calculations of h_offset and v_offset, in my commit (non-critical) error, to believe the code in the article :) Well, the more recent code in the repository is also already fixed.

Changes you can see here .

Stage 15: Depth Map

Our squares are a miracle, but only one problem: the distant monster looks around the corner, and the square is drawn entirely. How to be? Very simple. We draw sprites after the walls have been painted. Therefore, for each column of our screen, we know the distance to the nearest wall. Let's save these distances to an array of 512 values, and pass an array of the sprite drawing function. Sprites are also drawn column by column, so for each column of the sprite we will compare the distance to it with the value from our depth array.

Changes you can see here .

Step 16: Sprites Problem

Excellent turned out monsters, is not it? But at this stage I will not add any functionality, on the contrary I’ll break everything by adding another monster: You can see the

Step 17: Sorting Sprites

What was the problem? The problem is that I can have an arbitrary order of rendering sprites, and for each of them I compare its distance with the walls, but not with other sprites, so the distant creature came out on top of the nearest one. Is it possible to adapt a solution with a depth map for drawing sprites?

Hidden text
Правильный ответ: «можно». А вот как? Пишите в комментариях.

I will go another way, solving the problem stupidly in the forehead. I'll just draw all the sprites from the farthest to the nearest. That is, I will sort the sprites in descending order of distance, and draw them in that order.

Changes you can see here .

Stage 18: SDL Time

It's time to SDL. Cross-platform window libraries are very many different, and I don’t understand them at all. Personally, I like imgui , but for some reason my students prefer SDL, so I link with it. The task for this stage is very simple: create a window and display an image of it from the previous stage:

The changes made can be viewed here . I do not give a link to the guitar pod anymore, because SDL in the browser has not yet learned how to run :(

Update: LEARNED! You can run the code in one click in the browser!

Step 19: Event Processing and Purge

Add a response to keystrokes is not even funny, I will not describe. When adding SDL, I removed the dependency on stb_image.h. It is beautiful, but it takes a long time to compile.

For those who do not understand, the source code of the nineteenth stage lies here . Well, here is what a typical performance looks like:

Conclusion

My code at the moment contains only 486 lines, and at the same time I did not save them at all:

``````haqreu@daffodil:~/tinyraycaster\$ cat *.cpp *.h | wc -l
486
``````

I did not lick my code, deliberately dumping dirty laundry. Yes, I write like that (and I'm not the only one). One Saturday morning, I just sat down and wrote this :)

I didn’t do a complete game, my task is to just give an initial impulse for the flight of your fantasy. Write your own code, it will certainly be better than mine. Share your code, share your ideas, send pull requests.