Devlog lamera, albo wyważanie otwartych drzwi

BLOG
534V
Devlog lamera, albo wyważanie otwartych drzwi
Rankin | 19.09.2016, 09:09
Poniżej znajduje się treść dodana przez czytelnika PPE.pl w formie bloga.

W roku pańskim 2016 mamy mnóstwo narzędzi pozwalających na szybkie i wygodne tworzenie gier 3D. Jest Unreal Engine, jest Unity, są i inne rzeczy.

Więc oczywiście zawsze znajdzie się jakiś idiota próbujący odtworzyć silnik Wolfensteina 3D w pascalu czy innym basicu.

Programowanie luźno interesuje mnie od bardzo dawna, choć sukcesów w tym polu nigdy nie odnosiłem. Ot, rozległy program czy dwa w AMOS (wersja darmowa, czyli lekko okrojona i bez kompilowania), wieloletnie dłubanie w QBasic (pewnie dlatego że to jedyny język do którego mam podręcznik/obszerny kurs), oraz krótkie romanse z TurboPascalem stanowiły całość mojego doświadczenia przez pierwsze -naście lat.

Kilka lat temu jednak, w apogeum mojego zainteresowania roguelike'ami wpadłem na rzecz zwaną Let's Build a Roguelike. Jest obszerny przewodnik z objaśnieniami tłumaczący jak stworzyć dość nieskomplikowanego przedstawiciela tego gatunku w języku FreeBasic. FreeBasic jest nowoczesnym rozwinięciem starego QBasica, a więc językiem, który w znacznej mierze już znałem, a czego nie znałem, szybko się douczyłem dzięki dostępnym materiałom typu beginner's guide. Tak uzbrojony zdecydowałem się nie odtwarzać po prostu kroków, a wykręcić własny wariant na podstawie mechanik zawartych w LBaR. Projekt ten, zwany kreatywnie ShadowSpace, osiągnął całkiem sympatyczne, grywalne stadium (brakowało jedynie więcej contentu i paru mechanik typu sklep/konstrukcja), zanim nie wpadłem na pomysł na jeszcze inny projekt RL, którego nigdy więcej nie rozwinąłem ze względów m.in. IRL. Nie, serio ._.

ShadowSpace w swoim ostatnim buildzie (download link)

Parę lat później wpadłem na inną rzecz, FPS engine in 265 lines. Po obczajeniu rzeczy moim pierwszym instynktem było zaadaptować tę imprezę na FB i wykorzystać do napędzania nowej iteracji ShadowSpace. Wtedy jednak po paru tygodniach nieudanych prób straciłem zainteresowanie. Czyszcząc jednak HDD parę miesięcy temu wpadłem na pliki projektu, działający stary build SS i ruszony nostalgią do dłubania w dim as any ptr i innych type chara znowu podszedłem do reimplementacji javascriptowego silnika. Po kolejnych paru tygodniach prób efekt był taki, że skutecznie znienawidziłem javascript, a także nabyłem przekonania że jedynie w tak niedorobionym języku jak js dało radę wykonać taki bajzel kodu. Sama idea silnika raycastowego jednak zafascynowała mnie nawet bardziej.

Silnik raycastowy FPP jest automobilowym odpowiednikiem komarka albo innej warszawy - jest bardzo archaiczny, ale jednocześnie prosty do zrozumienia i rozwinięcia. Operując na nieskomplikowanym gridzie doskonale lubi się z mapową strukturą typowego rogalika. Działa na takiej zasadzie że pionowy pasek po pasku rzucany jest promień (stąd ray cast), który "leci" aż trafi w ścianę. Na podstawie tego, jak daleko zdołał ulecieć obliczana jest wysokość rysowanej ściany, zgodnie z perspektywą im dalej tym ściana mniejsza. Problem zasadza się w kwestii tego, jak dokładnie liczyć promienie tak, aby nie zajęło to zbyt wiele czasu, oraz jak te obliczenia przełożyć na spójny obraz bez deformacji. Szukając materiałów online (a jakże) wpadłem na genialną rzecz: raycasting tutorial dla języka c++, który zawierał odpowiedzi na te pytania, a także mnóstwo innych, jakie nawet nie wiedziałem że mam. Ponadto c++ jest niewiarygodnie bardziej czytelny od js, no i specyfika freebasic oznaczała że w najgorszym razie mogłem bezpośrednio zainterfejsować kod, bez konwersji. Projekt FPP został wskrzeszony. Po tygodniu grzebania miałem pierwsze efekty, w natywnym kodzie FB renderowała się scena ze ścianami pokrytymi jednolitym kolorem:

o, tak

Problemem jednak było dziwaczne zachowanie sterowania, generalnie silnik nie reagował na strzałki tak jak powinien, zamiast tego wykręcając dziwne tańce. W końcu okazało się że po prostu popełniłem durny błąd - literówkę. Po odkryciu i rozgnieceniu tego buga mogłem rozwinąć podstawy projektu. Z prostego random z szansą 0.3 wymieniłem generator mapy na ten z ShadowSpace (plus kilka tweaków, na dłuższą metę kulawych), dodałem strafe (którego w tutorialu nie ma), dodałem cieniowanie w funkcji odległości. Tak zakończony pierwszy etap silnika ważył po skompilowaniu 123 kilobajty i w rozdziałce 800x600 jechał z prędkością około 80 klatek na sekundę:

