Grać każdy może – trochę lepiej lub trochę gorzej :) Wśród gier komputerowych każdy gatunek ma pozycje wybitne, które pierwsze przychodzą na myśl, kiedy tylko o nim wspomnimy. Przy każdej z nich można przesiedzieć długie godziny i dni, a inne gry często bywają nazywane “podobnymi do…” – Starcrafta, Quake’a, SimCity, Baldur’s Gate, i tak dalej.
Ale jest też pewien szczególny gatunek gier, mających już na starcie uprzywilejowaną pozycję. Należące do niego tytuły nie zawsze muszą odznaczać się wyjątkową oprawą graficzną, nowatorskim gameplayem, wieloma możliwymi sposobami prowadzenia rozgrywki – a mimo to często zdobywają rzesze graczy, którzy są im wierni przez całe miesiące, a nawet lata. Pewnie nietrudno zgadnąć, jaki gatunek gier mam na myśli. Chodzi mi bowiem o te określane akronimem MMORPG – Massive Multiplayer Online Roleplaying Game.
Zastanawiałem się niedawno, jaka jest tego przyczyna. W końcu mam pewne doświadczenia z kilkoma grami tego typu (a zwłaszcza z jedną :)), więc miałem nadzieję dojść do jakichś sensownych wniosków. W końcu wymyśliłem trzy powody, które wydają mi się najważniejsze.
Są to:
Nie zdziwiłbym się naturalnie, gdyby powyższa lista okazała się o wiele za krótka. Przeciwnie, byłoby to dość zaskakujące, jeśli fenomen gier MMORPG dało się zanalizować w tak trywialny sposób. Nadal też nie rozwiązałem dylematu, czy nad grami tego typu pożyteczniej jest się zastanawiać, czy może w nie… grać. Jak na razie obie te czynności wydają mi się marnowaniem cennego czasu ;D
W większości języków możemy zdefiniować nową nazwę dla istniejącego typu danych; nazywa się ją zwykle aliasem. I tak na przykład w C/C++ jest to możliwe za pomocą słowa kluczowego typedef
. Analogicznie w Delphi mamy od tego słowo kluczowe type
:
[delphi]type TMyInt = Integer;[/delphi]
Tak powstały typ TMyInt
jest faktycznie tylko aliasem. Zmienne należące do tego typu są bowiem całkowicie kompatybilne ze zmiennymi zwykłego typu całkowitego Integer
. W razie potrzeby konwersja między nimi może bez problemu zachodzić w obie strony.
Jeżeli jednak użylibyśmy deklaracji w formie:
[delphi]type TMyInt = type Integer;[/delphi]
wówczas TMyInt
byłby już zupełnie innym typem niż Integer
. Mimo że oba mogłyby przechowywać wartości tego samego rodzaju (liczby całkowite) i z tego samego zakresu, konwersje między nimi wymagałyby rzutowania.
Można by sądzić, że tworzenie takich typów rozróżnialnych “na siłę” jest bezcelowe. Zauważmy jednak, że typy wyliczeniowe (deklarowane przez enum
) są przecież także w gruncie rzeczy liczbami z określonego zbioru. Najczęściej jednak nie chcemy, aby możliwa była niejawna konwersja między nimi a normalnymi typami liczbami. Wszystko dlatego, że w enumach liczby nie pełnią funkcji liczb, tylko identyfikatorów pewnych stanów.
Podobnie tutaj w przypadku TMyInt
nie chodzi nam zapewne o liczby w sensie ich wartości, tylko o coś w rodzaju uchwytów – identyfikatorów obiektow. Kopalnią typów przeznaczonych do takiego właśnie celu jest oczywiście Windows API, zawierające tak znane i lubiane typy jak HWND
, HINSTANCE
, HDC
, itd. Wszystkie one są w gruncie rzeczy liczbami 32-bitowymi, a mimo to nie są ze sobą kompatybilne. Gdyby API to było obiektowe, obiekty reprezentowane obecnie przez te uchwyty należałyby do różnych klas.
Efekt niezgodności uchwytów osiągnięto, deklarując ich typy nie jako aliasy na DWORD
:
lecz jako niezwiązane ze sobą typy wskaźnikowe:
Można to uznać za dość pokrętną sztuczkę, ale na pewno jest ona lepsza niż tworzenie typu wyliczeniowego zawierającego nieco ponad 4 miliardy (232) nazwanych stałych ;]
Spotykam się ostatnio z osobliwym podejściem do przeróżnych wiadomości tyczących się programowania. Mogę je określić dość zaskakująco jako nadmiar “chęci rozumienia” lub ewentualnie “nieuzasadnioną dociekliwość”. Dotyczy to bardzo wielu narzędzi używanych w zasadzie każdej dziedzinie programowania – jak języki, środowiska czy biblioteki – oraz większości jego aspektów.
Czym to się objawia? Otóż symptomem jest pragnienie dogłębnego zrozumienia jakiegoś mechanizmu w sposób jak najszerszy i jak najgłębszy jednocześnie – zanim jeszcze spróbuje się go zastosować. Ten pęd to wiedzy dla samej wiedzy skutkuje najczęściej pozyskaniem dużego zasobu informacji, z których sensownym połączeniem i – przede wszystkim – stosowaniem są później pewne, a często spore, kłopoty.
Czy twierdzę więc, że nadmiar wiedzy szkodzi? Bynajmniej. Chodzi mi raczej o chęć zadawania raczej pytań w rodzaju “jak to działa?” niż “jak tego użyć?”. Sądzę bowiem, że zdecydowana większość programistycznej wiedzy ma charakter czysto użytkowy i jako taka jest na tyle przydatna, na ile daje się użyć w praktyce. Koder powinien być raczej przygotowany na poszukiwanie potrzebnych informacji w trakcie rozwiązywania danego problemu, a nie na kompletne poznanie tematu przez przystąpieniem do pracy.
Jeśli aczkolwiek życzymy sobie poznać głębiej jakieś zagadnienie z racji tego, że jest ono interesujące, wszystko jest w porządku. Jeśli jednak chodzi nam tylko o osiągnięcie zamierzonego celu, nie musimy poznawać rozgałęzień każdej drogi, która do niego prowadzi. Jak zawsze, najważniejsze jest zachowanie odpowiedniej równowagi.
Niby kod można pisać w Notatniku, ale własnej równowagi psychicznej chyba lepiej zaopatrzyć się w edytor, który oferuje przynajmniej podświetlanie elementów składniowych języka. Wiadomo przecież, że mnogość kolorów poprawia samopoczucie :)
Co więc zrobić, gdy zamierzamy pisać efekty w języku HLSL (lub bardzo podobnym Cg)? Trzeba zdecydować się na jakieś narzędzie. Możliwych jest kilka wyjść:
Jak widać, nie jesteśmy więc skazani na surową, czarno-białą czcionkę. A to dobrze, bo po dodaniu tej całej skomplikowanej matematyki, dziwnej semantyki dla danych wierzchołków i niezliczonych dyrektyw kompilacji warunkowej, kod shaderów jest już wystarczająco skomplikowany :)
Model subtraktywny programowalnego potoku graficznego (nie ma to jak kilka trudnych słów na początek ;P) charakteryzuje się tym, że kody shaderów są w nim dość rozdęte objętościowo. Wynikową postać shadera otrzymuje się bowiem poprzez wybranie części kodu odpowiadającej aktualnym potrzebom związanym np. z materiałem i oświetleniem. Rzeczone części są wydzielone przy pomocy dyrektyw podobnych do preprocesora z C: #if
, #else
, itd.
Najprościej jest wtedy, gdy korzystamy z plików efektów (.fx) z DirectX. Wtedy można użyć funkcji D3DXCreateEffectFromFile
lub D3DXCreateEffectFromFile
, którym można przekazać wartości makr potrzebnych w danym przebiegu renderowania. Działa to tak, jakbyśmy użyli dyrektywy #define
bezpośrednio w kodzie efektu i podobnie do makr definiowanych w wierszu poleceń kompilacji w przypadku normalnych programów.
Otrzymany w ten sposób skompilowany shader należy oczywiście zachować, aby można było szybko przełączać między potrzebnymi wersjami w czasie renderowania. Wciąż jednak wymaga to ponownej kompilacji wszystkich używanych wersji shadera przy każdym uruchomieniu aplikacji – co jest marnotrawieniem czasu, jeżeli plik z kodem efektu się nie zmienia.
Można coś na to poradzić, stosując interfejs ID3DXEffectCompiler
zamiast zwykłego ID3DXEffect
. Ten pierwszy ma bowiem dodatkową, bardzo przydatną metodę CompileEffect
:
W wyniku jej użycia możemy dostać bufor (czyli w gruncie rzeczy kawałek pamięci wypełniony danymi binarnymi) zawierający efekt w postaci skompilowanej. Najważniejsze jest to, że w tej postaci możemy zapisać go do pliku (zwykle z rozszerzeniem .fxo) i później tylko szybko odczytać – bez czasochłonnej rekompilacji. W ten sposób można stworzyć mechanizm cache‘owania skompilowanych shaderów, który przyspieszy uruchamianie aplikacji.
Dzisiaj zastanowimy się nad refleksjami. Nie, nie jest to wcale masło maślane. Mechanizm refleksji (reflection – odbicie) jest zwany też introspekcją i polega na tym, iż działający program “wie” o swojej wewnętrznej strukturze. Prostą odmianą jest tutaj dynamiczna informacja o typie (RTTI), czyli możliwość określenia faktycznego typu obiektu znanego poprzez wskaźnik lub referencję.
W bardziej zaawansowanej wersji, obecnej na przykład w języku Java i na platformie .NET, refleksje są jednak czymś więcej. Stanowią faktyczne ‘odbicie’ struktury programu w postaci informacji dostępnych w czasie wykonania. Przy ich pomocy można na przykład uzyskać dokładne informacje o polach, metodach i właściwościach każdej klasy oraz jej miejscu w hierarchii dziedziczenia, nie mówiąc już o możliwości tworzenia jej instancji. Dostępność takich informacji umożliwia łatwe stworzenie modułu serializacji lub bardziej zaawansowanego odśmiecania pamięci, bazującego na przechodzeniu grafu odwołań między obiektami.
Ale C++ posiada tylko bardzo proste RTTI, które do takich celów jest daleko niewystarczające. Czy można jednak naprawić tę przypadłość? Jest na to kilka sposobów:
Gdybym aczkolwiek kiedyś cierpiał na dużą nadwyżkę wolnego czasu, to pewnie rozważyłbym implementację rozwiązania drugiego (lub jakiejś kombinacji rozwiązania drugiego i pierwszego, na przykład zlecającej rozwijanie makr programowi kompilującemu). Do tego czasu jednak C++ już dawno będzie posiadał wbudowany system refleksji ;)
Niektóre gry stają się legendarne… Dotyczy to w większości tych, które mają już swoje lata – jak choćby nieodżałowana pierwsza część Super Mario Bros.. Wielu przeszło ją nie raz i nie dwa razy (sam się do takich osób zaliczam :)), kiedy jeszcze królowała na ośmiobitowych konsolach.
Nie jest to oczywiście specjalne osiągnięcie, jako że jej zwykła wersja jest stosunkowo prosta. Jak przystało na grę nieśmiertelną, powstało jednak mnóstwo modów, hacków tudzież alternatywnych wersji.
Wśród nich jedna doczekała się całkiem sporej sławy jako najtrudniejszy wariant gry. A stało się tak z powodu tego oto filmu, na którym mierzy się z nią pewien gracz charakteryzujący się oryginalnym poczuciem humoru i – powiedzmy – dość mało powściągliwym sposobem wyrażania emocji ;) Zresztą tytuł mówi sam za siebie:
Super Mario Brothers – Frustration
Wszystkim fanom Mario i nie tylko, którzy jeszcze nie mieli okazji zobaczyć tej genialnej produkcji, niniejszym serdecznie ją polecam. Trzeba odkryć tajemnice technologii niewidzialnych bloczków ;]