从零开始用 Windows C++ 桌面程序制作方舟同人游戏(三)

一、将贴图信息整合到类中

承接上文,在完成地图绘制之后,现在的地图还仅仅只是一块图片而已。我们要将其与地图元素类 MapElement 绑定起来。这里我的想法是,在窗口的显示部分,要有一套完整的贴图用于画面表现。同时也有一套完整、抽象出来的游戏系统,比如发生战斗时,后台要计算攻击伤害,执行一系列函数;而窗口则要显示战斗动画。因此应该画面显示绑定到游戏逻辑中。

(当然,只是自己探索过程中粗浅的理解,或许其实看起来很好笑很不严谨呢?读者随便看看就好)

沿着这个思路,我们对上一章的地图绘制进行修改。注意到在第一章对各个类的定义中,MapElement 类总是要在地面上显示的,因此对这个类指定一个 wchar_t[] 字符串,保存其图片资源文件名.

不过想到地图资源并不会包含特别多不同的材质,所以尝试把所有材质资源命名改为数字。

在这里插入图片描述

这样,MapElement 类中存一个数字就可以表示是哪个文件了,不用再另外开字符数组。

MapElement 类:

#pragma once
#include <Windows.h>
#include <iostream>
class MapElement
{
protected:
	unsigned int priority = 0; // 绘制优先级
	unsigned int px = 0; // 在地图上的x坐标
	unsigned int py = 0; // 在地图上的y坐标
	unsigned int id = 0; // 图片资源的编号 
	const bool walkable = 1; // 能否在其上行走. 对于确定的类型,这一值是确定的.
	const bool interactable = 1; // 走入是否会发生交互
public:

	VOID SetPx(unsigned int x) { px = x; }

	VOID SetPy(unsigned int y) { py = y; }

	VOID SetID(unsigned int d) { id = d; }

	unsigned int GetPx() { return px; }

	unsigned int GetPy() { return py; }

	unsigned int getID() { return id; }
};

现在尝试在源文件中创建 MapElement 对象,并用该函数绘制出地图。

首先,开一个地图元素的全局数组。

MapElement gameMap[12][12]; // 游戏地图

写一个创建地图的函数 Map_Create().

// 前向声明
VOID Map_Create(); // 创建地图
/*...
...*/
// 函数实现
VOID Map_Create()
{
    unsigned int mapIndex[12][12] = {
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
        1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0,
        1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
        1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0,
        1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0,
        0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
        1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0,
        1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1
    };
    for (int i = 0; i < 12; ++i) {
        for (int j = 0; j < 12; ++j) {
            gameMap[i][j].SetPx(i);
            gameMap[i][j].SetPy(j);
            gameMap[i][j].SetID(mapIndex[j][i]);
        }
    }
}

修改 Map_Paint():

VOID Map_Paint(HWND hwnd)
{
    hdc = GetDC(hwnd);
    mdc = CreateCompatibleDC(hdc);
    mMapDC = CreateCompatibleDC(hdc);

    hMap = CreateCompatibleBitmap(hdc, CELL_SIZE * 12, CELL_SIZE * 12);

    SelectObject(mMapDC, hMap);
    
    HBITMAP hTexture[2]; // 材质句柄

    // STEP 0: 地图样式
    Map_Create();
    wchar_t filename[20];
    // STEP 1: 加载材质
    for (int i = 0; i < 2; ++i) {
        wsprintf(filename, L"res/%d.bmp", i);
        hTexture[i] = (HBITMAP)LoadImage(NULL, filename, IMAGE_BITMAP, CELL_SIZE, CELL_SIZE, LR_LOADFROMFILE);
    }

    // STEP 2: 绘制好地图
    for (int i = 0; i < 12; ++i) { // 行
        for (int j = 0; j < 12; ++j) { // 列
            SelectObject(mdc, hTexture[gameMap[i][j].getID()]);
            BitBlt(mMapDC, gameMap[i][j].GetPx() * CELL_SIZE, gameMap[i][j].GetPy() * CELL_SIZE, CELL_SIZE, CELL_SIZE, mdc, 0, 0, SRCCOPY);
        }
    }

    // STEP 3: 将地图贴到窗口里
    SelectObject(mMapDC, hMap);
    BitBlt(hdc, 10, 10, CELL_SIZE * 12, CELL_SIZE * 12, mMapDC, 0, 0, SRCCOPY);

    ReleaseDC(hwnd, hdc);
}

