RSS

Prototypowanie gier w języku BASIC

Liczba odsłon: 276

Gry komputerowe to programy najczęściej wykorzystujące kolorową grafikę, szybką animację i wielkie macierze opisujące wygląd świata. By gra działała płynnie, zawartość ekranu musi być odrysowywana kilkanaście lub kilkadziesiąt razy w ciągu jednej sekundy. Kiedyś – w erze mikrokomputerów ośmiobitowych – by osiągnąć taki efekt konieczne było programowanie w kodzie maszynowym i wykorzystywanie wszystkich możliwości układów komputera. Dzisiaj można już – bez znaczącej utraty wydajności – korzystać z języków takich jak C czy C++ i uniwersalnych bibliotek graficznych (na przykład DirectX czy SDL).

Mimo jednak, że programowanie w C++ jest o niebo mniej kłopotliwe, niż w asemblerze, trudno zmusić się do rozpoczęcia pracy. Nawet proste zadanie w stylu załadowania biblioteki SDL, ustalenia trybu graficznego, wypełnienia ekranu jednolitym kolorem i narysowania paru obrazków załadowanych z pliku BMP stawia przed programistą konieczność przebijania się przez dziesiątki funkcji i struktur. Jeszcze gorzej jest, jeżeli chce się skorzystać bezpośrednio z usług biblioteki DirectX: bardziej skomplikowane funkcje są bowiem dodatkowo obudowane interfejsem COM, wymagającym dodatkowego czasu potrzebnego na przypomnienie sobie sposobu zapisania kodu oraz na samo wpisanie tego kodu.

Pisanie kilkuset wierszy kodu tylko w celu udowodnienia, że genialny pomysł układu graficznego ekranu gry jest do niczego (lub wręcz przeciwnie, faktycznie wart jest zastosowania w prawdziwej grze) na pewno nie jest tym, co programista lubi najbardziej. Często taka wizja zniechęca do pisania w ogóle, albo skłania do trzymania się chybionego, ale już zakodowanego pomysłu.

Tymczasem w prototypie gry – mającym na celu jedynie zaprezentowanie, jak gra będzie się prezentowała na ekranie komputera – szybkość działania jest prawie bez znaczenia. Ważne, by maksymalnie wygodnie zaprogramować jakiś jeden statyczny ekran lub prostą animację. O ile zatem rezygnację z C/C++ w przypadku kodu gry można nazwać co najmniej dyskusyjną (a czasem nawet szaloną) decyzją, prototyp można zapisać w czymkolwiek. Ja do tego celu proponuję... język BASIC.


Język BASIC przez lata zyskał sobie opinię nieeleganckiego, sprzyjającego pisaniu nieczytelnego kodu, szybko tracącego na przejrzystości i – przede wszystkim – powolnego. Powolność była problemem przede wszystkim interpretowanych dialektów języka BASIC: po kompilacji szybkość realizacji programu była już zazwyczaj całkiem przyzwoita, choć nadal wyraźnie ustępowała językom C i C++. Język BASIC ma jednak wielką zaletę: umożliwia zapisanie wielu przewidzianych przez twórców dialektu konstrukcji w sposób bardzo zwarty i prosty. Tam, gdzie język C++ praktycznie wymusza tworzenie własnej klasy (a więc zapisanie kilkuset wierszy kodu), BASIC ogranicza się do kilku instrukcji. Problem pojawia się dopiero, gdy chce się zapisać w nim strukturę nie obsługiwaną przez język.

Właśnie ta zwartość powoduje, że BASIC świetnie nadaje się do stworzenia prototypu gry. Wystarczy wybrać dialekt, który obsłuży w prosty sposób operacje graficzne wysokiej rozdzielczości, pozwoli załadować grafikę z osobnych plików przygotowanych zwykłym programem graficznym i będzie się charakteryzował przyzwoitym tempem działania. Ja wybrałem FreeBASIC.

Załóżmy, że chcę przetestować pomysł na detektywistyczną grę przygodową. Wymarzył mi się prosty w programowaniu płaski rzut boczny z pewnej wysokości nad polem gry, nie jestem jednak pewien, czy będzie on wystarczająco elegancki i czytelny.

