C语言实现推箱子 (数据库连接+项目打包发布)

C语言实现推箱子 (数据库连接+项目打包发布)_第1张图片

    小时候都玩过推箱子这个经典的小游戏,它伴随着我们的童年,带给我们了许多的乐趣。今天呢小编为大家分享一下使用C语言来完成推箱子游戏。以及如何将游戏与数据库连接和项目的打包发布。

    下面是完成之后的效果展示:

①项目打包后我们会获得一个安装程序,点击安装程序就能将推箱子游戏安装在电脑上。这时候也可以将安装程序发给朋友安装试玩(因为时间原因没有做登录页面)

 ②游戏内效果,一共设计了四种地图,感兴趣的朋友可以继续在自己的数据库中添加地图。

​下面我按照 推箱子游戏实现 → 数据库连接 → 程序打包 的路线分享本次项目 

一、推箱子游戏源码(可直接拷贝运行)

游戏图片下载(将图片与代码放在同一文件夹下)。图片下载https://download.csdn.net/download/qq_54169998/81551789这里为分享的源码是以文档做数据结构。下面我会介绍与MySQL相连

#include
#include
#include
#include
#include	
#include
#include
using namespace std;

#define LINE 48
#define COLUMN 48

#define RATIO 40
#define SCREEN_WIDTH 740
#define SCREEN_HEIGHT 500

#define START_X 50
#define START_Y 75

#define KEY_UP 'W'
#define KEY_LEFT 'A'
#define KEY_RIGHT 'D'
#define KEY_DOWN 'S'
#define KEY_OUT 'Q'
#define GAME_AGAIN 'R'

#define MAX_RETRY_TIMES 4
#define BG_IMAGE -1
#define isValid(next_pos) next_pos.x > 0 && next_pos.x < LINE && next_pos.y>0 && next_pos.y < COLUMN

#define USERS "users.txt"
#define LEVELS "levels.txt"
#define ROUND 4

typedef enum  _PROPS {
	WALL,	//墙
	FLOOR,	//地板
	BOX_DES,//箱子目的地
	MAN,	//小人
	BOX,	//箱子
	HIT,	//箱子命中目标
	MAN_DES,	//人站在目标上
	VECTOR	//通关图片
}PROPS;

typedef enum  _DIRECTION {
	UP,
	DOWN,
	LEFT,
	RIGHT
}DIRECTION;

typedef struct _POS {
	int x;	//小人所在二维数组的行数
	int y;	//小人所在二维数组的列数
}POS;

typedef struct _userinfo {
	//这里将字符串初始为空
	_userinfo() :id(0), username(""), password(""), level_id(0) {};
	_userinfo(int _id, string _username, string _password, int _level_id) :id(_id), username(_username), password(_password), level_id(_level_id) {};
	int id;	// 用户id
	string username;
	string password;	// 密码
	int level_id;
}userinfo;

typedef struct _levelinfo {
	_levelinfo() :id(0), name(""), map_row(0), map_column(0), map_data(""), next_level(0) {};
	_levelinfo(int _id, string _name, int _map_row, int _map_column, string _map_data, int _next_level) :id(_id), name(_name), map_row(_map_row), map_column(_map_column), map_data(_map_data), next_level(_next_level) {};
	int id;	// 关卡id
	string name;	// 关卡名字
	int map_row;	// 地图行数
	int map_column;	//地图列数
	string map_data;	// 二维地图数据
	int next_level;	//下一关卡id
}levelinfo;

levelinfo game_data[4];
struct _POS man;
IMAGE images[9];
int map[LINE][COLUMN] = { 0 };

/******************************
 * 功能:数据重置,当数据文档错乱时可调用此函数一次将数据重装
 * 输入:
 *		无
 *
 * 返回值:
 *		无
 *****************************/
void resetting_data() {
	levelinfo levels[ROUND] = { {1,"小试牛刀→第一关",8,10,"-1,0,0,0,0,0,0,0,-1,-1|-1,0,2,2,2,2,1,0,-1,-1|0,0,0,2,2,2,4,0,0,0|0,1,1,4,0,4,1,4,1,0|0,1,4,4,1,1,0,4,1,0|0,1,1,1,3,0,1,1,1,0|0,0,0,0,1,1,1,0,0,0,|-1,-1,-1,0,0,0,0,0,-1,-1",2},
							{2,"绝地强者→第二关",6,6,"0,0,0,0,0,0|0,2,2,1,1,0|0,4,4,1,3,0|0,2,1,4,0,0|0,1,1,1,0,-1|0,0,0,0,0,-1",3},
							{3,"天外强人→第三关",8,10,"-1,-1,0,0,0,0,0,0,-1,-1|0,0,0,1,1,1,0,0,0,0|0,1,1,1,4,1,4,1,1,0|0,1,4,1,1,1,4,1,3,0|0,0,0,4,4,0,0,0,0,0,|-1,-1,0,1,1,2,2,0,-1,-1|-1,-1,0,2,2,2,2,0,-1,-1|-1,-1,0,0,0,0,0,0,-1,-1",4},
							{4,"死而复生→BOSS关",8,8,"-1,0,0,0,0,0,0,-1|-1,0,2,1,2,2,0,-1|-1,0,2,1,4,2,0,-1|0,0,0,1,1,4,0,0|0,1,4,1,1,4,1,0|0,1,0,4,0,0,1,0|0,1,1,1,3,1,1,0|0,0,0,0,0,0,0,0",0}
	};
	ofstream ofs;
	ofs.open(LEVELS, ios::out);
	for (int i = 0; i < ROUND; ++i) {
		stringstream str_stream;
		str_stream << levels[i].id << " " << levels[i].name << " " << levels[i].map_row << " " << levels[i].map_column << " " << levels[i].map_data << " " << levels[i].next_level << endl;
		ofs << str_stream.str();
	}
	ofs.close();
	ofs.open(USERS, ios::out);
	userinfo user = { 1,"Jack","123456" ,1 };
	stringstream str;
	str << user.id << " " << user.username << " " << user.password << " " << user.level_id;
	ofs << str.str();
	ofs.close();
}

/******************************
 * 功能:地图数据加载
 * 输入:
 *		无
 *
 * 返回值:
 *		无
 *****************************/
void load_data() {
	ifstream ifs;
	ifs.open(LEVELS);
	int index = 0;
	while (index < ROUND) {
		string line;
		getline(ifs, line);
		//cout << line << endl;
		int _id;
		char _name[64];
		int _map_row;
		int _map_column;
		char _map_data[2304];
		int _next_level_id;
		levelinfo info;
		sscanf_s(line.c_str(), "%d %s %d %d %s %d", &_id, _name, (unsigned int)sizeof(_name), &_map_row, &_map_column, _map_data, (unsigned int)sizeof(_map_data), &_next_level_id);
		game_data[index] = { _id,_name,_map_row,_map_column,_map_data,_next_level_id };
		index++;
	}
}


/******************************
 *功能:通过用户名和密码获取用户信息
 * 输入:
 *		user - 用户信息结构体
 *
 * 返回值:
 *		获取成功返回true,失败false
 *****************************/
bool fetch_user_info(userinfo& user) {
	ifstream ifs;
	ifs.open(USERS);
	if (!ifs) {
		return false;
	}
	string line;
	while (1) {
		getline(ifs, line);
		int _id;
		char _username[64];
		char _password[64];
		int _level_id;
		sscanf_s(line.c_str(), "%d%s%s%d", &_id, _username, (unsigned int)sizeof(_username), _password, (unsigned int)sizeof(_password), &_level_id);
		if (_username == user.username && _password == user.password) {
			user.id = _id;
			user.level_id = _level_id;
			return true;
		}
		if (ifs.eof()) {
			break;
		}
	}
	return false;
}

/*********************************
 *功能:根据关卡id获取完整的关卡信息(如:地图,下一关等)
 * 输入:
 *		level - 保存关卡信息的结构体
 *		level_id - 获取关卡id
 * 返回值:
 *		0  - 查找结果为空(用户已通关)
 *		1 - 查找成功
 *********************************/
