Last weekend I had the pleasure to attend the game development conference IGK in Siedlce, Poland. Although gamedev is not something I normally do, it was a fun experience.
One thing I was especially looking forward to was the one day-long game programming contest called Compo. Like last year and the one before, I formed a team with Adam “Reg” Sawicki and Krzysztof “Krzysiek K.” Kluczek, guest starring Kamil “Netrix” Szatkowski. That’s four coders in total, so we kinda had some problem with, say, more artistic part of the endeavor :)
Nevertheless, the result was pretty alright: we scored 4th place out of about dozen teams. The theme this time was an “artillery” game with few mandatory features: two types of energy-like resources (HP & MP), achievements and multiplayer gameplay.
We nailed the last one by implementing support for two mice at once. This allowed us to come up with an interesting idea inspired by the Atari game Rampart: two castles that expand and attack each other in a fast-paced duel full of frantic clicking. And because the attackers, or guardians of each castle are dragons, hence the game’s title: Here Be Dragons.
[2013-04-07] Here Be Dragons (3.2 MiB, 1,724 downloads)
In retrospect, we should probably have narrowed the scope of our project more. Doing it 3D was ambitious in itself, but packing it with all the features we planned the night before turned out to be slightly too much :) In the end we didn’t even have time to code any sound effects or music!… That’s certainly a lesson to learn here.
But all in all, it was great fun. It certainly inspired me to maybe try and brush up my gamedev skills a bit more :)
Yesterday I came back from IGK conference (Engineering of Computer Games) in Siedlce. Among few interesting lectures and presentations it also featured a traditional, 7-hour long game development contest: Compo. Those unfamiliar with the concept should know that it’s a competition where every team (of max. 4 people) has to produce a playable game, according to particular topic, e.g. theme or genre. When the work is done, results are being presented to the public while a comittee of organizers votes for winners.
As usual, I took part in the competition along with Adam Sawicki “Reg” and Krzystof Kluczek “Krzysiek K.“. Topic for this year revolved around the idea of “escape” or “survival” kind of game, so we designed an old school, pixel-artsy scroller where you play as a prisoner trying to escape detention by running and avoiding numerous obstacles. We coded intensely and passionately, and in the end it paid off really well because we actually managed to snatch the first place! Woohoo!
A whopping amount of 15 teams took part in this year’s Compo, so it might take some time before all the submissions are available online. Meanwhile, I’m mirroring our game here. Please note that it uses DirectX 9 (incl. some shaders), so for best results you should have a non-ancient GPU and Windows OS. (It might work under Wine; we haven’t tested it).
[2012-04-01] Prison Escape (5.8 MiB, 1,910 downloads)
Spośród wielu nowości wprowadzonych w HTML5, API do rysowania dwuwymiarowej grafiki rastrowej bez użycia wtyczek typu Flash jest z pewnością jednym z najbardziej interesujących. Wielu zresztą twierdzi (wliczając w to prominentne figury świata IT), że zwiastuje to początek końca wspomnianych pluginów. Czy rzeczywiście tak będzie, to oczywiście zobaczymy w niedalekiej przyszłości. Już teraz jednak można zobaczyć liczne przykłady na to, że technologia ta oferuje naprawdę spore możliwości.
W ramach zapoznawania się z poszczególnymi składowymi HTML5, nie mogłem więc nie przyjrzeć się bliżej elementowi <canvas>
– bo to o nim oczywiście mowa. Tradycyjnym testem dla tego rodzaju rozwiązań (tj. graficznych bibliotek 2D), który zawsze staram się zaimplementować na początku, jest… chmara odbijających się piłeczek :) Nie inaczej było i w tym przypadku, czego efekty zamieszczam tutaj wraz z krótkim opisem i wprowadzeniem w podstawy Canvasa.
Kolejna konferencja IGK zakończyła się wczoraj tradycyjnym konkursem zespołowego programowania gier Compo. Podobnie jak w zeszłym roku startowało wiele zespołów, w tym także i mój, czyli Rzeźnicy Inc. ;) I podobnie jak zawsze, wszyscy starali się zakodować jak najlepiej wyglądające, najbardziej grywalne, najoryginalniejsze i w ogóle najlepsze gry. Naturalnie nie wszystkim się to udało, ale akurat w przypadku mojej drużyny rezultat był chyba całkiem niezły. W każdym razie zasłużył na drugie miejsce, czyli analogicznie jak w zeszłym roku. Krótko mówiąc: trzymamy poziom :)
W najbliższych dniach postaram się zamieścić na stronie jakąś grywalną wersję naszej pracy.
Nauczyłem się już lubić fakt, że w przypadku informatyki powiedzenie o “ciekawych czasach” jest truizmem, bo ciekawie jest po prostu zawsze – głównie ze względu na tempo zmian w wielu dziedzinach. Nawet w tych, wydawałoby się, zastygłych na lata. Niecałe trzy lata temu zżymałem się na przykład na zbytnią ufność w doskonałość obiektowych metod programowania. Dzisiaj zaś przychodzi mi robić coś zdecydowanie przeciwnego.
Programowanie obiektowe jest obecnie sztandarowym kozłem ofiarnym i chłopcem do bicia, otrzymującym ciosy z wielu stron. Już nie tylko programiści gier twierdzą, że nie mogą sobie na nie pozwolić ze względu na wydajność i zamiast niego forsują Data Oriented Design. Pokazywałem niedawno, że sprzeczność między tymi dwoma podejściami jest raczej pozorna niż rzeczywista. Teraz natknąłem się na interesującą opinię, która podważa sens OOP-u jako metodologii, wychodząc z nieco innego punktu widzenia niż wydajność dla celów grafiki real-time:
Object-oriented programming (…) is both anti-modular and anti-parallel by its very nature, and hence unsuitable for a modern CS curriculum. [pogrubienie moje]
Anty-modularne i anty-współbieżne? Oczywiście; da się napisać kod obiektowy, który te dwa warunki będzie spełniał doskonale. Ale to nie oznacza, że każdy kod obiektowy je spełnia, a to właśnie jest implikowane powyżej. Nie da się tego określić inaczej niż jako stereotyp – i to w modelowej wersji, czyli negatywnego uogólnienia z pojedynczych przypadków.
Jako antidotum na te rzekome bolączki OOP-u często wymieniane jest programowanie funkcyjne. Nie ujmując mu niczego ze swojej elegancji, nie mogę jednak nie zauważyć, że zamiata ono wiele problemów pod dywan. Określanie wykonania programu jako serii transformacji danych nie rozwiązuje jednak problemu: gdzie i jak te dane mają być zapisywane i chronione przed równoczesnym dostępem z wielu ścieżek wykonania. Sytuacje, w których programowanie funkcyjne lub quasi-funkcyjne sprawdza się dobrze to takie, gdzie problemy te dały się w miarę łatwo rozwiązać. Tak jest chociażby w przypadku vertex i pixel shaderów, gdzie podział danych wejściowych i wyjściowych na rozłączne bloki jest wręcz naturalny. Fakt ten nie jest jednak zasługą programowania funkcyjnego, tylko natury zagadnienia – w tym przypadku renderowania grafiki opartej na wielokątach.
I właśnie o tym powinniśmy pamiętać, gdy wyzłośliwiamy się nie tylko na OOP, ale dowolny inny paradygmat programowania. Otóż porzucenie go nie sprawi od razu, że magicznie zaczniemy pisać kod doskonale modularny. A już nie pewno nie spowoduje, że niezwykle trudne zagadnienia współbieżności staną się nagle banalnie proste. To niestety tak nie działa.
Nie znaczy to oczywiście, że nie powinniśmy poszukiwać nowych, lepszych metodologii do konkretnych zastosowań. Dlatego przecież wiele języków (np. C++, C#, Python) ewoluuje w kierunku wieloparadygmatowości, aby możliwe było dobranie właściwych narzędzi dla danej sytuacji. Nie wydaje mi się jednak, aby uleganie trendy nurtom krytykowania jakichkolwiek rozwiązań poprzez odwoływanie się do stereotypów i nieuzasadnionych wyobrażeń o nich było w tym procesie specjalnie produktywne. Zdaję sobie jednak sprawę, że “funkcje wirtualne to zuo!” brzmi lepiej niż “wywoływanie funkcji wirtualnych skutkuje narzutem wydajnościowym związanym z dodatkowym adresowaniem pamięci (które nie jest cache-friendly) i może powodować niepożądane skutki uboczne, jeśli ich wersje w klasach pochodnych nie są thread-safe“. Mam jednak nadzieję, iż nikt nie ma wątpliwości, które z tych dwóch stwierdzeń jest bardziej racjonalne.
Podziękowania dla Rega za podesłanie linków, które zainspirowały mnie do podjęcia tego tematu.
Jako dzisiejszą ciekawostkę chcę przedstawić pewną technikę, która może być przydatna przy implementacji systemu GUI na przykład na potrzeby gier. Zazwyczaj bowiem elementami interfejsu są różnego rodzaju kontrolki, które – chociaż nierzadko nie przypominają klasycznych widgetów w rodzaju przycisków czy pól tekstowych – posiadają prostokątne obramowanie. Ta ramka musi mieć jedną istotną cechę: powinna dopasowywać się do rozmiaru kontrolki, dopuszczając elementy o dowolnie dużych wymiarach. A jednocześnie powinna ona być utrzymana w odpowiednim stylu graficznym, zwykle spójnym z resztą aplikacji lub gry.
Sposobem na sprostanie tym wymaganiom jest podział prostokąta kontrolki na dziewięć części, różniących się zakresem skalowania, jakiemu są poddawane, gdy kontrolka zmienia rozmiary. Wśród tych części możemy mianowicie znaleźć następujące:
Żeby więc poprawnie narysować naszą kontrolkę, potrzebujemy w ogólności aż dziewięciu różnych obrazków. Oczywiście niekiedy będzie ich mniej, jeśli sprytnie posłużymy się obrotami i odbicia obrazów. Prawie na pewno jednak będzie to więcej niż jeden sprite i każdy z nich trzeba będzie jakoś oznaczyć i zapisać razem z naszą aplikacją, wymyślając do tego mniej lub bardziej skomplikowaną konwencję.
To, co chcę przedstawić, to w gruncie rzeczy taka właśnie konwencja. Nazywa się ona nine-patch images, czyli tytułowe obrazki dziewięciołatkowe. Pochodzi ona z systemu Android i jest jedną z wielu może mało efektownych, ale pomysłowych rozwiązań, ukrytych w jego API.
Idea jest dość prosta. Jeśli mamy już obramowanie w postaci obrazka otoczonego nim prostokąta, nie musimy wycinać z niego poszczególnych części i zapisywać ich osobno. Zamiast tego zaznaczamy je bezpośrednio na obrazku, dodając wpierw jednopikselowe, czarno-białe obramowanie. Czarny fragment określa część podlegającą skalowaniu (czyli środek), zaś biały – niezmieniające rozmiaru mu rogi prostokąta. Najlepiej widać to na ilustracji po lewej stronie.
Można na niej zauważyć, że do określenia obszaru skalowania wystarczy zasadniczo jeden dodatkowy rząd i jedna dodatkowa kolumna pikseli. Pozostała część dodatkowego obramowania może nam wtedy posłużyć do zdefiniowania obszaru treści, czyli tego fragmentu kontrolki, który będzie mógł być wypełniony tekstem i innymi elementami, ustawionymi jako jej zawartość. Obszar treści nie musi pokrywać się z obszarem skalowania, co z kolei jest widoczne na obrazku po prawej stronie.
Z tak przygotowanej grafiki wciąż możemy w miarę łatwo odczytać programowo potrzebne informacje o sposobie rysowania obramowania kontrolki i wypełniania jej treści. Rozwiązanie to ma przy tym tę oczywistą zaletę, że przygotowanie takiej grafiki jest z pewnością mniej pracochłonne i kłopotliwe niż tworzenie od 3 do 9 osobnych sprite‘ów i podawanie informacji o obszarze treści w jakiegoś rodzaju pliku konfiguracyjnym.
Jeśli więc ktoś w ramach silnikologii stosowanej zajmuje się implementacją modułu GUI, może rozważyć zastosowanie podobnego systemu :)
Moi koledzy-częściowo-po-fachu, czyli programiści silników gier, wymyślili niedawno magiczny trzyliterowy akronim DOD – skrót od Data-Oriented Design, czyli projektowanie oparte o dane. Oczywiście określenie ‘niedawno’ jest względnym i pewnie wielu z nich orzekło by, że DOD jest z nimi już całkiem długo. Każdy mem potrzebuje jednak czasu na rozprzestrzenienie się, a w przypadku tego fala tweetów na jego temat dotarła do mnie dopiero niedawno. Niedługo potem rzecz wydała mi się cokolwiek podejrzana.
Podstawowe pytanie brzmi rzecz jasna: o co w tym właściwie chodzi?… Ponieważ mówimy o programowaniu gier, to odpowiedź jest jasna: jeśli nie wiadomo o co chodzi, to chodzi o wydajność. W zaawansowanych grach czasu rzeczywistego mamy do czynienia z ogromną ilością danych, na których trzeba wykonać wiele, często skomplikowanych operacji, a wszystko to jeszcze musi być zrobione dostatecznie szybko, aby możliwe było pokazanie na ekranie kolejnej klatki bez widocznych przycięć. Dlatego też już dawno zauważono, że kodowanie “blisko sprzętu” się opłaca, bo pozwala maksymalnie wykorzystać jego możliwości.
To oczywiście nakłada na kod pewne wymagania oraz stwarza konieczność zwrócenia uwagi na rzeczy, którymi “normalnie” nie ma potrzeby się zajmować. Ładnym przykładem jest chociażby zarządzanie pamięcią. W wielu językach jest ono albo kompletnie pomijalne (garbage collector), albo sprowadza się do dbania o to, aby każdy zaalokowany blok był w końcu zwolniony. Gdy jednak stawiamy na wydajność, powinniśmy też zainteresować się szybkością samej operacji alokacji oraz takim rozmieszczeniem przydzielanych bloków, aby komunikacja na linii procesor-pamięć odbywała się z jak najmniejszą liczbą zgrzytów.
Ten i wiele podobnych szczegółów platformy sprzętowej powodują, że pisanie efektywnego kodu w silnikach gier to często dość literalne postępowanie według zasady Do It Yourself, połączone z ignorowaniem części feature‘ów wysokopoziomowych języków programowania, o których wiadomo, że negatywnie odbijają się na wydajności. Cóż, życie; nie ma w tym nic zaskakującego. Myślę, że każdy co bardziej zaawansowany programista zdążył zdać sobie sprawę z tego, że wszelkie koderskie udogodnienia związane z podniesieniem poziomu abstrakcji mają swój koszt liczony w dodatkowych cyklach procesora (i nie tylko). Rezygnacja z nich jest więc dobrym posunięciem, jeśli chcemy te “stracone” cykle odzyskać.
Robiąc to, będziemy mieli ciastko, ale już nie będziemy mogli go zjeść – a to oczywiście nie jest przyjemne. I po części zapewne stąd wzięło się pojęcie DOD, które nie odnosi się do niczego w gruncie rzeczy nowego, ale pozwala łatwiej odnosić się do tego rodzaju koderskich praktyk poprzez nadanie im nazwy. A przy okazji – jak mi się wydaje – w jakiś nie do końca wytłumaczalny sposób redukuje dysonans poznawczy programistów silników gier, którzy świadomie muszą pozbawiać się możliwości przestrzegania “jedynie słusznych” zasad pisania kodu.
Jak dotąd wszystko jest w gruncie rzeczy bardzo ładne i sensowne, i bez problemu zgadzam się z postulatami Data-Oriented Design tam, gdzie się one aplikują. Zgadzam się nawet z tą domniemaną ukrytą motywacją, zwłaszcza że sam nieraz narzekałem na owe “jedynie słuszne” rady. Za to nijak nie mogę pojąć, dlaczego następnym krokiem – po wynalezieniu pojęcia DOD – był mniej lub bardziej frontalny atak na programowanie obiektowe, określane nieco bardziej znanym (ale naturalnie również trzyliterowym) akronimem OOP.
Nie, nie chodzi o to, że programowanie obiektowe jest doskonałe – bo nie jest, nie było, nigdy nie będzie i nawet nie aspiruje do miana finalnego rozwiązania dla dowolnego problemu (już nie wspominając o tym, że takowe po prostu nie istnieją). Rzecz w tym, że zwolennicy DOD (DOD-a? :]) w nieprzemyślany sposób wybrali sobie przeciwnika, nie zauważając, że jest on paradygmatem zupełnie innego rodzaju niż ich własny. A to przecież takie proste:
Widać to, prawda?… Miedzy powyższymi dwoma podejściami nie tylko nie ma sprzeczności. One są od siebie po prostu niezależne, co oznacza również, że mogą występować razem w jednym programie.
Jeśli Data-Oriented Design koniecznie potrzebuje jakiegoś przeciwnika, to są nim raczej inne xOD-y, których jest już przynajmniej kilka, chociaż wiele nie zostało jeszcze nawet nazwanych. (Dobry przykład to projektowanie oparte o user experience, czyli wrażenie użytkownika, gdzie priorytetem jest m.in. responsywność, nie będąca wcale synonimem wydajności). To, co piewcy DOD zdają się krytykować w swoich publikacjach, to jakieś “projektowanie oparte o eleganckie abstrakcje”, czyli pisanie kodu, który jest sztuką dla sztuki: ładnie wygląda (w założeniu), ściśle trzyma się założeń używanego paradygmatu przy jednoczesnym eksploatowaniu wszelkich jego “zdobyczy” (czyli np. wzorców projektowych). I chociaż bywają w swoich wysiłkach niezwykle twórczy (w prezentacjach z tego tematu spotkałem nawet cytaty z Baudrillarda), to nie zmienia to faktu, że kopią leżącego (czy raczej biją martwego konia, jakby to powiedzieli Amerykanie ;-)). Bo jeśli ktoś naprawdę posuwa się do takich absurdów jak czteropoziomowa hierarchia dziedziczenia obiektów gry, to znaczy że ma znacznie poważniejsze problemy niż okazjonalny cache miss :)