W przypadku języka C++ czekałoby mnie stworzenie pliku Makefile, zapisanie podstawowej struktury programu, stworzenie obiektów klas obsługujących ekran graficzny i żmudne uruchomienie całości. Dobre zintegrowane środowisko programistyczne trochę uprościłoby to zadanie, ale dalej stworzenie prototypu oznacza więcej pracy, niż jestem w stanie wykonać. Otwieram zatem edytor tekstu, tworzę plik test.bas i zapisuję:

Screen 14,32,2

Liczba 14 oznacza tryb graficzny 320×240, liczba 32 — rozdzielczość opisu koloru (32 bity), a liczba 2 — liczbę ekranów (jeden główny i jeden wirtualny, który może przydać się przy animacji). Zapisuję plik, przechodzę do wiersza polecenia, piszę:

fbc test.bas
test

i na ekranie miga okienko programu. Oczywiście — zapomniałem dopisać wiersz czekający na naciśnięcie klawisza przez użytkownika:

Screen 14,32,2
Sleep

Tym razem program działa właściwie. Dwa wiersze kodu umożliwiły osiągnięcie efektu, który w języku C wymaga zapisania pętli obsługującej komunikaty Windows lub SDL, kilku wierszy tworzących powierzchnie graficzne i wybierających tryb graficzny oraz kilku wierszy wyłączających grafikę i zwalniających pamięć. Poza tym program uruchamia się w zasadzie z biegu, bez tworzenia osobnego pliku Makefile czy wpisywania dziesiątek parametrów kompilatora!

Puste okno programu

Teraz czas na dołożenie fragmentu programu dzielącego okno na „klocki” o rozmiarze 32×16 pikseli. Taki rozmiar klocka pozwala stworzyć przyjemnie wyglądającą grafikę nawet osobie pozbawionej większych zdolności w tym względzie. Na ekranie zmieści się 10×15 takich klocków, co jest wielkością absolutnie zadowalającą. Proporcje są tak dobrane, by powstało złudzenie perspektywy.

Screen 14,32,2
Dim as integer r,c
For r=1 to 15
 Line (0,r*16)-(319,r*16)
Next r
For c=1 to 10
 Line (c*32,0)-(c*32,239)
Next c
Sleep

Okno z naniesioną kratą pól

Teraz chciałbym wypróbować, jak będzie wyglądała mapa gry, jeżeli wypełnię ją klockami przypominającymi trawę. Do zapełnienia mam 10×15 pól o rozmiarze 32×16 pikseli, ale na wszelki wypadek założę, że każde pole może wystawać nieco „ponad” siebie, nakładając się na pole w wyższym rzędzie. Takie przenikanie pól pozwoli uzyskać jeszcze lepsze wrażenie perspektywy.

Trawa Na początek przygotowuję odpowiedni klocek z wizerunkiem trawy. Obszar przezroczysty wypełniam soczystą purpurą (255,0,255), traktowaną wewnętrznie przez FreeBASIC jako kolor przezroczysty. Załadowanie pliku BMP wymaga tylko dwóch wierszy kodu: tworzącego nowy obrazek o rozmiarze 32×24 piksele i pobierającego go z pliku:

Dim trawa as any ptr = ImageCreate(32,24)
BLoad "grass.bmp", trawa

Zwolnienie pamięci zajmowanej przez obraz to tylko jeden wiersz kodu:

ImageDestroy(trawa)

Teraz wystarczy tylko zamienić dwie pętle rysujące kratownicę na dwie zagnieżdżone pętle rysujące kolejne rzędy pól:

Screen 14,32,2
Dim trawa as any ptr = ImageCreate(32,24)
BLoad "grass.bmp", trawa
Dim as integer r,c
For r=1 to 15
 For c=1 to 10
  Put (c*32,r*16-8),trawa,Trans
 Next c
Next r
Sleep
ImageDestroy(trawa)

Okno wypełnione trawą

W prawdziwej grze rodzaj podłoża nie będzie oczywiście wpisany na stałe w kod, a zapamiętany w macierzy (na przykład kodami stanowiącymi indeks do tablicy możliwych rodzajów pól: trawy, jeziora, brzegu jeziora, ścieżki i tak dalej). Dopóki jednak chcemy zobaczyć, jak będzie się ogólnie prezentowała forma graficzna, możemy zignorować uniwersalność, szybkość i wygodę i wypełnić mapę trawą, ewentualnie nakładając na nią pojedyncze pola innego typu.