bool fetch_level_info(levelinfo& level, int level_id) {
	if (level_id == 0) {
		return false;
	}
	else {
		level = game_data[level_id - 1];
		return true;
	}
}

/******************************
 *功能:将获得的关卡数据转换到map地图数组中
 * 输入:
 *		level - 关卡数据
 *		map - 二维地图数组
 *
 * 返回值:
 *		false - 转换失败
 *		true - 转换成功
 *****************************/
bool transform_map_db2array(levelinfo& level, int map[][COLUMN]) {
	if (level.map_row > LINE || level.map_column > COLUMN) {
		printf("地图过大,请重新设置\n");
		return false;
	}
	if (level.map_data.length() < 1) {
		printf("地图数据有误,请重新设置!\n");
		return false;
	}
	long long start = 0, end = 0;
	int row = 0, column = 0;
	do {
		/******************************
		 *find返回size_t类型值,size_t在64位下是8字节长度,因此这里把start和end设置成long long类型(不设置会有“数据丢失”警告)。当然也可以简单粗暴的修改警告设置
		******************************/
		end = level.map_data.find('|', start);
		if (end == -1) {
			end = level.map_data.length();
		}
		//合法性检查,结束时end=level.map_data.length,start=end+1
		if (start >= end)
			break;
		string line = level.map_data.substr(start, end - start);
		// 对行数据进行解析:0,1,0,1,1,1,1,1,1,1,0,0
		char* next_token = NULL;
		//line转为const char*(C语言中的字符串),strtok_s会修改line里的值,按照某个特定字符,所以需要将line.c_str()转为char *
		// strtok_s第一个参数是要劈开的字符串(要求是一个char *类型的);第二个参数是按某个字符劈开(会把“,”逗号变成‘\0’结束符,也就是说会修改原来字符串,所以要把原来的const char*转为char*);第三个参数是这个接口
		// 需要的,用来做定位功能,
		char* item = strtok_s((char*)line.c_str(), ",", &next_token);
		column = 0;
		//printf("%p", next_token);
		//::system("pause");
		//如果某行数据个数多余这个地图列数,控制只读取level.map_column-1个
		while (item && column < level.map_column) {
			map[row][column] = atoi(item);
			column++;
			//再使用strtok_s接口第一个参数可以写NULL,应该是next_token参数上一次做定位功能时已经记录了要劈开字符串给的某个位置
			item = strtok_s(NULL, ",", &next_token);
		}
		//合法性检查
		if (column < level.map_column) {	// 某行的数据个数小于这个地图的列数
			printf("地图数据解析出错,终止!\n");
			return false;
		}
		row++;

		//列数多余的话直接舍弃
		if (row >= level.map_row) {
			break;
		}
		start = end + 1;
	} while (1 == 1);
	//列数多余则直接报错
	if (row < level.map_row) {
		printf("地图行数少于设定,终止!");
		return false;
	}
	return true;
}

/******************************
 *功能:更新用户游戏进度信息
 * 输入:
 *		user - 用户信息
 *		next_level_id - 游戏进度
 *
 * 返回值:
 *		获取成功返回true,失败false
 *****************************/
bool update_user_level(userinfo& user, int next_level_id) {
	ifstream ifs;
	ifs.open(USERS);
	if (!ifs) {
		return false;
	}
	int user_index = 1;
	string strFileData = "";
	while (1) {
		if (user_index == user.id) {
			{string temp;
			getline(ifs, temp); }
			user_index++;
			continue;
		}
		if (ifs.eof()) {
			break;
		}
		string line;
		getline(ifs, line);
		strFileData += line;
		strFileData += '\n';

	}
	ifs.close();
	stringstream str_stream;
	str_stream << user.id << " " << user.username << " " << user.password << " " << next_level_id << endl;
	strFileData += str_stream.str();
	ofstream ofs;
	ofs.open(USERS);
	ofs.flush();
	ofs << strFileData;
	ofs.close();
	user.level_id = next_level_id;
	return true;
}

/*****************************
* 功能:判断游戏是否结束
* 输入:
*		无
* 输出:
*		true - 未结束
*		flase - 结束
*****************************/
bool isGameOver() {
	for (int i = 0; i < LINE; ++i) {
		for (int j = 0; j < COLUMN; ++j) {
			if (map[i][j] == BOX_DES)
				return false;
		}
	}
	return true;
}

/*****************************
* 功能:加载游戏结束图片
* 输入:
*		无
* 输出:
*		无
*****************************/
void show_over() {
	cleardevice();
	IMAGE game_over;
	loadimage(&game_over, _T("gameover.png"), SCREEN_WIDTH, SCREEN_HEIGHT, true);
	putimage(0, 0, &game_over);
}

/*****************************
* 功能:在指定位置更改地图信息 显示指定图片
* 输入:
*		next_pos - 指定位置
*		prop - 指定图片
* 输出:
*		无
*****************************/
void changeMap(POS* next_pos, PROPS prop) {
	map[next_pos->x][next_pos->y] = prop;
	putimage(START_X + next_pos->y * RATIO, START_Y + next_pos->x * RATIO, &images[prop]);
}

/*****************************
* 功能:在指定位置显示指定图片
* 输入:
*		next_pos - 指定位置
*		prop - 指定图片
* 输出:
*		无
*****************************/
void changeMap2(POS* next_pos, PROPS prop) {
	putimage(START_X + next_pos->y * RATIO, START_Y + next_pos->x * RATIO, &images[prop]);
}

/*****************************
* 功能:控制小人向指定方向移动
* 输入:
*		direct - 指定方向
* 输出:
*		无
*****************************/
void gameControl(DIRECTION direct) {
	POS next_pos = man;
	POS next_next_pos = man;
	switch (direct) {
	case UP:
		next_pos.x--;
		next_next_pos.x -= 2;
		break;
	case DOWN:
		next_pos.x++;
		next_next_pos.x += 2;
		break;
	case LEFT:
		next_pos.y--;
		next_next_pos.y -= 2;
		break;
	case RIGHT:
		next_pos.y++;
		next_next_pos.y += 2;
		break;
	}
	//宏展开next_pos.x > 0 && next_pos.x < LINE && next_pos.y>0 && next_pos.y < COLUMN
	// 人的前方是地板
	if (isValid(next_pos) && map[next_pos.x][next_pos.y] == FLOOR) {
		//人的脚下是目的地
		if (map[man.x][man.y] == BOX_DES) {
			changeMap(&next_pos, MAN);
			changeMap2(&man, BOX_DES);
		}
		else {
			changeMap(&man, FLOOR);
			changeMap(&next_pos, MAN);
		}
		man = next_pos;
	}
	// 人的前方是箱子
	else if (isValid(next_next_pos) && map[next_pos.x][next_pos.y] == BOX) {
		if (map[next_next_pos.x][next_next_pos.y] == FLOOR) {	// 箱子的前方是地板
			changeMap(&next_next_pos, BOX);
			changeMap(&next_pos, MAN);
			if (map[man.x][man.y] == BOX_DES) {
				changeMap(&man, BOX_DES);
			}
			else {
				changeMap(&man, FLOOR);
			}
			man = next_pos;
		}
		else if (map[next_next_pos.x][next_next_pos.y] == BOX_DES) {	// 箱子前面是目的地
			changeMap(&next_next_pos, HIT);
			changeMap(&next_pos, MAN);
			if (map[man.x][man.y] == BOX_DES) {
				changeMap(&man, BOX_DES);
			}
			else {
				changeMap(&man, FLOOR);
			}
			man = next_pos;
		}
	}
	//前方是目的地
	else if (isValid(next_pos) && map[next_pos.x][next_pos.y] == BOX_DES) {
		//小人脚下也是目的地
		if (map[man.x][man.y] == BOX_DES) {
			changeMap2(&next_pos, MAN_DES);
			changeMap(&man, BOX_DES);
		}
		else {
			changeMap(&man, FLOOR);
			changeMap2(&next_pos, MAN_DES);

		}
		man = next_pos;
	}
	//前方是箱子命中点
	else if (isValid(next_pos) && map[next_pos.x][next_pos.y] == HIT) {
		if (map[next_next_pos.x][next_next_pos.y] == FLOOR) {	// 箱子的前方是地板
			changeMap(&next_next_pos, BOX);
			changeMap(&next_pos, BOX_DES);
			changeMap2(&next_pos, MAN_DES);
			//人的脚下是目的地
			if (map[man.x][man.y] == BOX_DES) {
				changeMap(&man, BOX_DES);
			}
			else {
				changeMap(&man, FLOOR);
			}
			man = next_pos;
		}
		else if (map[next_next_pos.x][next_next_pos.y] == BOX_DES) {	// 箱子前面是目的地
			changeMap(&next_next_pos, HIT);
			changeMap(&next_pos, BOX_DES);
			changeMap2(&next_pos, MAN_DES);
			//人的脚下是目的地
			if (map[man.x][man.y] == BOX_DES) {
				changeMap(&man, BOX_DES);
			}
			else {
				changeMap(&man, FLOOR);
			}
			man = next_pos;
		}
	}
}

