当前位置:首页 » 《随便一记》 » 正文

推箱子(数据库篇)_jjswift的博客

28 人参与  2022年02月24日 17:06  分类 : 《随便一记》  评论

点击全文阅读


引言:

推箱子,经典小游戏,带我们回到小时候,来一波“爷童回”。可以在数据库表中设计不一样的关卡,自己设计地图,学习的同时又能体验游戏的乐趣~

实现:

采用C++语言,使用visual studio工具,与数据库进行相连,实现用户登录与获取关卡信息功能。

用户登录后可获取当前已到达哪一关卡,接着从那一关开始继续推箱子。同时加载地图也是通过读取关卡表,支持在表中添加多种不同地图,提升游戏体验。

效果:

游戏视频链接

源代码:

github地址

目录:

一、数据库表设计
二、连接数据库与用户登录
三、初始化游戏界面
四、获取并加载地图数据
五、操作游戏
六、跳转至下一关或结束游戏

一 、数据库表设计

1.1 用户表设计

用户表用来存用户的信息,包括用户id,用户名,密码,关卡id。

字段名类型是否为空备注
idint主键自动增长
usernamevarchar(64)唯一键用户名(登录时使用)
passwordvarchar(32)NA密码(用md5加密)
level_idintNA保存用户所在的关卡

用户表里中保存的关卡id(预设为1表示第一关),当用户下次登录时,从上次玩到的关卡开始继续游戏。

创建语句:

CREATE TABLE user (
  id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
  username varchar(64) NOT NULL UNIQUE,
  password varchar(32) NOT NULL,
  level_id int DEFAULT '1');

1.2 关卡表设计

关卡表用来存放地图信息,包括地图id,地图名字,地图行数,地图列数,地图数据,下一关地图id。

字段名类型是否为空备注
idint主键地图的id
namevarchar(64)NA地图名字
map_rowintNA地图行数
map_columnintNA地图列数
map_datavarchar(32)NA地图数据
next_level_idintNA下一关的地图id

其中,通过读取表中的地图数据来加载地图,next_level_id存下一关的地图id,没有下一关存0。

创建语句:

