Języki programowania o składni podobnej do C nie narzucają specjalnych ograniczeń na to, jak pisany w nich kod formatujemy wizualnie. Możemy więc robić dowolne wcięcia, a także w dowolny sposób dzielić poszczególne instrukcje między wiersze w pliku źródłowym. Jest tak dlatego, że to inne znaki – jak średniki czy nawiasy klamrowe – nadają listingowi strukturę odczytywaną przez kompilator.
W C/C++ są jednak przynajmniej dwie sytuacje, w których “fizyczne” wiersze w pliku z kodem mają znaczenie. Przekonał się o tym każdy, kto próbował zdefiniować (dyrektywą #define
) makro rozciągające się na kilka linijek. Ściśle rzecz ujmując, w ogóle nie da się tego zrobić – dyrektywy preprocesora z założenia zajmują zawsze dokładnie jeden wiersz. Można jednak sprawić, by ów wiersz… rozciągał się na kilka linijek:
Znak \
(backslash) robi właśnie ten rodzaj magii. “Skleja” on mianowicie dwie linijki kodu tak, że dla kompilatora stają się one jedną. Używając go, możemy więc rozbić makro preprocesora na wiele wierszy.
Druga sytuacja, w której tak stosowany backslash jest przydatny, to długie ciągi znaków pisane w kodzie:
std::cout << "To jest bardzo długi komunikat, który w konsoli pojawi się \
w pojedynczym wierszu mimo tego, że tutaj w pliku źródłowym jest on podzielony \
na kilka następujących po sobie linijek." << std::endl;[/cpp]
Potrafi on bowiem także "oszukać cudzysłów". Jednak identyczny efekt można też osiągnąć, jeśli wiemy, że kompilator sam skleja następujące po sobie napisy. “a” “b”
daje na przykład to samo "ab"
. Dlatego też backslash w wielolinijkowych napisach jest stosunkowo rzadko używany.
No i stało się – kierowany zapewne diabelskimi podszeptami, zarejestrowałem się na Facebooku. Zło (a nawet Zuo) w najczystszej postaci żem uczynił i oprócz formułowania próśb o przebaczenie mogę jedynie usprawiedliwiać się, co mnie do tego zgubnego w skutkach czynu popchnęło. A owym skutkiem (a “zupełnie przypadkiem” również przyczyną) było to, że przyjrzałem się największemu bogactwu tego serwisu, czyli… grom, rzecz jasna :)
Gry na Facebooku dzielą się w sumie na dwie kategorie. Pierwsza z nich to wyewoluowana forma małych, szybkich gier flashowych, które służą do zabijania tych krótkich odcinków czasu, gdy akurat nie chce się nam robić czegoś pożytecznego. Ewolucja polegała tutaj na wykształceniu możliwości porównywania wyników ze znajomymi, co oczywiście natychmiast podnosi grywalność przynajmniej o 120% i jednocześnie uspokaja nas, że nie jesteśmy jedynymi osobami, które się obijają :)
Drugi typ to zabawa w systematyczne klikanie: należy mniej więcej raz na kilkanaście godzin zalogować się i coś zrobić, by posunąć rozgrywkę do przodu. Cokolwiek to jest, nie możemy tego zrobić częściej, bo nie pozwala na to mechanika gry pod postacią regenerującej się w czasie energii/many/itp. (jak choćby w Mafia Wars) czy też nierealistycznie krótkiego okresu wegetacyjnego truskawek (tak, mam tu na myśli oczywiście Farmville).
Jak widać w obu przypadkach nie jest to specjalnie absorbujące zajęcie. Z gier pierwszego typu do gustu przypadła mi Word Challenge, dzięki której wydatnie (czyli o jakieś 5%) poszerzył się mój zasób angielskich słówek (niestety jedynie takich, które mają co najwyżej sześć liter). Wybraną przeze mnie pozycją z kategorii drugiej jest z kolei Castle Age – coś w rodzaju MMORPG-a przez przeglądarkę, całkiem zresztą udanego. I właśnie z tym związany jest pewien ciekawy problem…
W rzeczonej grze oprócz niewątpliwie ekscytującego odklikiwania kolejnych questów mamy też – że powiem nieco na wyrost – wątek ekonomiczny. Należy tam bowiem kupować nieruchomości, które później co godzinę generują nam przypływ świeżej gotówki. Budynki różnią się ceną i uzyskiwanym z nich przychodem, a ponadto możemy kupować je nie tylko pojedynczego, ale i w pakietach (po 5 lub 10).
Jak pewnie nietrudno się domyślić, nasuwającym się tu od razu pytaniem jest to o optymalną strategię nabywania kolejnych budynków, skutkującą największym zyskiem w dłuższej perspektywie czasowej. Tak naszkicowany problem inwestycji (nazwa brzmi cokolwiek poważnie!) miałby więc następujące założenia:
Pytamy tutaj o to, kiedy, ile i jakie budynki powinniśmy kupować, aby zmaksymalizować swój zysk w długim (najlepiej dowolnie długim) przedziale czasu. W szczególności zastanawiamy się, czy działa tu prosta strategia zachłanna – podobna do rozwiązania ciągłego problemu plecakowego – polegająca na zbieraniu zawsze takiej ilości gotówki, aby kupować maksymalną ilość najbardziej “efektywnych” budynków, tj. tych o największym ilorazie zysku do bieżącej ceny. Intuicja podpowiadałaby, że nie dla wszystkich zestawów danych musi tak być…
Zostawiam więc to zadanie jako materiał do przemyśleń na wolne dni. Nagroda za jego rozwiązanie będzie wielka: udowodni się w ten sposób, że gry z Facebooka mogą się do czegoś przydać!
Nie tak znowu dawno temu napisałem notkę na temat kilku typowych nieporozumień, jakie czasami pojawiają w temacie shaderów oraz związanych z nimi (przynajmniej w DirectX) plików .fx. Nie wspomniałem w niej jednak o pewnej kwestii, która jest kluczowa, dla wielu oczywista, a jednocześnie bywa nielichym i do tego niezbyt przyjemnym zaskoczeniem dla kogoś, kto dopiero zaczyna bliższe spotkanie z tematem programowania grafiki 3D.
Scenariusz wyglądać tu może mniej więcej tak. Na początku pracowicie zgłębiamy tajniki posługiwania się graficznym API (dla ustalenia uwagi możemy założyć, że będzie to DirectX :]), w idealnym przypadku zaznajamiając się też dogłębnie ze związaną z tym matematyką. Umiemy obiekty wyświetlać, teksturować, oświetlać, kontrolować ich widoczność, a może nawet i wczytywać skomplikowane modele z plików. Wydaje się, że to wszystko nie jest takie trudne… aż do momentu, gdy doznamy Szoku Typu Pierwszego i dowiemy się, że większość tych wszystkich technik opartych na fixed pipeline jest nam zupełnie niepotrzebna. Żeby bowiem osiągnąć jakiekolwiek sensowne i godne pokazania efekty, w tym chociażby te tak oczywiste jak dynamiczne światła czy cienie, trzeba używać shaderów…
No cóż, mówi się trudno i kodzi się dalej :) Pracowicie eksperymentujemy więc z różnymi efektami graficznymi, pisząc dla nich odpowiednie vertex i pixel shadery, ucząc się przekazywania danych od jednych do drugich, renderowania różnego rodzaju materiałów, korzystania z poszczególnych typów oświetlenia czy efektów postprocessingu i całej masy różnych innych, interesujących rzeczy. Aż w końcu przychodzi taki moment (i to raczej wcześniej niż później), że proste, pojedyncze efekty przestają nam wystarczać – i tutaj właśnie doznajemy Szoku Typu Drugiego.
A wszystko przez pewien prosty fakt. Staje się zresztą on tym bardziej oczywisty, im większą wiedzą na temat działania potoku graficznego dysponujemy. Ale nawet gdy zostaniemy już ekspertami od grafiki 3D, jest on – jak przypuszczam, rzecz jasna :) – wciąż irytujący i trudny do pogodzenia się. Co powoduje tego rodzaju rozterki egzystencjalne?…
To, że naraz można używać co najwyżej jednego shadera danego rodzaju (vertex lub pixel shadera). Tak, shader może być tylko jeden. Jeden, one, ein, un, uno, один, li pa (to ostatnie jest w lojbanie, rzecz jasna ;>). Dlatego właśnie nie istnieje jeden łatwy i szybki, a przede wszystkim ogólny sposób na to, by połączyć ze sobą dwie techniki, z których każda wymaga wykonania kawałka kodu na karcie graficznej dla wierzchołka i/lub piksela.
Nie znaczy to oczywiście, że sprawa jest beznadziejna – dowodem jest choćby to, że przecież gry 3D wciąż jakoś powstają ;) Różne rozwiązania są tu możliwe, jak choćby te opisane kiedyś przez Rega. Wahają się one na skali między złożonością i elastycznością, ale żadne z nich nie jest idealne.
Możliwość używania delegatów w C# to fajna rzecz. Przyjemne jest zwłaszcza definiowanie ich “w locie”, czyli bez konieczności tworzenia zupełnie nowej funkcji. Takiego delegata nazywamy wówczas anonimowym:
Przydaje się to zwłaszcza to podawana różnego rodzaju predykatów do funkcji sortujących lub wyszukujących. Takie nienazwane funkcje są zwykle krótkie i bardzo proste.
A co jeśli jest inaczej?… W szczególności interesująca sytuacja jest wtedy, gdy nasz delegat odwołuje się do zmiennej zewnętrznej – czyli takiej, która nie została w nim zadeklarowana, ale której zasięg zawiera definicję delegata. Oto przykład:
Powyższy kod (skompilowany pod .NET co najmniej 3.0) pokaże 0 i 1, bowiem anonimowy delegat wiązany jest z samą zmienną i
, nie zaś jej wartością w momencie definicji funkcji (czyli 0
). Że zachowanie to nie jest znowu tak oczywiste, można uzasadnić podając przykład biblioteki Boost.Lambda dla C++, gdzie jest odwrotnie. Tam domyślnie wiązane są same wartości zmiennych zewnętrznych (w momencie tworzenia anonimowej funkcji), ale można to zmienić niewielkim wysiłkiem (używając modyfikatora var
, jeśli kogoś to interesuje).
W C# podobnej możliwości nie ma, a do anonimowych delegatów wiązane są zawsze same zmienne, a nie ich wartości. Jeśli jednak potrzebowalibyśmy czegoś takiego, to prostym wyjściem jest wprowadzanie zmiennej pomocniczej, zainicjowanie jej wartością zmiennej pierwotnej i używanie jej wewnątrz delegatu:
Zarówno delegata, jak i deklarację owej zmiennej dobrze jest też zamknąć w osobnym bloku kodu – tak jak powyżej. Dzięki temu eliminujemy możliwość przypadkowej jej modyfikacji, która oczywiście zostałaby “zauważona” przez delegata.
Używanie zmiennych bez uprzedniego nadania im wartości to stosunkowo częsty błąd. Wiele języków kompilowanych będzie z tego powodu stroiło fochy wahające się od ostrzeżeń podczas kompilacji przez błędy tejże aż po wyjątki w czasie wykonania programu (przynajmniej w wersji debugowej). To zresztą bardzo dobrze, gdyż alternatywą jest korzystanie ze zmiennej, której zawartością są jakieś losowe śmieci w pamięci.
Tak więc zmienne trzeba inicjalizować i w niemal każdym języku da się to zrobić mniej więcej tak:
W C++ oczywiście też. Jednak w C++ mamy też feature, który nazywa się inicjalizacją domyślną. Opiera się on założeniu, że dany typ (tj. obiekt tego typu) może się sam zainicjalizować bez żadnych dodatkowych informacji – czyli że np. ma rozsądną wartość domyślną albo bezparametrowy konstruktor. Stąd T()
będzie “domyślnym obiektem typu T
“, który możemy łatwo uzyskać, nie mając w ogóle pojęcia, czym ów typ T
w istocie jest (codzienność szablonów, swoją drogą).
Inicjalizacja zmiennej tego typu domyślną wartością będzie więc wyglądała mniej więcej tak:
Co bardziej spostrzegawczy zauważą jednak, że mamy tutaj dwie operacje: konstrukcję obiektu i kopiowanie go. Nie radzę jednak próby “optymalizacji” w postaci zamiany na deklarację T foo();
– można się nielicho zaskoczyć. Najlepiej pozostawić to zadanie kompilatorowi; przynajmniej w teorii powinien on sobie z nim bez problemu poradzić.
W powyższy sposób możemy domyślnie inicjalizować dowolne typy zmiennych. Dla pewnej ich podgrupy – zwanej agregatami – możemy jednak zastosować inne rozwiązanie. Cóż to jednak są te agregaty? Otóż są to typy złożone (tablice, struktury, itp.), które nie posiadają zdefiniowanych przez programistę konstruktorów. Mimo że instrukcja T()
działa zupełnie dobrze również dla nich, dopuszczalne jest stosowanie nieco innej formy inicjalizacji.
Formą ta jest lista inicjalizacyjna. Można ją niekiedy spotkać w kodzie korzystającym z jakiegoś API, które operuje na dużych strukturach:
Chodzi tu po prostu o podanie wartości dla kolejnych elementów agregatu, czyli pól w strukturze/klasie lub komórek tablicy; całą tę listę zamykamy w nawiasy klamrowe. Nie musi ona być przy tym pełna: jeśli jakieś pozycje zostaną pominięte, to odpowiadające im pola/komórki zostaną automatycznie zainicjalizowane w sposób domyślny. W przykładzie powyżej inicjalizujemy więc strukturę tak, że pierwsze pole zawiera jej rozmiar, a reszta domyślne wartości (czyli pewnie zera).
Dane dla pierwszego pola zostały wprawdzie tutaj podane jawnie, jednak w ogólności nie jest to wymogiem. Na liście inicjalizacyjnej możemy równie dobrze opuścić wszystkie pozycje i to właśnie jest ten drugi, uniwersalny sposób inicjalizacji agregatu. Wygląda on więc po prostu tak:
Jakkolwiek nietypowo (jak na kod C++) linijka ta wygląda, jest ona najzupełniej poprawna. W wyniku jej wykonania nowo zadeklarowany wektor v
będzie miał wszystkie współrzędne wyzerowane.
Jeśli w C++ piszemy jakąś bibliotekę (albo nawet ogólniej: kod wielorazowego użytku), to zgodnie z dobrymi praktykami powinniśmy jej symbole zamknąć przynajmniej w jedną osobną przestrzeń nazw (namespace). Dzięki temu zapobiegniemy zminimalizujemy możliwość kolizji identyfikatorów w kodzie, który z naszego dzieła będzie korzystał – a także potencjalnie z innych bibliotek.
Nie wszystkie z nich jednak mogą być tak ładnie napisane – choćby dlatego, że któraś może być przeznaczona oryginalnie dla języka C. Najbardziej typowy przykład? Windows API. Dołączenie windows.h zasypuje globalną przestrzeń nazw istną lawiną symboli odpowiadających tysiącom funkcji czy typów zadeklarowanych w tym nagłówku. Nie jest to specjalnie dobre.
Jak temu zaradzić? Bardzo prostą, ale nierozwiązującą wszystkich problemów metodą jest stworzenie własnego nagłówka “opakowującego” ten biblioteczny w nową przestrzeń nazw:
Założenie jest takie, żeby wynikowego pliku nagłówkowego (foo.h) używać następnie w miejsce oryginalnego (foobar.h). Wtedy wszystkie symbole w nim zadeklarowane znajdą się wewnątrz nowej przestrzeni nazw, foo
.
Wszystkie?… Nie! Pakując kod napisany w stylu C bezpośrednio do przestrzeni nazw nie osiągniemy bowiem wszystkich celów, którym namespace‘y przyświecają. Owszem, da się co nieco poprawić: jeśli np. wspomniany windows.h zamknęlibyśmy w przestrzeni win
, to poniższy kod będzie jak najbardziej działał:
podczas gdy wersja bez przedrostków win::
już niezupełnie. Jednak nie jest to całkowity – nomen omen – win, bo z kolei takie wywołanie:
skutkuje już niestety failem :) Nasza przestrzeń nie może bowiem zamknąć wszystkiego, gdyż nie podlegają jej dyrektywy preprocesora – a w szczególności #define
. Pech polega na tym, że właśnie #define
jest w C podstawowym sposobem definiowania stałych, więc użyta wyżej nazwa SW_MINIMIZE
jest (w windows.h) określona po prostu jako:
Próba jej kwalifikowania powoduje zatem powstanie nieprawidłowego ciągu win::6
i słuszne narzekania kompilatora.
Nasz pojemnik (na nazwy) jest więc dziurawy i niestety nic z tym nie da się zrobić. Tak to już jest, gdy wciąż trzeba mieć do czynienia z API, które w tym przypadku liczy sobie – bagatelka – ponad 20 lat!
Zasadniczą i najważniejszą częścią DirectX SDK są pliki nagłówkowe oraz biblioteki (statyczne i dynamiczne), które pozwalają na pisanie programów korzystających z tego API. Do tego mamy jeszcze niezbędną dokumentację oraz przykładowe aplikacje (samples), pokazujące wykorzystanie poszczególnych jego elementów lub prezentujących implementacje różnych efektów graficznych.
Ale to nie wszystko, co można znaleźć w tym kilkusetmegabajtowym (i ciągle rosnącym) pakiecie. Niemal równie ważne są narzędzia pomocnicze, które można tam znaleźć. Podczas tworzenia aplikacji wykorzystujących zwłaszcza Direct3D umiejętność korzystania z tych programów jest niekiedy prawie tak samo ważna, jak znajomość samego API czy zagadnień z dziedziny grafiki.
Dlatego też postanowiłem pokrótce opisać niektóre z nich, żeby co mniej zaawansowani programiści DirectX mogli przynajmniej dowiedzieć się, że takowe istnieją :) Oto więc rzeczone aplikacje:
GetDeviceCaps
urządzenia – w postaci przejrzystego interfejsu drzewiastego. Dobrze jest rzecz jasna wiedzieć, czego szukamy, ale w większości przypadków programistów grafiki 3D interesować będzie gałąź Direct3D9/10 Devices/<model karty graficznej>/D3D Device Types/HAL/Caps.HRESULT
) na odpowiadające im stałe i komunikaty. To pierwsze potrafi też częściowo zrobić debuger Visual Studio (czujką $err,hr
lub $eax,hr
), ale mimo to programik ten bywa niekiedy przydatny.Niektóre z tych narzędzi są na tyle użyteczne, że warto zrobić sobie do nich skróty w łatwo dostępnych miejscach (dotyczy to chociażby Control Panelu). Wszystkie zaś możemy znaleźć wśród linków tworzonych w menu Start przez instalator SDK, w podkatalogu DirectX Utilities.