/*****************************
* 功能:用户登录
* 输入:
*		user - 用户信息
* 输出:
*		false - 登录失败
*		true - 登录成功
*****************************/
bool login(userinfo& user) {
	int times = 0;
	bool ret = false;
	do {
		cout << "请输入用户名:";
		cin >> user.username;
		cout << "请输入密码:";
		cin >> user.password;
		ret = fetch_user_info(user);
		times++;
		if (times >= MAX_RETRY_TIMES)
			break;
		if (ret == false) {
			cout << "登录失败,请重新输入!" << endl;
		}
	} while (!ret);
	return ret;
}

/*****************************
* 功能:加载游戏素材
* 输入:
*		无
* 输出:
*		无
*****************************/
void init_graph() {
	initgraph(SCREEN_WIDTH, SCREEN_HEIGHT);
	loadimage(&images[WALL], _T("wall_right.bmp"), RATIO, RATIO, true);
	loadimage(&images[FLOOR], _T("floor.bmp"), RATIO, RATIO, true);
	loadimage(&images[BOX_DES], _T("des.bmp"), RATIO, RATIO, true);
	loadimage(&images[MAN], _T("man.bmp"), RATIO, RATIO, true);
	loadimage(&images[BOX], _T("box.jpg"), RATIO, RATIO, true);
	loadimage(&images[HIT], _T("box_des.jpg"), RATIO, RATIO, true);
	loadimage(&images[MAN_DES], _T("man_des.bmp"), RATIO, RATIO, true);
	loadimage(&images[VECTOR], _T("vector.png"), 6 * RATIO, 6 * RATIO, true);
}

/*****************************
* 功能:显示指定地图
* 输入:
*		level - 指定地图
* 输出:
*		无
*****************************/
void show_images(levelinfo& level) {
	cleardevice();	// 每次贴图清屏一下
	IMAGE bg_img;
	//图片的宽,高最后一个参数“是否拉伸”
	loadimage(&bg_img, _T("bgimage.png"), SCREEN_WIDTH, SCREEN_HEIGHT, true);
	putimage(0, 0, &bg_img);
	for (int i = 0; i < level.map_row; ++i) {
		for (int j = 0; j < level.map_column; ++j) {
			if (map[i][j] == MAN) {
				man.x = i;
				man.y = j;
			}
			if (map[i][j] == BG_IMAGE)
				continue;
			putimage(START_X + j * RATIO, START_Y + i * RATIO, &images[map[i][j]]);
		}
	}
	const char* str = level.name.c_str();
	setbkmode(TRANSPARENT);
	setfont(RATIO, 0, _T("华文楷体"));
	setcolor(WHITE);
	outtextxy(200, 25, str);
}

/*****************************
* 功能:载入PNG图并去透明部分
* 输入:
*		无
* 输出:
*		无
*****************************/
void drawAlpha(IMAGE* picture, int  picture_x, int picture_y) //x为载入图片的X坐标,y为Y坐标
{
	// 变量初始化
	DWORD* dst = GetImageBuffer();    // GetImageBuffer()函数,用于获取绘图设备的显存指针,EASYX自带
	DWORD* draw = GetImageBuffer();
	DWORD* src = GetImageBuffer(picture); //获取picture的显存指针
	int picture_width = picture->getwidth(); //获取picture的宽度,EASYX自带
	int picture_height = picture->getheight(); //获取picture的高度,EASYX自带
	int graphWidth = getwidth();       //获取绘图区的宽度,EASYX自带
	int graphHeight = getheight();     //获取绘图区的高度,EASYX自带
	int dstX = 0;    //在显存里像素的角标

	// 实现透明贴图 公式: Cp=αp*FP+(1-αp)*BP , 贝叶斯定理来进行点颜色的概率计算
	for (int iy = 0; iy < picture_height; iy++)
	{
		for (int ix = 0; ix < picture_width; ix++)
		{
			int srcX = ix + iy * picture_width; //在显存里像素的角标
			int sa = ((src[srcX] & 0xff000000) >> 24); //0xAArrggbb;AA是透明度
			int sr = ((src[srcX] & 0xff0000) >> 16); //获取RGB里的R
			int sg = ((src[srcX] & 0xff00) >> 8);   //G
			int sb = src[srcX] & 0xff;              //B
			if (ix >= 0 && ix <= graphWidth && iy >= 0 && iy <= graphHeight && dstX <= graphWidth * graphHeight)
			{
				dstX = (ix + picture_x) + (iy + picture_y) * graphWidth; //在显存里像素的角标
				int dr = ((dst[dstX] & 0xff0000) >> 16);
				int dg = ((dst[dstX] & 0xff00) >> 8);
				int db = dst[dstX] & 0xff;
				draw[dstX] = ((sr * sa / 255 + dr * (255 - sa) / 255) << 16)  //公式: Cp=αp*FP+(1-αp)*BP  ; αp=sa/255 , FP=sr , BP=dr
					| ((sg * sa / 255 + dg * (255 - sa) / 255) << 8)         //αp=sa/255 , FP=sg , BP=dg
					| (sb * sa / 255 + db * (255 - sa) / 255);              //αp=sa/255 , FP=sb , BP=db
			}
		}
	}
}

/*****************************
* 功能:载入“恭喜通关”图片
* 输入:
*		无
* 输出:
*		无
*****************************/
void promot_over() {
	setbkmode(TRANSPARENT);
	drawAlpha(&images[VECTOR], 250, 130);     // 载入PNG图并去透明部分
	Sleep(1100);
}

/*****************************
* 功能:休眠
* 输入:
*		interval - 休眠时间
* 输出:
*		无
*****************************/
void wait(int interval) {
	int count = interval / 10;
	for (int i = 0; i < count; ++i) {
		Sleep(10);
		if (_kbhit())
			return;
	}
}

/*****************************
* 功能:游戏控制
* 输入:
*		level - 挡墙关卡数据
*		user - 用户信息
* 输出:
*		无
*****************************/
void game_operation(levelinfo& level, userinfo& user) {
	bool quit = false;
	do {
		//判断是否有按键按下
		if (_kbhit()) {
			//无缓冲读取
			char ch = _getch();
			if (ch == KEY_UP) {
				gameControl(UP);
			}
			else if (ch == KEY_DOWN) {
				gameControl(DOWN);
			}
			else if (ch == KEY_LEFT) {
				gameControl(LEFT);
			}
			else if (ch == KEY_RIGHT) {
				gameControl(RIGHT);
			}
			else if (ch == GAME_AGAIN) {
				fetch_level_info(level, user.level_id);
				transform_map_db2array(level, map);
				show_images(level);
			}
			else if (ch == KEY_OUT) {
				closegraph();
				exit(0);
			}
			if (isGameOver()) {
				//更新用户下一关关卡信息(用户通关后直接跳转下一关)
				update_user_level(user, level.next_level);
				quit = true;
			}
		}
		wait(100);
	} while (quit == false);
}

