Podsumowanie dotychczasowych lekcji DirectDraw + nowe wiadomości.
Witam w kursie ciągnącym się niczym brazylijski serial ; ). Po dość długiej przerwie wreszcie piszę ten artykuł. Zniknąłem z rynku ; ) ponieważ wszedłem w ważny etap życia programisty. Odkrywałem OpenGL jedynie na modułach OpenGL i GL. Głowiłem się jak stworzyć własne formaty bitmap z kompresją RLE, stworzyć 2D w OpenGL, odgrywać muzykę w DirectMusic i DirectSound, odgrywać mp3 w DirectShow, pobierać stan klawiatury przy użyciu DirectInput, no i zajmowałem się Direct3D jak i DirectDraw, poznawałem jeszcze Windows Api itp. itd. Możecie mi wierzyć-na to wszystko idzie mnóstwo czasu. Teraz będę pisać troszkę artykułów co jakiś czas, abyście wy nie musieli tracić tyle czasu na szukanie tego co ja znalazłem i wymyśliłem. Mogę z całą pewnością stwierdzić, iż wszedłem w etap wyżej średniozaawansowany (będziecie mieli od kogo się uczyć : P). Tylko muszę uważać by nie popaść w samouwielbienie ; ).
Przejdźmy jednak do rzeczy. W końcu piszę ten artykuł o DirectDraw a nie o jakichś tam innych interfejsach czy też o mnie. Poprzednie kursy pisałem w oparciu o kurs który był zamieszczony na warsztacie (konwertowałem z C++ na Delphi [no i zmieniałem treść :]). Teraz muszę stwierdzić, że był to mój ogromny błąd. DirectDraw zainicjowane w ten sposób „się krzaczy”. Przykład??? Pisałem prezentację o hałasie (oczywiście ja robiłem wszystko a moi kumple się obijali : ( ) w Delphi z użyciem DirectDraw i FMODa. Wszystko pięknie działało… Do czasu. Nauczycielka puściła prezentację na jednym kąpie by pokazać innej klasie moje „dokonanie”, gdy przy końcu wyłączyła się ale na dobre, komputer się popsuł, a obok mojej szóstki w dzienniku stanęła jedynka… Ehhhh… Raz na wozie raz pod wozem. Być może to prze FMOD. Są oczywiście inne sytuacje w których już wspomniane „krzaczenie” ujawnia się.
Zamiast wymieniać błędy poprzedniego rozwiązania zajmijmy się nowymi. Musimy napisać od nowa także obsługę windows.
Windows API (ale to już było… : )
Z reguły pisząc gry mamy zamiar wyświetlić je na pełnym ekranie a nie na jakimś tam przeklętym formularzu znanym nam doskonale z Vcl. Nowa funkcja WinInit wygląda teraz dużo lepiej :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
function WinInit( h_Inst : THANDLE; nCmdShow : Integer; var phWnd : HWND; var phAccel : HACCEL ) : HRESULT; var h_Wnd : HWND; wc : TWNDCLASS; h_Accel : HACCEL; begin // Rejestrowanie klasy okna wc.lpszClassName := 'nazwa'; wc.lpfnWndProc := @WindowProc; wc.style := CS_VREDRAW or CS_HREDRAW; wc.hInstance := h_Inst; wc.hIcon := 0;//LoadIcon( h_Inst, MakeIntResource( IDI_MAIN_ICON ) ); wc.hCursor := LoadCursor( 0, IDC_ARROW ); wc.hbrBackground := ( COLOR_WINDOW + 1 ); wc.lpszMenuName := nil; wc.cbClsExtra := 0; wc.cbWndExtra := 0; if ( RegisterClass( wc ) = 0 ) then begin result := E_FAIL; exit; end; // Wczytanie skrótów klawiatórowych h_Accel := LoadAccelerators( h_Inst, MakeIntResource( 0 ) ); // Stworzenie i pokazanie okna h_Wnd := CreateWindowEx( 0, 'nazwa', // Tu musi być tak samo jak w wc.lpszClassName 'tytul', WS_POPUP, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, h_Inst, nil ); if ( h_Wnd = 0 ) then begin result := E_FAIL; exit; end; ShowWindow( h_Wnd, nCmdShow ); UpdateWindow( h_Wnd ); phWnd := h_Wnd; phAccel := h_Accel; result := DD_OK; end; |
Jedyny problem może sprawić nowa funkcja LoadAccelerators. Wczytuje ona skróty klawiaturowe z zasobów. Bez tego zasobu (musimy go sami dołączyć) nie będzie działać Alt+F4 (co może być zaletą : ) jak i Escape. Jeśli olejemy ten zasób aplikacja nie będzie na nic reagować, po prostu nas „zablokuje”. Jeśli wyjątkowo uprzemy się by go nie dawać musimy obsłużyć „ręcznie” klawiaturę (pamiętasz tablicę wciśnięć klawiszy???). Więcej na ten temat znajdziesz w poprzednich artykułach o DirectDraw. Ten zasób możesz ściągnąć wraz z przykładami do tego artykułu u dołu strony. Dla porządku wyjaśnię czym jest zwracana wartość przez tą funkcję. Otóż jest to HRESULT. W DirectDraw, jeśli wszystko idzie dobrze funkcje zwracają wartość DD_OK. Tak dla porządku ta funkcja choć luźno związana z DirectDraw powinna stosować się do reguły. Teraz czas pokazać wam naszą główną okienkową funkcję.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
function WindowProc( h_Wnd : HWND; aMSG : Cardinal; wParam : Cardinal; lParam : Integer ) : Integer; stdcall; begin case aMSG of // Pause if minimized WM_COMMAND : begin case LOWORD( wParam ) of IDM_EXIT : begin // obsługa skrótów klawiaturowych PostMessage( h_Wnd, WM_CLOSE, 0, 0 ); result := 0; exit; end; end; end; WM_SETCURSOR : begin // Schowanie kursora SetCursor( 0 ); result := 1; exit; end; WM_SIZE : begin // Sprawdzenie czy okienko się skurczyło... if ( wParam = SIZE_MAXHIDE ) or ( wParam = SIZE_MINIMIZED ) then g_bActive := FALSE else g_bActive := TRUE; end; WM_EXITMENULOOP : begin // Ignoruje "zmarnowany" czas w menu g_dwLastTick := GetTickCount; end; WM_EXITSIZEMOVE : begin // Ignoruje "zmarnowany" czas podczas zmiany rozmiaru okna g_dwLastTick := GetTickCount; end; WM_SYSCOMMAND : begin // Zapobiega zmianie rozmaru okna i wyłączeniu monitora w trybie pełnoekranowym case wParam of SC_MOVE, SC_SIZE, SC_MAXIMIZE, SC_MONITORPOWER : begin result := 1; exit; end; end; end; WM_DESTROY : begin // Posprzątanie po aplikacji FreeDirectDraw; PostQuitMessage( 0 ); result := 0; exit; end; end; Result := DefWindowProc( h_Wnd, aMSG, wParam, lParam ); end; |
Nieco nam się to skomplikowało. Większość da się zrozumieć (nazwy mówią same za siebie). Wyjaśnienia zapewne może wymagać zmienna g_dwLastTick. Zmienna ta jest niezbędna by działał zegar naszej aplikacji (coś w stylu timera tyle, że lepsze). Ale o tym za chwilkę…
Timer naszej aplikacji
To jest coś. Grafika nie będzie działać szybciej na szybszych komputerach (o to w drugą stronę nieco trudniej ; ). Wszystko będzie działać w należytym tempie. Jednak zanim przejdziemy do odmierzania czasu przyjrzyjmy się bliżej głównej pętli programu.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
while ( true ) do begin // Pobierz komunikaty. if ( PeekMessage( aMSG, 0, 0, 0, PM_NOREMOVE ) ) then begin if not GetMessage( aMSG, 0, 0, 0 ) then // WM_QUIT - zamykamy... exit; // przetwórz i wyślij komunikaty if ( TranslateAccelerator( h_Wnd, h_Accel, aMsg ) = 0 ) then begin TranslateMessage( aMsg ); DispatchMessage( aMsg ); end; end else begin if ( g_bActive ) then begin // Narysowanie grafiki hr := ProcessNextFrame( h_Wnd ); if ( hr <> DD_OK ) then begin g_pDisplay := nil; if ( hr = E_NOTIMPL ) then begin MessageBox( h_Wnd, 'Masz pecha...' + #13#10 + 'Nastąpi wyjście z programu.', 'Błąd', MB_ICONERROR or MB_OK ); end else begin //DXTRACE_ERR( 'ProcessNextFrame', hr ); MessageBox( h_Wnd, 'Nie udało się narysować następnej klatki.' + #13#10 + 'Nastąpi wyjście z programu.', 'Błąd', MB_ICONERROR or MB_OK ); end; exit; end; end else begin // IdĽ spać jeśli nie masz nic do roboty ; ) WaitMessage; // Ignoruj czas g_dwLastTick := GetTickCount; end; end; end; |
Oczywiście przed główną pętlą wykonujemy funkcję:
1 2 3 4 5 6 7 8 9 10 11 12 |
//Zainicjowanie okna windows if WinInit(hInstance, SW_SHOW, h_Wnd, H_Accel) <> DD_OK then TWOJA OBSŁUGA BŁĘDU; Nie można zapomnieć o zmiennych : var //////Zmienne Windows h_Wnd : HWND; // uchwyt okna h_Accel : HACCEL; //uchwyt klawiatury aMSG : MSG; g_bActive : Boolean = False; // Aplikacja jest aktywna? g_dwLastTick : DWord; hr : HRESULT; |
Małymi kroczkami zbliżamy się ku końcowi. Teraz czas zaprezentować funkcję występującą w głównej pętli programu : ProcessNextFrame. To w niej właśnie rysujemy grafikę i bawimy się naszym stoperem.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
function ProcessNextFrame : HRESULT; var dwCurrTick : DWord; dwTickDiff : DWord; begin // Jak dużo czasu upłunęło od ostatniego czasu dwCurrTick := GetTickCount; dwTickDiff := dwCurrTick - g_dwLastTick; // Nie uaktualnij jeśli nie powinieneś if ( dwTickDiff = 0 ) then begin result := DD_OK; exit end; g_dwLastTick := dwCurrTick; // Wyświetlenie sceny na ekranie hr := DisplayFrame; if ( hr <> DD_OK ) then begin if ( hr <> DDERR_SURFACELOST ) then begin //Zwolnienie zasobów w razie niepowodzenia FreeDirectDraw; //DXTRACE_ERR( 'DisplayFrame', hr ); result := hr; exit; end; // Jeśli surfaces został utracony należy go zresetować RestoreSurfaces; end; result := DD_OK; end; |
Nasz regulator szybkości znajduje się na początku. Jeśli przyjrzysz mu się bliżej to na pewno go zrozumiesz. W dalszej części funkcji znajdują się dwie nowe funkcje : DisplayFrame i RestoreSurfaces. Są one ściśle powiązane z DirectDraw więc omówię je za „zakrętem”.
DirectDraw – Inicjacja
Teraz zaczynam omawiać DirectDraw – „Wreszcie!!!” – pewnie powiesz. Po tylu nudnych rzeczach związanych z WinApi wreszcie coś ciekawego… Heh… Teraz będzie to bajecznie proste (w porównaniu do inicjacji z poprzednich lekcji). Czemu??? A to dzięki modułowi DDUtils (wszystko jest dołączone do przykładu który możesz ściągnąć u dołu strony). Jest on bajeczny (no prawie :). Zawiera dwie bardzo przyjazne klasy, które aż proszą się o użycie. Co prawda rozmiar naszego „programiku” wzrośnie do 90KB, to i tak można to przeboleć. Klasy te zwalniają nas od konieczności babrania się w tym całym bagnie nazwanym DirectDraw, za cenę kilku kilobajtów (Tylko nie kojarz tego z Vcl i DelphiX).
Pierwszą rzeczą którą robimy jest dołączenie modułu DDUtil w sekcji uses. Następnym krokiem jest zadeklarowanie zmiennej:
1 2 |
var g_pDisplay : TDisplay; |
Jest to nasz swoisty „engin” DirectDraw. Teraz przejdźmy do zmiennych przechowujących grafikę (pojedynczą lub animację):
1 2 |
var grafika : TSurface; |
To nie będzie takie bolesne ; ). Do tworzenia w DirectDraw będziemy się posługiwać tylko dwiema zmiennymi !!! A to wszystko dzięki tym dwóm klasom zawartym w module DDUtil. Przejdźmy do inicjacji trybu graficznego.
1 2 3 4 5 6 7 8 9 10 11 12 |
g_pDisplay := TDisplay.Create; hr := g_pDisplay.CreateFullScreenDisplay( h_Wnd, SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP); //Jeśli hr jest różne od DD_OK to znaczy, że wystąpił //błąd. if ( hr <> DD_OK ) then begin MessageBox( h_Wnd, 'Twoja karta graficzna nie obsługuje takiej rozdzielczości!!!', 'Błąd', MB_ICONERROR or MB_OK ); exit; end; |
To tylko tyle. W sumie wystarczyły by nam tylko trzy pierwsze linijki !!! Ale obsługa błędów musi być ; ). Wracając do linijek to… Pierwsza linijka tworzy (inicjuje) naszą zmienną/klasę. W trzeciej natomiast inicjujemy tryb graficzny. Wynik tej inicjalizacji nadajemy zmiennej hr. Pierwsza zmienna to uchwyt naszej aplikacji (np. form1.handle) druga, szerokość ekranu, trzecia to wysokość a czwarta to liczba bitów. PrzejdĽmy teraz do obsługi błędu. Powiem tu tylko jedno. Jeśli coś jest nie tak to zawsze zwracany wynik jest różny od DD_OK (tak jest ze wszystkim w DirectDraw). To wszystko na temat inicjowania DD.
Teraz pokrótce omówię wczytywanie bitmap które jest po prostu banalną czynnością.
1 |
g_pDisplay.CreateSurfaceFromBitmap( grafika, PAnsiChar( 'm.bmp' ), 120, 120 ); |
Mój komentarz jest tu chyba zbędny…
DirectDraw – Wyświetlanie
Teraz przypomnij sobie pewną funkcję z funkcji ProcessNextFrame była tam taka jedna funkcja…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
function DisplayFrame : HRESULT; begin if g_pDisplay = nil then begin result := DD_OK; exit; end; // Czyścimy ekran g_pDisplay.Clear( $FFFFFF ); // Wyświetlamy bitmapkę w buforze g_pDisplay.Blt( 0, 0, grafika, nil ); // Wyświetlamy zawartość buforu na ekranie hr := g_pDisplay.Flip; if ( hr <> DD_OK ) then begin result := hr; exit; end; result := DD_OK; end; |
DirectDraw – Reset Surface
Czasami coś pójdzie nie tak i trzeba zresetować to badziewie (gdzie togo użyć pisałem wyżej [timer]). Oto i kod :
1 2 3 4 5 6 7 8 9 10 11 12 |
function RestoreSurfaces : HRESULT; begin hr := g_pDisplay.GetDirectDraw.RestoreAllSurfaces; if ( hr <> DD_OK ) then begin //DXTRACE_ERR( TEXT("RestoreAllSurfaces"), hr ); result := hr; exit; end; result := DD_OK; end; |
DirectDraw – Kluczowanie koloru
Zapewne rysowane przez nas grafiki nie są wyłącznie kształtu kwadratu. Mogą być takie i owakie ; ). Rysujemy sobie powiedzmy ludzika na białym tle. Nie chcemy tego białego tła. I właśnie tu z pomącą przychodzi nam kluczowanie koloru. Podajemy naszej zmiennej „graficznej” by ignorowała ten kolor. Oto przykładowy kod ( pomija kolor czarny ) :
1 2 3 4 5 6 |
// Pomijaj kolor czarny hr := grafika.SetColorKey( 0 ); if ( hr <> DD_OK ) then begin //Twoja obsługa błędu end; |
Najlepiej „kluczować” tuż po wczytaniu bitmapy z pliku.
DirectDraw – Animacja
Hoho… Bez tego chyba nikt nie chciał by grać w gry! Chyba nie muszę tłumaczyć co to animacja ; )? Kłopot zaczyna się już przy wczytywaniu :). Kod będzie wyglądać mniej więcej tak…
1 2 |
g_pDisplay.CreateSurfaceFromBitmap( grafika, PAnsiChar( 'm.bmp' ), szerokość klatki * liczba kolumn, wysokość klatki * liczba wierszy ); |
Rozumiesz? Może dam ci obraz dla takiego kodu :
1 |
g_pDisplay.CreateSurfaceFromBitmap( grafika, PAnsiChar( 'animate.bmp' ), 32 * 5, 32 * 6 ); |
A to jest plik animate.bmp:
Przejdźmy do wyświetlenia takiego potworka…
1 |
g_pDisplay.Blt( 0, 0, grafika, @myrect); |
Pierwsze dwa parametry to pozycja (x,y). Drugi to zmienna z naszą grafiką. Natomiast trzeci to „kwadrat” wycięty z tej grafiki i wyświetlony na ekranie. myrect to zmienna typu TRect;. Dla uproszczenia możemy napisać swoją procedurę wyświetlającą bitmapkę. Na początku musimy stworzyć pewien rekord.
1 2 3 4 5 6 7 8 |
type TAnimacja = record SzeokoscAnim : integer; WysokoscAnim : integer; LKolumn : integer; LWierszy : integer; grafika : TSurface; end; |
Następnie deklarujemy stałe z definicją rozdzielczości w której pracujemy:
1 2 3 4 |
const rWidth : integer = 640; rHeight : integer = 480; rBits : integer = 32; |
oczywiście wartości te możesz modyfikować :). Zadeklarujmy nową zmienną:
1 2 |
var animacja : TAnimacja; |
Teraz musimy napisać nową procedurę ładującą grafikę (praca od korzeni : D ).
1 2 3 4 5 6 7 8 9 10 11 |
procedure LoadAnimation(dir : string; SzerokoscKlatki, WysokoscKlatki, LiczbaKolumn, LiczbaWierszy : integer; var animacja : TAnimacja); begin animacja.SzerokoscAnim := SzerokoscKlatki; animacja.WysokoscAnim := WysokoscKlatki; animacja.LKolumn := LiczbaKolumn; animacja.LWierszy := LiczbaWierszy; g_pDisplay.CreateSurfaceFromBitmap( animacja.grafika, PAnsiChar(dir), SzerokoscKlatki * LiczbaKolumn, WysokoscKlatki * LiczbaWierszy ); end; |
A teraz przejdźmy do sedna sprawy-wyświetlenia klatki animacji:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
procedure DrawFrame(x, y : integer; animacja : TAnimacja; Klatka : integer); var myrect : TRect; begin if (x+animacja.SzerokoscAnim < 0) then exit; if (x > rWidth) then exit; if (y+animacja.WysokoscAnim < 0) then exit; if (y > rHeight) then exit; myrect.left := (klatka mod animacja.LKolumn)*animacja.SzerokoscAnim; myrect.top := (klatka div animacja.LKolumn)*animacja.WysokoscAnim; myrect.right := myrect.left + animacja.SzerokoscAnim; myrect.bottom := myrect.top + animacja.WysokoscAnim; g_pDisplay.Blt( x, y, animacja.grafika, @myrect); end; |
DirectDraw – Zwalnianie zasobów
Nikt nie lubi sprzątać, ale niestety trzeba. Oto i kod…
1 2 3 4 5 6 7 8 9 |
procedure FreeDirectDraw; begin animacja.grafika.Free; animacja.grafika := nil; grafika.Free; grafika := nil; g_pDisplay.Free; g_pDisplay := nil; end; |
DirectDraw – Podsumowanie
Uffffffff…….. To chyba wszystko co trzeba wiedzieć na temat wyświetlania grafiki by napisać swoją fajną gierkę 2D. Trochę długaśny ten artykuł…
Autor: HNB