18. Időzítők és animáció (Timers and Animation)
2014.06.22. 11:48
Példa: anim_one
A beállítás
Mielőtt nagyon nekiállnánk animálni, szükségünk lesz egy struktúrára, amiben a labda két frissítés közötti pozícióit tároljuk. Ez a struktúra tárolja a labda pozícióján túl a méretét, a delta értéket, és hogy mennyire mozogjanak a képkockák.
Ha megvan a struktúra típusa, akkor ebből egy globális példányt hozunk létre. Ez így csak egyetlen labdához alkalmas, de ha több kell, akkor valami tömbbe vagy listába kell szervezni.
const int BALL_MOVE_DELTA = 2; typedef struct _BALLINFO { int width; int height; int x; int y; int dx; int dy; }BALLINFO; BALLINFO g_ballInfo;
Létrehoztunk egy BALL_MOVE_DELTA nevű konstanst. Ez mutatja, hogy két képkocka között mennyit mozduljon el a labda. A delta értékeket azért tettük a struktúrába, mert így külön külön be tudjuk állítani vízszintesen és függőlegesen is őket. A BALL_MOVE_DELTA csak arra szolgál, hogy egységesen tudjuk megváltoztatni a deltákat.
Most betöltjük a képet, és inicializáljuk a struktúrát.
BITMAP bm;GetObject(g_hbmBall, sizeof(bm), &bm); ZeroMemory(&g_ballInfo, sizeof(g_ballInfo)); g_ballInfo.width = bm.bmWidth; g_ballInfo.height = bm.bmHeight; g_ballInfo.dx = BALL_MOVE_DELTA; g_ballInfo.dy = BALL_MOVE_DELTA;
A labda a bal felső sarokból indul, és jobbra balra, fel le mozog a struktúra dx és dy mezői szerint.
Időzítő beállítása
Ahhoz hogy egy egyszerű időzítőt adjunk az ablakunkhoz, legegyszerűbb a SetTimer() használata. Ez nagyon gyenge, játékhoz és komolyabb dolgokhoz már nem alkalmas, de ide jó lesz. Ha komolyabb kell, akkor nézzük meg a timeSetEvent()-et az MSDN-en.
const int ID_TIMER = 1;ret = SetTimer(hwnd, ID_TIMER, 50, NULL); if(ret == 0) MessageBox(hwnd, "Could not SetTimer()!", "Error", MB_OK | MB_ICONEXCLAMATION);
Deklaráltuk az ID_TIMER azonosítót hogy később tudjunk rá hivatkozni (a leöléséhez), és a fő ablak WM_CREATE handlerében beállítjuk az időzítőt. Minden alkalommal amikor letelik a beállított idő, küld egy WM_TIMER üzenetet az ablaknak, és wParam-ban átadja az ID-t. Erre az azonosítóra most nincs szükségünk, de ha több időzítőt használnánk, akkor meg tudnánk különböztetni őket egymástól. Most ez az idő 50ms-onként telik le, tehát kb 20 képkockát jelent másodpercenként. Csak kb, mert a SetTimer() pontatlan. Ide viszont teljesen jó, pár ms ide vagy oda annyira mindegy.
Animálás a WM_TIMER-ben
A WM_TIMER elkapásakor kiszámolhatjuk a labda új pozícióját, és frissítjük a képernyőn.
case WM_TIMER: { RECT rcClient; HDC hdc = GetDC(hwnd); GetClientRect(hwnd, &rcClient); UpdateBall(&rcClient); DrawBall(hdc, &rcClient); ReleaseDC(hwnd, hdc); } break;
Itt már szerepel az UpdateBall(), és DrawBall() is. Mindkettő a labda kezeléséhez egy-egy saját rutin. Érdemes így csinálni, mert így felhasználható akár másik timerben is anélkül hogy a kód újból szerepelne. Vegyük figyelembe, hogy a HDC minden esetben más és más, ezért érdemes ezt a kódot az üzenetkezelőben hagyni, és csak az eredményt átadni a DrawBall() eljárásnak.
void UpdateBall(RECT* prc) { g_ballInfo.x += g_ballInfo.dx; g_ballInfo.y += g_ballInfo.dy; if(g_ballInfo.x < 0) { g_ballInfo.x = 0; g_ballInfo.dx = BALL_MOVE_DELTA; } else if(g_ballInfo.x + g_ballInfo.width > prc->right) { g_ballInfo.x = prc->right - g_ballInfo.width; g_ballInfo.dx = -BALL_MOVE_DELTA; } if(g_ballInfo.y < 0) { g_ballInfo.y = 0; g_ballInfo.dy = BALL_MOVE_DELTA; } else if(g_ballInfo.y + g_ballInfo.height > prc->bottom) { g_ballInfo.y = prc->bottom - g_ballInfo.height; g_ballInfo.dy = -BALL_MOVE_DELTA; } }
Egy kis alap matek. A delta értékkel mozgatjuk a labda pozícióját. Ha eléri az ügyfélterület határát, akkor úgy módosítjuk a deltákat, hogy az megfeleljen a labda ütközésének, és most már visszafelé mozogjon.
void DrawBall(HDC hdc, RECT* prc) { HDC hdcBuffer = CreateCompatibleDC(hdc); HBITMAP hbmBuffer = CreateCompatibleBitmap(hdc, prc->right, prc->bottom); HBITMAP hbmOldBuffer = SelectObject(hdcBuffer, hbmBuffer); HDC hdcMem = CreateCompatibleDC(hdc); HBITMAP hbmOld = SelectObject(hdcMem, g_hbmMask); FillRect(hdcBuffer, prc, GetStockObject(WHITE_BRUSH)); BitBlt(hdcBuffer, g_ballInfo.x, g_ballInfo.y, g_ballInfo.width, g_ballInfo.height, hdcMem, 0, 0, SRCAND); SelectObject(hdcMem, g_hbmBall); BitBlt(hdcBuffer, g_ballInfo.x, g_ballInfo.y, g_ballInfo.width, g_ballInfo.height, hdcMem, 0, 0, SRCPAINT); BitBlt(hdc, 0, 0, prc->right, prc->bottom, hdcBuffer, 0, 0, SRCCOPY); SelectObject(hdcMem, hbmOld); DeleteDC(hdcMem); SelectObject(hdcBuffer, hbmOldBuffer); DeleteDC(hdcBuffer); DeleteObject(hbmBuffer); }
Ez lényegében ugyanaz, mint az előző példákban volt. Azzal a különbséggel, hogy a pozíciókat a BALLINFO struktúrából vesszük. Van azonban egy tényleges különbség..
A kettős pufferelés
Ha közvetlenül az ablak HDC-jére rajzolunk, akkor könnyen előfordulhat az az eset, hogy még nem fejeztük be a rajzolást, amikor a képernyőn már megjelenik az eredmény. Például éppen a maszkot rajzoljuk a színes kép előtt. Ennek az lesz az eredménye, hogy nem csak a végeredményt látjuk, hanem az egyes részfázisokat is, és ezek villognak, váltogatják egymást. Egy bonyolultabb képnél, vagy lassabb számítógépnél ez már olyan zavaró lehet, hogy szétesik a kép.
Ez borzasztóan zavaró dolog, de szerencsére könnyen orvosolható. Nem a képernyőre rajzolunk hanem a memóriába, majd amikor már készen vagyunk, akkor az egész mesterművel egyben átmásoljuk a képernyőre a BitBlt() utasítással. Így a képernyő frissülésekor a kész régi képet látjuk, majd pedig a kész újat. Tökéletes!
Ezért ideiglenesen létrehozunk egy HBITMAP-ot a memóriában, aminek a mérete pontosan megegyezik a képernyővel.
HDC hdcBuffer = CreateCompatibleDC(hdc); HBITMAP hbmBuffer = CreateCompatibleBitmap(hdc, prc->right, prc->bottom); HBITMAP hbmOldBuffer = SelectObject(hdcBuffer, hbmBuffer);
Most már tudunk hova rajzolni a memóriában. Minden rajzolási műveletet a hdcBuffer-be végzünk (és nem az ablak HDC-jébe). Az eredményt egészen addig itt tároljuk, amíg teljesen készen nincs. Ekkor egyetlen lendülettel átmásoljuk az ablakra.
BitBlt(hdc, 0, 0, prc->right, prc->bottom, hdcBuffer, 0, 0, SRCCOPY);
És ennyi. Ezután kitakarítjuk a használt HDC-ket, és HBITMAP-eket.
A gyorsabb kettős pufferelés
Ebben a példában minden képkockánál létrehozzuk és megsemmisítjük a kettős pufferhez használt bitmap-et, egyszerűen azért, hogy nyomon tudjuk követni, el ne felejtődjön, stb. Az ablakot is átméretezhetjük, mert új puffer létrehozásakor automatikusan követi a méretét.
Ellenben sokkal hatékonyabb, ha egyszer globálisan létrehozzuk ezt a bitmap-et, és nem engedjük az ablak átméretezését. Vagy ha engedjük, akkor a bitmap-et is átméretezzük. Így viszont nem kell minden kockánál újra létrehozni és megsemmisíteni. Ez rajtunk múlik, hogy melyik módszert követjük. Nyilván egy játék fejlesztésekor érdemes az utóbbi utat járni.
Timer leölése
Amikor az ablak megsemmisül, akkor érdemes az összes használt erőforrást felszabadítani. Ebbe beletartozik a timer is. Ahhoz hogy leállítsuk, csak hívjuk meg a KillTimer() eljárást, és adjuk át neki a timer ID-jét.
KillTimer(hwnd, ID_TIMER);
A bejegyzés trackback címe:
Kommentek:
A hozzászólások a vonatkozó jogszabályok értelmében felhasználói tartalomnak minősülnek, értük a szolgáltatás technikai üzemeltetője semmilyen felelősséget nem vállal, azokat nem ellenőrzi. Kifogás esetén forduljon a blog szerkesztőjéhez. Részletek a Felhasználási feltételekben és az adatvédelmi tájékoztatóban.