Sama trawa nie pozwala jeszcze ocenić, jak będzie wyglądała grafika w grze. Przydałoby się postawić na niej parę elementów, na przykład kawałek muru. Najpierw trzeba jednak przygotować grafiki (o szerokości 32 pikseli i wysokości powiedzmy 64 pikseli) odpowiadające ścianie widzianej od przodu i z boku, oraz czterem możliwym wariantom narożnika. Pozwoli to nie tylko budować dowolnego kształtu mury, ale też – w przyszłości – zaimplementować możliwość zmiany strony, od której na scenę „patrzy” kamera.

1 1 1 1 1 1

Ładowanie elementów ściany i zwalnianie pamięci na końcu programu najprościej jest zrealizować za pomocą pętli:

Dim sciana(6) as any ptr
Dim i as integer
For i=1 to 6
 sciana(i) = ImageCreate(32,64)
 BLoad "wall-"+Str(i)+".bmp", sciana(i)
Next i
... program ...
For i=1 to 6
 ImageDestroy(sciana(i))
Next i

Cały program – i efekt jego działania – wyglądają następująco:

Screen 14,32,2

Dim trawa as any ptr = ImageCreate(32,24)
BLoad "grass.bmp", trawa

Dim sciana(6) as any ptr
Dim i as integer
For i=1 to 6
 sciana(i) = ImageCreate(32,64)
 BLoad "wall-"+Str(i)+".bmp", sciana(i)
Next i

Dim as integer r,c
For r=0 to 14
 For c=0 to 9
  Put (c*32,r*16-8),trawa,Trans
 Next c
Next r

Put (0*32,0*16),sciana(1),Trans
Put (1*32,0*16),sciana(5),Trans
Put (2*32,0*16),sciana(5),Trans
Put (3*32,0*16),sciana(5),Trans
Put (4*32,0*16),sciana(5),Trans
Put (5*32,0*16),sciana(5),Trans
Put (6*32,0*16),sciana(4),Trans
Put (0*32,1*16),sciana(6),Trans
Put (6*32,1*16),sciana(6),Trans
Put (0*32,2*16),sciana(6),Trans
Put (6*32,2*16),sciana(6),Trans
Put (0*32,3*16),sciana(6),Trans
Put (6*32,3*16),sciana(6),Trans
Put (0*32,4*16),sciana(6),Trans
Put (3*32,4*16),sciana(1),Trans
Put (4*32,4*16),sciana(5),Trans
Put (5*32,4*16),sciana(5),Trans
Put (6*32,4*16),sciana(3),Trans
Put (3*32,5*16),sciana(6),Trans
Put (3*32,6*16),sciana(6),Trans
Put (3*32,7*16),sciana(6),Trans

Sleep

ImageDestroy(trawa)
For i=1 to 6
 ImageDestroy(sciana(i))
Next i

Mur na trawie

Trawa Otoczony murem obszar zarośnięty trawą wygląda dziwnie, spróbujmy go zatem wypełnić małymi płytkami — takimi, jak na obrazku po lewej stronie. Nie musimy w tym celu modyfikować pętli rysującej trawę: wystarczy dodać kod ładujący plik graficzny z dysku, dodatkowy zestaw pęli nakładających płytki na trawę i wiersz usuwający obiekt graficzny z pamięci:

Dim kafelek as any ptr = ImageCreate(32,24)
BLoad "kafel.bmp", kafelek
... program ...
i=6
For r=3 to 10
 If r=8 then i=3
 For c=0 to i
  Put (c*32,r*16),kafelek,Trans
 Next c
Next r
... program ...
ImageDestroy(kafelek)

Płytki

Od razu nasuwa się wniosek, że konieczne będzie przygotowanie pola łączącego w sobie kafelki i trawę. Najprościej będzie wprowadzić do kodu możliwość nakładania na siebie dwóch różnych elementów podłoża i na trawę nakładać „połówkowe" kafelki z określonym obszarem przezroczystości. W ten sposób pomieszczenia zapełnione kafelkami, parkietem czy brukiem będą mogły być stawiane nie tylko na trawie, ale też na piasku, ziemi czy betonie — wirtualnych, oczywiście.

