前言
阅读本博客的前置技能:
C++ 基础
好像没了 qwq
为什么开这个坑
很惭愧,虽然每每想要认真的做一个游戏 demo 时,我要么只是停留在了纸面描述上,要么只是建了几个新类,用一个又一个框架/引擎新建工程,做了一些很简单的东西,却从来没有做出来过一个完整的游戏(控制台游戏除外哈哈)。所以乘着对 Windows 桌面程序学习的势头,在此再开一坑,希望不要再浅尝辄止,做出一个能玩的完整 demo. 并同时把开发过程完完整整地记录下来,并将疑惑不解或者是有理解的地方讲出来,加深印象,也帮助他人. (虽然看得人不会很多,我的博客读起来估计也不会那么舒服,毕竟咱也不会写书)
我的想法
在开始代码工程之前,我们应当明确,我们是要做一个什么类型的游戏。接着,使用面向对象的思想,构思一下这个游戏,有哪些类,有哪些接口,怎么定义继承关系,来尽可能地减少工程复杂度,使要做的游戏更加清晰。最好是写设计文档,将游戏的雏形描绘在设计上,切忌一上来便新建文件夹。马克思说:“最蹩脚的建筑师从一开始就比最灵巧的蜜蜂高明的地方,是他在用蜂蜡建筑蜂房之前已经在自己头脑中把它建成了。”(当然,该系列博客中适当略去了前期工作,重点是如何用代码真正实现,做一个完完整整的游戏)
例如,在这里我想实现的是一个类似魔塔的 2D RPG 游戏,希望做出一部 2D RPG 的知名手游《明日方舟》的同人单机游戏。与魔塔不同的是,我希望能够在游戏中增加一些 Rougelike 元素,提高游戏的可玩性。至于具体设计嘛,设计文档中有一些描述,这里当然略过啦!
那么,让我们上手开发出一个属于自己的 Windows 游戏吧!
一、新建工程
使用的集成开发环境:Visual Studio 2019 Community (安装了c++开发环境)
打开VS,创建一个 Windows 桌面应用程序工程。
取好项目名称,选好目录。我的项目名称是 “PhantomAndCrimsonSolitaire”(傀影与猩红狐钻).
创建完成后,我们发现工程下多了好多好多文件。如果之前接触过 Windows 桌面程序,那对这些一定是不陌生的。
查看源文件中的 PhantomAndCrimsonSolitaire.cpp,这是整个工程最重要的一段代码,是应用程序的入口点:
其代码如下:
// PhantomAndCrimsonSolitaire.cpp : 定义应用程序的入口点。
//
#include "framework.h"
#include "PhantomAndCrimsonSolitaire.h"
#define MAX_LOADSTRING 100
// 全局变量:
HINSTANCE hInst; // 当前实例
WCHAR szTitle[MAX_LOADSTRING]; // 标题栏文本
WCHAR szWindowClass[MAX_LOADSTRING]; // 主窗口类名
// 此代码模块中包含的函数的前向声明:
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);
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);
// 执行应用程序初始化:
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_PHANTOMANDCRIMSONSOLITAIRE));
MSG msg;
// 主消息循环:
while (GetMessage(&msg, nullptr, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
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 = MAKEINTRESOURCEW(IDC_PHANTOMANDCRIMSONSOLITAIRE);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
return RegisterClassExW(&wcex);
}
//
// 函数: InitInstance(HINSTANCE, int)
//
// 目标: 保存实例句柄并创建主窗口
//
// 注释:
//
// 在此函数中,我们在全局变量中保存实例句柄并
// 创建和显示主程序窗口。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance; // 将实例句柄存储在全局变量中
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
//
// 函数: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// 目标: 处理主窗口的消息。
//
// WM_COMMAND - 处理应用程序菜单
// WM_PAINT - 绘制主窗口
// WM_DESTROY - 发送退出消息并返回
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
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 的任何绘图代码...
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;
}
怎么去理解这个 cpp 呢?或许可以去看一看《Windows 游戏编程之从零开始》前九章的内容(主要是前四章),也可以上微软官方文档学习:演练:使用 C++ Windows创建 (桌面) | Microsoft Docs
现在进行修改。
- 多加两个宏
WINDOW_WIDTH
和WINDOW_HEIGHT
来方便地修改窗口大小以及一些图片的显示参数。
- 我不需要窗口菜单。或者我们以后需要可以再加上,现在开一个没有实际内容的空菜单是没有意义的。窗口的注册被封装在了函数
MyRegisterClass()
中,查看,将窗口类的 lpszMenuName 修改为 NULL,这样就不会有窗口菜单出现了。
- 新建一个 hdc 绘制背景图片的函数并实现。
```cpp
VOID BackGround_Paint(HWND hwnd)
{
hdc = GetDC(hwnd);
hBackGround = (HBITMAP)LoadImage(NULL, L"主题图_傀影与猩红孤钻.bmp", IMAGE_BITMAP, WINDOW_WIDTH, WINDOW_HEIGHT, LR_LOADFROMFILE);
mdc = CreateCompatibleDC(hdc);
SelectObject(mdc, hBackGround);
BitBlt(hdc, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, mdc, 0, 0, SRCCOPY);
}
```
进行调用:
最后,再修改一下工程里的 ico 文件,尝试调试运行:
二、创建类
一个基础的窗体已经创建好了,现在我们需要根据设定把各个类的原型创建出来。
在资源管理器中选择添加 -> 新建项 -> 新建 C++ 类,系统就会自动创建其头文件和实现的 .cpp 文件.
我关于游戏中类的设定(摘自我自己的游戏设定):
干员类
每个干员的固定属性有:
生命值
防御力
法术抗性
攻击力
攻击类型
生命值成长
防御力成长
攻击力成长
技能接口动态属性有:
当前等级
当前生命值每个干员有4个等级,只可以通过晋升的方法来增加等级(没有经验值系统哦亲)。低星级的干员升级获得的收益更高,但是等级上限更低。
敌人类
敌人属性有:
生命值
防御力
法术抗性
战利品等级
根据战利品等级在战斗结束后会发放不同的奖励。
地图元素
地图(单元格)
地图用于承载地图元素,地图元素包括:地面、掉落物、陷阱、人物、障碍物、门、NPC和敌人. 每层地图都是由12x12单元格构成的.
地面
地面只有贴图的差别,地面所在的单元格可以叠放人物、门、NPC和敌人。通常情况下,地面是最先绘制的。当需要一些隐藏互动时,其也可能在之后被绘制。不论如何,同一个单元格只能叠放两个元素,且其中一个一定是地面。
事实上,设定地面类统一到地图元素类中的目的其实就是为了更方便地确定绘制优先级。
或许还可以为地图样式增彩吧。
掉落物
掉落物可以也仅可以被人物拾取。 拾取后进行交互。交互后,人物移动到掉落物所在单元格,且掉落物将消失。
交互应当为可以重写的接口,广泛使用。
障碍物
地图中不可通过的部分。和地面一样,也只有贴图的差别。只不过其上不可以叠放任何东西。
门
门实际上应当继承自障碍物。其继承了交互接口:开门方法。
交互后将消失。
陷阱
陷阱有可能会有先于地面绘制的情况。当然也可以直接显示出来。总之陷阱在人物通过时总是会进行交互。
交互后将消失。
人物
人物是交互的重要主体。交互不论如何重写,总是要和人物交互的。人物元素总是将当前出战的干员绘制在其所属的单元格上。
当交互后返回了人物死亡信息时游戏将结束;
返回了干员倒地信息时强制更换干员;
…
NPC与敌人
NPC和敌人都可以进行交互。但是交互后不会立刻消失,而需要更多的判定。敌人的属性细节见敌人类。
地图生成
理想的方法是使用随机种子生成地图。但在尚不清楚方法的情况下肯定是需要做一些固定地图来测试的。
边栏
SideBar需要存储相当的信息。
它存储游戏进行的实时信息,
干员库
干员库作为一个堆栈实现。其中要动态保存各种干员信息。干员库需要实时展示在窗口上。当干员库为空,上场干员也为NULL时,对于交互返回人物死亡信息。如果只有上场干员为NULL,返回干员倒地信息。
上场干员
无需多说.
图鉴(待定)
图鉴在测试模式中无可厚非,不用也可以测试。其实现过程中的难点无非是如何确定安装游戏后第一次进行某种操作,以此解锁图鉴。因此待定。
银行
银行中保存你当前的源石锭和部署费用。
例如我要写一个 “边栏” 类的原型,在此之前我已经写了角色类 Character 及其子类干员类 COperator的原型。那么我,创建一个名为 SideBar 的类,并将干员类 COperator 的头文件 Coperator.h 包含进 SideBar 类的头文件。并给出干员库、上场干员、源石锭和部署费用的定义:
#pragma once
#include <vector>
#include "Coperator.h"
class SideBar
{
std::vector<COperator> operators; // 当前干员
COperator currentOperator; // 当前出战的干员
int ingot; // 源石锭
int cost; // 部署费用
};
其他类的头文件:
角色类 Character.h
#pragma once
enum AttackType
{
Physical,
Magic
};
class Character
{
protected:
double healthPoint; // 生命值
double defensePoint; // 防御力
double magicResisitance; // 法抗
double attackDamage; // 攻击力
AttackType attackType; // 攻击类型
double Attack(Character& another); // 一次攻击响应的函数
};
敌人类 CEnemy.h
#pragma once
#include "Character.h"
class CEnemy :
public Character
{
unsigned int bootyLevel; // 战利品等级
};
干员类 COperator.h
#pragma once
#include "Character.h"
class COperator :
public Character
{
double healthPointGrow; // 生命值成长
double defensePointGrow; // 防御力成长
double attackDamageGrow; // 攻击力成长
double healthPointNow; // 当前生命
double levelNow; // 当前等级
void (*Ability)(int, double&); // 技能函数指针
void LevelUp(); // 升级
void UseAbility(); // 使用技能
};
地图元素类(基类)MapElement.h
#pragma once
class MapElement
{
protected:
unsigned int priority; // 绘制优先级
unsigned int px; // 在地图上的x坐标
unsigned int py; // 在地图上的y坐标
const bool walkable = 1; // 能否在其上行走. 对于确定的类型,这一值是确定的.
const bool interactable = 1; // 走入是否会发生交互
};
地图元素干员类 MCharacter.h
#pragma once
#include "MapElement.h"
class MCharacter :
public MapElement
{
};
地图元素门类 MDoor.h
#pragma once
#include "MapElement.h"
class MDoor :
public MapElement
{
const bool walkable = 0;
const bool interactable = 1;
};
地图元素掉落物类 MDrop.h
#pragma once
#include "MapElement.h"
class MDrop :
public MapElement
{
const bool walkable = 1;
const bool interactable = 1;
};
地图元素地面类 MGround.h
#pragma once
#include "MapElement.h"
class MGround :
public MapElement
{
const bool interactable = 0;
const bool walkable = 1;
};
地图元素 NPC 类 MNonPlayerCharacter.h
#pragma once
#include "MapElement.h"
class MNonPlayerCharacter :
public MapElement
{
const bool walkable = 0;
const bool interactable = 1;
};
地图元素障碍物类 MObstacle.h
#pragma once
#include "MapElement.h"
class MObstacle :
public MapElement
{
const bool walkable = 0;
const bool interactable = 0;
};
小憩
到这里,我的游戏制作完成了第一步:新建文件夹 QAQ
下一次更新要实现的,是地图的显示。尝试让我们 12x12 的地图完整地显示在窗口中。
有任何问题都可以问我捏。: )