CREATE TABLE levels(
  id int NOT NULL PRIMARY KEY,
  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');

二 、连接数据库与用户登录

2.1 连接数据库

创建了一个database文件,用来存放与数据库连接的方法,和需要与数据库交互的一些方法,database.h中为方法的声明,database.cpp中为方法的定义。
以下为连接数据库的方法,只写在database.cpp文件中:

#define DB_HOST "127.0.0.1"
#define DB_USER "root"
#define DB_PASSWD "123456!"
#define DB_NAME "push_box"
#define DB_PORT 3308

/**********************************************
 *功能:连接数据库
 *输入:
	mysql - 句柄,数据库连接的一些需要使用的方法中需用到
 *返回值:
	连接成功返回true,连接失败返回false
***********************************************/
bool connectDB(MYSQL &mysql) {
	mysql_init(&mysql);	//初始化句柄

	//设置字符编码
	mysql_options(&mysql,MYSQL_SET_CHARSET_NAME,"gbk");

	//连接数据库
	if (mysql_real_connect(&mysql, DB_HOST, DB_USER, DB_PASSWD, DB_NAME, DB_PORT, NULL, 0) == NULL) {
		printf("数据库连接失败,原因:%s",mysql_error(&mysql));
		return false;
	}

	return true;
}

其中, DB_HOST, DB_USER, DB_PASSWD, DB_NAME, DB_PORT都使用了宏定义。

2.2 实现用户登录

database文件中通过getUserInfo方法,用来获取用户信息并进行用户登录认证。

database.h:

#include "box.h"

bool getUserInfo(useInfo &user);

database.cpp:

#include <mysql.h>
#include <stdio.h>
#include "database.h"

//省略上面已写的宏定义与conactDB方法


/**********************************************
 *功能:获取用户信息
 *输入:
	user - 保存用户信息的结构体变量
 *返回值:
	获取成功返回true,获取失败返回false
***********************************************/
bool getUserInfo(useInfo &user) {
	MYSQL mysql;
	MYSQL_RES *result; //定义结果集
	MYSQL_ROW row;	//记录结构体
	char sql[256];	//将sql语句存入其中
	int ret;

	//连接到数据库
	if (!connectDB(mysql)) {
		return false;
	}
	snprintf(sql,256,"select id,level_id from user where username='%s' and password=md5('%s');", user.name.c_str(), user.pwd.c_str());
	ret=mysql_query(&mysql, sql);	//成功返回0

	if (ret) {
		printf("查询数据失败,%s,原因:%s",sql,mysql_error(&mysql));
		mysql_close(&mysql); //关闭数据库
		return false;
	}

	//获取结果
	result = mysql_store_result(&mysql);
	row = mysql_fetch_row(result);	//结果读出

	if (row == NULL) {		//没有查询到数据
		mysql_free_result(result);	//释放结果集
		mysql_close(&mysql);	//关闭数据库
		return false;
	}

	user.id = atoi(row[0]);		//atoi将字符串转为int
	user.levelID = atoi(row[1]);

	mysql_free_result(result);	//释放结果集
	mysql_close(&mysql);	//关闭数据库

	return true;
}

其中userInfo为结构体,存放在box.h头文件中,如下所写:

typedef struct _useInfo{
	int id;			//用户id
	string name;	//用户名
	string pwd;		//密码
	int levelID;	//关卡id
}useInfo;

从数据库中获取的用户数据会保存到结构体对象中。
同时main方法中的写法如下:

#include <iostream>
#include <string>
#include <Windows.h>
#include <graphics.h>
#include <conio.h>
#include <easyx.h>
#include <mysql.h>
#include "box.h"
#include "database.h"

useInfo user;	//创建user结构体对象

//用户登录方法,有四次输入密码的机会
bool login() {
//	useInfo user;
	int times = 0;
	bool ret = false;

	do {
		times++;
		cout << "请输入用户名:";
		cin >> user.name;
		cout << "请输入密码:";
		cin >> user.pwd;
		ret = getUserInfo(user);
		if (times > 4) {
			break;
		}
		if (ret == false) {
			cout << "请重新输入账号/密码!" << endl;
		}
	} while (!ret);

	return ret;
}

int main(){
	if (login() == false) {
		exit(-1);
	}
	
	return 0;
}

三 、初始化游戏界面

main.cpp文件中,创建了initGame方法,用来初始化窗口大小,加载人、箱子等图片。

更新main.cpp

#include <iostream>
#include <string>
#include <Windows.h>
#include <graphics.h>
#include <conio.h>
#include <easyx.h>
#include <mysql.h>
#include "box.h"
#include "database.h"

useInfo user;	//创建user结构体对象
//創建數組來存放不同種類的圖片
IMAGE img[ALL];
IMAGE bg_img;	//創建圖片對象

//用户登录方法,有四次输入密码的机会
//省略

//初始化游戏界面
void initGame() {
//	cleardevice();
	initgraph(BG_WIDTH, BG_HEIGTH);	//初始化窗口大小
	loadimage(&bg_img, _T("blackground.bmp"), BG_WIDTH, BG_HEIGTH, true);	//加載背景圖片
	putimage(0, 0, &bg_img);	//把圖片放到窗口上

	//加載墻,人等圖片
	loadimage(&img[WALL], _T("wall_right.bmp"), IMG_SIZE, IMG_SIZE, true);
	loadimage(&img[FLOOER], _T("floor.bmp"), IMG_SIZE, IMG_SIZE, true);
	loadimage(&img[BOX_DEC], _T("des.bmp"), IMG_SIZE, IMG_SIZE, true);
	loadimage(&img[MAN], _T("man.bmp"), IMG_SIZE, IMG_SIZE, true);
	loadimage(&img[BOX], _T("box.bmp"), IMG_SIZE, IMG_SIZE, true);
	loadimage(&img[HIT], _T("box.bmp"), IMG_SIZE, IMG_SIZE, true);

}

int main(){
	if (login() == false) {
		exit(-1);
	}
	initGame();
	
	return 0;
}

initGame方法中的窗口大小为宏定义,图片数组中墙、地板等为枚举类型,都定义在box.h文件中:

#pragma once
#include <string>

using namespace std;

#define IMG_SIZE    61
#define BG_WIDTH    800
#define BG_HEIGTH   640

//枚舉道具
enum PROP {
	WALL,
	FLOOER,
	BOX_DEC,
	MAN,
	BOX,
	HIT,
	ALL
};

四 、获取并加载地图数据

4.1 获取地图数据

在初始化界面后,需要从数据库中读取地图数据。同样也是在database文件中,通过getLevelInfo来获取关卡信息,如下所写:

//省略头文件,宏定义,conactDB方法和getUserInfo方法

/**********************************************
 *功能:获取关卡信息
 *输入:
	level - 保存关卡信息的结构体变量
	level_id - 传入的关卡id,根据id获取关卡信息
 *返回值:
	获取成功返回true,获取失败返回false
***********************************************/
bool getLevelInfo(levelInfo &level,int level_id) {
	MYSQL mysql;
	MYSQL_RES *result; //定义结果集
	MYSQL_ROW row;	//记录结构体
	char sql[256];	//将sql语句存入其中
	int ret;

	//连接到数据库
	if (!connectDB(mysql)) {
		return false;
	}
	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);	//成功返回0

	if (ret) {
		printf("查询数据失败,%s,原因:%s", sql, mysql_error(&mysql));
		mysql_close(&mysql); //关闭数据库
		return false;
	}

	//获取结果
	result = mysql_store_result(&mysql);
	row = mysql_fetch_row(result);	//结果读出

	if (row == NULL) {		//没有查询到数据
		mysql_free_result(result);	//释放结果集
		mysql_close(&mysql);	//关闭数据库
		return false;
	}

	level.name = row[0];		//atoi将字符串转为int
	level.map_row = atoi(row[1]);
	level.map_column = atoi(row[2]);
	level.map_data = row[3];
	level.next_level = atoi(row[4]);

	mysql_free_result(result);	//释放结果集
	mysql_close(&mysql);	//关闭数据库

	return true;
}

其中levelInfo为结构体,存放在box.h头文件中,如下所写:

typedef struct _levelInfo {
	int id;			 //关卡id
	string name;	 //关卡名字
	int map_row;	 //地图行数
	int map_column;  //地图列数
	string map_data; //地图数据
	int next_level;	 //下一关id
}levelInfo;

从数据库中获取的地图数据会保存到结构体对象中。

更新main.cpp中代码:

#include <iostream>
#include <string>
#include <Windows.h>
#include <graphics.h>
#include <conio.h>
#include <easyx.h>
#include <mysql.h>
#include "box.h"
#include "database.h"

useInfo user;	    //创建user结构体对象
levelInfo level;    //创建level结构体对象
//創建數組來存放不同種類的圖片
IMAGE img[ALL];
IMAGE bg_img;	//創建圖片對象

//用户登录方法,有四次输入密码的机会
//省略

//初始化游戏界面
//省略

int main(){
	if (login() == false) {
		printf("登录失败\n");
		exit(-1);
	}
	initGame();
	
	if (!getLevelInfo(level, user.levelID)) {	//	获取地图数据
		printf("获取地图数据失败\n");
		exit(-2);
	}
	
	return 0;
}

4.2 加载地图数据

读取出地图数据后,需要将数据解析出来,并保存到地图数组map中。在database中使用loadLevel方法:

/**********************************************
 *功能:加载关卡信息
 *输入:
	level - 保存关卡信息的结构体变量
 *返回值:
	获取成功返回true,获取失败返回false
***********************************************/
bool loadLevel(levelInfo &level, int map[MAP_LINE][MAP_COLUMN]) {
	if (level.map_row > MAP_LINE || level.map_column > MAP_COLUMN) {
		printf("地图设计太大了,请重新设计!\n");
		return false;
	}

	if (level.map_data.length() < 1) {
		printf("地图有误,请重新设计!\n");
		return false;
	}

	int start = 0, end = 0;
	int row = 0, column = 0;

	do {		
		end = level.map_data.find("|", start);		//从start开始找(即从字符串第一个字符开始),直到找到|符号,返回|所在位置的下标,没找到返回-1

		//当找到最后一串时,后面没有|符号了,end一定是返回-1,所以需要手动将end指向字符串结束符
		if (end < 0) {
			end = level.map_data.length();
		}

		string line = level.map_data.substr(start, end - start);	//把字符串取出来
		printf("line: %s\n", line.c_str());

		char *next_data = NULL;
		char *item = strtok_s((char *)line.c_str(), ",", &next_data);	//根据逗号解析字符串,返回

		column = 0;		//给列清零重新开始计数
		while (item && column<level.map_column) {	//一直解析到字符串结束
			printf("*item: %s\n", item);
			map[row][column] = atoi(item);
			column++;

			item = strtok_s(NULL, ",", &next_data);		//指向字符串中的下一个字符
		}

		printf("\n");

		if (column < level.map_column) {
			printf("地图数据有误,请重新设计\n");
			return false;
		}

		row++;

		if (row >= level.map_row) {		//读完每一行后退出
			break;
		}
		start = end + 1;	//指向下一串字符
	} while (1);
	
	if (row < level.map_row) {
		printf("地图数据有误,请重新设计\n");
		return false;
	}

	return true;
}

更新main.cpp文件:

#include <iostream>
#include <string>
#include <Windows.h>
#include <graphics.h>
#include <conio.h>
#include <easyx.h>
#include <mysql.h>
#include "box.h"
#include "database.h"

useInfo user;		//创建user结构体对象
levelInfo level;    //创建level结构体对象
//創建數組來存放不同種類的圖片
IMAGE img[ALL];
IMAGE bg_img;	//創建圖片對象
int map[MAP_LINE][MAP_COLUMN] = { 0 };	//创建地图数组

//用户登录方法,有四次输入密码的机会
//省略

//初始化游戏界面
//省略

int main(){
	if (login() == false) {
		printf("登录失败\n");
		exit(-1);
	}
	initGame();
	
	if (!getLevelInfo(level, user.levelID)) {	//	获取地图数据
		printf("获取地图数据失败\n");
		exit(-2);
	}
	
	if (!loadLevel(level, map)) {	//加载地图
		printf("加载地图失败\n");
		exit(-3);
	}
	return 0;
}

其中地图数组map中的行和列为宏定义,大小为50,在box.h文件中定义,更新box.h如下:

#pragma once
#include <string>

using namespace std;

#define IMG_SIZE    61
#define BG_WIDTH    800
#define BG_HEIGTH   640
#define MAP_LINE    50
#define MAP_COLUMN  50

//枚舉道具
enum PROP {
	WALL,
	FLOOER,
	BOX_DEC,
	MAN,
	BOX,
	HIT,
	ALL
};

4.3 构建地图

在解析出地图数据保存到map数组中后,需要将人物,地板构建出来,在main.cpp中进行编写,更细如下:

#include <iostream>
#include <string>
#include <Windows.h>
#include <graphics.h>
#include <conio.h>
#include <easyx.h>
#include <mysql.h>
#include "box.h"
#include "database.h"

useInfo user;		//创建user结构体对象
levelInfo level;    //创建level结构体对象
//創建數組來存放不同種類的圖片
IMAGE img[ALL];
IMAGE bg_img;	//創建圖片對象
int map[MAP_LINE][MAP_COLUMN] = { 0 };	//创建地图数组

//用户登录方法,有四次输入密码的机会
//省略

//初始化游戏界面
//省略

int main(){
	if (login() == false) {
		printf("登录失败\n");
		exit(-1);
	}
	initGame();
	
	if (!getLevelInfo(level, user.levelID)) {	//	获取地图数据
		printf("获取地图数据失败\n");
		exit(-2);
	}
	
	if (!loadLevel(level, map)) {	//加载地图
		printf("加载地图失败\n");
		exit(-3);
	}

	//構建地圖,同時記錄人和目的地所在的位置
	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] == BOX_DEC) {
				::dec.x = i;
				::dec.y = j;
			}
			putimage(WIDTH_MOVE + IMG_SIZE * j, HEIGTH_MOVE + IMG_SIZE * i, &img[map[i][j]]);
			}
		}
	return 0;
}

其中,putimage是将地图加载到界面上,WIDTH_MOVE和HEIGTH_MOVE为偏移量,IMG_SIZE为图片大小,都为宏定义,写在box.h文件中。更新box.h文件如下:

#pragma once
#include <string>

using namespace std;

#define IMG_SIZE    61
#define BG_WIDTH    800
#define BG_HEIGTH   640
#define MAP_LINE    50
#define MAP_COLUMN  50
#define WIDTH_MOVE  34
#define HEIGTH_MOVE 45
#define IMG_SIZE    61

//枚舉道具
enum PROP {
	WALL,
	FLOOER,
	BOX_DEC,
	MAN,
	BOX,
	HIT,
	ALL
};

五、操作游戏

5.1 读取键盘按键

写完以上流程,需要控制游戏人物移动,通过 _kbhit方法 来判断是否有按下键盘,若有按下则通过 _getch方法 获取键盘按下的键并以char字母返回。其中,键盘会一直被按下,应该是否循环来判断键盘事件。