/*****************************
* 功能:根据“由用户ID获取关卡数据”的返回结果进行判断
* 输入:
*		result - 返回结果(-1:获取失败  0:用户已通关  1:获取成功)
*		level - 关卡数据
*		user - 用户信息
* 输出:
*		无
*****************************/
void judge_by_result(levelinfo& level, userinfo& user) {
	show_over();
	do {
		//判断是否有按键按下
		if (_kbhit()) {
			//无缓冲读取
			char ch = _getch();
			if (ch == KEY_OUT) {
				closegraph();
				exit(0);
			}
			else if (ch == GAME_AGAIN) {
				if (!update_user_level(user, 1)) {
					std::system("pause");
					closegraph();
					exit(0);
				}
				break;
			}
		}
		Sleep(50);
	} while (1);
	fetch_level_info(level, user.level_id);
}
int main() {
//作为数据初始化,只需运行一次    
	resetting_data();
	load_data();
	//用户身份验证
	userinfo user;
	levelinfo level;
	if (!login(user)) {
		cout << "登录失败,请重新登录!" << endl;
		::system("pause");
		exit(-1);
	}
	init_graph();
	//循环(读取关卡→用户操作)
	do {
		//根据用户信息加载关卡数据
		if (!fetch_level_info(level, user.level_id)) {
			judge_by_result(level, user);
		}
		//将关卡数据数据转换到map游戏地图中
		transform_map_db2array(level, map);
		//加载游戏图片
		show_images(level);
		//小人移动
		game_operation(level, user);
		//恭喜通关
		promot_over();
	} while (1);
	std::system("pause");
	closegraph();
	return 0;
}

目录

一、推箱子游戏源码(可直接拷贝运行)

二、C语言推箱子游戏实现

1.地图初始化 init_graph( ) + show_images( )

2.地图表示

3.设计操作框架 循环(读取关卡→用户操作)

4.箱子控制game_operation( )

①设计用户操作框架

②控制小人指定方向移动

③游戏退出

④游戏重新开始

⑤游戏结束判断

⑥用户操作框架补充

5.加载游戏通关图片(如何加载png透明图片)

二、编写数据库连接程序

前言:与连接MySQL数据库源码

1.数据库表的设计 && C++结构体设计

①SQL语句创建数据

②C++结构体设计

2.用户登录 fetch_user_info( )

3.获取关卡信息

4.地图转换

5.跳转下一关

三、修改游戏代码适应与数据库连接

1.搞定用户登录功能

2.对用户的游戏进度做出判断

四、程序打包发布

1.下载程序打包组件

2.设置VS编译器

3.在同一解决方案下创建Setup项目

4.添加项目文件到Setup项目中

5.添加项目输出

6.将快捷方式放入用户桌面中

7.为程序选择一个图标

8.将图标与快捷方式绑定

五、不足之处


二、C语言推箱子游戏实现

1.地图初始化 init_graph( ) + show_images( )

C语言实现推箱子 (数据库连接+项目打包发布)_第2张图片

 我们首先需要初始化画布,加载游戏背景图以及所需其他图片素材。

typedef enum  _PROPS {
	WALL,	//墙
	FLOOR,	//地板
	BOX_DES,//箱子目的地
	MAN,	//小人
	BOX,	//箱子
	HIT,	//箱子命中目标
	MAN_DES,	//人站在目标上
	VECTOR	//通关图片
}PROPS;

/*****************************
* 功能:加载游戏素材
* 输入:
*		无
* 输出:
*		无
*****************************/
void init_graph() {
	initgraph(SCREEN_WIDTH, SCREEN_HEIGHT);
	loadimage(&images[WALL], _T("wall_right.bmp"), RATIO, RATIO, true);
	loadimage(&images[FLOOR], _T("floor.bmp"), RATIO, RATIO, true);
	loadimage(&images[BOX_DES], _T("des.bmp"), RATIO, RATIO, true);
	loadimage(&images[MAN], _T("man.bmp"), RATIO, RATIO, true);
	loadimage(&images[BOX], _T("box.jpg"), RATIO, RATIO, true);
	loadimage(&images[HIT], _T("box_des.jpg"), RATIO, RATIO, true);
	loadimage(&images[MAN_DES], _T("man_des.bmp"), RATIO, RATIO, true);
	loadimage(&images[VECTOR], _T("vector.png"), 6 * RATIO, 6 * RATIO, true);
}

/*****************************
* 功能:显示指定地图
* 输入:
*		level - 指定地图
* 输出:
*		无
*****************************/
void show_images(levelinfo& level) {
	cleardevice();	// 每次贴图清屏一下
	IMAGE bg_img;
	//图片的宽,高最后一个参数“是否拉伸”
	loadimage(&bg_img, _T("bgimage.png"), SCREEN_WIDTH, SCREEN_HEIGHT, true);
	putimage(0, 0, &bg_img);
	for (int i = 0; i < level.map_row; ++i) {
		for (int j = 0; j < level.map_column; ++j) {
			if (map[i][j] == MAN) {
				man.x = i;
				man.y = j;
			}
			if (map[i][j] == BG_IMAGE)
				continue;
			putimage(START_X + j * RATIO, START_Y + i * RATIO, &images[map[i][j]]);
		}
	}
	const char* str = level.name.c_str();
	setbkmode(TRANSPARENT);
	setfont(RATIO, 0, _T("华文楷体"));
	setcolor(WHITE);
	outtextxy(200, 25, str);
}

2.地图表示

游戏地图使用二维数组存储,对不同的图形用不同的数字表示

//墙: 0;地板: 1;箱子目的地: 2,;小人: 3;箱子: 4;背景图:-1;目标: 5
int mapdata[LINE][COLUMN] = {
    {-1,0,0,0,0,0,0,0,-1,-1 },
    { -1, 0, 2, 2, 2, 2, 1, 0, -1, -1 },
    {0, 0, 0, 2, 2, 2, 4, 0, 0, 0 },
    {0, 1, 1, 4, 0, 4, 1, 4, 1, 0 },
    {0, 1, 4, 4, 1, 1, 0, 4, 1, 0 },
    {0, 1, 1, 1, 3, 0, 1, 1, 1, 0 },
    {0, 0, 0, 0, 1, 1, 1, 0, 0, 0, },
    {-1, -1, -1, 0, 0, 0, 0, 0, -1, -1}
};

3.设计操作框架 循环(读取关卡→用户操作)

 每次读取关卡,然后是用户操作,用户通关后继续读取下一关卡。如果用户中途按下退出键则会退出游戏。

int main() {    
    //读取地图信息
    memcpy(map, mapdata, sizeof(mapdata));
    init_graph();//循环(读取关卡→用户操作)
    do {
        //加载游戏图片
        show_images();
        //用户操作
        game_operation();
        //恭喜通关
        promot_over();
        } while (1);
    std::system("pause");
    closegraph();
    return 0;
}

4.箱子控制game_operation( )

①设计用户操作框架

    通过循环,每次读取用户按键,根据按键做出相应游戏效果实现。小人每次可以上下左右四个方向移动,以及用户的退出和重新开始共六个热键。当游戏结束时退出循环,如果用户按下退出则直接结束游戏。

#define KEY_UP 'W'    //上移
#define KEY_LEFT 'A'    //左移
#define KEY_RIGHT 'D'    //右移
#define KEY_DOWN 'S'    //下移
#define KEY_OUT 'Q'    //退出
#define GAME_AGAIN 'R'    //重试/
*****************************
* 功能:游戏控制
* 输入:        
*    无
* 输出:
*    无
*****************************/
void game_operation() {
    bool quit = false;    //记录是否游戏结束
    do {
        //判断是否有按键按下
        if (_kbhit()) {
            //无缓冲读取
            char ch = _getch();
            if (ch == KEY_UP) {
                // to do
            }else if (ch == KEY_DOWN) {
                // to do
            }else if (ch == KEY_LEFT) {
                // to do
            }else if (ch == KEY_RIGHT) {
                // to do
            }else if (ch == GAME_AGAIN) {
                // to do
            }else if (ch == KEY_OUT) {
                // to do
            }if (isGameOver()) {
                // to do
            }
        }wait(100);
    } while (quit == false);
}

②控制小人指定方向移动

对于小人可以移动的情况可以分成四种:

Ⅰ:小人前面是地板

Ⅱ:小人前面是目的地

Ⅲ:小人项目是箱子

