Pengantar pemrograman: penembak 3D sederhana dari awal selama akhir pekan, bagian 1

Teks ini ditujukan bagi mereka yang baru menguasai pemrograman. Gagasan utamanya adalah untuk menunjukkan langkah demi langkah bagaimana Anda dapat secara mandiri membuat game à la Wolfenstein 3D . Perhatian, saya tidak akan bersaing dengan Carmack sama sekali, dia jenius dan kodenya indah. Saya bertujuan di tempat yang sama sekali berbeda: saya menggunakan kekuatan komputasi yang besar dari komputer modern sehingga siswa dapat membuat proyek lucu dalam beberapa hari tanpa terjebak dalam optimisasi. Saya secara khusus menulis kode lambat, karena jauh lebih pendek dan lebih mudah dipahami. Carmack menulis 0x5f3759df , saya menulis 1 / sqrt (x). Kami memiliki tujuan yang berbeda.

Saya yakin bahwa seorang programmer yang baik hanya diperoleh dari seseorang yang kode di rumah untuk kesenangan, dan tidak hanya duduk berpasangan di universitas. Di universitas kami, programmer diajarkan pada serangkaian tak berujung dari semua jenis katalog perpustakaan dan kebosanan lainnya. Sdr Tujuan saya adalah untuk menunjukkan contoh proyek yang menarik untuk diprogram. Ini adalah lingkaran setan: jika itu menarik untuk membuat proyek, maka seseorang menghabiskan banyak waktu di sana, mendapatkan pengalaman, dan melihat hal-hal yang lebih menarik di sekitarnya (itu telah menjadi lebih mudah diakses!), Dan sekali lagi tenggelam dalam proyek baru. Ini disebut pelatihan proyek, sekitar untung besar.

Lembar itu ternyata panjang, jadi saya memecah teks menjadi dua bagian:


Eksekusi kode dari repositori saya terlihat seperti ini:


Ini bukan permainan yang sudah selesai, tetapi hanya kosong untuk siswa. Contoh permainan selesai yang ditulis oleh dua mahasiswa baru, lihat bagian kedua .

Ternyata saya sedikit menipu Anda, saya tidak akan memberi tahu Anda cara membuat permainan penuh dalam satu minggu. Saya hanya membuat mesin 3D. Monster tidak berlari ke arahku, dan karakter utama tidak menembak. Tapi setidaknya saya menulis mesin ini dalam satu hari Sabtu, Anda dapat memeriksa riwayat komit. Pada prinsipnya, hari Minggu sudah cukup untuk membuat sesuatu dapat dimainkan, yaitu, satu akhir pekan yang dapat Anda temui.

Pada saat penulisan ini, repositori berisi 486 baris kode:

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

Proyek ini tergantung pada SDL2, tetapi secara umum antarmuka jendela dan pemrosesan acara dari keyboard muncul sangat terlambat, pada tengah malam pada hari Sabtu :), ketika semua kode render telah dilakukan.

Jadi, saya memecah semua kode menjadi langkah-langkah, dimulai dengan kompiler C ++ kosong. Seperti dalam artikel saya sebelumnya pada jadwal ( tyts , tyts , tyts ), saya mematuhi aturan "satu langkah = satu komit", karena github membuatnya sangat nyaman untuk melihat sejarah perubahan kode.

Tahap 1: menyimpan gambar ke disk


Jadi ayo pergi. Kami masih sangat jauh dari antarmuka jendela, sebagai permulaan kami hanya akan menyimpan gambar ke disk. Total, kita harus dapat menyimpan gambar di memori komputer dan menyimpannya ke disk dalam format yang dimengerti oleh beberapa program pihak ketiga. Saya ingin mendapatkan file ini:



