使用很简单的 mci 来添加背景音乐:
首先打开 framework.h, 在末尾添加如下代码:
#include#pragma comment (lib, "winmm.lib")
添加一个头文件,并链接一个库。
这样我们就可以使用 mci 播放器了。
在 gameInit() 中添加:
mciSendString(L"open 熏陶.mp3 alias music1", NULL, 0, NULL);
mciSendString(L"play music1", NULL, 0, NULL);
来开始播放。
在窗口过程函数 WndProc 的按键消息 VK_ESCAPE 的 case 下添加:
mciSendString(L"stop music1", NULL, 0, NULL);
mciSendString(L"close music1", NULL, 0, NULL);
来关闭播放器。
现在调试运行,我们就可以听到自己的 bgm 了!
二、进行一个继续优化上一章在之后的调试中又发现一个大问题:每次运行几分钟地图就绘制不出来了,或者背景也没了。仔细观察代码发现,每次重绘时,Game_Paint() 函数都加载了多次 DC,并且也进行了多次释放。这样肯定会导致资源大量浪费。应该把 DC 等句柄的加载与释放从绘图函数中分离出去才对。
因此,我将所有的加载工作都转到 Game_Init() 中去了。Release 函数也转到 按下ESC 的操作里去了。自然 Game_Init() 需要加一个 HWND hwnd 的句柄参数。
因此将 Map_Paint() 中内部变量地图材质句柄 HBITMAP hTexture[2] 也变为全局变量。
HBITMAP hTexture[2]; // 地图材质句柄
因为作了较多改动,故将整个程序代码再展示一遍:
(会不会太长了 qwq)
// PhantomAndCrimsonSolitaire.cpp : 定义应用程序的入口点。
//
#include "framework.h"
#include "PhantomAndCrimsonSolitaire.h"
#define MAX_LOADSTRING 100
#define WINDOW_WIDTH 1280
#define WINDOW_HEIGHT 720
#define CELL_SIZE 50
// 全局变量:
HINSTANCE hInst; // 当前实例
WCHAR szTitle[MAX_LOADSTRING]; // 标题栏文本
WCHAR szWindowClass[MAX_LOADSTRING]; // 主窗口类名
HBITMAP hBackGround; // 背景位图句柄
HBITMAP hMap; // 地图句柄
HBITMAP hChara; // 角色句柄
HBITMAP hTexture[2]; // 地图材质句柄
HDC hdc, mdc; // 设别环境句柄与内存设备环境句柄
HDC mMapDC; // 为地图指定的内存DC
HDC bufDC; // 缓冲DC
MapElement gameMap[12][12]; // 游戏地图
MCharacter gamePlayer; // 控制角色
ULonGLONG tPre = 0, tNow = 0; // 时间
// 此代码模块中包含的函数的前向声明:
ATOM MyRegisterClass(HINSTANCE hInstance);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK about(HWND, UINT, WPARAM, LPARAM);
VOID BackGround_Paint(HWND hwnd); // 菜单背景图
VOID Map_Create(); // 创建地图
VOID Map_Paint(HWND hwnd); // 地图绘制
VOID Chara_Paint(HWND hwnd, MapElement me); // 角色绘制
VOID Game_Init(HWND hwnd); // 游戏初始化
VOID Game_Paint(HWND hwnd); //完整游戏界面的绘制
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// TODO: 在此处放置代码。
// 初始化全局字符串
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_PHANTOMANDCRIMSONSOLITAIRE, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
// 执行应用程序初始化:
hInst = hInstance; // 将实例句柄存储在全局变量中
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, WINDOW_WIDTH, WINDOW_HEIGHT, nullptr, nullptr, hInstance, nullptr);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
Game_Init(hWnd);
HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_PHANTOMANDCRIMSONSOLITAIRE));
MSG msg;
// 主消息循环:
PeekMessage(&msg, NULL, 0, 0, PM_REMOVE); // 初始化
while (msg.message != WM_QUIT)
{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg); // 获取消息
DispatchMessage(&msg); // 分配消息并响应
}
else
{
tNow = GetTickCount64();
if (tNow - tPre >= 60)
Game_Paint(hWnd);
}
}
return (int) msg.wParam;
}
//
// 函数: MyRegisterClass()
//
// 目标: 注册窗口类。
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEXW wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_PHANTOMANDCRIMSONSOLITAIRE));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
return RegisterClassExW(&wcex);
}
//
// 函数: InitInstance(HINSTANCE, int)
//
// 目标: 保存实例句柄并创建主窗口
//
// 注释:
//
// 在此函数中,我们在全局变量中保存实例句柄并
// 创建和显示主程序窗口。
//
//
// 函数: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// 目标: 处理主窗口的消息。
//
// WM_COMMAND - 处理应用程序菜单
// WM_PAINT - 绘制主窗口
// WM_DESTROY - 发送退出消息并返回
//
//
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:
mciSendString(L"stop music1", NULL, 0, NULL);
mciSendString(L"close music1", NULL, 0, NULL);
ReleaseDC(hWnd, hdc);
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);
}
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// 分析菜单选择:
switch (wmId)
{
case IDM_about:
DialogBox(hInst, MAKEINTRESOURCE(IDD_aboutBOX), hWnd, about);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: 在此处添加使用 hdc 的任何绘图代码...
// 之前添加的 Paint 函数全都可以不需要了
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
// “关于”框的消息处理程序。
INT_PTR CALLBACK about(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
switch (message)
{
case WM_INITDIALOG:
return (INT_PTR)TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, LOWORD(wParam));
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}
VOID BackGround_Paint(HWND hwnd)
{
SelectObject(mdc, hBackGround);
BitBlt(hdc, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, mdc, 0, 0, SRCCOPY);
}
VOID Map_Paint(HWND hwnd)
{
// 绘制好地图
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);
}
}
// 将地图贴到窗口里
SelectObject(mMapDC, hMap);
BitBlt(hdc, 0, 0, CELL_SIZE * 12, CELL_SIZE * 12, mMapDC, 0, 0, SRCCOPY);
}
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]);
}
}
}
VOID Chara_Paint(HWND hwnd, MapElement me)
{
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);
}
VOID Game_Init(HWND hwnd)
{
gamePlayer.SetID(2);
hdc = GetDC(hwnd);
mdc = CreateCompatibleDC(hdc);
mMapDC = CreateCompatibleDC(hdc);
hMap = CreateCompatibleBitmap(hdc, CELL_SIZE * 12, CELL_SIZE * 12);
hBackGround = (HBITMAP)LoadImage(NULL, L"主题图_傀影与猩红孤钻.bmp", IMAGE_BITMAP, WINDOW_WIDTH, WINDOW_HEIGHT, LR_LOADFROMFILE);
wchar_t filename[20];
wsprintf(filename, L"res/%d.bmp", gamePlayer.getID()); // 贴图编号转为文件名
// 加载角色贴图
hChara = (HBITMAP)LoadImage(NULL, filename, IMAGE_BITMAP, CELL_SIZE * 2, CELL_SIZE, LR_LOADFROMFILE);
// 加载材质贴图
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);
}
mciSendString(L"open 熏陶.mp3 alias music1", NULL, 0, NULL);
mciSendString(L"play music1", NULL, 0, NULL);
Map_Create();
}
VOID Game_Paint(HWND hwnd)
{
BackGround_Paint(hwnd);
Map_Paint(hwnd);
Chara_Paint(hwnd, gamePlayer);
tPre = GetTickCount64();
}
现在终于彻底没有了闪烁的问题,长久运行也不会黑屏!好耶!
三、实现障碍物现在我想先尝试实现游戏中需要的一大地图元素类:障碍物。其特性也就是不可交互,不可以走入。最简单的想法是在移动时进行条件询问,问可不可以走入,再进行角色移动。为此,在 MapElement 类中新加一个函数 Walkable:
bool Walkable() { return walkable; }
现在,看到之前对于地图的设计:
地面只有贴图的差别,地面所在的单元格可以叠放人物、门、NPC和敌人。通常情况下,地面是最先绘制的。当需要一些隐藏互动时,其也可能在之后被绘制。不论如何,同一个单元格只能叠放两个元素,且其中一个一定是地面。
事实上,设定地面类统一到地图元素类中的目的其实就是为了更方便地确定绘制优先级。
如果需要开一个包含两个图层的单元格数组,总感觉还是多此一举了。地图的地面本质上只是一些贴图而已,没有必要这样,只需要先绘制地面,再绘制其他物体就够用了。所以,绘制优先级的设定这里摒弃了。
添加全局变量:
MapElement gameObjects[12][12]; // 游戏地图物体
为了方便区分材质贴图,非地面的物体统一采用透明遮罩贴图。
像 Map_Create() 里原来做的一样,我们再设置好各个位置的贴图编号。
这里重新定义:编号大于 100 的都为遮罩材质。特别地,编号 100 表示不绘制。
重新将我们的角色斯卡蒂的编号设置为 101,那么将其贴图从 2.bmp 改为 101.bmp.
在 Map_Create() 函数末尾加上:
for (int i = 0; i < 11; ++i) {
for (int j = 0; j < 12; ++j) {
gameObjects[i][j].SetPx(i);
gameObjects[i][j].SetPy(j);
gameObjects[i][j].SetID(100);
}
}
for (int j = 0; j < 12; ++j) {
gameObjects[11][j].SetPx(11);
gameObjects[11][j].SetPy(j);
gameObjects[11][j].SetID(102);
}
先比较随意的画了一张地图:只有最下面一行的编号为 102,其余都为 100,不绘制。这些代码仅用于调试,之后诸如 mapIndex 数组这样的信息都会被移到输入文件中以文件的形式读取。
别忘了更改斯卡蒂的贴图编号。
对了,我们的障碍物贴图 102.bmp:
因为本身是完全不透明的,遮罩贴图的遮罩层自然全黑了。
现在的资源文件夹:
但现在还没有加载位图句柄的操作。我们将地图材质句柄数组开到200。
HBITMAP hTexture[200]; // 地图材质句柄
Game_Init() 中加载材质的操作增加为:
// 加载材质贴图
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);
}
for (int i = 101; i < 103; ++i) {
wsprintf(filename, L"res/%d.bmp", i);
hTexture[i] = (HBITMAP)LoadImage(NULL, filename, IMAGE_BITMAP, CELL_SIZE * 2, CELL_SIZE, LR_LOADFROMFILE);
}
将绘制其他元素的过程添加进 Map_Paint() 函数中:
VOID Map_Paint(HWND hwnd)
{
// 绘制好地图
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);
}
}
for (int i = 0; i < 12; ++i) {
for (int j = 0; j < 12; ++j) {
if (gameObjects[i][j].getID() != 100) {
SelectObject(mdc, hTexture[gameObjects[i][j].getID()]);
BitBlt(mMapDC, gameObjects[i][j].GetPx() * CELL_SIZE, gameObjects[i][j].GetPy() * CELL_SIZE, CELL_SIZE, CELL_SIZE, mdc, 0, 0, SRCCOPY);
}
}
}
// 将地图贴到窗口里
SelectObject(mMapDC, hMap);
BitBlt(hdc, 0, 0, CELL_SIZE * 12, CELL_SIZE * 12, mMapDC, 0, 0, SRCCOPY);
}
现在调试运行,最右边已经出现了一排墙体:
但它们也只是一排贴图而已,并没有阻挡移动的作用。实际上,要实现角色的正确移动,我们应该指定这一行 gameObject 为 MObstacle 类。
如何通过文件读取指定类之后再实现,现在我们先直接将这一行指定为 MObstacle 类。
现在猛然意识到自己把 C++ 当 Java 来写了…… 子类对象不能直接赋值给父类。
于是乎不得不再次进行一次大修改,将 gameMap 和 gameObjects 都改为MapElement 类的对象指针。这一改,我们需要 new 申请空间来为指针创建对象。接着就可以这样来进行多态操作了:
for (int j = 0; j < 12; ++j) {
gameObjects[11][j] = new MObstacle;
gameObjects[11][j]->SetPx(11);
gameObjects[11][j]->SetPy(j);
gameObjects[11][j]->SetID(102);
}
修改角色移动指令:
case VK_UP:
if (chara_y > 0 && gameObjects[chara_x][chara_y - 1]->Walkable())
gamePlayer.SetPy(chara_y - 1);
break;
case VK_DOWN:
if (chara_y < 11 && gameObjects[chara_x][chara_y + 1]->Walkable())
gamePlayer.SetPy(chara_y + 1);
break;
case VK_LEFT:
if (chara_x > 0 && gameObjects[chara_x - 1][chara_y]->Walkable())
gamePlayer.SetPx(chara_x - 1);
break;
case VK_RIGHT:
if (chara_x < 11 && gameObjects[chara_x + 1][chara_y]->Walkable())
gamePlayer.SetPx(chara_x + 1);
break;
紧接着我又遇到了一个因为 C++ 基础不扎实产生的问题!想必对 C++ 面向对象熟练的各位都能猜到了!
子类对象赋值给父类指针时,父类的非虚成员不会改变。所以虽然我 MObstacle 的 walkable 为 0,但父类 MapElement 的 walkable 为 1,且其不会因为被 MObstacle 而覆盖掉这一属性!
《现在猛然意识到自己把 C++ 当 Java 来写了……》
没有办法,只能先改掉原来所想的类的关系了。
去掉 MapElement 类的 walkable 与 Interactable 成员变量的 const 前缀
bool walkable = 1; // 能否在其上行走. bool interactable = 1; // 走入是否会发生交互
将指针类改回来。
MapElement gameMap[12][12]; // 游戏地图 MapElement gameObjects[12][12]; // 游戏地图物体
暂时将所有除 MCharacter 以外的 MapElement 的子类从工程中移除,保存到备份文档中备用。
给 MapElement 新加一个函数用于修改其 walkable 值
VOID setWalkable(bool flag) { walkable = flag; }
修改 Map_Create 里对 gameObjects 的操作:
for (int j = 0; j < 12; ++j) {
gameObjects[11][j].SetPx(11);
gameObjects[11][j].SetPy(j);
gameObjects[11][j].SetID(102);
gameObjects[11][j].setWalkable(0);
}
运行,斯卡蒂果然在墙壁上停下来了。
碎碎念虽然做到这里障碍物终于正常工作了,但却并不是特别高兴,发现了之前设计的严重问题……现在有许多需要重构的地方,把面向对象做好,之后的工作才不至于过于复杂,代码才更加清晰……