Ⅳ:小人前面是已经在目的地上的箱子

    还有其他可能出现的情况都属于不能移动。那为什么要分成这四种情况呢?如果小人前面是地板或者箱子时,每次移动我们要去更改地图信息(为判断游戏结束做准备),而如果小人前面是目的地或者是已经在目的地上的箱子那就不能更改地图信息了。

#define isValid(next_pos) next_pos.x > 0 && next_pos.x < LINE && next_pos.y>0 && next_pos.y < COLUMN

/*****************************
* 功能:在指定位置更改地图信息 显示指定图片
* 输入:
*		next_pos - 指定位置
*		prop - 指定图片
* 输出:	
*		无
*****************************/
void changeMap(POS* next_pos, PROPS prop) {
	map[next_pos->x][next_pos->y] = prop;
	putimage(START_X + next_pos->y * RATIO, START_Y + next_pos->x * RATIO, &images[prop]);
}

/*****************************
* 功能:在指定位置显示指定图片
* 输入:
*		next_pos - 指定位置
*		prop - 指定图片
* 输出:
*		无
*****************************/
void changeMap2(POS* next_pos, PROPS prop) {
	putimage(START_X + next_pos->y * RATIO, START_Y + next_pos->x * RATIO, &images[prop]);
}

/*****************************
* 功能:控制小人向指定方向移动
* 输入:
*		direct - 指定方向
* 输出:
*		无
*****************************/
void gameControl(DIRECTION direct) {
	POS next_pos = man;
	POS next_next_pos = man;
	switch (direct) {
	case UP:
		next_pos.x--;
		next_next_pos.x -= 2;
		break;
	case DOWN:
		next_pos.x++;
		next_next_pos.x += 2;
		break;
	case LEFT :
		next_pos.y--;
		next_next_pos.y -= 2;
		break;
	case RIGHT:
		next_pos.y++;
		next_next_pos.y += 2;
		break;
	}	
	//宏展开next_pos.x > 0 && next_pos.x < LINE && next_pos.y>0 && next_pos.y < COLUMN
	// 人的前方是地板
	if (isValid(next_pos) && map[next_pos.x][next_pos.y] == FLOOR) {
		//人的脚下是目的地
		if (map[man.x][man.y] == BOX_DES) {
			changeMap(&next_pos, MAN);
			changeMap2(&man, BOX_DES);
		}
		else {
			changeMap(&man, FLOOR);
			changeMap(&next_pos, MAN);
		}	
		man = next_pos;
	}
	// 人的前方是箱子
	else if (isValid(next_next_pos) && map[next_pos.x][next_pos.y] == BOX) {	
		if (map[next_next_pos.x][next_next_pos.y] == FLOOR) {	// 箱子的前方是地板
			changeMap(&next_next_pos, BOX);
			changeMap(&next_pos, MAN);
			if (map[man.x][man.y] == BOX_DES) {
				changeMap(&man, BOX_DES);
			}
			else {
				changeMap(&man, FLOOR);
			}
			man = next_pos;
		}
		else if (map[next_next_pos.x][next_next_pos.y] == BOX_DES) {	// 箱子前面是目的地
			changeMap(&next_next_pos, HIT);
			changeMap(&next_pos, MAN);
			if (map[man.x][man.y] == BOX_DES) {
				changeMap(&man, BOX_DES);
			}
			else {
				changeMap(&man, FLOOR);
			}
			man = next_pos;
		}	
	}
	//前方是目的地
	else if (isValid(next_pos) && map[next_pos.x][next_pos.y] == BOX_DES) {
		//小人脚下也是目的地
		if (map[man.x][man.y] == BOX_DES) {
			changeMap2(&next_pos, MAN_DES);
			changeMap(&man, BOX_DES);
		}
		else {
			changeMap(&man, FLOOR);
			changeMap2(&next_pos, MAN_DES);
			
		}
		man = next_pos;
	}
	//前方是箱子命中点
	else if (isValid(next_pos) && map[next_pos.x][next_pos.y] == HIT) {
		if (map[next_next_pos.x][next_next_pos.y] == FLOOR) {	// 箱子的前方是地板
			changeMap(&next_next_pos, BOX);
			changeMap(&next_pos, BOX_DES);
			changeMap2(&next_pos, MAN_DES);
			//人的脚下是目的地
			if (map[man.x][man.y] == BOX_DES) {
				changeMap(&man, BOX_DES);
			}
			else {
				changeMap(&man, FLOOR);
			}
			man = next_pos;
		}
		else if (map[next_next_pos.x][next_next_pos.y] == BOX_DES) {	// 箱子前面是目的地
			changeMap(&next_next_pos, HIT);
			changeMap(&next_pos, BOX_DES);
			changeMap2(&next_pos, MAN_DES);
			//人的脚下是目的地
			if (map[man.x][man.y] == BOX_DES) {
				changeMap(&man, BOX_DES);
			}
			else {
				changeMap(&man, FLOOR);
			}
			man = next_pos;
		}
	}
}

③游戏退出

    如果用户按下“Q”键可以直接退出游戏,这个还是比较好写的直接一个exit(0)

else if (ch == KEY_OUT) {
    closegraph();exit(0);
}

④游戏重新开始

    如果用户按下“R”键,重置游戏。现在还没有连接数据库,这个功能还是容易完成,只要重置地图信息然后显示出来即可

else if (ch == GAME_AGAIN) {
    memcpy(map, mapdata, sizeof(mapdata));
    show_images();
}

⑤游戏结束判断

    每次将箱子推到目的地时我们已经更改地图信息,所以对于游戏是否结束判断我们可以遍历地图,只要找不到“目的地”则代表游戏结束。

/*****************************
* 功能:判断游戏是否结束
* 输入:
*		无
* 输出:
*		true - 未结束
*		flase - 结束
*****************************/
bool isGameOver() {
	for (int i = 0; i < LINE; ++i) {
		for (int j = 0; j < COLUMN; ++j) {
			if (map[i][j] == BOX_DES)
				return false;
		}
	}
	return true;
}

⑥用户操作框架补充

    将上述实现功能补充到用户操作框架中

/*****************************
* 功能:游戏控制
* 输入:
*		level - 挡墙关卡数据
*		user - 用户信息
* 输出:
*		无
*****************************/
void game_operation() {
	bool quit = false;
	do {
		//判断是否有按键按下
		if (_kbhit()) {
			//无缓冲读取
			char ch = _getch();
			if (ch == KEY_UP) {
				gameControl(UP);
			}
			else if (ch == KEY_DOWN) {
				gameControl(DOWN);
			}
			else if (ch == KEY_LEFT) {
				gameControl(LEFT);
			}
			else if (ch == KEY_RIGHT) {
				gameControl(RIGHT);
			}
			else if (ch == GAME_AGAIN) {
				memcpy(map, mapdata, sizeof(mapdata));
				show_images();
			}
			else if (ch == KEY_OUT) {
				closegraph();
				exit(0);
			}
			if (isGameOver()) {
				quit = true;
			}
		}
		wait(100);
	} while (quit == false);
}

5.加载游戏通关图片(如何加载png透明图片)

其实加载游戏通关图片只需使用putimage函数即可,这里之所以单独作为一个标题是因为使用putimage时如果时png透明图片加载出来时还是会有背景,这里为大家提供一个可以加载png透明背景图片的算法(可以单独拷贝使用)

C语言实现推箱子 (数据库连接+项目打包发布)_第3张图片