Ini adalah kode C ++ lengkap yang menggambarkan apa yang kita butuhkan:

 #include <iostream> #include <fstream> #include <vector> #include <cstdint> #include <cassert> uint32_t pack_color(const uint8_t r, const uint8_t g, const uint8_t b, const uint8_t a=255) { return (a<<24) + (b<<16) + (g<<8) + r; } void unpack_color(const uint32_t &color, uint8_t &r, uint8_t &g, uint8_t &b, uint8_t &a) { r = (color >> 0) & 255; g = (color >> 8) & 255; b = (color >> 16) & 255; a = (color >> 24) & 255; } void drop_ppm_image(const std::string filename, const std::vector<uint32_t> &image, const size_t w, const size_t h) { assert(image.size() == w*h); std::ofstream ofs(filename); ofs << "P6\n" << w << " " << h << "\n255\n"; for (size_t i = 0; i < h*w; ++i) { uint8_t r, g, b, a; unpack_color(image[i], r, g, b, a); ofs << static_cast<char>(r) << static_cast<char>(g) << static_cast<char>(b); } ofs.close(); } int main() { const size_t win_w = 512; // image width const size_t win_h = 512; // image height std::vector<uint32_t> framebuffer(win_w*win_h, 255); // the image itself, initialized to red for (size_t j = 0; j<win_h; j++) { // fill the screen with color gradients for (size_t i = 0; i<win_w; i++) { uint8_t r = 255*j/float(win_h); // varies between 0 and 255 as j sweeps the vertical uint8_t g = 255*i/float(win_w); // varies between 0 and 255 as i sweeps the horizontal uint8_t b = 0; framebuffer[i+j*win_w] = pack_color(r, g, b); } } drop_ppm_image("./out.ppm", framebuffer, win_w, win_h); return 0; } 

Jika Anda tidak memiliki kompiler, maka ini tidak masalah, jika Anda memiliki akun di github, Anda dapat melihat kode ini, mengeditnya dan menjalankan (sic!) Dalam satu klik langsung dari browser.

Buka di gitpod

Mengikuti tautan ini, gitpod akan membuat mesin virtual untuk Anda, meluncurkan VS Code, dan membuka terminal pada mesin jarak jauh. Dalam sejarah perintah terminal (klik pada konsol dan tekan panah ke atas), sudah ada seperangkat perintah lengkap yang memungkinkan Anda untuk mengkompilasi kode, menjalankannya, dan membuka gambar yang dihasilkan.

Jadi apa yang perlu Anda pahami dari kode ini. Pertama, warna yang saya simpan dalam tipe integer empat byte uint32_t. Setiap byte adalah komponen R, G, B atau A. Fungsi pack_color () dan unpack_color () memungkinkan Anda untuk mendapatkan masing-masing komponen masing-masing warna.

Gambar kedua, dua dimensi, saya simpan dalam array satu dimensi yang biasa. Untuk sampai ke pixel dengan koordinat (x, y) saya tidak menulis gambar [x] [y], tetapi saya menulis gambar [x + y * lebar]. Jika metode pengemasan informasi dua dimensi ke dalam array satu dimensi ini baru bagi Anda, maka sekarang ambil pena dan tangani. Bagi saya pribadi, tahap ini bahkan tidak mencapai otak, itu diproses langsung di sumsum tulang belakang. Array tiga dan lebih dimensi dapat dikemas dengan cara yang persis sama, tetapi kita tidak akan naik di atas dua komponen.

Kemudian saya menjalankan gambar saya dalam siklus ganda sederhana, mengisinya dengan gradien, dan menyimpannya ke disk dalam format .ppm.



Tahap 2: menggambar peta level


Kita membutuhkan peta dunia kita. Pada titik ini, saya hanya ingin menentukan struktur data dan menggambar peta di layar. Seharusnya terlihat seperti ini:



Perubahannya bisa Anda lihat di sini . Semuanya sederhana di sana: Saya mengubah kode peta menjadi array karakter satu dimensi, mendefinisikan fungsi menggambar persegi panjang, dan berjalan di sekitar peta, menggambar setiap sel.

Saya mengingatkan Anda bahwa tombol ini akan meluncurkan kode tepat pada tahap ini:

Buka di gitpod



Tahap 3: tambahkan pemain


Apa yang kita butuhkan untuk bisa menggambar pemain di peta? Koordinat GPS sudah cukup :)