更新main.cpp文件如下:

#include <iostream>
#include <string>
#include <Windows.h>
#include <graphics.h>
#include <conio.h>
#include <easyx.h>
#include <mysql.h>
#include "box.h"
#include "database.h"

useInfo user;		//创建user结构体对象
levelInfo level;    //创建level结构体对象
//創建數組來存放不同種類的圖片
IMAGE img[ALL];
IMAGE bg_img;	//創建圖片對象
int map[MAP_LINE][MAP_COLUMN] = { 0 };	//创建地图数组

//用户登录方法,有四次输入密码的机会
//省略

//初始化游戏界面
//省略

int main(){
	if (login() == false) {
		printf("登录失败\n");
		exit(-1);
	}
	initGame();
	
	if (!getLevelInfo(level, user.levelID)) {	//	获取地图数据
		printf("获取地图数据失败\n");
		exit(-2);
	}
	
	if (!loadLevel(level, map)) {	//加载地图
		printf("加载地图失败\n");
		exit(-3);
	}

	//構建地圖,同時記錄人和目的地所在的位置
	//省略
	
	//判断键盘按键进行处理
	do{
		if(_kbhit()){	//判断键盘是否有按键
			char ch = _getch();	//获取键盘按下的值,并转为char字母
		}
	}while(1);
	
	return 0;
}

5.2 实现人物移动

5.2.1 控制人物移动

以上获取键盘按键后,需要对按键进行处理,本例中使用w,s,a,d来表示人物上下左右移动,因为每次按键都要对人物进行移动,可以将人物移动的方法提取出来,传入参数为上/下/左/右。

更新main.cpp文件如下,增加controlGame方法:

#include <iostream>
#include <string>
#include <Windows.h>
#include <graphics.h>
#include <conio.h>
#include <easyx.h>
#include <mysql.h>
#include "box.h"
#include "database.h"

useInfo user;		//创建user结构体对象
levelInfo level;    //创建level结构体对象
//創建數組來存放不同種類的圖片
IMAGE img[ALL];
IMAGE bg_img;	//創建圖片對象
int map[MAP_LINE][MAP_COLUMN] = { 0 };	//创建地图数组

//用户登录方法,有四次输入密码的机会
//省略

//初始化游戏界面
//省略

//控制人物移动,传入参数为方向
void controlGame(enum direction direct){
	if(direct == UP){
		if(map[man.x][man.y-1] == FLOOER && man.y-1>0)	//判断人物前面是地板才能移动
		//移动后改变人物所在位置为地板,改变人物前所在地板为人物
		//changeMap(man.x,man.y,FLOOER);   //下一小节继续编写
	}
}

int main(){
	if (login() == false) {
		printf("登录失败\n");
		exit(-1);
	}
	initGame();
	
	if (!getLevelInfo(level, user.levelID)) {	//	获取地图数据
		printf("获取地图数据失败\n");
		exit(-2);
	}
	
	if (!loadLevel(level, map)) {	//加载地图
		printf("加载地图失败\n");
		exit(-3);
	}

	//構建地圖,同時記錄人和目的地所在的位置
	//省略
	
	//判断键盘按键进行处理
	do{
		if(_kbhit()){	//判断键盘是否有按键
			char ch = _getch();	//获取键盘按下的值,并转为char字母
			//判断人物移动方向
			if(ch == KEY_UP){
				controlGame(UP);
			}else if(ch == KEY_DOWN){
				controlGame(DOWN);
			}else if(ch == KEY_LEFT){
				controlGame(LEFT);
			}else if(ch == KEY_RIGHT){
				controlGame(RIGHT);
			}
		}
	}while(1);
	
	return 0;
}

其中,controlGame中的参数为枚举类型,在box.h文件中定义:

//定义人物移动的方向
#define KEY_UP    'w'
#define KEY_DOWN  's'
#define KEY_LEFT  'a'
#define KEY_RIGHT 'd'
#define QUIET	  'q'

//枚举方向
enum direction {
	UP,
	DOWN,
	LEFT,
	RIGHT
};

5.2.1 控制人物移动

当人物进行上下左右移动的时候,地图会发生变化,对人物前所在位置为地板还是箱子等情况进行处理,使用changeMap方法,对人物每次移动进行地图的变化。

更新main.cpp文件如下,增加changeMap方法:

#include <iostream>
#include <string>
#include <Windows.h>
#include <graphics.h>
#include <conio.h>
#include <easyx.h>
#include <mysql.h>
#include "box.h"
#include "database.h"

useInfo user;		//创建user结构体对象
levelInfo level;    //创建level结构体对象
//創建數組來存放不同種類的圖片
IMAGE img[ALL];
IMAGE bg_img;	//創建圖片對象
int map[MAP_LINE][MAP_COLUMN] = { 0 };	//创建地图数组

//用户登录方法,有四次输入密码的机会
//省略

//初始化游戏界面
//省略

/****************************
 *功能:改变地图数据
  *参数1:需改变的内容位置的x坐标
  *参数2:需改变的内容位置的y坐标
  *参数3:需改变的目标图形
*****************************/
void changeMap(int x, int y, enum PROP pro){
	map[x][y] = pro;	//将地图进行转变
	putimage(WIDTH_MOVE + IMG_SIZE * y, HEIGTH_MOVE + IMG_SIZE * x, &img[map[x][y]]);  //将需转变的图形进行加载
}

