Animation system with standard GDI to achieve game quality, Yanliang, January 2002 http://www.diamondgarden.net/
Foreword 2GDI base 3 Draw a bitmap object 3 Commonly used pixel format 4Windows Basic Animation System 4 Movies Drive Way 4 Play Animation 5 Eliminate Blinking 6 Transparent Color (Color Key) Processing 7Alpha Hybrid 9 Read JPEG, GIF File 10 Sub window management 12 advancement skills - use DIB 14 pixel operation 14RLE compression 15 Reference 15 Huashan Mars button 15 Other class library 16 Preface to realize the game quality animation, many people will immediately think of DirectX, have no big DirectDraw is very powerful, but It is not necessary to use DirectDraw. The theoretical and skills behind the animation are the same, this is not too big to use with the end of the API (if the API is not too ~~ slow). For the test results of the NewImage LIB implemented, all the storage and operation of all pixel data internally, the last step is output to the screen using GDI performance than DirectDraw less than 10%, and is about 20% lower on the Window9x system. This is absolutely acceptable for many software.
Now the more gorgeous gorgeous application interface, in addition to supporting Skin, many people want to join some of the techniques such as Sprite animation, such as Sprite animation, because this cause is introduced into the DirectX API, which is obviously not worth it (more The DX version is upgraded frequently, and DIRECTGRAPHIC has been replaced with DirectGraphic in DX8. This article will use the standard GDI function to use the standard GDI function as an example, bring you into high quality 2D animation programming, and ensure that its equipment is independent.
This article assumes that readers have C / C language knowledge, Windows programming foundation, GDI basic concept. Below I will mainly tell the experience and some skills I have accumulated in the past, but I will not explain the basic concepts above. The reader is preferably the MFC foundation. The code given in this article will mainly use MFC, but the truth is not limited to MFC.
GDI Basics Draw a bitmap object GDI all the operations are done on DC (Device Context), so you should have DC's concept, if you don't understand DC, now go over Windows Programming book. First we have to load a bitmap object, use the Win32 API to be written as this: file: // From the resource load a bitmap, if you can use it from the file load, you can use: loadimage () hbitmap hbmp = :: loadbitmap (Hinstance, makeintResource IDB_MYBMP); if you can write this: cbitmap bmp; bmp.loadBitmap (idb_mybmp); you want to draw this bitmap object to the window, you must first get the DC of the window, and then operate this DC. Please pay attention to the code that creates a MemoryDC, and it will be used later. Win32 API version: file: // Assume the bitmap size is 100 * 100 pixels file: // Assume handle hdc hwnddc = :: getDC (hwnd); hdc memdc = :: createcompatibledc (hwnddc) HBitmap oldbmp = :: selectobject (MEMDC, HBMP); :: Bitblt (HWNDDC, 0, 0, 100, 100, MEMDC, 0, 0, Srcopy); if (OldBMP) :: SelectObject (MEMDC, OLDBMP); Deletedc (MEMDC); :: releasedc (hwnd, hwnddc); MFC version: file: // Assumment is CclientDC DC (this) in a member function of a CWnd derived class; cdc memdc; memdc.createcompatibledc (& DC); cbitmap * Oldbmp = MEMDC.SELECTOBJECT (& BMP); DC.Bitblt (0, 0, 100, 100, & MEMDC, 0, 0, Srcopy); if (OldBMP) MEMDC.SelectObject (OldBMP); can also be: cclientdc DC (this); dc.drawState (CPOINT (0, 0), CSIZE (100, 100), & BMP, DST_BITMAP);
The basic code is like this, of course, more APIs can be used, this is going to see your own. J Commonly used pixel format To make image programming, the pixel format does not understand, it doesn't matter. I think there should be more people who don't know much, so I briefly introduced it here. 1. 8bit is also called 256 color mode. Each pixel takes up one byte and uses a palette. The palette is actually a color table. Simple telling is that we have 256 paint buckets (because the value of the pixels is 0 to 255), the colors of paint in each paint bucket are made of red, green, blue ( RGB) The basic paint is formed in different ratios. So when we specify a pixel color, we only need to specify the number bucket it uses. This model creates the magical pattern of the DOS era - 13h (320 * 200 * 256 colors), because 320 * 200 * 1byte is the range of 16bit pointer addressing capabilities. This mode has 2 18-way colors (by changing the palette), the color of 256 can be displayed at the same time. When this mode is just launched, some people exclaim this is the crystallization of human wisdom! It is also this model to create the "Karai Legend of WESTWOOD" in this pattern. "卡 卡 传> >> << 奇 侠 传> > Such classic games. The hardware palette under Windows should be rarely used, but you can use the software palette to compress your animation, which is also common to use in 2D games. 2. 16bit This is also the favorite pattern of the author. It does not use a palette. Each pixel takes two bytes to store the RGB value. I think the effect of this pixel format (simultaneous display color) and storage capacity (also affecting speed) have achieved a better unity. But if you are writing an app, I advise you not to use it. Because its RGB value is not the entire Byte, for example 565 mode (a mode of 16bit), its RGB occupied Bit is like this: RRRR RGGGGGB BBBB3. 24bit has three BYTEs each pixel, store RGB value, respectively, Is this very convenient for you? Is it great? Unfortunately, it is not that the Poor computer is, because the CPU visits the address of the odd number will be very hard, and there are many difficulties in hardware technology (I don't know how to specifically , Please have a hardware's expert pointing point), so you will find that your graphics card does not support this model, but you can use it in your own software. 4. 32bit 4 BYTEs each pixel, store RGBA, A value is Alpha, which is transparent, can be achieved with a pixel mixing algorithm, which will be seen later. The basic animation system of Windows has a short way to say the basic principle of the animation. The general process of the program playing animation is: Drawing - Erase - Drawing, such a repetition process, as long as you repeat, at least 16 times per second (Known as 16fps, frame per second), our poor eyes will distinguish the image of the single frame, it is an animation. There are two ways to drive such repetitive non-stop operations in a Windows environment: 1. Setting Timer This is very simple, just set a short enough Timer, then respond to WM_TIME (corresponding to the ONTIMER function in the MFC) to meet most The need for the application. The disadvantage is not accurate enough, and the accuracy of Win2000 and Win9X systems has great differences.
2. Performing animation operation in the message loop This is a commonly used method in the game, usually writes the message in WinMain to this: While (TRUE) {// Look for Messages, IF None Are Found dam // Update The State And Display IT IF (PeekMessage (& MSG, NULL, 0, 0, PM_NOREMOVE)) {IF (0 == GetMessage (& MSG, NULL, 0, 0)) {// WM_QUIT WAS POSTED, SO EXIT RETURN (INT) MSG. WPARAM;} TranslateMessage (& MSG); DISPATCHMESSAGE (& MSG);} else {if (g_bactive) // does not update when the main window is not activated to save resources {file: // Execute an animation update operation} // make Sure We Go Go Go To Sleep If We Have Nothing else to do else waitMessage ();}} If you use the MFC, you need to overload the ruin virtual function of CWINAPP to replace the above message. Playback Now We have an appropriate timing to perform updates, let us try animation now. The following code will no longer provide the Win32 version. For the convenience of narrative, I need a window of playing animation. It must be a CWND derive class, assume that this class is called CMYVIEW, and we will draw animations in this window. First we add a member function to this class "Void CMYVIEW:: RenderView ()", you can call this function using the method mentioned above. Now ready, how do our animations store? Don't mention the animation GIF89A format (if you think that only gif has an animation, then I advise you to do art, don't do the procedure), if you only If you want a simple animation to play, you can, but if you want to do a complex point, interactive animation, I advise you still don't use it. Suppose we have a 4-frame animation, how to store it? I first thought about saving 4 BMP files, then read into an array of cbitmap objects, but respected masters SCOTT Meyers warned that we don't use polymorphic arrays, because The compiler does not accurately calculate the size of the object in the array in some cases, so the subscript operator will generate a terrible effect. Then I thought of array of CBITMAP pointers, this is good, but it is a little troublesome. Now look at my final solution. Stitching a frame sequence into a file, like this: then use it to create a CImageList object, let's take a closer look, use Bool Cimagelist :: Create (int Cx, Int Cy, uint nflags, int Ninitial, INT nGrow); functions, the front two parameters are used to specify the size of our frame animation. This creates an empty imagelist, which is the advantage of being extensible.
Below we need to put the frame sequence file LOAD to a CBitmap object, you can save the JPG or GIF file to save capacity (later, you will mention the simple way to read these files, and attach a practical class). When we have a suitable CBitmap object, we can add him to our imagelist, use: BOOL CIMAGELIST :: INT Add (CBitmap * Pbmimage, ColorRef CRMASK); an instance: const int sprirt_width = 32; const int spot_height = 32; ... .m_myimglist.create (Spirit_Width, Spirit_Hight, ILC_COLOR24 | ILC_MASK, 1, 1); if (Bmp.Load ("Myani.Bmp")) M_MYIMGLIST.ADD (& BMP, RGB (152, 152, 152)); ok, Now we are ready to do this, let's work on the rendering function, below this code can loop the 4 frame animation above, and support transparent color (if you don't know this name, there is a remark later)! void CMyView :: RenderView () {CclientDC dc (this); Static int curframe = 0; m_myimglist.Draw (& dc, curframe, Cpoint (0,0), ILD_TRANSPARENT); curframe ; If (curframe> m_myimglist.GetImageCount ()) Curframe = 0;} The above code does not write an operation because it is different from the specific needs. If you only have a wizard animation, you can save the image of the rectangular area of the wizard with a Bitmap object. You may also need a big background diagram to update each frame (here I don't discuss the optimization method like Dirty Rect), so you only have to draw a background every time, then draw the elf. How? You have already achieved a basic animation system, which is as simple as it. Eliminating flashes If you really implement the above code, you will find that the screen flashes, very unhappy. L Many people will blame on the GDI head, they will MS, saying that GDI is too slow. In fact, it is not too (not a MS should not be embarrassed, huh, huh), any direct write screen will generate flashing, write directly in DOS or write Primary Surface directly using the DirectDraw API, because each update display is displayed It is said to be seen by the user (because the cause of vertical removal may be delayed). The easiest way to eliminate flashing is also the most classic method is dual buffer. The so-called double buffer is very simple, that is, we have stored in other places (simple saying is to open up a storage space, we don't display it), we have to rendering all the animations to this place, not direct Rendering to the screen (a storage area for the screen). In GDI, direct screen is a window DC, "unacceptable place" can generally use Memory DC. After rendering all animations to the background buffer, copy the whole to the screen buffer! In the pure software 2D graphics engine, the double buffer typically means that a region is opened in memory to store pixel data. In DirectDraw, you can create a Back Surface, after rendering all animations, then use the FLIP operation to make it visible, the FLIP operation is very fast because it is just the setup of the SURFACE, so it is very fast.
Let's override the void cmyview :: RenderView () function, use GDI to implement double buffer: void cmyview :: renderview () {cclientdc DC (this); CRECT RC; getClientRect (RC); cdc memdc; memdc.createcompatibledc & dc); CBitmap bmp;. Bmp CreateCompatibleBitmap (& dc, rc.Width (), rc.Height ()); CBitmap * oldbmp = memdc.SelectObject (& bmp); Static int curframe = 0; m_myimglist.Draw (& memdc, curframe, CPOINT (0, 0), ILD_TRANSPARENT); Curframe ; if (Curframe> M_MYIMGLIST.GetImageCount ()) Curframe = 0; if (OldBMP) MEMDC.SELECTOBJECT (OLDBMP); DC.bitblt (0, 0, rc.width () , rc.height (), src.Height (), SRCCopy;} where a BitMap object is created, then the Memory DC is required, because the DC created by CreateCompatibleDC contains only one 1 * 1 pixel monochrome Bitmap Object, so if this step is missing, any drawing operation on MemoryDC will have no effect. Extending a problem, the first parameter of the CreateCompatibleBitmap function is clearly unwritable & memdc. If you have created a monochrome bitmap, I think you know this. J Over-write function seems to have many unnecessary operations because we have only one animation object now. If we have multiple animations, but also need to draw animation's sub-windows, then the effect of this will be very Good, there will be no flash, but also to the charm MUD client mentioned in the article, can reach 60fps (on the Saiyang 433 of my home). So far, our basic animation system has had a good foundation. COLOR Key Processing Transparent Color means that when a picture is drawn, the color of this color will not be drawn, which is usually used to play the spirit animation of the game, so you can see the irregularities of various shapes. Character animation. But their data is a rectangular pixel area, but some pixels are not drawn when they draw. GDI provides a TransparentBlt () function to support the Color Key, you can check the description of the function in the MSDN. However, after using this function in my code, there has been a serious resource leak under Win9x system, but it is fine in Win2000, so if you also discover this problem, I suggest you use the following code to transparency. Draw to DC. Suppose you have a cbitmap derived CMYBITMAP:
Bool CMYBITMAP :: DrawTransparentinpoint (CDC * PDC, INT X, INT Y, ColorRef Mask / * To filter out color value * /) {file: // Quick Return IF (PDC-> getsafehdc () == null) Return False ; If (m_hobject == null) Return False;
CRect DRect; DRect = Rect (); DRect.OffsetRect (x, y); if return FALSE (pdc-> RectVisible (& DRect)!); COLORREF crOldBack = pdc-> SetBkColor (RGB (255,255,255)); COLORREF crOldText = pdc -> setTextColor (RGB (0,0,0));
CDC DCIMG, DCTRANS; IF (DCIMG.CREATECOMPALEDC (PDC)! = True) Return False; IF (DCTRANS.CREATECOMPALEDC (PDC)! = True) Return False;
CBITMAP * OLDBMPIMG = DCIMG.SELECTOBJECT (THIS);
CBitmap BMPTRANS; IF (BMPTRANS.CREATEBITMAP (Width (), Height (), 1, 1, NULL)! = True) Return False;
CBITMAP * OLDBMPTRANS = DCTRANS.SELECTOBJECT (& BMPTRANS);
Dcimg.setbkcolor (MASK); DCTRANS.BITBLT (0, 0, Width (), Height (), & DCIMG, 0, 0, SRCCopy;
PDC-> Bitblt (x, y, width (), height (), & dcimg, 0,0, srcinvert); PDC-> Bitblt (x, y, width (), Height (), & DCTRANS, 0, 0, SRCAND ); PDC-> Bitblt (x, y, width (), height (), & dcimg, 0,0, srcinvert);
IF (OldBmpimg) Dcimg.selectObject (OldBmpimg); if (OldBMPTrans) DCTRANS.SELECTOBJECT (Oldbmptrans); PDC-> setBkcolor (croldback); PDC-> setTextColor (croldtext);
Return True;} Alpha Hybrid Alpha Mix is a method of mixing a pixel. The so-called pixel mixing is to mix the value of the two pixels into a new pixel value using a certain algorithm (in the same way, and the value of the two pixels is usually called the source (SRC) and the purpose, respectively. DST), then store the mixed result in DST: DST = src Blend DST If the source pixel and destination pixels are RGBA format, you can use Alpha information (or Alpha channel) to combine various operations using each pixel alpha information (or Alpha channel) Formula, such as DST = SRC * Src.alpha DST * DST.ALPHA; or DST = SRC * Src.alpha DST * (1-src.alpha) // Here we assume that the alpha value is a floating point number of 0 to 1. Unfortunately, the standard GDI does not support functions like this operation (I started to find it), it only supports another alpha mix, I call it const alpha blend, that is, the two images do not include images of the alpha channel according to one The fixed Alpha value is mixed together, that is, each pixel uses the same alpha value. GDI supports this function operation is: AlphaBlend (HDC hdcDest, int nXOriginDest, int nYOriginDest, int nWidthDest, int hHeightDest, HDC hdcSrc, int nXOriginSrc, int nYOriginSrc, int nWidthSrc, int nHeightSrc, BLENDFUNCTION blendFunction); parameter a of the API There are fewer numbers, but I think the location parameters can be easily set, and there is a source DC and destination DC. Of course, our GDI can only operate DC, not our pixel data, and we only need Take my bitmap Select to DC. The last parameter is a structure. It is used to specify an alpha operation method. Please see an actual example: BLENDFunction BF; bf.alphaformat = 0; bf.blendflags = 0; bf.blendop = ac_src_over; bf.sourceConstantalpha = 100; // Indicate transparency, range of 0 ~ 255 alphablend (PDC-> getSafeHDC (), rc.left, rc.top, rc.width (), RC .Height (), MEMDC.GETSAFEHDC (), 0, 0, rc.width (), rc.height (), bf); Maybe you have seen a lot of games, are on the game screen when pop-up dialog box Meng Yizhen is translucent black, then impact on this. This effect can be achieved using the above operation. You can build a Memory DC first, then fill him as black, then set the alpha value to 128, then mix it to the DC you want to draw (not necessarily window DC, remember the double buffer we will have before?) OK is OK. Read JPEG, GIF file JPEG compression algorithm, synthesized signal, visual psychology, and GIF format, especially supported GIF89A format, has made many very varied optimizations in order to save capacity, so I have to write a complete support for these standards. The format decoder is quite difficult and there is no need. If you need to read and write the JPEG file I recommend you to use Intel JPEG LIB, speed is quite satisfactory. GIF has no read and write code provided by any official organization due to authorization issues.
If you just need to read JPEG and static gif (or only one frame dynamic GIF), I recommend you use the OLELOADPICTURE function provided by Windows, the following code can read a JPG, GIF, BMP into the Bitmap object: Bool CIJLBitmap :: Load (LPCTSTR lpszPathName) {BOOL bSuccess = FALSE; file: // Free up any resource we may currently have DeleteObject (); file:! // open the file CFile f; if (f.Open (lpszPathName, CFile :: ModeRead) {Trace ("Failed to Open File% S, Error:% x / n"), lpszpathname, :: getLastError ()); return false;} file: // get the file size dword dwfilesize = f.GetLength (); file: // Allocate memory based on file size LPVOID pvData = NULL; hGLOBAL hGlobal = GlobalAlloc (GMEM_MOVEABLE, dwFileSize); if (hGlobal == NULL) {TRACE (_T ( "Failed to allocate memory for File% s, error:% x / n "), lpszpathname, :: getLastError ()); returnaf (hglobal); file: // assert (pvdata); if (pvdata == null) { Trace ("Failed to Lock Memory / R / N")); Return False;} // Read File and Store I N Global Memory IF (F. Read (Pvdata, DwFileSize)! = dwfilesize) {trace ("Failed to Read In Image Date from File% S, Error:% X / N"), LPSZPATHNAME, :: getLastError () ); GlobalUnlock (hGlobal); GlobalFree (hGlobal); return FALSE;} file: // Tidy up the memory and close the file handle GlobalUnlock (hGlobal); file: // create IStream * from global memory LPSTREAM pStream = NULL; if (Faled (Hglobal, True, & PStream)) {trace ("Failed to create istream interface from file% s, error:% x / n"), lpszpathname, :: getLastError ()); GlobalFree (Hglobal );
return FALSE;} // Create IPicture from image file if (SUCCEEDED (:: pStream OleLoadPicture (, dwFileSize, FALSE, IID_IPicture, (LPVOID *) & m_pPicture))) {short nType = PICTYPE_UNINITIALIZED; if (SUCCEEDED (m_pPicture-> get_Type ( & nType)) && (nType == PICTYPE_BITMAP)) {OLE_HANDLE hBitmap; OLE_HANDLE hPalette; if (SUCCEEDED (m_pPicture-> get_Handle (& hBitmap)) && SUCCEEDED (m_pPicture-> get_hPal (& hPalette))) {Attach ((HBITMAP) hBitmap) ; M_palette hpalette; bsuCcess = true;}}} file: // free up the istream * interface pstream-> release (); return bsuccess;} This class is complete code, please see the article last reference . Sub-window Management You may have noticed that the windows in almost all game interfaces are flying from the screen from the screen (and is translucent, this can be done). I usually use my UI system in the game. Here we can make a variety of animation window with Windows's management of windows. First let us start from the simplest start. Suppose it is necessary to do a beautiful button in our animation window, I advise you not to use CBitmapButton, because you have already opened more than 16 thief boats per second, I suggest you call every heavy When the parent window retrays all sub-windows, if there is a movie operation on the sub-window, it can be easily implemented. Since doing, do it best. J, how do we define a button? You may think that you define a rectangular area, then detect whether it is to operate on this area in the message response function of the parent window, so that special draw is a special draw at this rectangular area at the time of redraw the parent window. Enough. This is achievable, but it is obviously not in line with our OOP spirit, and there is more element element. You are likely to mess with your feet. The final solution is of course using our cute CWND classes, apparently all interface elements can be used as a CWND derived object. However, I suggest you derive from CButton, which brings more troubles than its value. Delicate a class from CWND, then pay attention to using the WS_CHILD style while CREATE, and specifies the parent window for our animation window.
The following question is how to call these sub-window heavy draws? The first better solution is to build such a virtual base class: CMYANIWND: PUBLIC CWND {... Virtual void render (CDC * PDC) = 0; ...} Suppose you have a Button class and a TextBox class: CMYBUTTON: PUBLIC CMYANIWNDCMYTEXTBOX: PUBLIC CMYANIWND These two classes must implement the render function, so that you can save a pointer array in the parent window class, for example: cptrarray m_allchild; create one This is written like this: cmybutton * pbtn = new cmybutton; m_allchild.add (pbtn); PBTN-> Create (...); then in our parent window's renderView function (previously mentioned, each update call) Yes: CMYANIWND * PCHILD = NULL; For (INT i; i
There is also a question that you want to pay attention to is the order of drawing. If you want the child window to cover some animation, you should first render those animations, then render the window, and vice versa. Advanced Skills - All operations with DIB pixels are limited to standard GDI functions, if we want to achieve a further operation, for example, when you want to put the color of the entire picture, you have to put it in the evening. The entire picture is dark, and it will restore it to the original brightness in the morning. These GDI can't help you. If you want to achieve the above effect, you must operate the RGB value of the pixel. First let us get pixel data in a Bitmap object. Let us look at how to do it. Suppose we have a MyBMP is a cbitmap object (or its derived class object). The following code takes out pixels in cbitmap: Bitmap BM; MyBmp.getBitmap (& BM); BitmapInfo Binfo; ZerMemory (& Binfo, Sizeof (BitmapInfo); binfo.bmiHeader.biBitCount = 24; file: // 24bit pixel format binfo.bmiHeader.biCompression = 0; binfo.bmiHeader.biHeight = -bm.bmHeight; binfo.bmiHeader.biPlanes = 1; binfo.bmiHeader.biSizeImage = 0; Binfo.Bmiheader.bisize = sizeof (BitmapInfoHead); binfo.bmiheader.biwidth = bm.bmwidth;