Okno Teraz wykorzystajmy przezroczystość i zastąpmy dwa kawałki muru oknami. Mam nadzieję, że mając do dyspozycji obrazek bez problemu już samodzielnie uzupełnicie kod o niezbędnych pięć wierszy.

Płytki


Celem tego artykułu nie było zaprezentowanie gotowego programu gry, czy nawet sposobu zapisania gry. Przedstawiony pomysł wymagałby jeszcze– w razie faktycznej realizacji – mnóstwa pracy i zapisania kodu w języku C++.

Tutaj widać przewagę języka BASIC. W ciągu kilku godzin można przygotować szkielet modułu rysującego i uzyskać na ekranie obraz podobny, jak po kilku dniach żmudnego wstukiwania kodu. Owszem, funkcjonalność kodu będzie bliska zeru, ale liczy się możliwość zobaczenia, czy wymyślona forma graficzna sprawdzi się, czy też będzie całkowicie nieczytelna. A im mniejszym kosztem taka weryfikacja będzie następowała, tym chętniej programista sprawdzi każdy pomysł przed jego zaimplementowaniem.

Język BASIC może mieć mnóstwo wad i słusznie może być spychany do zastosowań niszowych, lecz – jak widać – w niszach tych trudno znaleźć cokolwiek prostszego i efektywniejszego od niego.


He, ale to nic nowego. :)
Mój kolega z demosceny małego Atari (znany jako Charlie) prototypował efekty do dem (!) w Basicu. Oczywiście klatka rysowała się nieraz kilkanaście i kilkadziesiąt sekkund, ale działało — tak sprawdzał swoje koncepcje. Widziałem więc np. renderowane i teksturowane kule i efekt Dooma w Basicu.
Nawiasem mówiąc, dużo gier — nawet gier akcji — było na Atari pisanych w Basicu i kompilowanych jedynie. Czasami trudno było uwierzyć, że to zwykły Turbo Basic XL.
Oczywiście, nie jest to nic nowatorskiego ani niesamowitego, ale obstawiam, że mało kto wie, że można tak prosto oprogramować grafikę rastrową 2D. W zasadzie trzeba mieć więcej uzdolnień rysowniczych (i cierpliwości!), niż zdolności programistycznych. Artykuł ma być tutorialem dla osób mających pomysły, ale nie mających dość umiejętności czy po prostu odwagi, by zabrać się za programowanie.
Autor naciąga trochę z tą trudnością DirectX – sam w swoim czasie walczyłem z tym i idzie przeżyć choć nie jest łatwe. To co jest napisane w Basicu można z powodzeniem zrealizować w Pascalu, albo w GDI+ (o czym autor nie raczył wspomnieć), a dodatkowo mamy całkiem uproszczony interfejs dla XBOX 360 oraz Windows – XNA (ale tego miłośnik linuxa nie musi przecież sprzedawać). Kolejną sprawą są narzędzia (klikacze) do gier – BALTIE, GAME Factory, etc.
Nie naciągam. Jest zdecydowanie trudniejsze niż przedstawiony przykład w BASICu, trzeba kilkunastu linii żeby w ogóle uruchomić tryb graficzny, nie mówiąc o dziesiątkach parametrów do funkcji kopiujących obrazki  ekran.
Poza tym nie jest moim obowiązkiem przedstawianie wszystkich możliwych rozwiązań. Zaciekawiło mnie prototypowanie w BASICu i o nim napisałem. Pascala już dawno nie dotykam. Narzędzi .NET również. A GDI+ to inna bajka.
Generalnie prototypowanie jest dobre, ale faktycznie, jeśli ktoś się już i tak nastawia na dalsze pisanie w DirectX (swoją drogą DirectX był bardzo złożony dosyć dawno temu, w okolicy wersji 8 (miałem dziurę między 5 i 8) zmieniło się to, choć faktycznie nadal nie jest to kwestia dwóch linijek, zresztą w OpenGL też ;) ), to równie dobrze może prototyp zrobić przy pomocy XNA. Tak naprawdę to obecnie można się pokusić o pisanie gier, przynajmniej prostszych, w C# (choć ja się nadal boję automatycznego zarządzania pamięcią przy czymś większym).