Czasy sielanki jednak szybko się kończyły, gdyż rozbestwiony dobrymi wynikami zająłem się kolejnym etapem - teksturowaniem ścian. Tu już nie było tak łatwo, musiałem znaleźć metodę na szybkie pobieranie koloru dowolnego piksela z tekstury. Okazało się, że FB ma kilka narzędzi pozwalających na takie zabawy, jednak w tym miejscu musiałem już osobiście zająć się dostępem do pamięci (Dim as UInteger Ptr, Dim as ScreenPtr i podobne). Wyniki osiągnąłem po paru ledwo dniach:

Mimo, że taka metoda jest cholernie szybka, nie była dostatecznie szybka. Raycasting działa wyłącznie w sofcie i polega na sile obliczeniowej procesora (jednego rdzenia!). Wolfenstein 3D działał przecież w rozdzielczości ekranu 320x200, a wewnętrznie castował może nawet w mniejszej! Dlatego został dość szybko wyparty przez nowsze, inteligentniejsze rozwiązania, które też bez kilkudziesięciu megaherców na pokładzie nie szalały. Było to impulsem (ImpulseM cha cha cha) do wprowadzenia pierwszej rundy optymalizacji. Efekt był taki, że miałem fpp.ini, który zawierał rozdzielczość ekranu, a także "rendering resolution", wskazujący z jaką dokładnością robiło się castowanie (1-co do piksela, 2-co dwa piksele itd.). Brudny hack, ale działał póki co zadowalająco. Mogłem przejść do kolejnego etapu, floor casting - renderowania podłogi i sufitu (swoją drogą dość skomplikowanego). Implementacja wyszła, ale tutaj brudny hack na szybkość okazał się być nieadekwatny i podszedłem do sprawy po raz drugi. Kilka programów testowych na szybkość działań array/pointer/bufor/Point() różnymi metodami wpadłem na pomysł z którego nieomal byłem dumny. [nerd talk] Ponieważ ScreenPtr, najszybsza w takim zastosowaniu metoda rysowania na ekranie najlepiej działała z dostępem sekwencyjnym (czyli 1,2,3,4,5,6, a nie np 1, 3, 6), zamiast powielania pikseli przy każdym indywidualnym pasku (raz dla ścian i drugi raz dla podłóg jeszcze) wprowadziłem wirtualny bufor o rozdzielczości castingu, do którego szły obliczenia raycastowe, po czym zostawał błyskawicznie mnożony przy przepisywaniu na ekran. Zamiast dłubania [3, obok też 3, poniżej też 3 i 3, 5, obok 5...] w miarę napływania wyników castowania, program leci błyskawicznie [2x 1, 2x 2, 2x 3, poniżej tak samo, kolejna linijka]. Program zaczął śmigać nawet na dość dużych rozdziałkach typu 1400x800, wyglądając tak:

(na lewo render z dokładnością 1, na prawo toż samo z dokładnością 4, kliknij aby zobaczyć pełny obraz)

Próbując przyspieszyć sprawę jeszcze bardziej ("a może uda się skopiować od razu cały rządek?") "wynalazłem" jeszcze efekt scanlines, który przy okazji faktycznie przyspieszał biznes:

(klik)

Dla jaj zrobiłem z tego efekt śnieżenia stareńkich monitorów monochromatycznych, który nie spowalniał sprawy:

(tyż klik)

Efekt ten był mi chwilowo zbędny, więc go wycofałem, ale w przyszłości kto wie. "Scanlines" mają jeszcze taką przewagę, że w pewnym stopniu maskują kanciastość wynikającą z redukcji rozdzielczości castingu.

Nadszedł wreszcie ostatni weekend. Pod kątem przyszłej integracji z różnymi programami (to już prawie silnik!) sprawiłem jeszcze, że gra zamiast czytać każdą teksturę z osobna, czyta jeden plik z teksturami podłogi, jeden ścian i jeden z sprite'ami (których jeszcze nie widać). Na tym etapie jednak wychyliły głowę bugi (yay). Przede wszystkim, jak widać na ekranach powyżej, kafelki podłogi rozjeżdżają się z gridem i ścianami. Same ściany też nieco się rozjeżdżały w bok i lekko w górę/dół. Po około półgodzinie testów i guglowania doszedłem do sedna. Okazało się że biorąc dane tekstur bezpośrednio z pamięci (ekran[x+y*szer]=tekstura[xt+yt*szert]) zapomniałem, że pierwsze 32 bajty zajmuje wewnętrzny nagłówek bitmapy, który powoduje właśnie takie rozjechanie wszystkiego. Dodając "8" (tl;dr 8x4 bajty) do wszystkich obliczeń rozwiązałem i tę kwestię, wszelkie bugi zniknęły! Następnym etapem implementacji jest ostatni fragment tutoriala, sprite'y, które też wesoło będzie się liczyło. Mając to wszystko zaimplementowane, wprowadzę już samodzielnie statyczne oświetlenie, a potem już tylko krok do faktycznego grania. Na tę chwilę silnik wygląda tak:

Całkiem chyba nieźle jak na rzecz tworzoną dla zabicia czasu?

Oceń bloga:
14

Komentarze (3)

SORTUJ OD: Najnowszych / Najstarszych / Popularnych

cropper