通过对一组动画帧IMAGE
对象色彩缓冲区的操作,实现了动画帧翻转、无敌帧闪烁、冻结 三种常见的游戏效果,本文介绍这三种效果的相关概念和实现思路。程序中所使用的帧动画素材为提瓦特幸存者进阶版 中分享的素材,需要测试的小伙伴可以在文末找到相关素材的下载链接。
色彩缓冲区 我们首先来看一下EasyX中IMAGE
类的声明:
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 class IMAGE { public : int getwidth () const ; int getheight () const ; private : int width, height; HBITMAP m_hBmp; HDC m_hMemDC; float m_data[6 ]; COLORREF m_LineColor; COLORREF m_FillColor; COLORREF m_TextColor; COLORREF m_BkColor; DWORD* m_pBuffer; LINESTYLE m_LineStyle; FILLSTYLE m_FillStyle; virtual void SetDefault () ; public : IMAGE (int _width = 0 , int _height = 0 ); IMAGE (const IMAGE &img); IMAGE& operator = (const IMAGE &img); virtual ~IMAGE (); virtual void Resize (int _width, int _height) ; };
如果我们将IMAGE
用来表示图片对象,那么其中最关键的部分就是m_pBuffer
,这个缓冲区中存储了图片的像素色彩数据,可以使用GetImageBuffer
函数获取到缓冲区地址。
在缓冲区中,每个像素点的色彩数据占用 4 个字节,这些数据按照图片从左到右、从上向下的顺序依次排列,也就是说,虽然EasyX的绘图函数大多不会考虑透明通道的信息,但是在使用loadimage
加载到程序中的图片数据还是保留了透明通道的数据。
除此之外,还需要特别注意的是,缓冲区中的每个像素点数据色彩是B、G、R顺序排布的,所以如果我们想要为它重新赋值,在使用RGB()
宏构造COLORREF
后,还需要使用BGR()
宏交换对应位的数据才能表达正确的颜色;同理,如果想要直接获取缓冲区像素对应的颜色分量,GetBValue()
宏获取到的实际是红色分量的值,而GetRValue()
宏获取到的才是蓝色分量的值。
动画帧翻转 在很多2D游戏的制作中,玩家角色是分为向左和向右两套动画,而大多数来自互联网的素材包中大多只会提供一个方向的动画帧序列,翻转的操作一般是在现代化的游戏制作工具中动态处理的。虽然我们可以使用PS将图片处理为水平镜像,但是这有点过于冗余了,我们希望可以在游戏运行时动态地生成翻转后的图片,可是EasyX并没有提供将图片翻转的函数,所以我们需要自己实现这部分内容。
首先是动画序列帧图片的加载:
1 2 3 4 5 6 7 8 9 IMAGE img_player_left[6 ]; for (int i = 0 ; i < 6 ; i++){ static TCHAR img_path[256 ]; _stprintf_s(img_path, _T("img/paimon_left_%d.png" ), i); loadimage (&img_player_left[i], img_path); }
随后,我们可以对每一张向左的动画图片,逐行翻转水平坐标的像素,从而实现左右镜像的效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 IMAGE img_player_right[6 ]; for (int i = 0 ; i < 6 ; i++){ int width = img_player_left[i].getwidth (); int height = img_player_left[i].getheight (); Resize (&img_player_right[i], width, height); DWORD* color_buffer_left_img = GetImageBuffer (&img_player_left[i]); DWORD* color_buffer_right_img = GetImageBuffer (&img_player_right[i]); for (int y = 0 ; y < height; y++) { for (int x = 0 ; x < width; x++) { int idx_left_img = y * width + x; int idx_right_img = y * width + (width - x - 1 ); color_buffer_right_img[idx_right_img] = color_buffer_left_img[idx_left_img]; } } }
无敌帧闪烁 动画闪白的效果很常见,一般来说,当玩家受伤后,会进入到一小段时间的无敌状态,来避免每帧进行伤害检定时造成多余的伤害,而动画序列帧的闪白可以说是一种廉价且经典的效果。在很多游戏的素材包中,制作者会提供角色动画序列帧对应的白色剪影序列帧;但是随着角色动画越来越多,每种动画都需要对应一套额外的白色剪影动画,这就显得很麻烦了。
我们同样可以在运行时通过对像素缓冲的处理生成一套全新的白色剪影序列帧,只需要对透明度不为0的像素的RGB值设置为白色即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 IMAGE img_player_left_sketch[6 ]; for (int i = 0 ; i < 6 ; i++){ int width = img_player_left[i].getwidth (); int height = img_player_left[i].getheight (); Resize (&img_player_left_sketch[i], width, height); DWORD* color_buffer_raw_img = GetImageBuffer (&img_player_left[i]); DWORD* color_buffer_sketch_img = GetImageBuffer (&img_player_left_sketch[i]); for (int y = 0 ; y < height; y++) { for (int x = 0 ; x < width; x++) { int idx = y * width + x; if ((color_buffer_raw_img[idx] & 0xFF000000 ) >> 24 ) color_buffer_sketch_img[idx] = BGR (RGB (255 , 255 , 255 )) | (((DWORD)(BYTE)(255 )) << 24 ); } } }
而在渲染时,只需要每隔一段时间交替绘制正常的序列帧图片和白色剪影图片,即可实现闪烁的效果。
冻结效果 冻结的效果在游戏中也很常见,主要由两部分构成:暂停动画帧、混叠结晶图 。我们需要准备好一张与角色动画尺寸相同或稍大的冰冻结晶图素材:
暂停动画帧很简单,只需要当角色进入到冻结状态时停止动画帧计时器的更新,让图片序列帧的索引停止在当前数值即可;混叠结晶图指的是将结晶贴图的RGB分量与当前动画帧的RGB分量进行混合,相当于将结晶贴图处理为了半透明状态贴到了玩家贴图之上;当然,为了让结晶只出现在玩家身上,所以只需要处理玩家动画帧中透明通道不为0的像素。
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 IMAGE img_current_frame (img_player_left[counter]) ;int width = img_current_frame.getwidth ();int height = img_current_frame.getheight ();DWORD* color_buffer_ice_img = GetImageBuffer (&img_ice); DWORD* color_buffer_frame_img = GetImageBuffer (&img_current_frame); for (int y = 0 ; y < height; y++){ for (int x = 0 ; x < width; x++) { int idx = y * width + x; static const float RATIO = 0.25f ; DWORD color_ice_img = color_buffer_ice_img[idx]; DWORD color_frame_img = color_buffer_frame_img[idx]; if ((color_frame_img & 0xFF000000 ) >> 24 ) { BYTE r = (BYTE)(GetBValue (color_frame_img) * RATIO + GetBValue (color_ice_img) * (1 - RATIO)); BYTE g = (BYTE)(GetGValue (color_frame_img) * RATIO + GetGValue (color_ice_img) * (1 - RATIO)); BYTE b = (BYTE)(GetRValue (color_frame_img) * RATIO + GetRValue (color_ice_img) * (1 - RATIO)); color_buffer_frame_img[idx] = BGR (RGB (r, g, b)) | (((DWORD)(BYTE)(255 )) << 24 ); } } }
这样就可以实现最基础的冻结效果,如果想要让效果更逼真一点,让结晶之上存在流动的高光效果,那么就可以用一个循环的条带,不断地从头到脚扫描冻结后的贴图,条带范围内的像素被处理为白色,也就是这样的效果:
但是这样规则的扫描线有点太过于生硬了,结合现实情况考虑,冰堆的高光一般是因为某些冰面的反射方向恰好处于观察者视角处导致的,也就是结晶贴图中较亮的部分,所以流光效果本质是为了强化冰堆中较亮的那部分内容;所以,我们可以对混叠后的贴图首先进行亮度提取,只有亮度大于阈值的部分,才会显示为白色的条带,这样整体效果就会自然很多:
冻结效果动画相关的完整代码:
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 void RenderFrozenPlayer () { static const POINT position = { 1075 , 345 }; static int counter = 0 ; static int anim_timer = 0 ; static int frozen_timer = 0 ; static const int THICKNESS = 5 ; static int highlight_pos_y = 0 ; static bool is_frozen = false ; if ((!is_frozen) && (++anim_timer % 3 == 0 )) counter = (counter + 1 ) % 6 ; if (++frozen_timer % 100 == 0 ) { is_frozen = !is_frozen; highlight_pos_y = -THICKNESS; } putimage_alpha (position.x + (80 - 32 ) / 2 , position.y + 80 , &img_shadow); if (is_frozen) { IMAGE img_current_frame (img_player_left[counter]) ; int width = img_current_frame.getwidth (); int height = img_current_frame.getheight (); highlight_pos_y = (highlight_pos_y + 2 ) % height; DWORD* color_buffer_ice_img = GetImageBuffer (&img_ice); DWORD* color_buffer_frame_img = GetImageBuffer (&img_current_frame); for (int y = 0 ; y < height; y++) { for (int x = 0 ; x < width; x++) { int idx = y * width + x; static const float RATIO = 0.25f ; static const float THRESHOLD = 0.84f ; DWORD color_ice_img = color_buffer_ice_img[idx]; DWORD color_frame_img = color_buffer_frame_img[idx]; if ((color_frame_img & 0xFF000000 ) >> 24 ) { BYTE r = (BYTE)(GetBValue (color_frame_img) * RATIO + GetBValue (color_ice_img) * (1 - RATIO)); BYTE g = (BYTE)(GetGValue (color_frame_img) * RATIO + GetGValue (color_ice_img) * (1 - RATIO)); BYTE b = (BYTE)(GetRValue (color_frame_img) * RATIO + GetRValue (color_ice_img) * (1 - RATIO)); if ((y >= highlight_pos_y && y <= highlight_pos_y + THICKNESS) && ((r / 255.0f ) * 0.2126f + (g / 255.0f ) * 0.7152f + (b / 255.0f ) * 0.0722f >= THRESHOLD)) { color_buffer_frame_img[idx] = BGR (RGB (255 , 255 , 255 )) | (((DWORD)(BYTE)(255 )) << 24 ); continue ; } color_buffer_frame_img[idx] = BGR (RGB (r, g, b)) | (((DWORD)(BYTE)(255 )) << 24 ); } } } putimage_alpha (position.x, position.y, &img_current_frame); } else putimage_alpha (position.x, position.y, &img_player_left[counter]); }
素材下载 序列帧素材链接:https://pan.baidu.com/s/1CQ86MR3AnTUbSqHbkCgBRg?pwd=7777
视频讲解