01-动画与角色移动
1 EasyX
1.1 从文件读取图像
EasyX 文档 - loadimage
这个函数用于从文件中读取图像。
// 从图片文件获取图像(bmp/gif/jpg/png/tif/emf/wmf/ico)
int loadimage(
IMAGE* pDstImg, // 保存图像的 IMAGE 对象指针
LPCTSTR pImgFile, // 图片文件名
int nWidth = 0, // 图片的拉伸宽度
int nHeight = 0, // 图片的拉伸高度
bool bResize = false // 是否调整 IMAGE 的大小以适应图片
);
举例,加载
test.jpgIMAGE img;
loadimage(&img, _T("test.jpg"));
LPCTSTR是Windows编程中使用的数据类型宏,用于表示字符编码的兼容性处理。其名称由"Long Pointer to Constant T Character String"缩写构成,本质为指向常量字符的指针,具体类型由编译环境是否定义_UNICODE宏决定,可自动切换为Unicode或ANSI字符串类型。
该类型包含五个组成部分:L代表历史遗留的32位长指针标识,P表示指针类型,C指代常量属性,T对应适配不同字符集的_T宏,STR声明字符串用途。在代码中通常与CString类通用,得益于CString内置的隐式类型转换运算符,允许直接作为常量字符串指针使用。但向非常量字符串(LPTSTR)转换时需通过GetBuffer/ReleaseBuffer方法安全操作,避免直接强制类型转换导致的内存隐患。
1.2 渲染图片
这个函数的几个重载用于在当前设备上绘制指定图像。
// 绘制图像
void putimage(
int dstX, // 绘制位置的 x 坐标
int dstY, // 绘制位置的 y 坐标
IMAGE *pSrcImg, // 要绘制的 IMAGE 对象指针
DWORD dwRop = SRCCOPY // 三元光栅操作码
);
举例,将
test.jpg渲染到(100,200)的位置IMAGE img;
loadimage(&img, _T("test.jpg"));
putimage(100, 200, &img);

