Celem tego kursu jest nauka programowania pod AmigaOS z wykorzystaniem Visual Studio 2005 Express Edition i VBCC. Oczywiście ten zestaw można zastąpić innym, bardziej nam odpowiadającym, lecz wówczas kilka pierwszych akapitów należy po prostu pominąć. Jako że nauka najlepiej idzie na przykładach, w dodatku praktycznych, postaramy się stworzyć prostą grę logiczną.
Na wstępie określmy, co będziemy potrzebowali, aby w miarę bezboleśnie tworzyć oprogramowanie dla AmigaOS 2.x/3.x z procesorem mc680x0 na Windowsie używając Visuala C++ 2005 Express Edition i VBCC. Prawdopodobnie można zmusić nowsze wersje, czyli 2008 i 2010 do pracy na rzecz Amigi, lecz mój PC jest dosyć leciwy i nawet nie próbowałem instalować nowszych wersji Visual Studio. Będziemy potrzebować wspomniany Visual 2005 C++ Express Edition, VBCC generujący kod dla AmigaOS 2.x/3.x z mc680x0 z poziomu Windows i NDK 3.9 (a w zasadzie tylko dwa katalogi include_h i include_i). Dodatkowo, jeśli pokusimy się o kompilowanie narzędzia Vasm, to będziemy potrzebowali Windows SDK. Co do Visual C++ 2005 Express Edition to milcząco zakładam, że jest on już zainstalowany. Przejdźmy teraz do VBCC. Mamy dwa wyjścia: na podstawie źródeł skompilować potrzebne narzędzia bądź poszperać w sieci i pobrać je w wersji dla Windowsa. Trzeba zaznaczyć, że wersje znalezione w sieci niekoniecznie muszą być najświeższymi i warto to sprawdzić uruchamiając narzędzia z linii poleceń. Z tej strony można pobrać wersję dla Windows.
Kompilacja VBCC
Zajmiemy się kompilacją źródeł na maszynie 32-bitowej. Przede wszystkim tworzymy katalog VBCC na dysku C:\. Oczywiście to tylko przykładowa lokalizacja i można ją sobie dowolnie zmienić, ale trzeba pamiętać o zmianie odwołań wszędzie tam, gdzie to stosowne. Od razu z marszu stwórzmy w katalogu VBCC podkatalogi: bin, config, doc, targets. Słówko o katalogu docs - jak nazwa wskazuje, należy w nim umieścić dokumentację do narzędzi, którą bez problemu można znaleźć w postaci plików PDF na stronie Franka Wille.
1. VBCC
Po pobraniu źródeł VBCC rozpakowujemy je (najlepiej do jakiegoś katalogu tymczasowego, na przykład C:\tmp). Uruchamiamy Visual Studio 2005 Command Prompt. Przechodzimy do katalogu ze źródłami VBCC i tworzymy katalog bin (mamy więc C:\tmp\VBCC\bin) i uruchamiamy nmake /f Makefile.vs TARGET=m68k. Dalej otrzymamy szereg pytań. Na wszystkie pytania typu y/n odpowiadamy y, a na inne pytania wpisujemy w kolejności: signed char, unsigned char, signed short, unsigned short, signed int, unsigned int, signed long long, unsigned long long, float, double. Po kilku chwilach, w zależności od szybkości maszyny, w katalogu bin (C:\tmp\VBCC\bin) powinny znaleźć się pliki: dtgen.exe, VBCCm68k.exe, vc.exe, vprof.exe. Kopiujemy wszystkie oprócz dtgen.exe do c:\VBCC\bin.
Jeśli archiwum ze źródłami VBCC nie zawiera pliku Makefile.vs, to nie pozostaje nam nic innego jak stworzenie go na podstawie istniejącego Makefile. Na początku usuwamy reguły od lini 20 (doc/vbcc.pdf) aż do lini 33 (bin/osekrm:). Regułę bin/osekrm zostawiamy i usuwamy regułę dist: oraz wszystkie reguły vcpp. Następnie zamieniamy wszystkie wystąpienia znaku '/' znakiem '\'. We wszystkich regułach kończących się na .o zamieniamy ciąg trzyliterowy "-o " (spacja jest istotna) na "/Fo". Ponadto zamieniamy wyżej wymienione ".o" na ".obj". Odszukujemy pozostałe reguły, gdzie występuje "-o " i dodajemy ".exe" na koniec reguły i w pliku wynikowym, a ciągi "-o " zamieniamy na "/link /NOLOGO /OUT:". Na przykład:
bin/vbcc$(TARGET): $(fobjects) $(CC) $(LDFLAGS) $(fobjects) -o bin/vbcc$(TARGET) zostanie zamieniony na
bin\vbcc$(TARGET).exe: $(fobjects) $(CC) $(LDFLAGS) $(fobjects) /link /NOLOGO /OUT:bin\vbcc$(TARGET).exe Zamieniamy regułę CC na
CC = cl /D_CRT_SECURE_NO_WARNINGS /D_CRT_SECURE_NO_DEPRECATE /nologo /O2 /MT /Zp1 /wd4715 /wd4068 /wd4048 a regułę LM zmieniamy na pustą. Usuwamy "-Idatatypes" z reguły bin/dtgen. |
2. Vasm
Pobieramy źródła i rozpakowujemy tak, jak poprzednio do katalogu tymczasowego (C:\tmp). Uruchamiamy Visual Studio 2005 Command Prompt i przechodzimy do katalogu C:\tmp\vasm. Tworzymy katalog obj_win32 i uruchamiamy nmake /f Makefile.Win32 CPU=m68k SYNTAX=mot. Po udanej kompilacji zmieniamy nazwę z vasmm68k_mot_win32.exe na vasmm68k_mot.exe i kopiujemy ten plik do C:\VBCC\bin. Warto dodać, że aby poprawnie skompilować Vasm za pomocą Express Edition, należy posiadać Windows SDK zainstalowany i poprawnie ustawione zmienne środowiskowe INCLUDE i LIB. Najłatwiej jest uruchomić VS2005 Command Prompt i przejść do katalogu, gdzie mamy zainstalowany SDK i uruchomić skrypt SetENV, który ustawi zmienne środowiskowe, a dopiero potem kompilować VBCC.
Postępujemy podobnie, jak w poprzednich krokach, czyli pobieramy źródła i rozpakowujemy do katalogu tymczasowego. Uruchamiamy Visual Studio 2005 Command Prompt i przechodzimy do katalogu C:\tmp\vlink. Tworzymy katalog objects w C:\tmp\vlink i uruchamiamy nmake /f Makefile.Win32. Na obrazku obok widzimy proces kompilacji. Po zakończeniu kompilacji kopiujemy plik vlink.exe do C:\VBCC\bin.
4. Target
Pobieramy Target archiwum dla AmigaOS 2.x/3.x . Kopiujemy katalog targets z zawartością do katalogu c:\VBCC. Tworzymy katalog config w c:\VBCC i tworzymy tam plik konfiguracyjny vc.cfg, który zawiera następujące wpisy:
-cc=VBCCm68k -quiet %s -o= %s %s -O=%ld -IC:/VBCC/targets/m68k-amigaos/include/ -IC:/VBCC/include_h/ -ccv=VBCCm68k %s -o= %s %s -O=%ld -IC:/VBCC/targets/m68k-amigaos/include/ -IC:/VBCC/include_h/ -as=vasmm68k_mot -quiet -Fhunk -phxass %s -o %s -IC:/VBCC/include_i/ -asv=vasmm68k_mot -Fhunk -phxass %s -o %s -IC:/VBCC/include_i/ -rm=del %s -rmv=del %s -ld=vlink -bamigahunk -x -Bstatic -Cvbcc -nostdlib -LC:/VBCC/targets/m68k-amigaos/lib/ C:/VBCC/targets/m68k-amigaos/lib/startup.o %s %s -lvc -o %s -l2=vlink -bamigahunk -x -Bstatic -Cvbcc -nostdlib -LC:/VBCC/targets/m68k-amigaos/lib/ %s %s -o %s -ldv=vlink -bamigahunk -t -x -Bstatic -Cvbcc -nostdlib -LC:/VBCC/targets/m68k-amigaos/lib/ C:/VBCC/targets/m68k-amigaos/lib/startup.o %s %s -lvc -o %s -l2v=vlink -bamigahunk -t -x -Bstatic -Cvbcc -nostdlib -LC:/VBCC/targets/m68k-amigaos/lib/ %s %s -o %s -ldnodb=-s -Rshort -ul=-l%s -cf=-F%s -ml=500
Instalacja VBCC
W zasadzie, aby korzystać z dobrodziejstwa skrośnej kompilacji, pozostaje nam ustawić zmienne środowiskowe i przekopiować dwa katalogi z NDK. Ustawiamy zmienne, a dla VBCC dajemy c:\VBCC (czyli mamy VBCC=C:\VBCC) i do PATH dodajemy ścieżkę c:\VBCC\bin. Teraz bierzemy się za NDK. Rozpakowujemy archiwum w katalogu tymczasowym C:\tmp kopiujemy katalogi include_h i include_i wraz z zawartością do C:\VBCC. W ten sposób jesteśmy w stanie sprawdzić czy poprawnie wykonaliśmy instalację VBCC. Uruchamiamy wiersz poleceń i wpisujemy vc. Naszym oczom powinna ukazać się informacja "No objects to link".
VS2005 i pierwszy projekcik
Wybieramy z menu "Tools/Options" i przechodzimy do "Projects and Solutions". Tam wybieramy "VC++ Directories" i z prawego górnego drop down box zaznaczamy "Include files". Dodajemy dwa nowe wpisy C:\VBCC\include_h i C:\VBCC\include_i. Dzięki temu będziemy mogli korzystać z "intelisense", czyli mówiąc kolokwialnie - podpowiadaczki. Wybieramy z górnego menu "File/New/Project" bądź wciskamy CTRL+SHIFT+N. Zaznaczamy "Makefile Project" (rozwijamy "Visual C++" i wybieramy "General") w "Name" wpisujemy "Boxo" i ustawiamy "Location". W moim przypadku wygląda to tak jak na obrazkach. Naszym oczom ukaże się Wizard i w tym przypadku przechodzimy dalej klikając w "Next". W "Debug Configuration Settings" wpisujemy następujące polecenia:
Dla Build command line:
nmake /f makefile.mak all
Dla Clean commands:
nmake /f makefile.mak clean
Dla Rebuild command line:
nmake /f makefile.mak clean all.
Jako "Output" wpisujemy "boxo.exe". W "Release Configuration Settings" zaznaczamy checkbox "same as debug configuration" i klikamy w "Finish". Kasujemy filtry "Header files", "Resource Files", "Source Files" i plik "readme.txt". Porzucamy na chwilę VS2005 i przechodzimy do katalogu Boxo. Tam kasujemy "readme.txt" i tworzymy pliki "boxo.c" i "makefile.mak" a w nim umieszczamy:
# # boxo # CC = vc CFLAGS =-c -c99 -IC:/VBCC/targets/m68k-amigaos/include/ -IC:/VBCC/include_h/ BIN = boxo LIBS =-LC:/VBCC/targets/m68k-amigaos/lib -lvc -lamiga LINK = vlink -bamigahunk -Bstatic -CVBCC -nostdlib -s -x c:/VBCC/targets/m68k-amigaos/lib/startup.o OBJ = boxo.o LINKOBJ = boxo.o RM = DEL .PHONY: all clean all: $(BIN) clean: $(RM) $(OBJ) $(BIN) $(BIN): $(OBJ) $(LINK) $(LINKOBJ) -o $(BIN) $(LIBS) boxo.o: boxo.c $(CC) $(CFLAGS) boxo.c -o boxo.o
Pobieżnie omówię, co z czym się je w tym pliku, a potem zajmiemy się boxo.c. Otóż zawiera on reguły, które automatyzują proces kompilacji. Znak '#' oznacza komentarz, czyli dalej za nim aż do końca linii tekst nie będzie brany pod uwagę. Dalej widzimy konstrukcję typu Nazwa = wartość. Są to zmienne, dzięki którym nie będziemy musieli za każdym razem wpisywać tych samych rzeczy. Do zmiennej dobieramy się pisząc $(nazwa). Pierwsza reguła to .PHONY - która mówi, że reguły all i clean nie są plikami. Na wypadek, gdyby ktoś chciał kompilować pliki o takich nazwach. Reguła all jest od kompilacji całego projektu. Za pomocą clean możemy wykasować obiekty i utworzony plik wykonywalny. $(BIN) nakazuje stworzenie obiektów (plików pośrednich) i zlinkowanie ich. Ostatnie dwie linie określają, w jaki sposób powstaje plik boxo.o.
Pierwszy przykładzik zaczniemy klasycznie. Wypiszemy na wyjście "hello". Oto i kod:
#include <stdio.h> int main(void) { printf("hello\n"); return 0; }
i budujemy za pomocą skrótu klawiszowego CTRL+SHIFT+B bądź wybierając "Build->Build Solution" z menu. Jeśli nie popełniliśmy błędu, to w katalogu boxo powinniśmy mieć plik boxo. Przenosimy go na Amigę i uruchamiając go, możemy podziwiać rezultat. Archiwum boxo-01.zip zawiera powyższy przykład.
Po klasycznym przykładzie "hello" rzucamy się w głęboką wodę i napiszemy programik, który pokazuje okienko. Zaczniemy od ogólnego planu, który będziemy sukcesywnie rozbudowywać. Początkowy plan składa się z trzech bardzo podstawowych kroków. Pierwszy to inicjacja, drugi to główna pętla, a trzeci to zakończenie/zamknięcie. Skupmy się na tym, co chcemy tak naprawdę osiągnąć. Mamy pokazać okienko, czyli musimy je otworzyć, a co za tym idzie trzeba je też później zamknąć. Aby zamknąć nasze okienko, użytkownik musi kliknąć w gadżet zamknięcia, zatem w jakiś sposób musimy czekać na ruch ze strony użytkownika. Nasz plan wygląda teraz tak:
1. Inicjacja - otwarcie okna 2. Pętla główna - czekanie aż użytkownik kliknie w gadżet zamknięcia okna 3. Zamknięcie - zamykamy okno
Zamieńmy plan w kod.
//============================================================================ static int init(void); static void loop(void); static void close(void); //============================================================================ int main(void) { if(0 == init()) { loop(); } close(); return 0; //ok } //============================================================================ static int init() { //otwarcie okna return 0; //ok } //============================================================================ static void close() { //zamknięcie okna } //============================================================================ static void loop() { //czekanie aż użytkownik kliknie w gadżet zamknięcia okna } //============================================================================
Spostrzegawczy czytelnik zauważy, że występują tu, w źródle do języka C, komentarze rodem z C++. Dzieje się to dlatego, że używam standardu C99, który pozwala na takie komentarze, jak również inne użyteczne rzeczy typu deklarowanie zmiennych, gdzie potrzebuję, a nie tylko na początku bloku (jak ma to miejsce w ANSI C). Kilka słów komentarza o kodzie. Zakładamy, że funkcja init() zwraca 0 i oznacza to, że wszystko, co związane z inicjacją, zostało pomyślnie wykonane. Z tego względu wykonanie funkcji loop() następuje dokładnie wtedy, gdy init() zwraca zero. Warto odnotować, że funkcja close() zostanie wywołana nawet, gdy init() nie zwróci zero. Oczywistą konsekwencją tego będzie sprawdzanie czy okno było otwarte - bo przecież bez sensu zamykać okno, które nie było otwarte.
Aby otworzyć okno, skorzystamy z biblioteki intuition i funkcji OpenWindowTags. Wynika z tego, że przed otwarciem okna musimy otworzyć bibliotekę intuition i sprawdzić czy otwarcie się powiodło, a po zamknięciu okna musimy intuition zamknąć, o ile nie korzystamy w dalszym ciągu z tejże biblioteki. Pozostała nam najtrudniejsza sprawa, czyli pętla główna. Z pomocą przyjdzie nam funkcja Wait z biblioteki exec. Na szczęście exec.library jest zawsze otwarta i nie musimy się martwić sprawdzaniem i zamykaniem jej. Zadaniem Wait jest oczekiwanie na zadane sygnały. W wielkim skrócie - sygnały to podstawowy mechanizm odpowiedzialny za komunikację w OS. W każdym razie, jeśli klikniemy w naszym oknie w gadżet zamknięcia, to zostanie wysłany odpowiedni sygnał do naszego okna i naszym zadaniem będzie jego właściwa obsługa. W naszym programiku za pomocą Wait będziemy czekali między innymi na sygnał z naszego okna. Plan rozrósł się do postaci:
1. Inicjacja - otwarcie intuition - sprawdzenie czy się udało otworzyć, jeśli nie to wychodzimy z programu - otwarcie okna - sprawdzenie czy okno otwarte, w przeciwnym razie przechodzimy do kroku 3 2. Pętla główna - za pomocą Wait czekamy na sygnał z okna lub sygnał CTRL+C - obsługa sygnałów (jeśli użytkownik kliknął w gadżet zamknięcia, to wychodzimy z pętli) 3. Zamknięcie - jeśli okno było otwarte, to je zamykamy - jeśli intuition było otwarte, to je zamykamy
Jak widać, plan nabrał kolorów. Przedstawię teraz pełny przykład:
#include <proto/exec.h> #include <proto/intuition.h> #include <proto/graphics.h> #include <dos/dos.h> #include <intuition/intuition.h> //============================================================================ struct IntuitionBase* IntuitionBase; struct Window* m_pWin = NULL; //--------------------------------------------------------- static int init(void); static void loop(void); static void close(void); static BOOL handleWinSignal(); //============================================================================ int main(void) { if(0 == init()) { loop(); } close(); return 0; //ok } //============================================================================ static int init(void) { IntuitionBase = (struct IntuitionBase*)OpenLibrary("intuition.library", 36L); if(NULL == IntuitionBase) { return -1; } //----------------------------------------------------- m_pWin = (struct Window*) OpenWindowTags(NULL, WA_Left, 0, WA_Top, 0, WA_Width, 320, WA_Height, 256, WA_CloseGadget, TRUE, WA_Title, (ULONG)"boxo", WA_Activate, TRUE, WA_DragBar, TRUE, WA_GimmeZeroZero, TRUE, WA_IDCMP, IDCMP_CLOSEWINDOW | IDCMP_RAWKEY, TAG_END); if(NULL == m_pWin) { return -1; } return 0; //ok } //============================================================================ static void close(void) { if(m_pWin) { CloseWindow(m_pWin); } //----------------------------------------------------- if(IntuitionBase) { CloseLibrary((struct Library*) IntuitionBase); } } //============================================================================ static void loop(void) { BOOL bEnd = FALSE; while(!bEnd) { ULONG winSignal = 1L << m_pWin->UserPort->mp_SigBit; ULONG signals = Wait(winSignal | SIGBREAKF_CTRL_C); if(signals & SIGBREAKF_CTRL_C) { bEnd = TRUE; } if(signals & winSignal) { bEnd = handleWinSignal(); } } } //============================================================================ static BOOL handleWinSignal() { BOOL bEnd = FALSE; while(TRUE) { struct IntuiMessage* pMsg = (struct IntuiMessage*) GetMsg(m_pWin->UserPort); if(NULL == pMsg) { break; } ULONG msg_class = pMsg->Class; ReplyMsg((struct Message*) pMsg); if(IDCMP_CLOSEWINDOW == msg_class) { bEnd = TRUE; } } return bEnd; } //============================================================================
Programik rozrósł się trochę i z pewnością funkcje loop() i handleWinSignal() wymagają większego komentarza. W loop() czekamy w pętli na dwa sygnały: naciśnięcie kombinacji klawiszy CTRL i C lub sygnał z naszego okna. Dalej badamy czy przyszły oczekiwane sygnały i podejmujemy odpowiednie kroki. W przypadku nadejścia CTRL+C ustawiamy flagę wyjścia z pętli i tym samym wychodzimy z loop(). Po nadejściu sygnału z naszego okna wywołujemy funkcję handleWinSignal, która zajmie się obsługą komunikatów pochodzących z naszego okna. handleWinSignal() składa się z pętli, w której odbieramy wszystkie komunikaty. Za pomocą GetMsg odbieramy wiadomości z naszego portu i po przepisaniu interesującej zmiennej odpowiadamy na tenże komunikat. Dopiero po tym badamy, co takiego ciekawego się wydarzyło. Gdy został wciśnięty gadżet zamykania okna, to ustawiamy flagę wyjścia.
Warto uruchomić przykładzik z CLI i poeksperymentować wciskając gadżet zamknięcia okna czy też wciskając CTRL+C w oknie CLI. Jak się można domyśleć, kombinacja CTRL i C nie zadziała w otwartym przez nas oknie, bo nie pochodzi od sygnału z naszego okna. Powyższy przykładzik posłuży nam jako baza do dalszej rozbudowy, bo nie zapominajmy o głównym celu jakim jest napisanie gry logicznej. Będziemy robić to metodą małych, ale widocznych kroków. Warto w tym miejscu się zatrzymać i podać więcej szczegółów dotyczących naszej logicznej gry.
W dalekiej przyszłości, gdy loty kosmiczne były tak powszechne, jak przepysznie wypiekany chleb z piekarni pana Zenka, a ludzie pouciekali z planety, na której zostały tylko roboty, pojawiło się zagrożenie w postaci braku mechaników konserwujących roboty. Najzwyczajniej w świecie inne roboty brzydziły się tą robotą. W tym krytycznym momencie rada najwyższych robotów podjęła wielkie ryzyko i odważyła się wysłać ekipę, która miała na jakimś zagubionym promie kosmicznym odnaleźć ludzi skłonnych podjąć się tego zadania. Ponieważ chętnych nie było i jakoś dziwnym trafem cała populacja robotów akurat skoczyła na kolejkę oleju, zorganizowano wielkie losowanie (coś w stylu totolotka). W ten oto dziwaczny sposób wybrano do tej arcytrudnej ekspedycji średnio rozgarniętego robota, którego umieszczono w niewielkim statku kosmicznym. W całym zamieszaniu zapomniano wlać większej ilości paliwa do baku pojazdu. Nie było też wzmianki o fatalnym sterowaniu statkiem (tylko w cztery strony). Nie przeszkodziło to jednak naszemu bohaterowi udać się z misją. Niestety poziom inteligencji robota nie pozwalał na poszukiwanie paliwa w kosmosie, lecz na szczęście wynaleziono bardzo ekonomiczny sposób poruszania się: odbijanie się od przeszkód w przestrzeni kosmicznej - czyli zużywamy mikroskopijną ilość paliwa na nadanie kierunku naszemu statkowi i dalej czekamy, aż w coś uderzymy.
Tak o to pokrótce przedstawia się historia związana z naszą grą. Zadaniem gracza będzie dotarcie do miejsca, gdzie mogą być ludzie. Kierujemy niewielkim statkiem, który może poruszać się w czterech kierunkach i zatrzymuje się dopiero wtedy, gdy uderzy w przeszkodę. Statek ma niewielką wytrzymałość i nie może się odbijać od przeszkód w nieskończoność, a dodatkowo niektóre przeszkody powodują natychmiastową śmierć.
Jak widać czeka nas sporo pracy nad grą, ale to już w następnym odcinku. Mogę zdradzić, że zajmiemy się timer.device i zrobimy porządek w naszym kodzie źródłowym.
Artykuł oryginalnie pojawił się w szóstym numerze Polskiego Pisma Amigowego.