Ale i tak, jeśli chodzi o prototyp, to powinno się go móc napisać prosto i szybko, a nie zapoznawać się tygodniami z językiem, z którego się korzystać nigdy nie będzie. Basic jest ok, ale jeśli też ktoś ma własne biblioteki (lub zna jakieś) do ustawiania trybu graficznego, wczytywania bitmap, modeli itd. to równie dobrze może na tej bazie prototyp napisać. No ale jeśli ktoś nie ma czegoś takiego, a chce zrobić grę z grafiką 2D (strategie!) to Basic jest dobrym rozwiązaniem na stworzenie prototypu.
Cechą BASICa jest właśnie to, że nie trzeba się go uczyć: jest tak prymitywny, że pojedyncze wiersze programu wystarczają do osiągnięcia konkretnego rezultatu, w innych językach wymagającego znania większych struktur i zasad programowania.
Rozpoczęcie programu choćby w C wymaga jednak pewnych czynności początkowych (przygotowania pliku Makefile, wpisania kilkudziesięciu wierszy kodu). Owszem, jeżeli od razu chce się przejść z prototypu do programu, może się to opłacać. Jeżeli jednak chce się tylko sprawdzić kilka pomysłów, BASIC będzie mniej pracochłonny.
Co do pisania w C#, akurat im większy program, tym racjonalność wykorzystania pamięci jest wyższa. Właśnie w najmniejszych programach narzuty są największe. Mimo to nie mam specjalnego entuzjazmu do tworzenia gier w językach nadzorowanych (i do prototypowania w nich, szczerze mówiąc, również).
BASIC jednak według trzeba poznać. Nie jest to jakaś skomplikowana nauka, ale trzeba choćby wiedzieć, że wyświetlanie bitmapy jest możliwe i robi się je tak a tak ;) I czy takim problemem jest stworzenie prostego programu w C? Oczywiście, że można potrzebować stworzyć plik Makefile, ale równie dobrze można skorzystać z dowolnego środowiska, które wszystko dla nas przygotuje (ostatnio korzystam z CodeBlocks, któremu brakuje tego i owego i przyzwyczajony jestem do VS, z którego korzystam w pracy, ale wystarczy do moich zastosowań). I z grafiką 3D już nie wiem czy tak szybko można sobie poradzić w BASICu, podczas gdy wystarczy SDL i można już wygodnie w openGLu (jestem zwolennikiem DirectX'a swoją drogą ;) ) sprawdzić to i owo. A i pewne rzeczy nie wymagające prezentacji graficznej prościej chyba będzie każdemu sprawdzić w języku, który lepiej zna niż zastanawiać się czy w danym narzeczu BASICa są struktury czy ich nie ma albo mieć inny podobny problem.

Więc zgodzę się, że BASIC się przydać może przy prototypowaniu, ale tyle o ile się wie, na co go stać i jest się w stanie szybciej napisać program sprawdzający coś niż w innym języku.
Cały „dowcip” w tym, że BASIC jest o niebo łatwiej poznać, niż C czy C#. Po prostu nie ma w nim jakiejkolwiek struktury programu, a tylko prosty ciąg instrukcji, z ewentualnymi pętlami i skokami. Oczywiście, w nowych dialektach są podprogramy, ale nie trzeba z nich nawet korzystać (a C czy C# wymuszają używanie podprogramów czy nawet klas).
Po prostu uważam, że w BASICu znacznie szybciej i mniejszym nakładem pracy „wyrzeźbi się” prościutki przykład graficzny.
Ktoś kto zna C++,C# czy ObiectPascal bez trudu przyswoi sobie niemal dowolny dialekt Basica.
Przy niewielkich projektach zastanawiał bym się nawet czy jest sens używać C++ czy C#, dzisiejsze dialekty basic'a nie są już takie jak jeszcze 10 lat temu. Dzisiaj Basic potrafi pokazać pazur. Warto też wspomnieć że znaczna część gier klasy casual powstaje dzisiaj w takich dialektach basic jak: Blitz3D, BlitzMax, FreeBasic, DarkBasic. Dzisiejsze basic'i są raczej wyspecjalizowane i przystosowane do tworzenia gier (przeważnie), ze szczególnym naciskiem na odciążenie programisty od wielu zbędnych rzeczy o których musi pamiętać (i stosować) programując w c++ czy C#. Swoją drogą, FreeBasic jest demonem prędkości i zwięzłości kodu wynikowego w porównaniu do VisualBasic firmy Microsoft.