/*****************************
* 功能:载入PNG图并去透明部分
* 输入:
*		无
* 输出:
*		无
*****************************/
void drawAlpha(IMAGE* picture, int  picture_x, int picture_y) //x为载入图片的X坐标,y为Y坐标
{

	// 变量初始化
	DWORD* dst = GetImageBuffer();    // GetImageBuffer()函数,用于获取绘图设备的显存指针,EASYX自带
	DWORD* draw = GetImageBuffer();
	DWORD* src = GetImageBuffer(picture); //获取picture的显存指针
	int picture_width = picture->getwidth(); //获取picture的宽度,EASYX自带
	int picture_height = picture->getheight(); //获取picture的高度,EASYX自带
	int graphWidth = getwidth();       //获取绘图区的宽度,EASYX自带
	int graphHeight = getheight();     //获取绘图区的高度,EASYX自带
	int dstX = 0;    //在显存里像素的角标

	// 实现透明贴图 公式: Cp=αp*FP+(1-αp)*BP , 贝叶斯定理来进行点颜色的概率计算
	for (int iy = 0; iy < picture_height; iy++)
	{
		for (int ix = 0; ix < picture_width; ix++)
		{
			int srcX = ix + iy * picture_width; //在显存里像素的角标
			int sa = ((src[srcX] & 0xff000000) >> 24); //0xAArrggbb;AA是透明度
			int sr = ((src[srcX] & 0xff0000) >> 16); //获取RGB里的R
			int sg = ((src[srcX] & 0xff00) >> 8);   //G
			int sb = src[srcX] & 0xff;              //B
			if (ix >= 0 && ix <= graphWidth && iy >= 0 && iy <= graphHeight && dstX <= graphWidth * graphHeight)
			{
				dstX = (ix + picture_x) + (iy + picture_y) * graphWidth; //在显存里像素的角标
				int dr = ((dst[dstX] & 0xff0000) >> 16);
				int dg = ((dst[dstX] & 0xff00) >> 8);
				int db = dst[dstX] & 0xff;
				draw[dstX] = ((sr * sa / 255 + dr * (255 - sa) / 255) << 16)  //公式: Cp=αp*FP+(1-αp)*BP  ; αp=sa/255 , FP=sr , BP=dr
					| ((sg * sa / 255 + dg * (255 - sa) / 255) << 8)         //αp=sa/255 , FP=sg , BP=dg
					| (sb * sa / 255 + db * (255 - sa) / 255);              //αp=sa/255 , FP=sb , BP=db
			}
		}
	}
}

/*****************************
* 功能:载入“恭喜通关”图片
* 输入:
*		无
* 输出:
*		无
*****************************/
void promot_over() {
	setbkmode(TRANSPARENT);
	drawAlpha(&images[VECTOR], 250, 130);     // 载入PNG图并去透明部分
	Sleep(1100);
}

到这里推箱子游戏就已经完成了,接下来就是将数据库与游戏连接起来。

二、编写数据库连接程序

上面 分享的源码是基于文本文档做为数据库存储,小编就使不做过多说明。这里为大家分享如何与MySQL数据库连接

前言:与连接MySQL数据库源码

连接MySQL数据库源码https://download.csdn.net/download/qq_54169998/81166262 在编写程序之前,首先要做的是配置VS编译器,如果有不会的朋友可以参考下面的配置教程

C/C++连接MySQL数据库 配置VS编译器https://blog.csdn.net/qq_54169998/article/details/122764798?spm=1001.2014.3001.5501

1.数据库表的设计 && C++结构体设计

  •  用户表

表名

Users

字段名

类型

是否为空

默认值

主、外键

备注

id

int(11)

NOT

1,自增长

PK

用户id

username

varchar(64)

NOT

用户名:英文字符、数字和特殊符号的组合

password

varchar(32)

NOT

密码:英文字符、数字和特殊符号的组合,8-16位

level_id

int

1

当前关卡,关联Levels表中的id

  • 关卡表

表名

Levels

字段名

类型

是否为空

默认值

主、外键

备注

id

int

NOT

1

PK

游戏关卡序号,从1开始

name

varchar(64)

NOT

地图名称

map_row

int

NOT

地图二位组的总行数

map_column

int

NOT

地图二维组的总列数

map_data

varchar(4096)

NOT

地图数据,二维数组对应的行列式,多行以|分开,列以逗号分隔,最大接近支持45x45的地图

next_level_id

int

0

下一关的关卡id,0代表通关

①SQL语句创建数据

create database box_man if not exists;
use box_man;
create table users ( 
    id int not null auto_increment,
    username varchar(64) not null,
    password varchar(32) not null,
    level_id int default 1,
    primary key(id),
    unique key username(username)
);

create table levels(
    id int not null auto_increment,
    name varchar(64) not null,
    map_row int not null,
    map_column int not null,
    map_data varchar(4096) not null,
    next_level_id int default 0 comment '下一关的id,0代表通关',
    primary key(id)
);
insert into users 
    values(1,'Jack',md5(123456),1);
insert into levels 
    values(1,'小试牛刀→第一关',8,10,'-1,0,0,0,0,0,0,0,-1,-1|-1,0,2,2,2,2,1,0,-1,-1|0,0,0,2,2,2,4,0,0,0|0,1,1,4,0,4,1,4,1,0|0,1,4,4,1,1,0,4,1,0|0,1,1,1,3,0,1,1,1,0|0,0,0,0,1,1,1,0,0,0,|-1,-1,-1,0,0,0,0,0,-1,-1',2),
    (2,'绝地强者→第二关',6,6,'0,0,0,0,0,0|0,2,2,1,1,0|0,4,4,1,3,0|0,2,1,4,0,0|0,1,1,1,0,-1|0,0,0,0,0,-1',3),
    (3,'天外强人→第三关',8,10,'-1,-1,0,0,0,0,0,0,-1,-1|0,0,0,1,1,1,0,0,0,0|0,1,1,1,4,1,4,1,1,0|0,1,4,1,1,1,4,1,3,0|0,0,0,4,4,0,0,0,0,0,|-1,-1,0,1,1,2,2,0,-1,-1|-1,-1,0,2,2,2,2,0,-1,-1|-1,-1,0,0,0,0,0,0,-1,-1',4),
    (4,'死而复生→BOSS关',8,8,'-1,0,0,0,0,0,0,-1|-1,0,2,1,2,2,0,-1|-1,0,2,1,4,2,0,-1|0,0,0,1,1,4,0,0|0,1,4,1,1,4,1,0|0,1,0,4,0,0,1,0|0,1,1,1,3,1,1,0|0,0,0,0,0,0,0,0',0);

②C++结构体设计

typedef struct _userinfo {
	//这里将字符串初始为空
	_userinfo() :id(0), username(""), password(""), level_id(0) {};
	int id;	// 用户id
	string username;
	string password;	// 密码
	int level_id;
}userinfo;

typedef struct _levelinfo {
	_levelinfo() :id(0), name(""), map_row(0), map_column(0), map_data(""), next_level(0) {};
	int id;	// 关卡id
	string name;	// 关卡名字
	int map_row;	// 地图行数
	int map_column;	//地图列数
	string map_data;	// 二维地图数据
	int next_level;	//下一关卡id
}levelinfo;

2.用户登录 fetch_user_info( )

    实现用户登录功能首先是获取用户输入的账号和密码,接着在数据库中进行查找,如果找到则读取游戏进度然后进入游戏界面,如果没找到则提示用户是否有输入错误。

    获取用户输入的账号和密码因为种(本)种(人)原(较)因(懒)没有设置页面,而是在控制台中输入,感兴趣的朋友可以根据自己风格设计页面。在控制台中读取用户输入内容还是非常容易实现,关键还是在于和数据库的操作。如果是第一次MySQL连接C++使用的化其实也不用惊慌,因为很多语法都是高重复性,学会一些其余的只需要:

C语言实现推箱子 (数据库连接+项目打包发布)_第4张图片

 就可以了。

    当我们在MySQL中对数据操作时,我们只需要启动MySQL,执行操作,关闭MySQL。而与C++连接后这些操作则会稍有改变,在每次单一操作结束后我们就要去关闭与数据库连接,例如我查找找完用户信息后我就要去关闭数据库连接,接着是读取关卡信息,那么我们就要继续打开数据库,读取关卡信息,然后关闭。

    因此我们可以先将与数据库连接功能实现,这里大家记得将用户名和密码改成自己的

#define DB_NAME	"box_man"    //数据库名
#define DB_HOST	"127.0.0.1"    //IP地址
#define DB_PORT 3306            //端口号
#define DB_USER "root"           //用户名
#define DB_USER_PASSWORD "******"    //密码



/******************************
 *功能:数据库链接
 * 输入:
 *		mysql - 数据库访问句柄
 *
 * 返回值:
 *		true - 连接成功
 *		false - 连接失败
 *****************************/
