System GUI #5 – OknoDawno temu w firmie Xerox wymyślono, że interfejs użytkownika można zapakować w zestaw prostokątnych, nakładających się na siebie okien. Pomysł okazał się niezwykle trafiony i zaowocował nawet wielce kreatywną nazwą pewnego systemu operacyjnego ;) Od tamtej pory trudno sobie wyobrazić zaawansowane UI posługujące się czymś innym niż właśnie zestawem okien.
Nie jest trudno otrzymać na ekranie puste okno. W środowiskach RAD – w rodzaju Visual C# czy Delphi – mamy je najczęściej dane automatycznie, gdy tworzymy nowy projekt. W przypadku programowania niewizualnego (jak np. z użyciem czystego Windows API) sprawa jest nieco bardziej skomplikowana, ale i tak zamyka się w nie więcej niż kilkudziesięciu linijkach.
Takie puste okno wydawać się może mało interesujące czy wręcz zbyt oczywiste, aby zwracać na nie uwagę. Rzadko zwracamy uwagę na to, że ten pozornie trywialny prostokąt sam w sobie potrafi bardzo wiele. Wśród typowych możliwości mamy chociażby:

Uzyskanie podobnej funkcjonalności, zaczynając od zera, jest bardziej pracochłonne niż może się wydawać. W moim przypadku otrzymanie czegoś, co przypomina w pełni funkcjonalne i interaktywne okno, zamknęło się w ok. dwóch tysiącach linijek kodu – nie licząc oczywiście modułu grafiki 2D, potrzebnego do rysowania okien.
To całkiem sporo. Skutkiem ubocznym tej pisaniny jest też to, że obecnie patrzę na stare poczciwe okna systemu Windows z nieco większym respektem :)
AutopowitanieW programie mIRC - kliencie IRC dla Windows - możemy definiować sobie przeróżne skrypty wywoływane przez komendy rozpoczynające od znaku backslash. Zintegrowany z programem język jest wprawdzie wybitnie toporny składniowo, ale oferuje całkiem spore możliwości.
Pozwala on na przykład definiować sposób, w jaki program ma reagować na pewne zdarzenia. Tego typu odpowiedzi programujemy za pomocą wbudowanego w program edytora skryptów. Wystarczy wybrać z menu Tools | Script Editor i przejść na zakładkę Remote, by wyświetlić i edytować plik remote.ini.
Możemy tam wpisać np. taką linijkę:
W ten sposób stworzymy prosty mechanizm automatycznego odpowiadania na powitania wypowiadane przez nowo przybyłych na kanał IRC. Bardzo podobnie można też - będąc opem lub halfopem - stworzyć prosty filtr wykopujący userów, którzy w wypowiedziach użyją przekleństw czy innych niedozwolonych słów.
Losowanie tabelkoweW niemal każdym programie chociaż raz zdarza się potrzeba, by "rzucić kośćmi" i pozwolić, by wydarzyło się coś losowego. Oznacza to skorzystanie z generatora liczb pseudolosowych, aby uzyskać 'przypadkową' wartość. Nie będzie ona faktycznie losowa, lecz dzięki zastosowaniu matematycznych formuł o dużej nieregularności rezultat może być bardzo zbliżony do ideału. Istnieje oczywiście wiele algorytmów wyznaczania liczb pseudolosowych, różniących się faktyczną przypadkowością uzyskiwanego wyniku.
W różnych językach programowania mamy natomiast odmienne sposoby na uzyskanie liczby 'losowej'. Zwykle najwygodniejszym jest wartość z zakresu [0..1], bo odpowiada to matematycznemu pojęciu prawdopodobieństwa. Aby w C++ uzyskać taki rezultat, wystarczy napisać proste opakowanie na biblioteczną funkcję rand:
Mając taki generator, możemy już łatwo sprawdzić, czy "zdarzyło się" coś, czego prawdopodobieństwo znamy:
W ten sposób możemy na przykład rzucać wirtualną monetą.
Sprawa się jednak komplikuje, jeżeli możliwych wyników doświadczenia jest więcej, a przy okazji mają one różne prawdopodobieństwa zajścia. Tak się dzieje na przykład w grach RPG, w których konieczne jest obliczanie rezultatów zadanych ciosów (trafienie, pudło, unik, blok, itp.). Skuteczność postaci w walce zależy zwykle od jej statystyk, więc szanse poszczególnych wyników nie są stałe i zmieniają się w trakcie gry.
Dobre generatory liczb pseudolosowych są zaś nierzadko względnie kosztowne obliczeniowo. Dlatego zamiast wykonywać po jednym losowaniu dla każdego możliwego rezultatu (zaszedł - nie zaszedł), znacznie lepiej jest załatwić wszystko jednym losowaniem. Nie jest to trudne:
// błąd
return -1;
}
Tak naprawdę liczymy tutaj dla każdego możliwego rezultatu wartość tzw. dystrybuanty. Ale chyba nie warto wnikać w takie teoretyczne szczegóły - grunt, że powyższa metoda działa w praktyce :) Trzeba tutaj jednak pamiętać, aby prawdopodobieństwa sumowały się do 1. Jeśli tak nie jest, można przeskalować wylosowaną liczbę.
Bolączki C++ #1 – Pimp(l) my codeKiedy programuje się w języku, który jest - jak to ładnie mówią Amerykanie - 'standardem przemysłowym' w danej dziedzinie, chcąc nie chcąc trzeba się do niego przyzwyczaić. A to oznacza, że musimy nauczyć się żyć z jego wadami, które niekiedy mogą być tylko irytujące, a niekiedy bardzo nieprzyjemne. Ważne, by mieć świadomość ich istnienia i w miarę możliwości sobie z nimi radzić.
C++ jako ustandaryzowany język ma już prawie dziesięć lat, więc lista jego niedoskonałości w porównaniu z nowszymi językami siłą rzeczy staje się coraz dłuższa. Takie listy są jednak w dużym stopniu subiektywne, zatem i ten, który rozpoczynam poniżej, absolutnie nie pretenduje do miana ostatecznej wyroczni :) Na pewno inni usunęliby z niego część pozycji, niektóre uznali za mniej lub bardziej dotkliwe, a także dodali własne propozycje.
Dla mnie sprawą, która w C++ jest powodem największego bólu zębów, jest sposób organizacji kodu zaproponowany w tym języku. Jest on bodaj jedynym mi znanym (poza swoim poprzednikiem C), w którym występuje podział plików z kodem na dwa rodzaje: pliki nagłówkowe (*.h, *.hpp) i moduły kodu (*.cpp). W tych pierwszych teoretycznie umieszczany jest interfejs klas i funkcji, czyli informacje potrzebne do ich użycia w innym miejscu programu. W tych drugich jest zaś zawarta implementacja rzeczonych klas oraz funkcji.
Tyle teorii. W praktyce osiągnięcie idealnej separacji obu tych elementów wymaga sztuczki nazywanej szumnie wzorcem projektowym, o nazwie Pimpl (skrót od private implementation). Wygląda on na przykład następująco:
// właściwa klasa
class CFoo
{
private:
CFoo_Impl* m_pImpl;
public:
CFoo();
~CFoo();
void SomeOperation1();
};
// -- foo.cpp --
#include "foo.hpp"
class CFoo_Impl
{
// implementacja
};
CFoo::CFoo() : m_pImpl(new CFoo_Impl) { /* ... */}
CFoo::~CFoo() { delete m_pImpl; }
void CFoo::SomeOperation1() { m_pImpl->SomeOperation1(); }
// itd.
Wtedy faktycznie nie widać, co siedzi w środku naszej klasy, ale cena takiej hermetyzacji to konieczność stworzenia dodatkowej klasy i przekierowania do niej metod. Może nie jest to dwa razy więcej roboty, ale przynajmniej 10-20%.
Z drugiej strony większość języków nowszych niż C++, jak Java, C# czy Python, w ogóle zna takich pojęć jak 'prototyp funkcji' czy 'deklaracja zapowiadająca'. Tam kod piszemy od początku do końca: funkcję zawsze z jej treścią, a klasę zawsze w całości łącznie ze wszystkimi polami i kodem wszystkich metod. Nie ma podziału na pliki z 'interfejsem' i z implementacją.
A w C++ ten podział istnieje i tak naprawdę służy on tylko i wyłącznie... wygodzie kompilatora. Dzięki temu, że treść plików nagłówkowych jest taka, a nie inna (i np. zawierają one deklaracje prywatnych pól klas, które są przecież częścią implementacji), mogą być one zwyczajnie dołączane do modułów przy pomocy arcyprymitywnej dyrektywy #include. Dla kompilatora jest to najprostsze rozwiązanie, bo może on pracować nad każdym modułem osobno i nie musi się martwić żadnymi niejawnymi zależnościami między plikami. A dla programisty oznacza to wybór między wygodą kodowania, a jakością i "odpornością" stworzonego kodu.
C++ stoi więc w pewnym sensie pośrodku i wcale nie jest to złoty środek. Z jednej strony mógłby pójść w kierunku wyznaczonym przez wspomniane nowsze języki, czyli wprowadzenia jednego typu plików źródłowych, zawierających kod bez podziału na deklaracje i implementacje. To by było jednak mało oryginalne :) Bardziej interesujące byłoby, jak sądzę, polepszenie istniejącego rozwiązania w odwrotnym kierunku; być może automatyczne stosowanie wzorca Pimpl dla każdej klasy jest jednym ze sposobów.
Niestety, patrząc na obecny roboczy szkic standardu C++0x, w którym nic na ten temat nie znajdziemy, nie możemy raczej oczekiwać, że coś tu się zmieni w przewidywalnej przyszłości.
I ty zostaniesz transportowcemGry, które są dobre, potrafią przykuć uwagę przeciętnego gracza na kilka czy kilkanaście miesięcy. Gry wyjątkowe są pamiętane nawet po kilku latach i ciągle mają zagorzałych zwolenników. Ale tylko nieliczne produkcje obrastają taką legendą, że po jakimś pojawiają się serwisy podobne do Diablo Evolution. Można tam obejrzeć w szczegółach, jak wyglądały kolejne stadia (alfa, beta, itd.) powstawania kultowego cRPG Diablo. Jeśli ktoś pamięta, jaka nowatorska była w swoim czasie ta gra, nie powinien być zdziwiony, że jej maniacy tworzą podobne strony.
Samo wspominanie przeszłości mało jednak wnosi i dlatego o wiele bardziej odpowiadają mi inicjatywy reanimacji starych gier. Taka operacja może być tylko dostosowaniem jej do uruchamiania na aktualnym sprzęcie i we współczesnych systemach operacyjnych - czego przykładem jest choćby emulator DOSa DOSBox. Niekiedy jednak fani mogą pokusić się o coś więcej...
Świetnym przykładem takiej udanej reanimacji jest OpenTTD - remake doskonałej gry ekonomicznej Transport Tycoon Deluxe sprzed, bagatelka, 12 lat. Muszę przyznać, że mam do niej ogromną słabość, gdyż TT było jedną z pierwszych 'prawdziwych' gier (czyli żadnych tam platformówek), w którą miałem okazję grać. Ahh, stare dobre czasy ;]
W obecnych czasach oryginalna wersja Transport Tycoon Deluxe zdecydowanie trąciłaby już myszką (bynajmniej nie tą z rolką i przyciskami), lecz jej open source'owa wersja - OpenTTD jest jak najbardziej do przyjęcia. Zmian w stosunku do oryginału jest sporo i obejmują też te techniczne, jak choćby możliwość gry w znacznie wyższych rozdzielczościach niż oryginalne 640x480. Pewnym modyfikacjom uległ też gameplay, ale są one na tyle przemyślane, że absolutnie nie zabijają pierwotnego ducha gry. Aż dziw, że taka subtelność uchowała się w programie rozwijanym jako open source ;P
Jeżeli więc też pamiętasz oryginalny Transport Tycoon, albo po prostu lubisz tego rodzaju starsze produkcje, OpenTTD powinien przypaść ci do gustu. Aby się o tym przekonać, musisz zrobić dwie rzeczy:
Piaskownica dla programistówZdarza się, że chcemy napisać coś "na szybko". Możliwe na przykład, że znaleźliśmy przepis na ciekawy efekt graficzny i chcemy go od razu wypróbować. Albo natrafiliśmy na nietypowy algorytm rozwiązujący jakiś problem i mamy ochotę go wypróbować w akcji. Zależnie od tego, jak bardzo lubimy eksperymentować, takie sytuacje mogą się zdarzać się z różną częstotliwością.
Tradycyjne środowiska programistyczne nie są w tym celu zbyt pomocne, gdyż stworzenie w nich nawet prostego programu wymaga ustawienia przynajmniej kilku opcji, stworzenia katalogu na kilkanaście wygenerowanych przy okazji plików - i tak dalej. Zamiast sedna musimy więc skupić się na nieistotnych szczegółach.
Kiedyś pomyślałem sobie, że dobrze byłoby mieć takie proste środowisko, w którym można by było oszczędzić sobie całego tego zachodu. Gdzie nie trzeba przejmować się takimi sprawami, jak kompilacja, wejście od użytkownika, ustawienie trybu graficznego , właściwa konstrukcja głównej pętli aplikacji, itp.
W zamierzchłych i na szczęście dawno minionych czasach namiastką czegoś takiego było środowisko dla "języka" LOGO, którym to niekiedy zamęczano biedne dzieci w gimnazjach :) W czasach nieco bliższych powstały interpretowane języki programowania z linią poleceń - jak na przykład Python - mogące służyć do wspomnianej 'niezobowiązującej zabawy'. Fachowo nazywa się ją prototypowaniem.
Ostatnio zaś znalazłem (za sprawą noogi z kanału #warsztat) coś, co wydaje się o wiele bliższe ideałowi. To open source'owe środowisko o ambitnej nazwie Processing, mające nieporównywalnie większe możliwości niż te wspomniane wyżej. Język programowania w nim zawarty jest bardzo przyjemny i ma "normalną" składnię z wyodrębnionymi blokami kodu - jak C(++/#), PHP czy Java - i możliwości zbliżone do ww. języków. Programy napisane w oparciu o Processing są krótkie i zwięzłe, bowiem większość potrzebnych narzędzi, takich jak inicjalizacja grafiki (opartej o OpenGL) czy podpięcie wejścia klawiatury i myszy, zostało już zintegrowanych. Naszym zadaniem jest tylko wypełnienie treścią dość oczywistych funkcji jak draw, setup czy mousepressed; niekoniecznie zresztą wszystkich.
Od strony technicznej Processing też prezentuje się całkiem znośnie. Jego IDE ma aczkolwiek jeden bolesny mankament: jest napisane w Javie i wszyscy wiemy, co to oznacza dla szybkości jego uruchamiania :) Cały system jest zresztą oparty właśnie o Javę, ale to staje się akurat bardziej zaletą niż wadą. Jedną z nich jest ładny język programowania, a inną to choćby możliwość bezbolesnego eksportu napisanych programów do postaci zwykłych plików wykonywalnych dla różnych systemów operacyjnych. Można też tworzyć z nich aplety do umieszczenia na stronach WWW, co pokazuje choćby ten atraktor Lorenza.
Aplikacje wygenerowane w ten sposób na pewno nie zastąpią tych pisanych z użyciem prawdziwych kompilatorów, ale podejrzewam, że nie taki jest cel. Chodzi przede wszystkim o tę 'piaskownicę', w której można bezboleśnie wypróbowywać nowe idee.
Cztery metody debugowaniaNikt nie jest nieomylny - zwłaszcza jeżeli chodzi o programistów. Błędy w kodzie zawsze były, są i będą się pojawiały z mniejszą lub większą regularnością. Najważniejsze więc to umieć sobie z nimi radzić. Według mnie jest to tak samo ważna umiejętność jak posługiwanie się jakimś językiem programowania czy biblioteką graficzną. Tyle że jest ona o wiele przydatniejsza, bo znacznie bardziej uniwersalna.
Często - przede wszystkim w firmach tworzących oprogramowanie - usuwaniem błędów zajmują się wyspecjalizowane osoby, czyli testerzy. Jak wiadomo w każdym programie jest przynajmniej jeszcze jeden błąd, a przy użyciu odpowiednich technik można wyłapać przynajmniej te, z którymi miałaby szansę zetknąć się przynajmniej pewna część końcowych użytkowników.
Tego rodzaju błędy muszą być zwykle specjalnie wyszukiwane, jednak na co dzień koderzy spotykają się z takimi, które same 'znajdują' programistów. Mówiać wprost, często (o wiele za często) zdarza się, że po prostu coś nie działa - albo nie działa tak, jak powinno.
Co wtedy zrobić? Osobiście stosuję poniższe metody. Uszeregowałem je w kolejności od najmniej do najbardziej drastycznej, a jednocześnie od najmniej do najbardziej skutecznej. Kryteriami rosnącymi w dół listy jest także czasochłonność oraz stopień desperacji programisty :) A rzeczone sposoby są następujące:
x zamiast y.
Przegląd kodu bez asysty debuggera wydaje się krokiem wstecz, lecz tak naprawdę jest on bardziej skuteczny (i jednocześnie bardziej czasochłonny). To sposób znany też jako 'metoda pluszowego misia'. Polega na on wytłumaczeniu wspomnianemu misiowi działania naszego kodu - linijka po linijce - a miś znajdzie błąd. Oczywiście prawdziwą przyczyną rozwiązania problemu będzie fakt, że zastanawiając się ponownie nad działaniem napisanego kodu mamy znacznie większe szanse na dostrzeżenie popełnionym w nim gaf. Podobno mistrzowie tej techniki debugowania potrafią nawet obywać się bez misia :)Jeżeli wszystko zawodzi, pozostaje jeszcze ostatnia deska ratunku, czyli szukanie pomocy z innych źródeł, np. na forach. Z praktyki widać jednak, że często preferowana kolejność postępowania jest dokładnie odwrotna. A to na dłuższą metę nie jest to rozsądne, ponieważ osobą najlepiej przygotowaną do znalezienia błędu w kodzie jest sam jego autor.
Świat były aczkolwiek o wiele piękniejszy, gdyby konieczność takich poszukiwań nie zdarzała się zbyt często :)