Wersja 2.3 WordPressa pojawiła się już kilkanaście tygodni temu, ale przez bardzo długi czas broniłem się przed uaktualnieniem. Ta wersja ma bowiem sporo zmian w stosunku do poprzedniej, a skutkiem mogła być niekompatybilność niektórych używanych tutaj pluginów i konieczność co najmniej ich uaktualnienia.
Faktycznie nie obyło się bez kilku problemów, ale ostatecznie całą tę operację upgrade‘u mogę chyba uznać za udaną. W każdym widać bez wątpienia, że strona nadal działa :)
Zapewne najważniejszą zmianą serii 2.3.x jest to, że pojawia się wbudowana obsługa tagów, którymi można opatrywać posty. Ale z faktu, że coś takiego istnieje, nie wynika jeszcze, bym miał z tego korzystać ;P Jakoś mam przeczucie, że pojawienie się na stronie przesławnej chmurki tagów wcale nie byłoby posuwaniem się w dobrym kierunku…
W C++ nadal można używać “tradycyjnego” rzutowania w postaci (typ)wyrażenie
, bo cały czas zachowywana jest przynajmniej częściowa kompatybilność wstecz ze starym C. Jak wiadomo jednak, zaleca się raczej stosowanie nowych (tzn. wprowadzonych w C++; faktycznie są już one raczej stare) operatorów rzutowania, czyli static_cast
, dynamic_cast
, reinterpet_cast
i const_cast
. Jest tak między innymi dlatego, iż pozwalają one rozróżnić rzutowania bezpieczne – które można sprawdzić w czasie kompilacji i które zawsze “mają sens” – od pozostałych.
Składnia takiego rzutowania to oczywiście something_cast<typ>(wyrażenie)
. Jeśli przyjrzymy się jej bliżej, to zauważymy interesującą rzecz. Otóż jest to bardzo podobne do pewnej formy wywołania szablonu funkcji. Ten fakt plus pewne zasady dotyczące dedukcji argumentów takich szablonów sprawiają, że zasadniczo możemy w C++ tworzyć… własne operatory rzutowania.
Prawdopodobnie najbardziej znanym jest implicit_cast
, czyli operator rzutowania niejawnego. Możemy go zdefiniować następująco:
i wywoływać w bardzo porządny sposób:
Ze strony swojego dzialania operator ten nie jest specjalnie ciekawy, bo to przykład językowego purytanizmu czystej wody. implicit_cast
wykonuje bowiem te konwersje, które kompilator i tak by przeprowadził niejawnie; widać to zresztą po kodzie odpowiadającej mu funkcji.
No właśnie – warto zauważyć, że jest w gruncie rzeczy zwykła funkcja szablonowa. Działa ona jak operator rzutowania, gdyż drugi z argumentów tego szablonu – typ źródłowy – może zostać wydedukowany z argumentu funkcji (czyli wyrażenia, które rzutujemy). Musimy jedynie podać pierwszy z nich, czyli typ docelowy – ale tego można się było spodziewać. Trzeba zaznaczyć, że kolejność obu parametrów szablonu musi być właśnie taka, ponieważ ich przestawienie spowodowałoby, że automatyczna dedukcja nie mogła by zostać przeprowadzona. Zachodzi ona bowiem począwszy od końcowych parametrów.
Prawdopodobnie taki własny operator rzutowania to przede wszystkim efektowna sztuczka. Nic więc dziwnego, że kilka przykładów (np. lexical_cast
) jest częścią biblioteki Boost, która niemal w całości jest zbiorem takich językowych gadżetów :) Sam aczkolwiek wymyśliłem dosyć dawno temu operator “rzutowania bezwzględnego”, który wygląda mniej więcej tak:
Służy on do dosłownego rzutowania dowolnego typu na dowolny inny w sensie zmiany interpretacji bajtów. Zdaje się on działać dla większości typów w C++ (głównym wyjątkiem są typy referencyjne, co nie jest specjalną niespodzianką) i bywa całkiem przydatny.
Naturalnie można wymyślić jeszcze sporo innych niestandardowych operatorów rzutowania, jak choćby konwersje między napisami w różnych kodowaniach (ANSI, Unicode) czy do/z jakichś własnych typów, opakowujących klasy zewnętrzne. W każdym razie widać znowu (i pewnie nie ostatni raz), że C++ pozwala nam na bardzo dużo, skoro nawet te nieco rozwlekłe *_cast
y można jakoś oswoić do własnych potrzeb :]
Wektor wyznacza pewną linią na płaszczyźnie lub w przestrzeni. A skoro tak jest, to czasami możemy potrzebować wektora, który będzie prostopadły do jakiegoś innego, który jest nam znany. Wyznaczenie go nie jest specjalnie trudne, ale w zależności od tego, czy wszystko dzieje się w 2D czy 3D, trzeba uwzględnić pewne szczegóły.
Takim szczegółem na pewno nie jest jednak długość wynikowego wektora, bo możemy go przecież zawsze przeskalować do żądanego rozmiaru. W tym celu wystarczy go najpierw znormalizować (podzielić przez aktualną długość), a potem pomnożyć przez nową długość.
Na płaszczyźnie wektor prostopadły jest wyznaczony w miarę jednoznacznie:
Mówiąc ściślej, znany jest jego kierunek, a przy ustalonej długości może mieć jedynie dwa ustalone, przeciwne zwroty. To w zasadzie dość oczywiste: prostą prostopadłą do wektora wyznaczyć jest… prosto (;D), natomiast kwestia jej skierowania zależy już od nas.
Dla wektora [x, y] prostopadłymi do niego będą więc wektory [-y, x] i [y, –x].
W przestrzeni trójwymiarowej jedno-(a właściwie dwu-)znacznie można wyznaczyć jedynie wektor prostopadły do dwóch innych. Oczywiście w takim przypadku tworzą one płaszczyznę, więc tak naprawdę poszukujemy wektora (prostej), przecinającego pod kątem prostym tę właśnie płaszczyznę. Jak wiadomo, można to uczynić, obliczając iloczyn wektorowy (cross product) tych dwóch wektorów.
W przypadku, gdy mamy tylko jeden wektor, musimy się rzecz jasna liczyć z tym, że kierunków prostopadłych do niego jest nieskończenie wiele. Dlatego możliwe jest co najwyżej wyznaczenie dowolnego z nich, co jednak czasami jest zadowalające. (Możemy mieć np. płaszczyznę wyznaczoną przez punkt i normalną; jeżeli chcemy otrzymać wektor leżący na tej płaszczyźnie, musimy znaleźć wektor prostopadły do normalnej). Aby tego dokonać, trzeba po prostu dobrać drugi wektor do iloczynu. Jedynym wymaganiem dla niego jest to, aby nie był on równoległy z pierwszym:
W celu wybrania drugiego argumentu możemy zastosować powyższą sztuczkę. Zamieniamy po prostu składowe pierwotnego wektora tak, aby żadna nie była na swoim miejscu – tutaj dla wektora [x, y, z] taką permutacją jest np. [z, x, y]. Taki wektor prawie na pewno nie będzie równoległy z pierwowzorem.
Wiemy jednak, że prawie robi wielką różnicę :) Problematyczne są te wektory, których wszystkie trzy elementy mają taką samą wartość. Musimy więc sprawdzić ten przypadek. Zamiast permutacji można wtedy zastosować chociażby wektor równoległy do którejś z osi układu współrzędnych.
Kiedy uczyłem się biblioteki DirectX, miałem dość spore kłopoty z kwestią właściwej kolejności przekształceń opisanych przez macierze. Jak wiadomo, w grafice każdą transformację możemy opisać macierzą, a złożenie takich przekształceń możemy być reprezentowane przez odpowiedni iloczyn macierzy. Wówczas pomnożenie wektora (odpowiednio rozszerzonego o czwartą współrzędną) przez taką macierz skutkuje zastosowaniem do niego tych wszystkich przekształceń. Może być ich bardzo wiele, lecz wymagana jest tylko jedna macierz i jedno mnożenie przezeń wektora. Jest to więc efektywne, jeśli mamy dużą ilość geometrii do przetworzenia – czyli, co tu ukrywać, w zasadzie zawsze :)
Rzeczone macierze opisujące przekształcenia są kwadratowe; w przypadku grafiki 3D mają rozmiar 4×4. Dlatego też możliwe jest ich mnożenie w dowolnej kolejności. Wiemy jednak, że operacja mnożenia macierzy nie jest przemienna. Odpowiada to zresztą temu, iż przy przekształcaniu punktów w przestrzeni też liczy się kolejność: obrót, a potem przesunięcie to nie to samo, co przesunięcie, a potem obrót.
I tu się zaczyna problem, bowiem w bardzo wielu źródłach wprowadzone jest niezłe zamieszanie, jeśli chodzi o kolejność mnożenia macierzy opisujących geometryczne przekształcenia. Najczęściej pomieszane są konwencje tego, jaki porządek jest poprawny w danej bibliotece graficznej, a jaki “w matematyce”. Ostatecznie więc nie wiadomo, czy trzeba iloczyn macierzy zapisywać w kolejności, w jakiej chcemy aplikować przekształcenia, które reprezentują – czy może na odwrót. Dość prosto można oczywiście sprawdzić, jak to jest akurat w naszej bibliotece graficznej, lecz to nie mówi nic o istocie problemu…
Właściwie to dopiero niedawno dowiedziałem się, gdzie jest tu pies pogrzebany. Otóż matematycy z pewnych przyczyn lubią traktować wektory jako kolumnowe, tj. jako macierze Nx1 (N wierszy, 1 kolumna). Przy takiej interpretacji tylko iloczyn w postaci:
macierz1 * wektor_kolumnowy
daje w wyniku wektor (także kolumnowy, rzecz jasna). W tym przypadku będzie on przekształcony przez macierz1. Jeżeli teraz zechcemy dodać drugie przekształcenie, to mnożenie przez odpowiednią macierz również musimy zapisać z przodu:
macierz2 * (macierz1 * wektor_kolumnowy)
Ale mnożenie jest oczywiście łączne, więc:
(macierz2 * macierz1) * wektor_kolumnowy = macierz * wektor_kolumnowy
a wynikowa macierz = macierz2 * macierz1 opisuje złożenie naszych przekształceń. Jak widać wyżej, najpierw jest stosowane to opisane przez macierz1, a dopiero potem to z macierzy2 – mimo że są one mnożone w porządku odwrotnym. Tak bowiem wygląda sprawa kolejności przekształceń dla wektorów kolumnowych.
Twórcy DirectX uznali prawdopodobnie, że jest to nieintuicyjne dla nie-matematyków i dokonali pewnego “triku”. Opiera się on na tym, że gdy w dwóch macierzach zamienimy ze sobą wiersze i kolumny – czyli dokonamy transpozycji – pomnożymy je przez siebie, a następnie transponujemy wynik, to rezultat będzie taki, jakbyśmy mnożyli wyjściowe macierze w odwrotnej kolejności. Wyjątkowo trzeba tutaj przyznać, że wzór mówi więcej niż jego opis, więc spójrzmy na ten wzór :)
(A * B)T = BT * AT
W DirectX dokonano więc transpozycji wszystkich macierzy opisujących przekształcenia. Przykładowo, funkcja D3DXMatrixTranslation
zwraca macierz z wartościami przesunięć wpisanych w ostatnim wierszu, podczas gdy w wersji “matematycznej” powinny być one w ostatniej kolumnie. Podobnie jest ze wszystkimi innymi macierzami… ale także z wektorami!
Chociaż wektory z programistycznego punktu widzenia to cztery składowe i nic więcej, to w DirectX należy je traktować jako wektory wierszowe, czyli macierze 1xN. Dla nich zaś sensownym sposobem mnożenia przez macierz jest tylko następujący:
wektor_wierszowy * macierz1
Dodając kolejne przekształcenie, mamy:
(wektor_wierszowy * macierz1) * macierz2
i znów opierając się na łączności mnożenia otrzymujemy ostatecznie:
wektor_wierszowy * (macierz1 * macierz2) = wektor_wierszowy * macierz
Tutaj z kolei widać wyraźnie, że przekształcenia są stosowane w takiej samej kolejności, w jakiej odpowiadające im macierze występują w iloczynie.
Ponieważ, jak wspomniałem wyżej, cała sprawa jest kwestią czysto arbitralną (wystarczy transpozycja, aby odwrócić porządek), powinniśmy tym bardziej zwrócić na nią uwagę. A jeśli programujemy w DirectX, nie należy dopuścić do tego, by matematycy wmawiali nam ‘właściwą’ kolejność :P
Stare chińskie przysłowie mówi, że klient zazwyczaj nie wie dokładnie, czego tak naprawdę chce. (Inne przysłowie mówi też, że jeśli nie znamy pochodzenia danej sentencji, to najlepiej powiedzieć, że jest to stare chińskie przysłowie). Dlatego też często nie potrafi swoich potrzeb przełożyć na opis wymagań co do oprogramowania. Jest to jeden z problemów, przy rozwiązywaniu których ma pomagać Unified Modelling Language, czyli UML. W założeniu jest to notacja, umożliwiająca rozpisanie całego procesu tworzenia aplikacji na szereg różnego rodzaju diagramów, na których znaczenie poszczególnych symboli jest ściśle określone – na pewno ściślej niż języka naturalnego. Założenie jest szczytne i bardzo ambitne, a z praktyką jest jak zwykle nieco gorzej :)
Niewykluczone, że jedną z idei przyświecających twórcom UML-a było przynajmniej częściowe zasypanie tej przepaści między dwoma etapami tworzenia: kiedy wiemy, co mamy zrobić, ale jeszcze nie mamy wielkiego pojęcia o tym, jak to zrobić. Przeskoczenie dystansu pomiędzy tymi punktami jest bowiem często kwestią odpowiedniego pomysłu, który najlepiej realizuje całą koncepcję systemu. Jak zaś wiadomo, pomysły biorą się głównie z niezbadanych obszarów pewnego organu znajdującego się między uszami i ich jakość zależy głównie od tego, do kogo ów organ należy. UML stara się więc usystematyzować programistyczną kreatywność, aby zaprojektowanie dobrze działającego systemu nie było tylko wypadkową wymysłów kłębiących się pod czaszką analityka.
Trzeba przyznać, że wychodzi mu to dość średnio. Różne rodzaje diagramów, jakie mamy do dyspozycji, nie bardzo pomagają w płynnym przechodzeniu od wymagań funkcjonalnych do projektu, który te wymagania ma spełniać. Mam raczej wrażenie, że ich celem jest głównie spoglądanie na aplikację z coraz to nowych punktów widzenia. Patrzymy więc na projekt z perspektywy użytkownika zewnętrznego (diagram przypadków użycia), zmieniających się stanów obiektów (diagramy stanów), przepływu danych i obiektów między “miejscami” w programie (diagramy interakcji), i tak dalej. W sumie widzimy coraz więcej pojęć, związków, relacji i zależności, przez co projekt – zamiast upraszczać się, co z pewnością kieruje nas bliżej implementacji – komplikuje się jeszcze bardziej.
Większość z tych konstrukcji nie jest zresztą widoczna w wynikowym kodzie. Nic więc dziwnego, że spośród całego UML-a zdecydowanie najpopularniejsze i najczęściej stosowane są diagramy klas i diagramy sekwencji (przepływu zdarzeń). Odpowiadają one bowiem niemal bezpośrednio strukturze klas i ich składowych oraz przepływowi sterowania w metodach i funkcjach. W ich przypadku przyznaję, że schemat graficzny jest bardziej przejrzysty niż tekst w języku naturalnym czy kod. Co więcej, używana przy okazji notacja jest też najszerzej znana. Autorzy wielu książek dotyczących języków programowania bardzo często bowiem “przemycają” w nich zwłaszcza diagramy klas, z nieśmiertelną strzałką w górę jako symbolem dziedziczenia.
Jeśli zaś chodzi o resztę diagramów, to ich użyteczność wydaje mi się wątpliwa. Mówiąc wprost, uważam je póki co za zwyczajne zawracanie głowy :)
Do pisania programów wystarczy dowolny edytor tekstu i linia poleceń, ale dzisiaj trudno przecenić wygodę, jaką dają zintegrowane środowiska programistyczne (IDE). I chociaż każda porządna aplikacja tego typu – niezależnie od języka, dla którego jest przeznaczona – posiada pewną określoną i oczywistą funkcjonalność (jak choćby rozbudowane zarządzanie projektami), to w wielu można znaleźć drobne i interesujące dodatki.
Taki NetBeans (IDE do Javy) posiada na przykład pasek boczny, widoczny po prawej stronie paska przewijania w polu z kodem. Reprezentuje on cały otwarty plik, zaś naniesione na niego kreski odpowiadają linijkom, w których “dzieje się” coś potencjalnie interesującego. Mogą to być wiersze zawierające błędy kompilacji albo wystąpienia identyfikatora (nazwy), w którym aktualnie znajduje się kursor. Najważniejsza cechą tych wskaźników jest to, że można w nie klikać i w ten sposób przenosić się do odpowiednich miejsc w kodzie.
Według mnie to całkiem zmyślne rozwiązanie, przydatne zwłaszcza przy poprawianiu błędów, gdyż oszczędza przechodzenia do okienka z outputem kompilatora, a potem z powrotem do kodu. Jego użyteczność może się aczkolwiek ujawnić najlepiej tylko w przypadku, gdy IDE przeprowadza kompilację w tle i na bieżąco podkreśla i zaznacza napotkane błędy.
Przez kilka ostatnich dni zaglądałem na forum Warsztatu dość nieregularnie i kiedy w końcu przyjrzałem się mu dokładniej, byłem odrobinę zdziwiony. Mógłbym wręcz powiedzieć, że beze mnie forum jest zostawione niemal na pastwę losu, ale na szczęście nie cierpię na aż taki brak skromności :P Prawdą jest raczej to, że jako moderator nie jestem obecnie odosobniony w potyczkach zaliczeniowo-sesyjnych, na czym jakość sprawowania pieczy nad forum oczywiście nieco cierpi.
Zostało to zresztą zauważone i wywiązała się przy okazji ciekawa dyskusja. Według mnie najciekawszym spostrzeżeniem, jakie w niej padło, jest hipoteza dotycząca przyczyn, dla których na Warsztacie pojawiają się często osoby mające nikłe pojęcie o programowaniu w ogóle – mimo że o wiele lepszym dla nich miejscem są fora ogólnokoderskie, jakich sporo można znaleźć w sieci. Otóż wskazuje ona, że wiele osób zaczyna się interesować programowaniem głównie dlatego, że chce pisać właśnie gry. Wszelkie problemy, na jakie nieuchronnie natrafiają, uważają więc za związane z programowaniem gier. Nie zdają sobie sprawy, że osoba rozpoczynająca przygodę z kodowaniem z zupełnie innego powodu również z dużym prawdopodobieństwem może natrafić na podobne kłopoty. Cel (tworzenie gier) wydaje się im bowiem o wiele ważniejszy niż środek (nauka programowania), szukają więc pomocy w środowisku związanym z tym pierwszym.
I tak Warsztat staje się naturalnym miejscem, do którego początkujący programiści (z aspiracjami bycia programistami gier) kierują swoje kroki. Dla bardziej doświadczonych bywalców wydaje się to niezmiernie dziwne. Przecież “wiadomo”, że aby zająć się gamedevem, należy wpierw być dobrym programistą w sensie ogólnym. No cóż, rzecz w tym, że właśnie nie wiadomo – a przynajmniej nie wiedzą tego ci, którzy powinni.
Jeżeli tezę tę uznamy za prawdziwą, to wiemy już, skąd się bierze ten strumień newbies, przepływających codziennie przez forum. Aby go powstrzymać, można oczywiście budować kolejne (coraz silniejsze) tamy regulaminów i moderacji, ale chyba bardziej zależałoby nam na skierowaniu go na inne – właściwe – tory. A do tego droga jest tylko jedna: uświadamiać, że programistą gier nie zostaje się w chwili, gdy rozpoczyna się naukę jakiegoś języka i że jest to dopiero początek bardzo długiej drogi. Ładnie obrazuje ją na przykład stworzone przez Rega drzewko umiejętności programisty gier.