其实只是把原来的一些属性封装到了 MapElement 类中,其余并无改变。

二、干员的创建

到这一步,我们终于要在地图上绘制我们的角色了!

为了让窗口能够响应键盘上的消息,我们对窗口过程函数 WndProc 进行修改,在其 switch 语句中加一个 WM_KEYDOWN 的 case,先尝试添加一个输入 Esc 就关闭程序的条件:

switch (message)
    {
    case WM_KEYDOWN:
        switch (wParam)
        {
        case VK_ESCAPE:
            DestroyWindow(hWnd);
            break;
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
        }
    ...
          

调试开始后,按 ESC 窗口就关闭了。消息响应正常。现在我们再将方向按键加进来:

首先添加全局变量

MCharacter gamePlayer; // 控制角色

别忘记这里的 x 和 y 表示的是行和列。也就是说坐标轴的原点在左上角,x 是纵坐标且向下增加,y 是横坐标且向右增加。

还要注意坐标的范围是 [ 0 , 11 ] [0,11] [0,11],而非 [ 1 , 12 ] [1,12] [1,12].

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    DWORD chara_x = gamePlayer.GetPx(), chara_y = gamePlayer.GetPy();
    switch (message)
    {
    case WM_KEYDOWN:
        switch (wParam)
        {
        case VK_ESCAPE:
            DestroyWindow(hWnd);
            PostQuitMessage(0);
            break;
        case VK_UP:
            if (chara_y > 0)
                gamePlayer.SetPy(chara_y - 1);
            break;
        case VK_DOWN:
            if (chara_y < 11)
                gamePlayer.SetPy(chara_y + 1);
            break;
        case VK_LEFT:
            if (chara_x > 0)
                gamePlayer.SetPx(chara_x - 1);
            break;
        case VK_RIGHT:
            if (chara_x < 11)
                gamePlayer.SetPx(chara_x + 1);
            break;
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
        }
    ...

写好移动之后,还需要将其绘制出来。

但注意,这里人物是绘制在地面之上的,其肯定不会占满整个正方单元格,所以图片是部分透明的。而位图的透明绘制,自然需要遮罩法了。所以我们得制作一个遮罩贴图:

在这里插入图片描述

斯卡蒂的遮罩像素贴图

(当然也是随便做做用于测试的,不是最终方案哈哈)

现在添加一个角色绘制的函数声明:

VOID Chara_Paint(HWND hwnd, MapElement me); // 角色绘制

为了其还能画一些 NPC 和敌人等其他使用透明贴图的地图元素,在参数中加了一个 MapElement 类. 其实现:

VOID Chara_Paint(HWND hwnd, MapElement me)
{
    hdc = GetDC(hwnd);
    mdc = CreateCompatibleDC(hdc);
    
    wchar_t filename[20];
    wsprintf(filename, L"res/%d.bmp", me.getID()); // 贴图编号转为文件名

    // 加载贴图
    hChara = (HBITMAP)LoadImage(NULL, filename, IMAGE_BITMAP, CELL_SIZE * 2, CELL_SIZE, LR_LOADFROMFILE);

    SelectObject(mdc, hChara);
    // 透明遮罩法
    BitBlt(hdc, me.GetPx() * CELL_SIZE, me.GetPy() * CELL_SIZE, CELL_SIZE, CELL_SIZE, mdc, CELL_SIZE, 0, SRCAND);
    BitBlt(hdc, me.GetPx() * CELL_SIZE, me.GetPy() * CELL_SIZE, CELL_SIZE, CELL_SIZE, mdc, 0, 0, SRCPAINT);

    ReleaseDC(hwnd, hdc);
}

注意斯卡蒂的图片编号为 2,我们还需要将全局变量 gamePlayer 的编号指定为 2. 发现现在少了个游戏初始化的函数,加入之。

VOID Game_Init(); // 游戏初始化
...
VOID Game_Init()
{
    // 现在函数中只有这一句话,未来会有更多需要初始化的操作.
    gamePlayer.SetID(2);
}

将这个函数加入到 WinMain() 插入代码的地方去。

在这里插入图片描述

现在执行,发现正确生成了一张带角色的地图:

在这里插入图片描述

可是斯卡蒂好像动不起来……按方向键没有反应。

看代码发现:

在 WM_PAINT 处框架已经预先给出了一个 hdc,然而我没注意到,也加了一个全局变量的 hdc,这里就导致了重名,可能会有一些错误……

在这里插入图片描述

在这里插入图片描述

所以我将 WM_PAINT 中这句预置去掉尝试了一下,发现可以正常移动角色!但是屏幕会出现严重的闪烁,肯定也是不行的。所以没有再改动。

上网搜索才知道,原来只有在:

当第一次创建一个窗口时,当改变窗口的大小时,当把窗口从另一个窗口背后移出时,当最大化或最小化窗口

时,系统才会发送一个 WM_PAINT 命令。也就是说平常是不会长期重绘的。我尝试了一下,果然在移动之后,只有我拖拽一下屏幕,人物才被重新绘制到正确的位置。

那我们如何修改绘图方式以达到在必要的时候重绘,并不会使屏幕闪烁,且资源消耗不过大的目的呢?

重新捡起在《WINDOWS游戏编程之从零开始》学过的——游戏循环

加入两个全局时间变量 tNow 和 tPre:

DWORD tPre = 0, tNow = 0; // 时间

把原来的一系列 Paint 函数全部整合到 Game_Paint() 中去:

VOID Game_Paint(HWND hwnd)
{
    BackGround_Paint(hwnd);
    Map_Paint(hwnd);
    Chara_Paint(hwnd, gamePlayer);
}

这里还得对 Map_Paint() 作修改:之前 Map_Paint() 中调用了 Map_Create(). 但实际上Map_Create() 调用一次就可以了,不用每次循环都 Create一次。所以我把对 Map_Create() 的调用移动到 Game_Init() 中。

修改原来的消息循环:

    while (msg.message != WM_QUIT)
    {
        if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
        {
            TranslateMessage(&msg); // 获取消息
            DispatchMessage(&msg); // 分配消息并响应
        }
        else
        {
            tNow = GetTickCount64();
            if (tNow - tPre >= 100)
                Game_Paint(hWnd);
        }
    }

因为框架把窗口句柄封装到 InitInstance 中去了,为了调用 Game_Paint(hWnd),需要将其暴露出来,所以把 InitInstance() 这个函数去掉,其函数体直接放到 WinMain 中来,效果是一样的。(当然,记得去掉最后一句 return TRUE

因为已经有游戏循环中的 Game_Paint 了,这个时候我们完全可以把 WM_PAINT 弃之不顾,删除掉窗口过程函数中所有绘制函数。

在这里插入图片描述

Game_Paint() 函数中加上一句 tPre = GetTickCount64(),获取绘制时的时间。

VOID Game_Paint(HWND hwnd)
{
    BackGround_Paint(hwnd);
    Map_Paint(hwnd);
    Chara_Paint(hwnd, gamePlayer);
    tPre = GetTickCount64();
}

接着再调试运行,发现画面不闪烁了,斯卡蒂也能正常走路了!

总结

感觉最后实现的地图仍然有一点点闪烁情况。或许可以通过增大绘制间隔或者再增加缓冲区来消除……但缓冲区再增加开销可就大了吧。不过基本上没有影响了。(再测试了一下,好像只有录制的时候闪烁会很明显,自己测试肉眼观察的时候几乎没有闪烁)

总之我的斯卡蒂动起来啦……

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值