Tambahkan dua variabel x dan y, dan gambar pemain di tempat yang sesuai:



Perubahannya bisa Anda lihat di sini . Tentang gitpod saya tidak akan mengingatkan lagi :)

Buka di gitpod



Tahap 4: alias pengintai ray jejak pertama virtual


Selain koordinat pemain, alangkah baiknya untuk mengetahui ke arah mana dia mencari. Oleh karena itu, kami menambahkan variabel lain player_a, yang memberikan arah pandangan pemain (sudut antara arah pandangan dan sumbu absis):



Dan sekarang saya ingin bisa meluncur di sepanjang sinar oranye. Bagaimana cara melakukannya? Sangat sederhana. Mari kita lihat segitiga kanan hijau. Kita tahu bahwa cos (player_a) = a / c, dan bahwa dosa (player_a) = b / c.



Apa yang terjadi jika saya secara sewenang-wenang mengambil nilai c (positif) dan menghitung x = player_x + c * cos (player_a) dan y = player_y + c * sin (player_a)? Kita akan menemukan diri kita pada titik ungu; dengan memvariasikan parameter c dari nol hingga tak terbatas, kita dapat membuat slide titik ungu ini di sepanjang sinar oranye kita, dan c adalah jarak dari (x, y) ke (player_x, player_y)!

Inti dari mesin grafis kami adalah siklus ini:

  float c = 0; for (; c<20; c+=.05) { float x = player_x + c*cos(player_a); float y = player_y + c*sin(player_a); if (map[int(x)+int(y)*map_w]!=' ') break; } 

Kita memindahkan titik (x, y) di sepanjang sinar, jika ia menemukan rintangan pada peta, maka kita mengakhiri siklus, dan variabel c memberikan jarak ke rintangan! Apa itu bukan pengintai laser?



Perubahannya bisa Anda lihat di sini .

Buka di gitpod



Tahap 5: Tinjauan Sektor


Satu balok baik-baik saja, tetapi mata kita masih melihat seluruh sektor. Mari kita sebut sudut pandang fov (bidang tampilan):



Dan mari kita lepaskan 512 sinar (omong-omong, mengapa 512?), Dengan lancar menyapu seluruh sektor tampilan:


Perubahannya bisa Anda lihat di sini .

Buka di gitpod



Tahap 6: 3D!


Dan sekarang titik kuncinya. Untuk masing-masing dari 512 sinar, kami mendapat jarak ke rintangan terdekat, kan? Dan sekarang mari kita membuat gambar kedua dengan lebar 512 piksel (spoiler); di mana untuk setiap ray kita akan menggambar satu segmen vertikal, dan ketinggian segmen berbanding terbalik dengan jarak ke rintangan:



Sekali lagi, ini adalah kunci untuk menciptakan ilusi 3D, pastikan Anda memahami apa yang dipertaruhkan. Menggambar segmen vertikal, sebenarnya, kita menggambar pagar piket, di mana ketinggian masing-masing pasak lebih kecil, semakin jauh dari kita:



Perubahannya bisa Anda lihat di sini .

Buka di gitpod



Tahap 7: Animasi Pertama


Pada tahap ini, untuk pertama kalinya, kami menggambar sesuatu yang dinamis (saya hanya memasukkan 360 gambar ke disk). Semuanya sepele: Saya mengubah player_a, menggambar, menyimpan, mengubah player_a, menggambar, menyimpan. Untuk membuatnya sedikit lebih menyenangkan, saya menetapkan nilai warna acak untuk setiap jenis sel di peta kami.


Perubahannya bisa Anda lihat di sini .

Buka di gitpod



Tahap 8: koreksi mata ikan


Apakah Anda memperhatikan betapa besar efek mata ikan yang kita dapatkan ketika kita melihat dinding dari dekat? Ini terlihat seperti ini:



Mengapa Ya, sangat sederhana. Di sini kita melihat tembok:



Untuk menggambar dinding kita, kita menyoroti sektor tampilan biru kita dengan sinar ungu. Ambil nilai spesifik dari arah balok, seperti pada gambar ini. Panjang segmen oranye jelas kurang dari panjang ungu. Karena untuk menentukan ketinggian setiap segmen vertikal yang kita gambar di layar, kita membagi dengan jarak ke penghalang, mata ikan itu cukup alami.