1.3 cleardevice()
cleardevice() 是 EasyX 图形库中最基础的函数之一
- 作用:将当前图形窗口(画布)的所有像素点恢复为默认背景色(默认是黑色,也可以通过
setbkcolor()修改),相当于用背景色 “覆盖” 掉画布上所有已绘制的内容,让画布回到 “干净” 的初始状态。 - 适用场景:在游戏 / 动画循环中,每一次绘制新内容前清空画布,避免上一帧的图像残留,防止画面重叠、模糊。
2 代码实现
2.1 显示背景图片
#include<graphics.h>
int main()
{
initgraph(1280, 720);
bool running = true;
ExMessage msg;
IMAGE img_background;
loadimage(&img_background, _T("..\\img\\background.png"));
BeginBatchDraw();
while(running) {
DWORD start_time = GetTickCount();
while (peekmessage(&msg))
{
}
cleardevice();
putimage(0, 0, &img_background);
FlushBatchDraw();
DWORD end_time = GetTickCount();
DWORD delta_time = end_time - start_time;
if(delta_time < 1000/144) {
Sleep(1000 / 144 - delta_time);
}
}
EndBatchDraw();
return 0;
}
2.1.1 基本绘图流程
BeginBatchDraw、EndBatchDraw、FlushBatchDraw 是用来实现批量绘图的:先把所有要画的图形 “暂存” 起来,等全部绘制指令执行完,再一次性把所有图形刷新到屏幕上,从而消除闪烁、提升绘图效率。
BeginBatchDraw()
- 作用:开启批量绘图模式。调用后,后续所有的绘图指令都不会立刻显示在窗口上,而是被缓存到内存中。
- 时机:在所有批量绘制的代码之前调用。
EndBatchDraw()
- 作用:结束批量绘图模式,并立即将缓存中所有未显示的图形一次性刷新到屏幕上。
- 时机:在所有批量绘制的代码之后调用,是批量绘图的 “收尾” 操作。
FlushBatchDraw()
- 作用:在批量绘图模式中(即
BeginBatchDraw之后、EndBatchDraw之前),手动强制刷新缓存中的图形到屏幕上(但不会结束批量绘图模式)。 - 适用场景:批量绘制过程中,需要 “阶段性显示” 部分图形(比如绘制进度条时,每画一段就刷新一次)。
三个函数都无参数、无返回值
2.1.2 windows.h
windows.h 是 Windows 系统编程的核心头文件。
GetTickCount()的作用是获取从电脑开机到现在的毫秒数(比如开机后过了 10000 毫秒,就返回 10000)。Sleep()的作用是让程序 “暂停” 指定的毫秒数(比如Sleep(10)就是暂停 10 毫秒)。DWORD类型本质是 Windows 定义的一个 “别名”,等价于unsigned long(无符号长整型),用来存储非负的整数(比如时间、内存地址等)。
2.1.3 tchar.h
_T() 宏的核心是 “自动适配编码”:
- 当程序编译为 “多字节编码” 时,
_T("字符串")等价于普通字符串"字符串"; - 当程序编译为 “Unicode 编码” 时,
_T("字符串")等价于宽字符字符串L"字符串"。
2.2 显示角色动画
2.2.1 批量导入图片素材
const int PLAYER_ANIM_NUM = 6;
IMAGE img_player_left[PLAYER_ANIM_NUM];
IMAGE img_player_right[PLAYER_ANIM_NUM];
之后实现
loadAnimation()函数,需要注意的是如果直接使用path.c_str()会返回wchar_t *类型。而loadimage()只能接受char *类型,因此需要编写一个函数wstring_to_utf8()实现把utf16转化为utf8的功能。
void LoadAnimation()
{
for (size_t i = 0; i < PLAYER_ANIM_NUM; i++) {
std::wstring path = L"..\\img\\player_left_" + std::to_wstring(i) + L".png";
loadimage(&img_player_left[i], wstring_to_utf8(path).c_str());
}
for (size_t i = 0; i < PLAYER_ANIM_NUM; i++) {
std::wstring path = L"..\\img\\player_right_" + std::to_wstring(i) + L".png";
loadimage(&img_player_right[i], wstring_to_utf8(path).c_str());
}
}
// UTF-16 (wstring) 转 UTF-8
std::string wstring_to_utf8(const std::wstring& wstr) {
if (wstr.empty()) return {};
// 先计算需要的字节长度
int len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, nullptr, 0, nullptr, nullptr);
if (len == 0) return {};
std::string utf8_str(len - 1, '\0');
WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &utf8_str[0], len, nullptr, nullptr);
return utf8_str;
}
loadimage()函数是 EasyX 的图片加载函数,要求传入 UTF-8 编码的 C 风格字符串(const char*)。
Windows 系统中,中文 / 特殊字符的路径如果用普通 string 存储容易乱码,所以先用 wstring(UTF-16)拼接路径,再转 UTF-8 传给图片加载函数,避免路径乱码导致图片加载失败。
2.2.2 显示带透明通道的图片
2.2.3 序列帧播放
inline void putimage_alpha(int x, int y, IMAGE* img)
{
int w = img->getwidth();
int h = img->getheight();
AlphaBlend(GetImageHDC(NULL), x, y, w, h,
GetImageHDC(img), 0, 0, w, h, {AC_SRC_OVER, 0, 255, AC_SRC_ALPHA});
}
alphaBlend 函数 (wingdi.h) - Win32 apps | Microsoft Learn
AlphaBlend 函数显示具有透明或半透明像素的位图。
BOOL AlphaBlend(
[in] HDC hdcDest,
[in] int xoriginDest,
[in] int yoriginDest,
[in] int wDest,
[in] int hDest,
[in] HDC hdcSrc,
[in] int xoriginSrc,
[in] int yoriginSrc,
[in] int wSrc,
[in] int hSrc,
[in] BLENDFUNCTION ftn
);
[in] ftn
源位图和目标位图的 alpha 混合函数、要应用于整个源位图的全局 alpha 值,以及源位图的格式信息。 源和目标混合函数目前仅限于AC_SRC_OVER。 请参阅 BLENDFUNCTION 和 EMRALPHABLEND 结构。
typedef struct _BLENDFUNCTION {
BYTE BlendOp;
BYTE BlendFlags;
BYTE SourceConstantAlpha;
BYTE AlphaFormat;
} BLENDFUNCTION, *PBLENDFUNCTION;
BlendOp
源混合操作。 目前,唯一定义的源和目标混合操作是AC_SRC_OVER。 有关详细信息,请参阅以下“备注”部分。
BlendFlags
必须为零。
SourceConstantAlpha
指定要用于整个源位图的 alpha 透明度值。 SourceConstantAlpha 值与源位图中的任何每像素 alpha 值组合在一起。 如果将 SourceConstantAlpha 设置为 0,则假定图像是透明的。 如果只想使用每像素 alpha 值,请将 SourceConstantAlpha 值设置为 255 (不透明) 。
AlphaFormat
此成员控制解释源位图和目标位图的方式。 AlphaFormat 具有以下值。
| 值 | 含义 |
|---|---|
| AC_SRC_ALPHA | 当位图具有 Alpha 通道 (即每像素 alpha) 时,将设置此标志。 请注意,API 使用预乘 alpha,这意味着位图中的红色、绿色和蓝色通道值必须使用 alpha 通道值进行预乘。 例如,如果 alpha 通道值为 x,则红色、绿色和蓝色通道必须在调用之前乘以 x 并除以0xff。 |
EasyX 文档 - GetImageHDC
这个函数用于获取绘图设备句柄(HDC)。
HDC GetImageHDC(IMAGE* pImg = NULL);
参数pImg: 绘图设备指针。如果为 NULL,表示默认的绘图窗口。
函数返回绘图设备句柄(HDC)。
获取到的 HDC 句柄可以用在 Windows GDI 函数中。
每个 IMAGE 对象都有一个 HDC 句柄,可以通过 HDC 句柄实现对该 IMAGE 的 GDI 函数操作。在同一个 IMAGE 设备中,请勿混用 EasyX 绘图函数和 GDI 绘图函数。
HDC(设备上下文句柄)
HDC(Handle to Device Context) 是 Windows 图形设备接口(GDI)中的核心概念,并非结构体,而是一个指向设备上下文(Device Context, DC)的句柄。设备上下文是操作系统维护的绘图环境数据结构,封装了图形设备(如显示器、打印机)的属性(如分辨率、调色板、字体、画笔等),开发者通过 HDC 操作这些属性实现跨设备绘图。
3 控制角色移动
使用bool变量来标记角色移动的方向
bool is_move_up = false;
bool is_move_down = false;
bool is_move_left = false;
bool is_move_right = false;
ExMessage msg;
在主循环中读取键盘输入,按下方向键时,将对应的bool变量设置为true,抬起方向键时将对应的bool变量设置为false。在游戏帧循环中根据bool变量来更新角色的位置。
while (peekmessage(&msg)) {
if (msg.message == WM_KEYDOWN) {
switch (msg.vkcode) {
case VK_UP:
is_move_up = true;
break;
case VK_DOWN:
is_move_down = true;
break;
case VK_LEFT:
is_move_left = true;
break;
case VK_RIGHT:
is_move_right = true;
break;
}
} else if (msg.message==WM_KEYUP) {
switch (msg.vkcode) {
case VK_UP:
is_move_up = false;
break;
case VK_DOWN:
is_move_down = false;
break;
case VK_LEFT:
is_move_left = false;
break;
case VK_RIGHT:
is_move_right = false;
break;
}
}
}
if (is_move_up) { player_pos.y -= PLAYER_SPEED; }
if (is_move_down) { player_pos.y += PLAYER_SPEED; }
if (is_move_left) { player_pos.x -= PLAYER_SPEED; }
if (is_move_right) { player_pos.x += PLAYER_SPEED; }
如果直接在按键信息处理时更新角色的未知会导致两个问题
- 当我们按下按键时,会首先有一个WM_KEYDOWN消息进入到事件队列中,随后保持按下一段时间后,才有连续不断地WM_KEYOWN消息触发。这导致持续按键一小段事件后才开始连续移动。
- WM_KEYDOWN消息的产生与游戏主循环是异步进行的。WM_KEYDOWN触发频率受硬件设备和操作系统影响。每个游戏帧中,事件处理部分处理WM_KEYDOWN消息的数量不同。因此可能出现某些帧角色移动的多,某些帧移动少,导致运动不均匀。
现在的实现方法解决了这两个问题,同时还实现了“左上”这种45度方向的移动。但是还有一些不足,比如角色运动的方向被限制到8个方向上,且没有保证其速度大小恒定。
评论
如果你已登录 GitHub,就可以直接在这里评论。