bool connect_db(MYSQL& mysql) {
	//1.初始数据库句柄
	mysql_init(&mysql);

	//2.设置字符编码(为句柄设置)
	//windows支持中文,在windows上一般是gbk字符集,gbk包含中文简体和繁体,简体是gbk2312
	mysql_options(&mysql, MYSQL_SET_CHARSET_NAME, "gbk");

	//3.连接数据库
	//句柄,主机,用户名,密码,数据库,端口号,——,——
	if (mysql_real_connect(&mysql, DB_HOST, DB_USER, DB_USER_PASSWORD, DB_NAME, DB_PORT, NULL, 0) == NULL) {
		printf("数据库连接失败,错误原因:%s\n", mysql_error(&mysql));
		return false;
	}
	return true;
}

 数据库连接功能实现好后接下来就是传入用户输入的信息,编写SQL查询语句进行查找。我将这套动作分成四步,这四步也基本可以满足平时设计程序所需

1.连接数据库

2.编写SQL语句

3.进行查找或修改

4.返回查询结果并关闭数据库

/******************************
 *功能:通过用户名和密码获取用户信息
 * 输入:
 *		user - 用户信息结构体
 *
 * 返回值:
 *		获取成功返回true,失败false
 *****************************/
bool fetch_user_info(userinfo& user) {
	MYSQL mysql;	//定义一个句柄,对mysql的访问都是通过这个句柄(就是一个集合)
	MYSQL_RES* res;		// 查询的结果集
	MYSQL_ROW row;		// 如果结果集有多条,可以使用row获取
	char sql[256];
	bool ret = false;
	//1.连接数据库
	if (connect_db(mysql) == false) {
		return false;
	}
	//2.设置SQL查询语句
	//snprintf函数,设将可变参数(...)按照 format 格式化成字符串,并将字符串复制到 str 中,size 为要写入的字符的最大数目,超过 size 会被截断。
	snprintf(sql, 256, "select id,level_id from users where username='%s' and password = md5('%s');", user.username.c_str(), user.password.c_str());
	ret = mysql_query(&mysql, sql);	//mysql查询,成功返回0
	if (ret) {
		printf("数据库查询出错,%s错误原因:%s\n", sql, mysql_error(&mysql));
		// 关闭数据库
		mysql_close(&mysql);
		return false;
	}
	
	//3.获取查到的结果
	res = mysql_store_result(&mysql);
	//row获得结果集里的一行记录,再通过下标访问记录里的数据,如果结果集有多行时可以通过 while(row = mysql_fetch_row(res)){ }循环获得
	row = mysql_fetch_row(res);

	if (row == NULL) {	//没有查到记录
		//1.释放结果集
		mysql_free_result(res);
		//1.关闭数据库
		mysql_close(&mysql);
		return false;
	}
	user.id = atoi(row[0]);	// 字符串转整数
	user.level_id = atoi(row[1]);
	//4.返回结果
	mysql_free_result(res);
	mysql_close(&mysql);
	return true;
}

3.获取关卡信息

C语言实现推箱子 (数据库连接+项目打包发布)_第5张图片

 地图数据使用二维数组表示,对于不同图形用不同编号表示

 在数据库录入中,我们使用不同数字代表不同图形,并且使用“|”符号作为每行的分隔符,我们只需根据读取到的用户信息中的游戏进度,来获得对应关卡数据即可。

/*********************************
 *功能:根据关卡id获取完整的关卡信息(如:地图,下一关等)
 * 输入:
 *		level - 保存关卡信息的结构体
 *		level_id - 获取关卡id
 * 返回值:
 *		-1 - 数据库连接或查询出错
 *		0  - 查找结果为空
 *		1 - 查找成功
 *********************************/
int fetch_level_info(levelinfo& level, int level_id) {
	MYSQL mysql;	//定义一个句柄,对mysql的访问都是通过这个句柄(就是一个集合)
	MYSQL_RES* res;		// 查询的结果集
	MYSQL_ROW row;		// 如果结果集有多条,可以使用row获取
	char sql[256];
	bool ret = false;
	//1.连接数据库
	if (connect_db(mysql) == false) {
		return -1;
	}
	//2.编写SQL语句
	snprintf(sql, 256, "select name,map_row,  map_column,map_data,next_level_id from levels where id=%d;", level_id);
	ret = mysql_query(&mysql, sql);	//mysql查询,成功返回0
	if (ret) {
		printf("数据库查询出错,%s错误原因:%s\n", sql, mysql_error(&mysql));
		// 关闭数据库
		mysql_close(&mysql);
		return -1;
	}

	//3.获取查到的结果
	res = mysql_store_result(&mysql);
	//row获得结果集里的一行记录,再通过下标访问记录里的数据,如果结果集有多行时可以通过 while(row = mysql_fetch_row(res)){ }循环获得
	row = mysql_fetch_row(res);

	if (row == NULL) {	//没有查到记录
		//1.释放结果集
		mysql_free_result(res);
		//1.关闭数据库
		mysql_close(&mysql);
		return 0;
	}
	level.id = level_id;
	level.name = row[0];
	level.map_row = atoi(row[1]);
	level.map_column = atoi(row[2]);
	level.map_data = row[3];
	level.next_level = atoi(row[4]);
	//string用printf输出,使用.c_str()可以返回一个const char* 的指针
	//printf("level id: %d  name: %s map row: %d  map column: %d map data: %s next level: %d\n", level.id, level.name.c_str(), level.map_row, level.map_column, level.map_data.c_str(), level.next_level);
	
	//4.返回结果
	mysql_free_result(res);
	mysql_close(&mysql);
	return 1;
}

4.地图转换

获得的关卡数据是由结构体存储,我们需要将地图信息转为二维数组存储,也就是将string存储的地图信息转为int map[ ][ ] 存储

/******************************
 *功能:将获得的关卡数据转换到map地图数组中
 * 输入:
 *		level - 关卡数据
 *		map - 二维地图数组
 *
 * 返回值:
 *		false - 转换失败
 *		true - 转换成功
 *****************************/
bool transform_map_db2array(levelinfo& level, int map[][COLUMN]) {
	if (level.map_row > LINE || level.map_column > COLUMN) {
		printf("地图过大,请重新设置\n");
		return false;
	}
	if (level.map_data.length() < 1) {
		printf("地图数据有误,请重新设置!\n");
		return false;
	}
	long long start = 0, end = 0;
	int row = 0, column = 0;
	do {
		/******************************
		 *find返回size_t类型值,size_t在64位下是8字节长度,因此这里把start和end设置成long long类型(不设置会有“数据丢失”警告)。当然也可以简单粗暴的修改警告设置
		******************************/
		end = level.map_data.find('|', start);
		if (end == -1) {
			end = level.map_data.length();
		}
		//合法性检查,结束时end=level.map_data.length,start=end+1
		if (start >= end)
			break;
		string line = level.map_data.substr(start, end - start);
		//printf("get-line:%s\n", line.c_str());
		// 对行数据进行解析:0,1,0,1,1,1,1,1,1,1,0,0
		char* next_token = NULL;
		//line转为const char*(C语言中的字符串),strtok_s会修改line里的值,按照某个特定字符,所以需要将line.c_str()转为char *
		// strtok_s第一个参数是要劈开的字符串(要求是一个char *类型的);第二个参数是按某个字符劈开(会把“,”逗号变成‘\0’结束符,也就是说会修改原来字符串,所以要把原来的const char*转为char*);第三个参数是这个接口
		// 需要的,用来做定位功能,
		//printf("%p\n", &line[2]);
		char* item = strtok_s((char*)line.c_str(), ",", &next_token);
		column = 0;
	    //printf("%p", next_token);
		//::system("pause");
		//如果某行数据个数多余这个地图列数,控制只读取level.map_column-1个
		while (item&&column= level.map_row) {
			break;
		}
		start = end + 1;
	} while (1 == 1);
	//列数多余则直接报错
	if (row < level.map_row) {
		printf("地图行数少于设定,终止!");
		return false;
	}
	return true;
}

5.跳转下一关

每当 用户通关后,便更新用户的游戏进度,实现下一关的跳转