Untuk memperbaiki distorsi ini tidak sulit sama sekali, lihat bagaimana ini dilakukan . Pastikan Anda mengerti dari mana asal cosinus. Menggambar diagram pada selembar kertas sangat membantu.



Buka di gitpod



Langkah 9: memuat file tekstur


Saatnya berurusan dengan tekstur. Saya malas menulis pengunduh gambar sendiri, jadi saya mengambil pustaka stb yang bagus. Saya menyiapkan file dengan tekstur untuk dinding, semua tekstur persegi dan dikemas secara horizontal dalam gambar:



Pada titik ini, saya hanya memuat tekstur ke dalam memori. Untuk menguji kode tertulis, saya cukup menggambar karena tekstur dengan indeks 5 di sudut kiri atas layar:


Perubahannya bisa Anda lihat di sini .

Buka di gitpod



Tahap 10: penggunaan tekstur yang belum sempurna


Sekarang saya membuang warna yang dihasilkan secara acak dan mewarnai dinding saya dengan mengambil piksel kiri atas dari tekstur yang sesuai:


Perubahannya bisa Anda lihat di sini .

Buka di gitpod



Tahap 11: membuat tekstur dinding menjadi nyata


Dan sekarang saat yang ditunggu-tunggu telah tiba ketika kita akhirnya melihat dinding bata:



Gagasan dasarnya sangat sederhana: di sini kita meluncur di sepanjang sinar saat ini dan berhenti pada titik x, y. Mari kita asumsikan bahwa kita menetap di dinding "horizontal", maka y hampir bilangan bulat (tidak juga, karena cara kita bergerak di sepanjang sinar memperkenalkan kesalahan kecil). Mari kita ambil bagian pecahan dari x dan menyebutnya hitx. Bagian fraksinya kurang dari satu, oleh karena itu, jika kita mengalikan hitx dengan ukuran tekstur (saya punya 64), maka ini akan memberi kita kolom tekstur yang perlu digambar di tempat ini. Tetap meregangkannya ke ukuran yang tepat dan benda itu ada di topi:



Secara umum, idenya sangat primitif, tetapi membutuhkan eksekusi yang hati-hati, karena kami juga memiliki dinding "vertikal" (yang dengan hitx mendekati nol [x integer]). Bagi mereka, kolom tekstur ditentukan oleh hity, bagian fraksional dari y. Perubahannya bisa Anda lihat di sini .

Buka di gitpod



Tahap 12: waktu untuk refactor!


Pada tahap ini, saya tidak melakukan sesuatu yang baru, saya baru saja mulai membersihkan secara umum. Sampai sekarang, saya punya satu file raksasa (185 baris!), Dan menjadi sulit untuk bekerja di dalamnya. Oleh karena itu, saya memecahnya menjadi awan yang kecil, sayangnya, secara sepintas, hampir menggandakan ukuran kode (319 baris), tanpa menambahkan fungsionalitas apa pun. Tapi kemudian menjadi lebih nyaman untuk digunakan, misalnya, untuk menghasilkan animasi, cukup membuat loop seperti itu:

  for (size_t frame=0; frame<360; frame++) { std::stringstream ss; ss << std::setfill('0') << std::setw(5) << frame << ".ppm"; player.a += 2*M_PI/360; render(fb, map, player, tex_walls); drop_ppm_image(ss.str(), fb.img, fb.w, fb.h); } 

Nah, inilah hasilnya:


Perubahannya bisa Anda lihat di sini .

Buka di gitpod

Untuk dilanjutkan ... segera


Pada catatan optimis ini, saya menyelesaikan separuh lembar saya saat ini, separuh kedua tersedia di sini . Di dalamnya kami akan menambahkan monster dan tautan ke SDL2 sehingga Anda dapat berjalan-jalan di dunia virtual kami.

Source: https://habr.com/ru/post/id439698/


All Articles