//控制人物移动,传入参数为方向
void controlGame(enum direction direct){
	if(direct == UP){
		if(map[man.x-1][man.y] == FLOOER && man.x-1>0)	//判断人物前面是地板才能移动
		//移动后改变人物所在位置为地板,改变人物前所在地板为人物
		changeMap(man.x,man.y,FLOOER);
		changeMap(man.x-1,man.y,MAN);
	}else if(direct == DOWN){
		if(map[man.x+1][man.y] == FLOOER && man.x+1<level.map_row)	//判断人物前面是地板才能移动
		//移动后改变人物所在位置为地板,改变人物前所在地板为人物
		changeMap(man.x,man.y,FLOOER);
		changeMap(man.x+1,man.y,MAN);
	}else if(direct == LEFT){
		if(map[man.x][man.y-1] == FLOOER && man.y-1>0)	//判断人物前面是地板才能移动
		//移动后改变人物所在位置为地板,改变人物前所在地板为人物
		changeMap(man.x,man.y,FLOOER);
		changeMap(man.x,man.y-1,MAN);
	}else if(direct == RIGHT){
		if(map[man.x][man.y+1] == FLOOER && man.y+1<level.map_column)	//判断人物前面是地板才能移动
		//移动后改变人物所在位置为地板,改变人物前所在地板为人物
		changeMap(man.x,man.y,FLOOER);
		changeMap(man.x+1,man.y,MAN);
	}
}

int main(){
	if (login() == false) {
		printf("登录失败\n");
		exit(-1);
	}
	initGame();
	
	if (!getLevelInfo(level, user.levelID)) {	//	获取地图数据
		printf("获取地图数据失败\n");
		exit(-2);
	}
	
	if (!loadLevel(level, map)) {	//加载地图
		printf("加载地图失败\n");
		exit(-3);
	}

	//構建地圖,同時記錄人和目的地所在的位置
	//省略
	
	//判断键盘按键进行处理
	do{
		if(_kbhit()){	//判断键盘是否有按键
			char ch = _getch();	//获取键盘按下的值,并转为char字母
			//判断人物移动方向
			if(ch == KEY_UP){
				controlGame(UP);
			}else if(ch == KEY_DOWN){
				controlGame(DOWN);
			}else if(ch == KEY_LEFT){
				controlGame(LEFT);
			}else if(ch == KEY_RIGHT){
				controlGame(RIGHT);
			}
		}
	}while(1);
	
	return 0;
}

5.3 优化

以上虽然可以对人物进行移动,但很多代码有重复,显得有些冗余,可以进行优化。并且上一小节值对人物前是地板进行处理,本小节增加人物前为箱子和目的地进行处理。

1、 首先,对controlGame方法进行优化:

//人物不能移除地图外
#define isValid(pos)  pos.x>=0 && pos.x<MAP_LINE && pos.y>=0 && pos.y <MAP_COLUMN

//控制人物移动,传入参数为方向
void controlGame(enum direction direct){
	struct _POS preMan = man;	//创建人物前对象,用来判断人前面是不是地板,进而处理人移动的情况,初始化为人所在位置
	
	switch(direct){
		case UP:
			preMan.x--;		//当人物向上移动,人物的x会-1,即对应人物上边的位置
			break;
		case DOWN:
			preMan.x++;		//当人物向下移动,人物的x会+1,即对应人物下边的位置
			break;
		case LEFT:
			preMan.y--;		//当人物向左移动,人物的y会-1,即对应人物左边的位置
			break;
		case RIGHT:
			preMan.y++;		//当人物向右移动,人物的x会+1,即对应人物右边的位置
			break;
	}
	
	//只需判断人物前进的前一步是不是地板,不用再写方向的判断
	if(map[preMan.x][preMan.y] == FLOOER && isValid(preMan))	//判断人物前面是地板才能移动
		//移动后改变人物所在位置为地板,改变人物前所在地板为人物
		changeMap(man.x,man.y,FLOOER);
		changeMap(preMan.x,preMan.y,MAN);
	}

以上优化后,判断人物前是不是箱子只需要一个判断,不需要增加方向的判断,同时也需增加防御型编程(即人物不能移到地图外)

2、 接着,对controlGame方法增加人物前为箱子与目的地的判断:

//人物不能移除地图外
#define isValid(pos)  pos.x>=0 && pos.x<MAP_LINE && pos.y>=0 && pos.y <MAP_COLUMN

