Funkcje, których dziedziną jest podzbiór R2 (czyli płaszczyzna) ciężko jest przedstawić na płaskim ekranie w sposób poglądowy, a jednocześnie pozwalający na odczytanie jakichś wartości. O ile bowiem izometryczny rzut 3D wygląda efektownie, to poza ogólnym kształtem powierzchni nie przedstawia wielkiej ilości informacji. Sprawa wygląda trochę lepiej, jeśli taką figurę możemy jeszcze obracać… co aczkolwiek może być z kolei trudne do zrealizowania np. na wydruku :]
Jednym z wyjść może być tutaj zastosowanie sposobu używanego do map wysokości w grach. Nietrudno zauważyć, że są one niczym innym jak właśnie funkcjami typu R2->R, a reprezentowane są najczęściej jako bitmapy w odcieniach szarości: jasność piksela x,y odpowiada w nich wysokości odpowiedniego punktu terenu.
Takie heightmapy wprawdzie łatwo się rysuje, ale niespecjalnie nadają się do pokazania jako ilustracja czegokolwiek – odcienie szarości są przecież z definicji szare i nieciekawe :) Dlatego bardzo spodobał mi się prosty pomysł na uczynienie tej reprezentacji znacznie ładniejszą, o którym to usłyszałem niedawno.
Idea jest prosta: po co używać samych szarości, skoro mamy do dyspozycji całą paletę kolorów?… Każdy kolor w reprezentacji RGB jest przecież tożsamy z liczbą, co widać najbardziej, jeśli weźmiemy jego typową reprezentację 32-bitową:
W niej “największym” kolorem jest kolor biały (0x00FFFFFF
, czyli 224 – 1), zaś “najmniejszym” czarny (0x0
), a pomiędzy nimi włącznie występuje każdy z 2563 możliwych kolorów. Jeśli teraz przeskalujemy wartości naszej funkcji do tak określonego przedziału (lub nieco mniejszego), a następnie przekonwertujemy każdą z tych 32-(a właściwie 24-)bitowych liczb na odpowiadający jej kolor RGB, to otrzymamy w ten sposób heightmapę o znacznie bogatszej palecie barw niż poprzednio.
Jeśli ponadto używamy dokładnie takiej reprezentacji liczbowej kolorów, jak powyżej (najmłodsze bity na kolor niebieskie, a najstarsze używane na czerwony), to zauważymy, że rozkład barw w wyniku jest całkiem znajomy. I tak obszary niebieskie odpowiadają wartościom najmniejszym, zielone średnim, żółte – wysokim, a czerwone – najwyższym. Łącznie przypomina mapę hipsometryczną – czyli, jakby nie patrzył, mapę wysokości właśnie :)
Do czego może przydać się takie cudo, oprócz wspomnianej już wizualizacji funkcji R2->R na płaszczyźnie? Pewnie do niewielu rzeczy :) Ja wpadłem jednak na jeszcze jedno potencjalne zastosowanie.
Ponieważ wynikowy obrazek używa tak “geograficznych” barw, może on całkiem dobrze wyglądać jako… tło minimapy pokazywanej w grze, która rozgrywa się na terenie opisanym mapą wysokości. Oczywiście rzecz wymagałaby dopracowania: przede wszystkim bardziej starannego dobrania kolorów brzegowych (niekoniecznie białego i czarnego) oraz być może kolorów pośrednich (np. kolor poziomu morza), a i pewnie zastosowania jakichś filtrów na wynikowym obrazku (np. wygładzania). Efekty mogłyby być jednak interesujące, co widać obok.
Sporo algorytmów jako swoje parametry przyjmuje różnego typu funkcje, które potem są wykorzystywane w trakcie ich działania. Prostym przykładem są tu wszelkiego rodzaju sortowania czy wyszukiwania, umożliwiające często podanie własnego predykatu (funkcji zwracającej wartość logiczną). W bardziej skomplikowanej wersji może chodzić chociażby o algorytm genetyczny lub przeszukujący drzewo gry, który wykorzystuje do działania jakąś funkcję oceniającą (np. osobników w populacji).
Na takie okazje różne języki programowania oferują różne narzędzia, lecz w większości przypadków sprowadzają się one do jednego z poniższych:
Niektóre języki nie mają jednak ani wskaźników, ani delegatów – w nich zwykle stosuje się sposób następny.
()
. Może więc to być zwykły wskaźnik, jakaś ładna zewnętrzna implementacja mechanizmu delegatów (np. FastDelegate) lub jakikolwiek obiekt z przeciążonym operatorem nawiasów ()
. Właśnie te ostatnie nazywa się zwykle funktorami, a jeśli ktoś nieco bardziej zagłębił się w bibliotekę STL, na pewno nie raz się z nimi spotkał.Trzeba też powiedzieć, że właściwie istnieje też inny sposób: zamiast samej funkcji przekazywanie jej… nazwy. “I niby jak ją potem wywołać?”, można zapytać. Ano to już indywidualna kwestia każdego języka – w tym przypadku zwykle interpretowanego :)
W takiej dużej aplikacji jak IDE ilość opcji jest na tyle spora, że część z nich pochowana jest głęboko w czeluściach wielopoziomowego menu. Często nie znaczy to jednak, że są one mniej przydatne; na pewno jednak są trudniej dostępne. Dodatkowo też ilość klawiszy na klawiaturze jest ograniczona, więc nie wszystkie opcje mogą mieć przypisane skróty klawiszowe… Chyba że zastosuje się jakieś sztuczkę :)
W vimie i jego pochodnych taką sztuczką były polecenia wpisywane od dwukropka (na czele z najbardziej przydatnym, czyli
Jak to działa? Otóż bardzo prosto: po wciśnięciu specjalnej kombinacji ‘aktywującej’ (np. Ctrl + K) nic się wprawdzie nie dzieje, ale aplikacji przełącza się w tryb pozwalający na wykrycie drugiej kombinacji (np. Ctrl + C), która odpowiada już konkretnej akcji. Całość można więc zapisać jako Ctrl + K,C; oznacza to po prostu: wciśnij i przytrzymaj Ctrl, a potem naciśnij kolejno K i C. Nic specjalnie trudnego, prawda? :)
A jakież to wspaniałe opcje IDE są dostępne w ten sposób? Ano jest ich całkiem sporo, spośród których wylistuję kilka:
/* */
czy #if 0/#endif
, żeby wyłączyć jakiś kawałek kodu z kompilacji :) Mamy bowiem takie oto skróty:
//
z początku każdej linijki)for
), pozwalając przy tym na łatwą zmianę ich szczegółów (np. nazwy zmiennej będącej licznikiem pętli).try
–catch
, if
, itd.Polecam przynajmniej jednokrotne spróbowanie każdej z powyższych opcji. Możliwe, że w ten sposób odkryjecie coś, czego brakowało wam przez cały czas :) Nie jest to też kompletna lista – więcej skrótów/opcji można znaleźć przeglądając menu Edit.
Teoretycznie najlepszym sposobem na usuwanie elementów z pojemników STL jest posłużenie się idiomem erase
–remove
:
W praktyce bywa on dość kłopotliwy jeśli stosowany predykat (tutaj oznaczony jako funktor ToBeDeleted
) musi być napisany specjalnie do tego celu. Zresztą gra często nie jest warta świeczki, bo implementacje algorytmów w rodzaju remove
to w środku często nic innego, jak zwykłe pętle.
No a pętle możemy przecież napisać sobie sami, prawda?… Otóż nieprawda – zwłaszcza jeśli wcześniej nie pomyślimy przez chwilę. Łatwo bowiem wyprodukować coś takiego:
Z poprawnością ma to niewiele wspólnego. Jeśli bowiem natrafimy na element do usunięcia, to po dokonaniu tego (w nagłówku pętli) zinkrementujemy iterator, który na ów usunięty element wcześniej pokazywał. W ten sposób możemy pominąć element następny, bo go zwyczajnie “przeskoczymy”. Odpowiednik powyższej pętli używający zwykłego licznika i operatora []
jest zresztą obarczony identycznym problemem.
Jak więc usuwać? Umiejętnie, rzecz jasna :) Skoro po skasowaniu jednego elementu pozostałe przesuną się o jedno miejsce do tyłu, nie powinniśmy zawsze bezmyślnie przechodzić dalej. Trzeba to robić tylko wtedy, gdy dany element zostawiliśmy w spokoju:
To w sumie całkiem oczywiste, jeśli chwilę się nad tym zastanowić. Bowiem – w przeciwieństwie do dodawania – usuwanie elementów z kontenerów wymaga właśnie chwili zastanowienia :]
Jeśli tylko liczymy coś bardziej skomplikowanego niż rozmieszczenie kontrolek w oknie, to niechybnie zaczniemy potrzebować którejś ze stałych matematycznych – na przykład π. Co wtedy robimy? Ano całkiem często skutkuje to włączeniem do programu deklaracji typu:
Nie jest to oczywiście wielkie zło (a już na pewno mniejsze niż bezpośrednie korzystanie z definicji typu π = 4atan(1)), ale w większości przypadków jest to też wyważanie otwartych drzwi. Potrzebne stałe mamy bowiem często już gdzieś zdefiniowane – trzeba tylko wiedzieć, gdzie ich poszukać:
M_PI
, M_E
, a nawet M_LN10
czy M_SQRT2
zdefiniowane w math.h lub cmath. Definicje te nie są jednak częścią standardu, więc dbające o zgodność kompilatory (czyli większość kompilatorów w ogóle) wymagają pewnych #define
‘ów przed dołączeniem ww. nagłówków. I tak dla Visual C++ jest to _USE_MATH_DEFINES
, a dla GCC bodajże prawie dowolne z makr _SOURCE
(jak _GNU_SOURCE
czy _ALL_SOURCE
).DX3DX_PI
i D3DX1BYPI
(1/π) – co nie dziwi, bo przecież w grafice więcej do szczęścia nie potrzeba ;) Obie te stałe są zdefiniowane w d3dx#math.h (gdzie #
to numer wersji DirectX), który to nagłówek jest dołączany automatycznie jeśli korzystamy z D3DX.PI
i E
zdefiniowane jako statyczne pola klasy System.Math
. W Javie klasa java.lang.Math
zawiera dwa identycznie zdefiniowane pola.Jak zatem łatwo zauważyć, wynajdywanie koła w postaci definicji π czy e jest w wielu przypadkach nieuzasadnione :]
Visual Studio 2008 zasadniczo służy do pracy z .NET Framework w wersji 3.5, ale ma tę przyjemną cechę, że pozwala określić używaną przez projekt wersję frameworka. A to oznacza, że można zdecydować się na wersję 2.0, co pozwala na współpracę także z edycją 2005.
Pewnym problemem pozostaje jednak fakt, że próba załadowania w Visual Studio 2005 solucji stworzonej w edycji 2008 kończy się takim oto uroczym komunikatem:
Co z tym zrobić?… Można oczywiście utworzyć nową, pustą solucję, a potem dołączyć do niej projekty z tej oryginalnej. To dość kłopotliwe, a VS wymaga też, aby solucje tworzyć zawsze w pustym lub nieistniejącym katalogu. Ostatecznie musielibyśmy więc przekopiować ją wpierw w docelowe miejsce, a potem zająć się przyłączaniem doń naszych projektów. Warto też zauważyć, że stworzenie zupełnie nowej solucji oznacza utratę wszelkich niestandardowych konfiguracji budowania projektów; w gruncie rzeczy bowiem właśnie te konfiguracje to – poza samą listą projektów – jedyna istotna rzecz przechowywana w pliku .sln.
Na szczęście jednak ów plik jest zwykłym plikiem tekstowym, którego format tak naprawdę nie zmienił się od dawna (jak sądzę co najmniej od wersji 2001). Dlatego całkiem prawdopodobne jest to, że jedyną przeszkodą przy wczytywaniu nowej solucji w starszym VS jest… numer wersji zapisany w pierwszym wierszu pliku:
Microsoft Visual Studio Solution File, Format Version 10.00
Nie zaszkodzi więc zmienić owe 10.00 (VS 2008) na 9.00 (VS 2005) i ponownie spróbować otworzyć solucję. Jeśli próba się powiedzie, oszczędzimy sobie pracochłonnego kombinowania. A jeśli nie… to pewnie warto pomyśleć o upgrade ;]
Dość powszechne na forum Warsztatu jest pytanie: z czego się uczyć? – programowania w ogóle i gamedevu w szczególności. Chodzi tu po prostu o źródła wiedzy. I chociaż (częściowa) odpowiedź znajduje się w warsztatowej Wiki, to pewnie podobne zapytania wciąż będą się pojawiać. Nie ma się tu specjalnie czemu dziwić.
Zupełnie inaczej jest, jeśli owo ‘uczenie się’ występuje w trochę innym kontekście. “Czy mogę nauczyć się programowania, jeśli …?”, “Czy powinienem wykonywać ćwiczenia z książki X?” albo najgorsze z tych pytań: “Ile czasu poświęcacie dziennie na naukę programowania?”. Natychmiast służę oczywiście odpowiedzią na nie: dokładnie trzy godziny, między 19 a 22, przez dwa dni w tygodniu, tj. we wtorki i piątki. I bardzo, bardzo przy tym pilnuję, aby tego czasu nauki nie skracać nawet o minutę!… Czy ta odpowiedź jest satysfakcjonująca? Przypuszczam, że niezbyt ;]
A mówiąc już trochę poważniej… Jeśli nie zajmujemy się programowaniem zawodowo, to jest to pewnie nasze hobby – czyli coś, co robimy dla przyjemności. Chodzi o tworzenie czegoś własnego, eksperymentowanie z różnymi technologiami, a także – nie ma co ukrywać – uczenie się czegoś nowego. Jednak mówimy tu o uczeniu się rzeczy, które chcemy, kiedy chcemy i jak długo chcemy – wedle naszego własnego uznania, a nie czegoś w rodzaju “planu treningowego'”, ułożonego na podstawie odpowiedzi ludzi z jakiegoś forum internetowego (z całym szacunkiem dla Warsztatu oczywiście). Trudno przecież oczekiwać, że powiedzą nam oni, co będzie nam bardziej odpowiadało; do tego trzeba dojść samodzielnie.
Czy więc właściwa odpowiedź na wspomniane pytanie istnieje? Oczywiście, że tak. Brzmi ona: na naukę programowania przeznaczamy tyle czasu, ile chcemy i/lub możemy. Tak po prostu.