/******************************
 *功能:更新用户游戏进度信息
 * 输入:
 *		user - 用户信息
 *		next_level_id - 游戏进度
 *
 * 返回值:
 *		获取成功返回true,失败false
 *****************************/
bool update_user_level(userinfo& user, int next_level_id) {
	MYSQL mysql;	//定义一个句柄,对mysql的访问都是通过这个句柄(就是一个集合)
	char sql[256];
	bool ret = false;
	//1.连接数据库
	if (connect_db(mysql) == false) {
		return false;
	}
	//2.编写SQL语句
	snprintf(sql, 256, "update users set level_id=%d where id=%d", next_level_id, user.id);
	ret = mysql_query(&mysql, sql);

	if (ret) {
		printf("数据库更新出错,%s错误原因:%s\n", sql, mysql_error(&mysql));
		// 关闭数据库
		mysql_close(&mysql);
		return false;
	}
	user.level_id = next_level_id;
	mysql_close(&mysql);
	return true;
}

三、修改游戏代码适应与数据库连接

1.搞定用户登录功能

数据库连接程序写好后,下面就是对推箱子游戏的程序升级,首先呢就是更改游戏框架,在初始化画布之前就是先让用户登录,我们规允许用户输入失败次数,若达到次数限制后则强制退出。

/*****************************
* 功能:用户登录
* 输入:
*		user - 用户信息
* 输出:
*		false - 登录失败
*		true - 登录成功
*****************************/
bool login(userinfo& user) {
	int times = 0;
	bool ret = false;
	do {
		cout << "请输入用户名:";
		cin >> user.username;
		cout << "请输入密码:";
		cin >> user.password;
		ret = fetch_user_info(user);
		times++;
		if (times >= MAX_RETRY_TIMES)
			break;
		if (ret == false) {
			cout << "登录失败,请重新输入!" << endl;
		}
	} while (!ret);
	return ret;
}

2.对用户的游戏进度做出判断

用户登录成功后就要对用户的游戏进度做出判断,如果返回-1,则代表数据库查询中失败了,此时就要强制退出程序;如果返回0,则代表用户已通关,此时就要是要重新开始还是要退出;如果返回1,则代表查询成功,进入游戏页面。

对于返回-1或者0需要单独处理

/*****************************
* 功能:根据“由用户ID获取关卡数据”的返回结果进行判断
* 输入:
*		result - 返回结果(-1:获取失败  0:用户已通关  1:获取成功)
*		level - 关卡数据
*		user - 用户信息
* 输出:
*		无
*****************************/
void judge_by_result(int result, levelinfo& level, userinfo& user) {
	if (result == 1) {
		return;
	}
	else if (result == -1) {
		closegraph();
		cout << "获取关卡数据失败,请重试!" << endl;
		std::system("pause");
		exit(-1);
	}
	else if (result == 0) {
		show_over();
		do {
			//判断是否有按键按下
			if (_kbhit()) {
				//无缓冲读取
				char ch = _getch();
				if (ch == KEY_OUT) {
					closegraph();
					exit(0);
				}
				else if (ch == GAME_AGAIN) {
					if (!update_user_level(user,1)) {
						std::system("pause");
						closegraph();
						exit(0);
					}
					break;
				}
			}
			Sleep(50);
		} while (1);
		fetch_level_info(level, user.level_id);
	}
}

读取 用户进度后,接下来便是调用地图信息转换功能,将地图信息转换到二维数组中,到这里对程序的升级工作也算是大功告成了。

四、程序打包发布

将推箱子游戏连接数据库之后也算是给游戏升了一个逼格,但当我们每次想玩的时候还要去打开编译器,甚至想发给自己的朋友都不行,难道还要让别人下载一个编译器还不成。因此,这里为大家带来一种程序打包方法,让你的项目也可以像软件安装一样直接安装运行

1.下载程序打包组件

VS 2022程序打包组件

VS 2017-2019程序打包组件

下载好的组件直接打开安装

C语言实现推箱子 (数据库连接+项目打包发布)_第6张图片

2.设置VS编译器

1)设置为release模式,并把相关的设置也重新配置(sdl检查、字符集等等)

2)把项目属性中的运行库,更改为“多线程(/MT)”

C语言实现推箱子 (数据库连接+项目打包发布)_第7张图片

3.在同一解决方案下创建Setup项目

C语言实现推箱子 (数据库连接+项目打包发布)_第8张图片

4.添加项目文件到Setup项目中

这里演示我的推箱子项目

这里将演示两种情况,一种是程序需要的素材文件与程序文件在同一目录下,另一种是素材文件在二级目录下

第一种情况

C语言实现推箱子 (数据库连接+项目打包发布)_第9张图片

在应用程序文件夹(Application Folder)中添加项目路径

C语言实现推箱子 (数据库连接+项目打包发布)_第10张图片​ 因为我的图片和运行程序在同一目录下,所以这里添加文件是要全部包括

C语言实现推箱子 (数据库连接+项目打包发布)_第11张图片


第二种情况

如果运行程序和图片不在同一级目录时

C语言实现推箱子 (数据库连接+项目打包发布)_第12张图片

 在应用程序文件夹(Application Folder)中先将全部程序文件添加进来

C语言实现推箱子 (数据库连接+项目打包发布)_第13张图片

在 应用程序文件夹(Application Folder) 创建一个子文件夹

C语言实现推箱子 (数据库连接+项目打包发布)_第14张图片

将子文件夹重命名,并将全部图片添加进来

C语言实现推箱子 (数据库连接+项目打包发布)_第15张图片

添加好后在程序文件夹里便是所有的程序文件和一个images文件,在images文件中是全部的图片文件

C语言实现推箱子 (数据库连接+项目打包发布)_第16张图片

如果在imagse还有子文件夹,则需要按上面步骤继续添加同级文件夹或则子文件夹即可

5.添加项目输出

C语言实现推箱子 (数据库连接+项目打包发布)_第17张图片

选择“主输出”后点击确定,在已导入的文件目录里就会多了一个主输出

C语言实现推箱子 (数据库连接+项目打包发布)_第18张图片

右键单击主输出,创建快捷方式,可以给快捷方式改一个想要的名字(最终程序安装时名字便会以这个名字为准)。

C语言实现推箱子 (数据库连接+项目打包发布)_第19张图片

6.将快捷方式放入用户桌面中

截切快捷方式,到用户桌面(User's Desktop)中

C语言实现推箱子 (数据库连接+项目打包发布)_第20张图片​ 如下效果

C语言实现推箱子 (数据库连接+项目打包发布)_第21张图片

7.为程序选择一个图标

 找一张正方形图片,例如我的推箱子游戏的图标为

C语言实现推箱子 (数据库连接+项目打包发布)_第22张图片

将选择的图片转为ico图标,这里给大家提供一个ico图标在线制作

C语言实现推箱子 (数据库连接+项目打包发布)_第23张图片

8.将图标与快捷方式绑定

将图标放入应用程序文件夹(Application Folder)中

C语言实现推箱子 (数据库连接+项目打包发布)_第24张图片

 右击快捷方式,选择属性窗口,在icon中浏览到刚才添加的图标,一直点击OK

C语言实现推箱子 (数据库连接+项目打包发布)_第25张图片

选择安装项目,右击选择“重新生成”

C语言实现推箱子 (数据库连接+项目打包发布)_第26张图片

C语言实现推箱子 (数据库连接+项目打包发布)_第27张图片

C语言实现推箱子 (数据库连接+项目打包发布)_第28张图片

安装程序并运行

C语言实现推箱子 (数据库连接+项目打包发布)_第29张图片

C语言实现推箱子 (数据库连接+项目打包发布)_第30张图片

 运行结果简直就是三个字:棒棒棒!!!(๑•̀ㅂ•́)و✧

C语言实现推箱子 (数据库连接+项目打包发布)_第31张图片

五、不足之处

  1. 游戏未设计注册功能,登录页面过于简单
  2. 使用的是本地数据库,云端服务器会更好一点
  3. 关卡量较少

如果觉得有帮助,请点个赞支持一下吧

C语言实现推箱子 (数据库连接+项目打包发布)_第32张图片

你可能感兴趣的:(C/C++,实现各种小游戏,MySQL数据库,C/C++,c++,算法)