目录
项目目录结构
创建项目
在Visual Studio 2022中创建一个空白的项目,起名为:NumberMemoryGame。
命名规范
常规的变量、函数以及文件名等命名规范不再赘述,这里主要说一下项目中变量和函数的命名规范。
由于C语言没有命名空间的概念,所以在定义变量、函数的时候要避免与系统、第三方类库以及自身项目里的变量、函数重名,否则会出现难以预料的bug,本项目采取了增加前缀的措施来确保不会重名。
前缀构成:项目名首字母+文件名+表意函数名,中间用下划线连接,比如:NMG_view_init()。
目录结构
根据上一篇文章中提到的模块拆分,我们建立具体的源文件、头文件与之对应。
下面简单介绍一下每个文件的作用:
-
main.cpp是项目的入口文件,主函数main()就定义在这里
-
两个头文件:config.h定义一些常量,models.h定义项目里用到的数据结构模型
-
data.cpp,文件读写模块,就是项目里用到的文件数据库
-
game.cpp,游戏模块,控制整个游戏进程和交互
-
rank.cpp,排行榜模块,排行榜的展示以及更新
-
settings.cpp,设置模块,游戏设置中心
-
timer.cpp,计时器模块,计时和展示,作为排行榜的依据
-
views.cpp,窗口显示模块,控制整个项目的UI初始化、显示、更新等
编程模式
MVC
以上文件目录结构创建好后,我们就往面向对象的编程思路上去靠拢,首选是常用的MVC模式:
-
Model,定义在models.h中,管理所有抽象化对象的数据结构模型
-
View,视图,相当于浏览器,只管展示视图,尽量不要参与业务逻辑的编码
-
Controller,控制器,控制视图的数据展示以及用户和视图交互背后的逻辑处理
笔者GUI开发经验不多,再加上C语言自身的一些问题,很难去按照严格意义上的MVC模式去开发,但是我们的编程思想要向MVC看齐。
如果不理清关系、把逻辑分层、模块拆分开来,直接上手开发的话肯定会无从下手,而且过程会出现难以控制的bug,日后维护也很困难。
实例
下面来简单看一个实例,我们来实现排行榜这个模块的功能。
首先定义好数据模型,前期先加两个字段,后期实际开发中再去扩展。
/*******************************************************
* 项目中的数据模型
* 数据模型前缀:NMG_MODEL_
*
* Author: Lunixy
* Date: 2022-10-22
*******************************************************/
/*
* 排行榜数据模型
*/
//struct NMG_model_rank {
// char nickername[20];
// int take_time;
//};
// 使用typedef来定义,使用时更加便捷
typedef struct{
char nickername[20];
int take_time;
} NMG_MODEL_RANK;
/*
* 游戏相关数据模型
*/
// 棋盘格子
typedef struct{
int left;
int top;
int right;
int bottom;
int width;
int height;
} NMG_MODEL_CELL;
/*
* 设置选项数据模型
*/
typedef struct{
int game_sound; // 游戏音效开关
int bgm_sound; // 背景音乐开关
char *bg_image_src; // 背景图地址
} NMG_MODEL_SETTINGS;
// 设置项
typedef enum {
nmg_settings_game_sound,
nmg_settings_bgm_sound,
nmg_settings_bg_image_src
} NMG_MODEL_SETTINGS_OPTION;
// 设置项的值有多种数据类型,采用union构造类型
typedef union{
int int_value; // 整数类型的值
char *str_value; // 字符串类型的值
} NMG_MODEL_SETTINGS_VALUE;
/*
* 语言种类
*/
typedef struct{
char name[30];
int code;
} NMG_MODEL_SETTINGS_LANGUAGE_TYPE;
然后,在控制器中实现业务逻辑——做底层数据和外层视图的桥梁。排行榜模块功能很简单,前期就做两个功能:展示和更新。
/*******************************************************
* 排行榜模块函数
* 函数前缀:NMG_rank_
*
* Author: Lunixy
* Date: 2022-10-22
*******************************************************/
#include <stdlib.h>
#include "models.h"
extern NMG_MODEL_RANK *NMG_data_rank_query();
extern int NMG_data_rank_update(NMG_MODEL_RANK *list);
/*
* 读取文件 && 获取排行榜TOP10
*
* 返回排行榜数组指针
*/
NMG_MODEL_RANK *NMG_rank_top_10() {
return NMG_data_rank_query();
}
/*
* 排行榜插入一条数据 && 写入文件
*
* 返回新的排行榜数组指针
*/
NMG_MODEL_RANK *NMG_rank_insert(NMG_MODEL_RANK rank) {
NMG_MODEL_RANK *list = (NMG_MODEL_RANK *) calloc(sizeof(NMG_MODEL_RANK), 10);
list = NMG_rank_top_10();
// todo: insert && update
int res = NMG_data_rank_update(list);
if (res){
return list;
}
return nullptr;
}
不过,在控制器中,并没有直接去数据库(当前项目的文件系统)去直接读写数据,而是又加了一个文件data.cpp专门去读写数据库,这样做能让底层的数据分离更加彻底,使得程序逻辑更加清晰。
/*******************************************************
* 文件读写模块函数
* 函数前缀:NMG_data_
*
* Author: Lunixy
* Date: 2022-10-22
*******************************************************/
#include <stdlib.h>
#include "config.h"
#include "models.h"
/*
* 排行榜数据读写
*/
// 读取排行榜
NMG_MODEL_RANK *NMG_data_rank_query() {
NMG_MODEL_RANK *list = (NMG_MODEL_RANK *) calloc(sizeof(NMG_MODEL_RANK), 10);
// todo: use fread to query
return list;
}
// 更新排行榜
int NMG_data_rank_update(NMG_MODEL_RANK *list) {
// todo: use fwrite to update
return 1;
}
/*
* 用户设置数据读写
*/
// 读取用户设置
NMG_MODEL_SETTINGS NMG_data_settings_query() {
NMG_MODEL_SETTINGS settings = {};
// todo: use fread to query
return settings;
}
// 更新用户设置
int NMG_data_settings_update(NMG_MODEL_SETTINGS settings) {
// todo: use fwrite to update
return 1;
}
注意到,这里有些函数只是做了定义(输入和输出),这点上一篇文章提到过:先把逻辑走通,回头再完善具体的功能性的函数。
初步的窗口布局
效果
由于要整理开发教程的步骤,所以整体进度上受点影响,今天先把初步的窗口布局实现以下,效果如下。
左侧就是游戏区了,目前已经划分好了棋盘,接下来就是把数字随机到棋盘格子里去,然后再接收玩家的鼠标点击事件,挑战成功则开启下一关,直至全部通关,基本上游戏的闭环就完成了。
右侧是功能区,主要是计时、排行榜、功能设置三块。
EasyX的使用
咱们项目的重点是通过EasyX来完成GUI开发,下面就来简单看一下EasyX的使用方法。
/*******************************************************
* 项目主函数文件
*
* Author: Lunixy
* Date: 2022-10-22
*******************************************************/
#include <stdio.h>
#include "config.h"
#include "models.h"
extern void NMG_game_init();
// 游戏区域所有的格子
NMG_MODEL_CELL cell_list[NMG_CFG_CELL_COUNT];
int main(int argc, char *argv[])
{
printf("This is my first gui project.\n");
NMG_game_init();
for (int i = 0; i < NMG_CFG_CELL_COUNT; i++){
printf("left:%5d top:%5d right:%5d bottom%5d\n",
cell_list[i].left, cell_list[i].top,
cell_list[i].right, cell_list[i].bottom);
}
return 0;
}
在主函数里调用,NMG_game_init()游戏初始化函数,然后在NMG_game_init()里调用NMG_view_init()视图初始化函数。
/*******************************************************
* 游戏模块函数
* 函数前缀:NMG_game_
*
* Author: Lunixy
* Date: 2022-10-22
*******************************************************/
extern void NMG_view_init();
// 游戏初始化
void NMG_game_init() {
NMG_view_init();
// todo: 第一次定义一个数字随机显示在格子上
}
至于为何要嵌套一层调用,这个就是我们上面提到的要使用MVC模式编程。这里的函数还有要完善的地方,到时候代码会变多,就能显现出MVC的优点了。
/*******************************************************
* 窗口UI绘制
* 函数前缀:NMG_view_
*
* Author: Lunixy
* Date: 2022-10-22
*******************************************************/
#include <easyx.h>
#include <conio.h>
#include "config.h"
#include "models.h"
extern NMG_MODEL_CELL cell_list[NMG_CFG_CELL_COUNT];
static void _NMG_view_left_init(int width, int height);
// 窗口初始化
void NMG_view_init() {
initgraph(NMG_CFG_WINDOW_WIDTH, NMG_CFG_WINDOW_HEIGHT, EX_SHOWCONSOLE);
cleardevice();
int game_area_width = NMG_CFG_WINDOW_HEIGHT;
// 左侧游戏区域
_NMG_view_left_init(game_area_width, NMG_CFG_WINDOW_HEIGHT);
// 右侧功能区域
setfillcolor(BROWN);
fillrectangle(game_area_width, 0, NMG_CFG_WINDOW_WIDTH, NMG_CFG_WINDOW_HEIGHT);
_getch(); // 按任意键继续
closegraph(); // 关闭绘图窗口
}
// 左侧游戏区域初始化:填满小格子
static void _NMG_view_left_init(int width, int height) {
setfillcolor(YELLOW);
fillrectangle(0, 0, width, height);
int size_w = width / NMG_CFG_CELL_COLUMN_COUNT;
int size_h = height / NMG_CFG_CELL_ROW_COUNT;
for (int i = 0; i < NMG_CFG_CELL_ROW_COUNT; i++){
for (int j = 0; j < NMG_CFG_CELL_COLUMN_COUNT; j++){
setfillcolor((i + j) % 2 == 0 ? WHITE : BLACK);
fillrectangle(j * size_w, i * size_h, (j + 1) * size_w, (i + 1) * size_h);
// 初始化所有格子的数据
cell_list[i * NMG_CFG_CELL_ROW_COUNT + j] = { j * size_w, i * size_h, (j + 1) * size_w, (i + 1) * size_h, size_h, size_w };
}
}
}
在视图里简单实现一下窗口布局,EasyX主要就是在这里发挥它的作用的。
总结
OK,到这一步我们游戏开发的基本框架算是搭建好了,接下来需要开发游戏的核心玩法了。
如果你对C语言的GUI编程感兴趣的话,请持续关注本系列文章,有建议意见欢迎留言或私信。
PS:需要源码的话请留言,人数多的话我就把项目推到GitHub上去供大家参考。