跳转至

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.jpg
IMAGE 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);


Pasted image 20260217150802.png

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 基本绘图流程

BeginBatchDrawEndBatchDrawFlushBatchDraw 是用来实现批量绘图的:先把所有要画的图形 “暂存” 起来,等全部绘制指令执行完,再一次性把所有图形刷新到屏幕上,从而消除闪烁、提升绘图效率。
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; }

如果直接在按键信息处理时更新角色的未知会导致两个问题

  1. 当我们按下按键时,会首先有一个WM_KEYDOWN消息进入到事件队列中,随后保持按下一段时间后,才有连续不断地WM_KEYOWN消息触发。这导致持续按键一小段事件后才开始连续移动。
  2. WM_KEYDOWN消息的产生与游戏主循环是异步进行的。WM_KEYDOWN触发频率受硬件设备和操作系统影响。每个游戏帧中,事件处理部分处理WM_KEYDOWN消息的数量不同。因此可能出现某些帧角色移动的多,某些帧移动少,导致运动不均匀。
    现在的实现方法解决了这两个问题,同时还实现了“左上”这种45度方向的移动。但是还有一些不足,比如角色运动的方向被限制到8个方向上,且没有保证其速度大小恒定。

评论

如果你已登录 GitHub,就可以直接在这里评论。