//控制人物移动,传入参数为方向
void controlGame(enum direction direct){
	//创建人物前对象,用来判断人前面是不是地板,进而处理人移动的情况,初始化为人所在位置
	struct _POS preMan = man;
	//人物前为箱子,创建箱子前对象,用来判断箱子前是不是地板,进而处理人推动箱子移动的情况,初始化为人所在位置
	struct _POS preBox = man;
	//人物前为目的地,创建目的地前对象,用来判断目的地前是不是地板,进而处理人移动的情况,初始化为人所在位	
	struct _POS preHit = man;
	
	switch(direct){
		case UP:
			preMan.x--;		//当人物向上移动,人物的x会-1,即对应人物上边的位置
			preBox.x-=2;	//当人物向上移动,人物前箱子的x会-2,即对应箱子上边的位置
			preHit.x-=2;	//当人物向上移动,人物前目的地的x会-2,即对应目的地上边的位置
			break;
		case DOWN:
			preMan.x++;		//当人物向下移动,人物的x会+1,即对应人物下边位置
			preBox.x+=2;	//当人物向下移动,人物前箱子的x会+2,即对应箱子下边的位置
			preHit.x+=2;	//当人物向下移动,人物前目的地的x会+2,即对应目的地下边的位置
			break;
		case LEFT:
			preMan.y--;		//当人物向左移动,人物的y会-1,即对应人物左边边位置
			preBox.y-=2;	//当人物向左移动,人物前箱子的y会-2,即对应箱子左边的位置
			preHit.y-=2;	//当人物向左移动,人物前目的地的y会-2,即对应目的地左边的位置
			break;
		case RIGHT:
			preMan.y++;		//当人物向右移动,人物的x会+1,即对应人物右边位置
			preBox.y+=2;	//当人物向右移动,人物前箱子的y会+2,即对应箱子右边的位置
			preHit.y+=2;	//当人物向右移动,人物前目的地的y会+2,即对应目的地右边的位置
			break;
	}
	
	//只需判断人物前进的前一步是不是地板,不用再写方向的判断
	if(map[preMan.x][preMan.y] == FLOOER && isValid(preMan))	//判断人物前面是地板才能移动
		//移动后改变人物所在位置为地板,改变人物前所在地板为人物
		changeMap(man.x,man.y,FLOOER);
		changeMap(preMan.x,preMan.y,MAN);
	}
	else if (isValid(preMan) && map[preMan.x][preMan.y] == BOX) {	//判断人物前面是不是箱子
		if (map[preBox.x][preBox.y] == FLOOER) {	//判断箱子前是不是地板
			//判断人物现在是不是站在目的地上
			if (::dec.x == man.x && ::dec.y == man.y) {
				changeProp(preMan.x, preMan.y, MAN);
				changeProp(man.x, man.y, BOX_DEC);
				changeProp(preBox.x, preBox.y, BOX);
				man = preMan;
				//steps++;   //记录步数
			}
			else {
				changeProp(preBox.x, preBox.y, BOX);
				changeProp(preMan.x, preMan.y, MAN);
				changeProp(man.x, man.y, FLOOER);
				man = preMan;
				//steps++;   //记录步数
			}
		}
		else if (map[preBox.x][preBox.y] == BOX_DEC) {	//判断箱子前是不是箱子目的地
			changeProp(preBox.x, preBox.y, HIT);
			changeProp(preMan.x, preMan.y, MAN);
			changeProp(man.x, man.y, FLOOER);
			man = preMan;
			//steps++;   //记录步数
		}
	}
	//判断人前面是不是箱子目的地
	else if (isValid(preMan) && map[preMan.x][preMan.y] == BOX_DEC) {
		changeProp(preMan.x, preMan.y, MAN);
		changeProp(man.x, man.y, FLOOER);
		man = preMan;
		::dec = preMan;
		//steps++;   //记录步数
	}
	//判断人前面是不是箱子已经移动到了目的地
	else if (isValid(preMan) && map[preMan.x][preMan.y] == HIT) {
		//箱子已经移动到目的地,判断箱子前的位置是不是地板
		if (map[preHit.x][preHit.y] == FLOOER) {
			changeProp(preMan.x, preMan.y, MAN);
			changeProp(man.x, man.y, FLOOER);
			changeProp(preHit.x, preHit.y, BOX);
			man = preMan;
			::dec = preMan;
			//steps++;   //记录步数
		}
	}

六、游戏结束或跳转至下一关

处理完游戏的操作逻辑后,需要判断是否有下一关了,有则跳转并加载下一关,否则游戏结束。

6.1 游戏结束

在循环中加入gameOver方法,当所有箱子都推入到目的地中的时候,游戏结束(下一节考虑有下一关情况)。

更新main.cpp文件如下,增加gameOver方法:

#include <iostream>
#include <string>
#include <Windows.h>
#include <graphics.h>
#include <conio.h>
#include <easyx.h>
#include <mysql.h>
#include "box.h"
#include "database.h"

useInfo user;		//创建user结构体对象
levelInfo level;    //创建level结构体对象
//創建數組來存放不同種類的圖片
IMAGE img[ALL];
IMAGE bg_img;	//創建圖片對象
int map[MAP_LINE][MAP_COLUMN] = { 0 };	//创建地图数组

//用户登录方法,有四次输入密码的机会
//省略

//初始化游戏界面
//省略

//改变地图数据
//changeMap方法省略

//控制人物移动,传入参数为方向
//controlGame方法省略

bool gameOver(){
	//当地图上没有目的地的时候quiet为true
	for (int i = 0; i < level.map_row; i++) {
		for (int j = 0; j < level.map_column; j++) {
			if (map[i][j] == BOX_DEC) {
				return false;
			}
			//判断人是不是站在地图上
			if (::dec.x == man.x && ::dec.y == man.y) {
				return false;
			}
		}
	}
	return true;
}

int main(){
	bool quiet = false;	 //定义quiet变量,控制游戏结束
	if (login() == false) {
		printf("登录失败\n");
		exit(-1);
	}
	initGame();
	
	if (!getLevelInfo(level, user.levelID)) {	//	获取地图数据
		printf("获取地图数据失败\n");
		exit(-2);
	}
	
	if (!loadLevel(level, map)) {	//加载地图
		printf("加载地图失败\n");
		exit(-3);
	}

	//構建地圖,同時記錄人和目的地所在的位置
	//省略
	
	//判断键盘按键进行处理
	do{
		if(_kbhit()){	//判断键盘是否有按键
			char ch = _getch();	//获取键盘按下的值,并转为char字母
			//判断人物移动方向
			if(ch == KEY_UP){
				controlGame(UP);
			}else if(ch == KEY_DOWN){
				controlGame(DOWN);
			}else if(ch == KEY_LEFT){
				controlGame(LEFT);
			}else if(ch == KEY_RIGHT){
				controlGame(RIGHT);
			}
			if (gameOver()) {
				quiet = true;
			}
		}
		
	}while(quiet == false);		//当游戏没有结束,会一直循环
	
	closegraph();	//关闭easyx 
	return 0;
}

6.2 绘制结束画面

当游戏结束后,使用gameOverScren方法绘制结束画面。

更新main.cpp文件:

#include <iostream>
#include <string>
#include <Windows.h>
#include <graphics.h>
#include <conio.h>
#include <easyx.h>
#include <mysql.h>
#include "box.h"
#include "database.h"

