目录
1. 项目简介
涉及知识
主要涉及:
- C++ 基础 - 使用标准库
- 图形编程 - 使用 `graphics.h` 进行图形绘制
- 数据结构 - 使用二维数组表示棋盘
- 事件处理 - 处理鼠标点击事件
- 游戏逻辑 - 检测胜利和平局条件
- 绘制棋子 - 根据棋子类型绘制图形
- 文本绘制 - 显示当前棋子类型
- 时间控制 - 控制帧率
- 批量绘制 - 优化绘图性能
- 消息框 - 显示游戏结果
- 常量定义 - 定义窗口尺寸常量
- 字符集兼容性 - 处理字符串以支持 ANSI 和 Unicode
2. 环境配置
开发工具
- 开发环境:Microsoft Visual Studio Community 2022 版本 17.10.3
- EasyX库:超简单EasyX 全过程不超过一分钟
3. 核心功能
-
函数总结
-
initgraph(); 初始化窗口
HWND initgraph( int width, //宽度 int height, //高度 int flag = NULL //窗口样式 默认NULL );
-
绘图函数仅作了解使用,深入理解请期待后续内容
BeginBatchDraw(); //开始批量绘制
cleardevice(); //清空画布
FlushBatchDraw(); //执行未完成的绘制任务
EndBatchDraw(); //结束批量绘制 -
Exmessage结构体(仅作简单使用,无需深入理解)
struct ExMessage { USHORT message; // 消息标识 union { // 鼠标消息的数据 struct { bool ctrl :1; // Ctrl 键是否按下 bool shift :1; // Shift 键是否按下 bool lbutton :1; // 鼠标左键是否按下 bool mbutton :1; // 鼠标中键是否按下 bool rbutton :1; // 鼠标右键 short x; // 鼠标的 x 坐标 short y; // 鼠标的 y 坐标 short wheel; // 鼠标滚轮滚动值,为 120 的倍数 }; // 按键消息的数据 struct { BYTE vkcode; // 按键的虚拟键码 BYTE scancode; // 按键的扫描码(依赖于 OEM) bool extended :1; // 按键是否是扩展键 bool prevdown :1; // 按键的前一个状态是否按下 }; // 字符消息的数据 TCHAR ch; // 窗口消息的数据 struct { WPARAM wParam; LPARAM lParam; }; }; };
-
circle(); 绘制无填充圆
void circle( //传入参数 int x, //圆心x坐标 int y, //圆心y坐标 int radius //圆半径 );
- line(); 绘制直线
void line( int x1, //起点x值 int y1, //起点y值 int x2, //终点x值 int y2 //终点y值 );
- settextcolor(); 设置文字输出颜色
使用RGB宏来定义颜色void settextcolor(RGB(255,0,0)); //红色
- _T(); 格式化字符串提供Unicode编码支持,提高可移植性
- _stprintf_s(); 将格式化的字符串存入指定区域
- outtextxy(); 指定位置输出字符串
void outtextxy( int x, //x坐标 int y, //y坐标 TCHAR c //输出字符串 );
- GetTickCount(); 检索自系统启动以来已用过的毫秒数
DWORD GetTickCount(); //返回DWORD (无符号双字(32位)整数类型)
- Sleep(); 睡眠函数(暂停当前线程的执行,直到超时间隔结束)
void Sleep( [in] DWORD dwMilliseconds //执行等待,单位毫秒(ms) );
-
库函数
#include <iostream> //标准输入输出库
#include <vector> //STL-Vector容器(动态数组)
#include <graphics.h> //Eaxyx库
-
画布大小
使用#define宏命令
程序编译时将代码中所有内容替换为定义的内容,利于统一管理
#define H 600
#define L 600
//初始化为600*600
-
数据存储
通过二维数组(3*3)存储当前格子内容,并全部初始化为'-'记为空
当前落子类型CurType初始化为O (O先手),以便后续修改
std::vector<std::vector<char>> board_data(3, std::vector<char>(3, '-'));
char CurType = 'O';
-
绘制格子
使用line函数对画布长宽三等分(九宫格)
void DrawBorder() {
line(0, L / 3, H, L / 3); //x轴1/3处
line(0, (L / 3) * 2, H, (L / 3) * 2); //x轴2/3处
line(H / 3, 0, H / 3, L); //y轴1/3处
line((H / 3) * 2, 0, (H / 3) * 2, L); //y轴2/3处
}
//原点位于窗口左上为(0,0)
//向右为x轴正方向
//向下为y轴正方向
-
绘制棋子
遍历矩阵,通过switch判断当前格子的类型,执行特定语句
使用circle();绘制无填充圆 "O"类型
因为矩阵为3*3,但画布为H*L,所以需要计算之间的倍数关系
并且需要计算圆心位置,圆心位置需要加上当前所在格子的一半
因为H==L 所以半径:H/6
(因为代数相乘仅能计算到左上角,所以x,y需要加上H/6,也就是100,才是圆心的位置)
使用line();绘制叉 "X"类型
同上:因为代数相乘仅能计算到左上角,所以画直线就很简单,
仅需要找到坐标:
左上角:(x, y) 连接 右下角:(x + (H / 3), y + (L / 3))
左下角:(x, y + (L / 3)) 连接 右上角:(x + (H / 3), y)
void DrawPieces() {
for (int i = 0; i < 3; ++i) { //遍历
for (int j = 0; j < 3; ++j) {
int x = j * (L / 3); //计算偏移量
int y = i * (H / 3); //计算偏移量
switch (board_data[i][j]) {
case 'O':
circle(x + (H / 6), y + (L / 6), H/6);
break;
case 'X':
line(x, y, x + (H / 3), y + (L / 3));
line(x, y + (L / 3), x + (H / 3), y);
break;
default:
break;
}
}
}
}
-
获胜检测
通过遍历来判断是否出现相同类型连成直线,并且返回一个bool值
// 获胜检测
bool DetectionWin(char c) {
for (int i = 0; i < 3; ++i) {
if ((board_data[i][0] == c && board_data[i][1] == c && board_data[i][2] == c) || //判断行是否出现三子连珠
(board_data[0][i] == c && board_data[1][i] == c && board_data[2][i] == c)) { //判断列是否出现三子连珠
return true;
}
}
//判断对角线
if ((board_data[0][0] == c && board_data[1][1] == c && board_data[2][2] == c) ||
(board_data[0][2] == c && board_data[1][1] == c && board_data[2][0] == c)) {
return true;
}
return false;
}
-
平局检测
通过判断是否还存在空位,来判断是否平局
// 平局检测
bool DetectionDraw() {
for (size_t i = 0; i < 3; ++i) {
for (size_t j = 0; j < 3; ++j) {
if (board_data[i][j] == '-') { //没有获胜,且没有空位的情况下即为平局
return false; //出现空位'-'返回false,未平局
}
}
}
return true;
}
-
绘制提示信息
提示当前落子类型
// 绘制提示信息
void DrawMessage() {
static TCHAR str[64]; //定义静态字符串
_stprintf_s(str, _T("当前棋子类型: %c"), CurType); //格式化并存入str
settextcolor(RGB(255, 175, 45)); //设置文字输出色彩,传入RGB格式
outtextxy(0, 0, str); //输出
}
-
交互设计
通过ExMessage创建存储鼠标信息的实例,并且在主循环中不断地去更新鼠标的位置
通过if判断是否按下,并且将窗口600*600的坐标转换为3*3的坐标
然后进行边界检测,并且判断当前点击格子是否能落子,落子成功后交换棋子类型
//主循环内
while (peekmessage(&msg)) { //更新交互信息循环
if (msg.message == WM_LBUTTONDOWN) { //判断左键是否按下
int x = msg.x; //鼠标按下的x坐标
int y = msg.y; //鼠标按下的y坐标
int index_x = x / (H / 3); //将窗口x坐标转换为3*3矩阵坐标
int index_y = y / (L / 3); //将窗口y坐标转换为3*3矩阵坐标
if (index_x >= 0 && index_x < 3 && index_y >= 0 && index_y < 3) { //边界检测
if (board_data[index_y][index_x] == '-') { //判断格子是否为空
board_data[index_y][index_x] = CurType;
CurType = (CurType == 'O') ? 'X' : 'O'; //落子后交换'O'与'X'
}
}
}
-
帧速率控制
通过在游戏开始获取一次时间(ms),并在画面渲染结束时再次获取(ms)
计算差值,通过判断,限制在60帧以内,避免循环导致高占用
//主循环
while (running) {
DWORD start_time = GetTickCount(); //循环开始获取(ms)
//---------------------循环主体---------------------
//主循环内
DWORD end_time = GetTickCount(); //循环结束获取(ms)
DWORD delta_time = end_time - start_time; //计算循环所耗时间
if (delta_time < 1000 / 60) { //通过判断一次循环的时间与60帧的速率
Sleep(1000 / 60 - delta_time); //小于60帧的时间,则说明比60帧所耗时间短,则计算差值,并等待
}
}
-
主循环
设置一个主循环是否继续进行的bool值running初始化为true
并且设置为主循环判断条件
判断获胜与平局的状态
帧速率的控制
while (running) {
DWORD start_time = GetTickCount();
while (peekmessage(&msg)) {
if (msg.message == WM_LBUTTONDOWN) {
int x = msg.x;
int y = msg.y;
int index_x = x / (H / 3);
int index_y = y / (L / 3);
if (index_x >= 0 && index_x < 3 && index_y >= 0 && index_y < 3) {
if (board_data[index_y][index_x] == '-') {
board_data[index_y][index_x] = CurType;
CurType = (CurType == 'O') ? 'X' : 'O';
}
}
}
}
cleardevice();
DrawBorder(); //绘制格子
DrawPieces(); //绘制棋子
DrawMessage(); //绘制提示信息
FlushBatchDraw();
//获胜以及平局检测
if (DetectionWin('X')) {
MessageBox(GetHWnd(), _T("X 玩家获胜"), _T("游戏结束"), MB_OK);
running = false; //修改循环条件
}
else if (DetectionWin('O')) {
MessageBox(GetHWnd(), _T("O 玩家获胜"), _T("游戏结束"), MB_OK);
running = false; //修改循环条件
}
else if (DetectionDraw()) {
MessageBox(GetHWnd(), _T("平局"), _T("游戏结束"), MB_OK);
running = false; //修改循环条件
}
//帧速率控制
DWORD end_time = GetTickCount();
DWORD delta_time = end_time - start_time;
if (delta_time < 1000 / 60) {
Sleep(1000 / 60 - delta_time);
}
}
5. 项目总结
学习收获
通过简单的项目了解Eaxyx库的简单使用
简单了解游戏与现实输入设备的交互
窗口与矩阵方向不同
未来改进方向
可以通过后续学习构建简单的本地二人联机
如有错误之处,还请指出,不要留情,感谢支持!
源代码
关注公众号:ComPute发现