useInfo user;		//创建user结构体对象
levelInfo level;    //创建level结构体对象
//創建數組來存放不同種類的圖片
IMAGE img[ALL];
IMAGE bg_img;	//創建圖片對象
int map[MAP_LINE][MAP_COLUMN] = { 0 };	//创建地图数组

//用户登录方法,有四次输入密码的机会
//省略

//初始化游戏界面
//省略

//改变地图数据
//changeMap方法省略

//控制人物移动,传入参数为方向
//controlGame方法省略

bool gameOver(){
	//当地图上没有目的地的时候quiet为true
	for (int i = 0; i < level.map_row; i++) {
		for (int j = 0; j < level.map_column; j++) {
			if (map[i][j] == BOX_DEC) {
				return false;
			}
			//判断人是不是站在地图上
			if (::dec.x == man.x && ::dec.y == man.y) {
				return false;
			}
		}
	}
	return true;
}

//绘制结束界面
void gameOverScren(IMAGE *bg, bool quiet) {
	if (quiet) {
		IMAGE victory;
		putimage(0, 0, bg);
		loadimage(&victory, _T("victory.png"), VICTORY_WIDTH, VICTORY_HEIGTH, true);	//加載背景圖片
		putimage(96, 161, &victory);

		settextcolor(WHITE);
		settextstyle(25, 0, "宋体");
		//繪製矩形
		RECT rc = { 0,0,BG_WIDTH,BG_HEIGTH - VICTORY_HEIGTH - 161 };
		//顯示文字,垂直居中,水平居中,文字顯示在同一行
		drawtext(_T("恭喜您通关了"), &rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	}
}

int main(){
	bool quiet = false;	 //定义quiet变量,控制游戏结束
	if (login() == false) {
		printf("登录失败\n");
		exit(-1);
	}
	initGame();
	
	if (!getLevelInfo(level, user.levelID)) {	//	获取地图数据
		printf("获取地图数据失败\n");
		exit(-2);
	}
	
	if (!loadLevel(level, map)) {	//加载地图
		printf("加载地图失败\n");
		exit(-3);
	}

	//構建地圖,同時記錄人和目的地所在的位置
	//省略
	
	//判断键盘按键进行处理
	do{
		if(_kbhit()){	//判断键盘是否有按键
			char ch = _getch();	//获取键盘按下的值,并转为char字母
			//判断人物移动方向
			if(ch == KEY_UP){
				controlGame(UP);
			}else if(ch == KEY_DOWN){
				controlGame(DOWN);
			}else if(ch == KEY_LEFT){
				controlGame(LEFT);
			}else if(ch == KEY_RIGHT){
				controlGame(RIGHT);
			}
			if (gameOver()) {
				quiet = true;
			}
		}
		Sleep(100);
	}while(quiet == false);		//当游戏没有结束,会一直循环
	gameOverScren(&bg_img, quiet);
	::system("pause");	//暂停
	
	closegraph();	//关闭easyx 
	return 0;
}

6.3 跳转下一关

当地图存在下一关的时候,不显示游戏结束页面,跳转到下一关继续游戏。

更新main.cpp文件,在gameOver方法中添加判断:

#include <iostream>
#include <string>
#include <Windows.h>
#include <graphics.h>
#include <conio.h>
#include <easyx.h>
#include <mysql.h>
#include "box.h"
#include "database.h"

useInfo user;		//创建user结构体对象
levelInfo level;    //创建level结构体对象
//創建數組來存放不同種類的圖片
IMAGE img[ALL];
IMAGE bg_img;	//創建圖片對象
int map[MAP_LINE][MAP_COLUMN] = { 0 };	//创建地图数组
bool haveNextLeve = false;	//定义变量,用于判断是否有下一关

//用户登录方法,有四次输入密码的机会
//省略

//初始化游戏界面
//省略

//改变地图数据
//changeMap方法省略

//控制人物移动,传入参数为方向
//controlGame方法省略

//绘制上一关结束后跳转到下一关的画面
void nextLevelMessage(IMAGE *bg) {
	//IMAGE victory;
	putimage(0, 0, bg);
	//loadimage(&victory, _T("victory.png"), VICTORY_WIDTH, VICTORY_HEIGTH, true);	//加載背景圖片
	//putimage(96, 161, &victory);

	settextcolor(WHITE);
	settextstyle(25, 0, "宋体");
	//繪製矩形
	RECT rc = { 0,0,BG_WIDTH,BG_HEIGTH };
	//顯示文字,垂直居中,水平居中,文字顯示在同一行
	drawtext(_T("恭喜您通过了这一关,点击任意键继续"), &rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	::system("pause");
	cleardevice();
}

bool gameOver(){
	//当地图上没有目的地的时候quiet为true
	for (int i = 0; i < level.map_row; i++) {
		for (int j = 0; j < level.map_column; j++) {
			if (map[i][j] == BOX_DEC) {
				return false;
			}
			//判断人是不是站在地图上
			if (::dec.x == man.x && ::dec.y == man.y) {
				return false;
			}
		}
	}
	if (level.next_level > 0) {
		nextLevelMessage(&bg_img);
		user.levelID = level.next_level;
		level = nextLevel;	//当前关卡变为下一关
		haveNextLeve = true;
	}
	else {
		haveNextLeve = false;
	}
	return true;
}

//绘制结束界面
//省略gameOverScren方法

int main(){
	bool quiet = false;	 //定义quiet变量,控制游戏结束
	if (login() == false) {
		printf("登录失败\n");
		exit(-1);
	}
	initGame();
	
	do{
		if (!getLevelInfo(level, user.levelID)) {	//	获取地图数据
			printf("获取地图数据失败\n");
			exit(-2);
		}
		
		if (!loadLevel(level, map)) {	//加载地图
			printf("加载地图失败\n");
			exit(-3);
		}
	
		//構建地圖,同時記錄人和目的地所在的位置
		//省略
		
		//判断键盘按键进行处理
		do{
			if(_kbhit()){	//判断键盘是否有按键
				char ch = _getch();	//获取键盘按下的值,并转为char字母
				//判断人物移动方向
				if(ch == KEY_UP){
					controlGame(UP);
				}else if(ch == KEY_DOWN){
					controlGame(DOWN);
				}else if(ch == KEY_LEFT){
					controlGame(LEFT);
				}else if(ch == KEY_RIGHT){
					controlGame(RIGHT);
				}
				if (gameOver()) {
					quiet = true;
				}
			}
			Sleep(100);
		}while(quiet == false);		//当游戏没有结束,会一直循环
	while(haveNextLeve);
	gameOverScren(&bg_img, quiet);
	::system("pause");	//暂停
	
	closegraph();	//关闭easyx 
	return 0;
}

因为第一次加载过地图,可以直接使用循环,第二次可以继续走原来那段代码,使用nextLevelMessage方法绘制这一关结束后跳转到下一关时的画面。

6.4 更新用户信息

当我们加载下一关的地图时,需要对数据库中的用户表中的level_id更新,更新为下一关地图的id。

更新main.cpp文件,增加updateUserLevel方法:

#include <iostream>
#include <string>
#include <Windows.h>
#include <graphics.h>
#include <conio.h>
#include <easyx.h>
#include <mysql.h>
#include "box.h"
#include "database.h"

useInfo user;		//创建user结构体对象
levelInfo level;    //创建level结构体对象
//創建數組來存放不同種類的圖片
IMAGE img[ALL];
IMAGE bg_img;	//創建圖片對象
int map[MAP_LINE][MAP_COLUMN] = { 0 };	//创建地图数组
bool haveNextLeve = false;	//定义变量,用于判断是否有下一关

//用户登录方法,有四次输入密码的机会
//省略

//初始化游戏界面
//省略

//改变地图数据
//changeMap方法省略

//控制人物移动,传入参数为方向
//controlGame方法省略

//绘制上一关结束后跳转到下一关的画面
void nextLevelMessage(IMAGE *bg) {
	//IMAGE victory;
	putimage(0, 0, bg);
	//loadimage(&victory, _T("victory.png"), VICTORY_WIDTH, VICTORY_HEIGTH, true);	//加載背景圖片
	//putimage(96, 161, &victory);

	settextcolor(WHITE);
	settextstyle(25, 0, "宋体");
	//繪製矩形
	RECT rc = { 0,0,BG_WIDTH,BG_HEIGTH };
	//顯示文字,垂直居中,水平居中,文字顯示在同一行
	drawtext(_T("恭喜您通过了这一关,点击任意键继续"), &rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	::system("pause");
	cleardevice();
}

bool gameOver(){
	//当地图上没有目的地的时候quiet为true
	for (int i = 0; i < level.map_row; i++) {
		for (int j = 0; j < level.map_column; j++) {
			if (map[i][j] == BOX_DEC) {
				return false;
			}
			//判断人是不是站在地图上
			if (::dec.x == man.x && ::dec.y == man.y) {
				return false;
			}
		}
	}
	if (level.next_level > 0) {
		updateUserLevel(level.next_level,user);  //更新用户表中用户信息
		nextLevelMessage(&bg_img);
		user.levelID = level.next_level;
		level = nextLevel;	//当前关卡变为下一关
		haveNextLeve = true;
	}
	else {
		haveNextLeve = false;
	}
	return true;
}

//绘制结束界面
//省略gameOverScren方法

int main(){
	bool quiet = false;	 //定义quiet变量,控制游戏结束
	if (login() == false) {
		printf("登录失败\n");
		exit(-1);
	}
	initGame();
	
	do{
		if (!getLevelInfo(level, user.levelID)) {	//	获取地图数据
			printf("获取地图数据失败\n");
			exit(-2);
		}
		
		if (!loadLevel(level, map)) {	//加载地图
			printf("加载地图失败\n");
			exit(-3);
		}
	
		//構建地圖,同時記錄人和目的地所在的位置
		//省略
		
		//判断键盘按键进行处理
		do{
			if(_kbhit()){	//判断键盘是否有按键
				char ch = _getch();	//获取键盘按下的值,并转为char字母
				//判断人物移动方向
				if(ch == KEY_UP){
					controlGame(UP);
				}else if(ch == KEY_DOWN){
					controlGame(DOWN);
				}else if(ch == KEY_LEFT){
					controlGame(LEFT);
				}else if(ch == KEY_RIGHT){
					controlGame(RIGHT);
				}
				if (gameOver()) {
					quiet = true;
				}
			}
			Sleep(100);
		}while(quiet == false);		//当游戏没有结束,会一直循环
	while(haveNextLeve);
	gameOverScren(&bg_img, quiet);
	::system("pause");	//暂停
	
	closegraph();	//关闭easyx 
	return 0;
}

更新database.cpp文件,增加updateUserLevel方法:

//更新用户关卡信息
bool updateUserLevel(int nextLevel, useInfo &user) {
	MYSQL mysql;
	char sql[256];	//将sql语句存入其中
	int ret;

	//连接到数据库
	if (!connectDB(mysql)) {
		return false;
	}
	snprintf(sql, 256, "update user set level_id=%d where id='%d';", nextLevel,user.id);
	ret = mysql_query(&mysql, sql);	//成功返回0

	if (ret) {
		printf("更新数据失败,%s,原因:%s", sql, mysql_error(&mysql));
		mysql_close(&mysql); //关闭数据库
		return false;
	}

	mysql_close(&mysql);	//关闭数据库

	return true;
}

结语:

以上就是个人推箱子游戏的学习与总结,有任何不足的地方,欢迎大家提出意见与建议,感谢~


点击全文阅读


本文链接:http://zhangshiyu.com/post/35295.html

人物  地图  省略  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1