项目实战 - 推箱子优化
数据库表设计
- 用户表
表名 | 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代表通关 | |||
mysql> create database box_man; #创建数据库box_man mysql> mysql> create table users( #创建用户表 id int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT, username varchar(64) NOT NULL UNIQUE, password varchar(32) NOT NULL , level_id int default 1 ); mysql> create table levels( #创建关卡表 id int NOT NULL PRIMARY KEY default 1, name varchar(64) NOT NULL UNIQUE, map_row int NOT NULL, map_column int NOT NULL, map_data varchar(4096) NOT NULL, next_level_id int default 0 ); mysql> insert into users values(1000, 'martin', md5('123456qweQWE'), 1); mysql> insert into levels values(1, '牛刀小试', 9, 12, '0,0,0,0,0,0,0,0,0,0,0,0|0,1,0,1,1,1,1,1,1,1,0,0|0,1,4,1,0,2,1,0,2,1,0,0|0,1,0,1,0,1,0,0,1,1,1,0|0,1,0,2, 0,1,1,4,1,1,1,0|0,1,1,1,0,3,1,1,1,4,1,0|0,1,2,1,1,4,1,1,1,1,1,0|0,1,0,0,1,0,1,1,0,0,1,0|0,0,0,0,0,0,0,0,0,0, 0,0',0);
|
代码优化
- 登陆认证
database.cpp
#include "database.h" #include <mysql.h> #include <stdio.h> #define DB_NAME "box_man" #define DB_HOST "127.0.0.1" #define DB_PORT 3306 #define DB_USER "root" #define DB_USER_PASSWD "123456qweQWE" static bool connect_db(MYSQL& mysql); /*************************************************** *功能:通过用户名和密码从数据库获取用户信息 *输入: * user - 用户信息结构体 * *返回值: * 获取成功返回true, 失败false ***************************************************/ bool fetch_user_info(userinfo& user) { MYSQL mysql; MYSQL_RES* res; //查询结果集 MYSQL_ROW row; //记录结构体 char sql[256]; bool ret = false; //1.连接到数据库 if (connect_db(mysql) == false) { return false; } //2.根据用户名和密码获取用户信息(id, level_id) snprintf(sql, 256, "select id, level_id from users where username='%s' and password=md5('%s');", user.username.c_str(), user.passwd.c_str()); ret = mysql_query(&mysql, sql); //成功返回0 if (ret) { printf("数据库查询出错,%s 错误原因: %s\n", sql, mysql_error(&mysql)); mysql_close(&mysql); return false; } //3.获取结果 res = mysql_store_result(&mysql); row = mysql_fetch_row(res); if (row == NULL) {//没有查找到记录 mysql_free_result(res); mysql_close(&mysql); return false; } user.id = atoi(row[0]); user.level_id = atoi(row[1]); printf("userid: %d level_id: %d\n", user.id, user.level_id); //打印ID //4.返回结果 //释放结果集 mysql_free_result(res); //关闭数据库 mysql_close(&mysql); return true; } bool connect_db(MYSQL& mysql) { //1.初始化数据库句柄 mysql_init(&mysql); //2.设置字符编码 mysql_options(&mysql, MYSQL_SET_CHARSET_NAME, "gbk"); //3.连接数据库 if (mysql_real_connect(&mysql, DB_HOST, DB_USER, DB_USER_PASSWD, DB_NAME, DB_PORT, NULL, 0) == NULL) { printf("数据库连接出错, 错误原因: %s\n", mysql_error(&mysql)); return false; } return true; } |
database.h
#pragma once #include <string> using namespace std; //用户信息 typedef struct _userinfo{ int id; //用户id string username; //用户名 string passwd; //密码 int level_id; //关卡id }userinfo; bool fetch_user_info(userinfo &user); |
boxman.cpp
bool login(userinfo& user) { int times = 0; bool ret = false; do{ cout << "请输入用户名: "; cin >> user.username; cout << "请输入密码: "; cin >> user.passwd; //返回 bool ,成功返回true ,失败返回false . ret = fetch_user_info(user); times++; if (times >= MAX_RETRY_TIMES) { break; } if (ret == false) { cout << "登陆失败,请重新输入!" << endl; } } while (!ret); return ret; } |
- 获取关卡
//database.h typedef struct _levelinfo { int id; //关卡的id string name; //关卡的名字 int map_row; //地图总行数 int map_column; //地图总列数 string map_data; //二维地图数据 int next_level; //下一关卡的id }levelinfo; bool fetch_level_info(levelinfo &level, int level_id); //database.cpp /*************************************************** *功能:根据关卡id 获取完整的关卡信息(如: 地图,下一关等) *输入: * level - 保存关卡信息的结构体变量 * level_id - 要获取详细关卡信息的关卡id *返回值: * 获取成功返回true, 失败false ***************************************************/ bool fetch_level_info(levelinfo& level, int level_id) { MYSQL mysql; MYSQL_RES* res; //查询结果集 MYSQL_ROW row; //记录结构体 char sql[256]; bool ret = false; //1.连接到数据库 if (connect_db(mysql) == false) { return false; } //2.根据关卡id查询数据库获取关卡地图信息 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\n", sql, mysql_error(&mysql)); mysql_close(&mysql); return false; } //3.获取结果 res = mysql_store_result(&mysql); row = mysql_fetch_row(res); if (row == NULL) {//没有查找到记录 mysql_free_result(res); mysql_close(&mysql); return false; } 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[5]); if(debug) 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);
//释放结果集 mysql_free_result(res); //关闭数据库 mysql_close(&mysql); return true; } |
- 地图适配
//database.h bool transform_map_db2array(levelinfo &level, int map[LINE][COLUMN]); //database.cpp bool transform_map_db2array(levelinfo& level, int map[LINE][COLUMN]) { if (level.map_row > LINE || level.map_column > 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); if (end < 0) { end = level.map_data.length(); } if (start >= end) break; string line = level.map_data.substr(start, end - start); printf("get line: %s\n", line.c_str()); //对行地图数据进行解析 char *next_token = NULL; char* item = strtok_s((char*)line.c_str(), ",", &next_token); column = 0; while (item && column < level.map_column) { printf("%s ", item); map[row][column] = atoi(item); column++; item = strtok_s(NULL, ",", &next_token); } if (column < level.map_column) { printf("地图数据解析出错,终止!\n"); return false; } printf("\n"); row++; if (row >= level.map_row) { break; } start = end + 1; } while (1 == 1); if (row < level.map_row) { printf("地图行数少于设定, %d(need: %d),终止!\n", row, level.map_row); return false; } return true; } //boxman.cpp //把数据库中的地图数据转换到map 中 ret = transform_map_db2array(level, map); |
- 下一关跳转
//database.h bool update_user_level(userinfo& user, int next_level_id); //database.cpp bool update_user_level(userinfo& user, int next_level_id) { MYSQL mysql; MYSQL_RES* res; //查询结果集 MYSQL_ROW row; //记录结构体 char sql[256]; bool ret = false; //1.连接到数据库 if (connect_db(mysql) == false) { return false; } //2.根据用户id 更新下一关的level_id 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; } //关闭数据库 mysql_close(&mysql); return true; } //boxman.cpp //...............前面省略N行代码.................... void gameNextScene(IMAGE* bg) { putimage(0, 0, bg); settextcolor(WHITE); RECT rec = { 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT }; settextstyle(20, 0, _T("宋体")); drawtext(_T("恭喜您~ \n此关挑战成功,任意键跳转到下一关!"), &rec, DT_CENTER | DT_VCENTER | DT_SINGLELINE); ::system("pause"); cleardevice(); } //...............中间省略N行代码.................... if (isGameOver()) { if (level.next_level < 1) { gameOverScene(&bg_img); quit = true; break; } gameNextScene(&bg_img); //更新用户下一关的关卡信息 if (update_user_level(user, level.next_level)) { user.level_id = level.next_level; } break; //quit = true; } |
- 实战 棋牌游戏数据库开发
- 用户表
表名 | UserInfo | ||||
字段名 | 类型 | 是否为空 | 默认值 | 主、外键 | 备注 |
id | int(11) | NOT | 1,自增长 | PK,FK | 用户id |
userName | varchar(64) | NOT | 用户名:英文字符、数字和特殊符号的组合 | ||
nickName | varchar(64) | NOT | 昵称 | ||
passWord | varchar(32) | NOT | 密码:英文字符、数字和特殊符号的组合,8-16位 | ||
gender | enum (‘男’,’女’) | 性别,男/女 | |||
userRight | bit(64) | NOT | 基本权限 | 玩家权限标志,每一位代表一种权限, 比如旁观权限,大厅公聊权限,私聊权限等。 | |
faceID | int(11) | 默认头像 | 玩家头像索引号码 | ||
registerDate | datetime | NOT | 玩家注册日期 | ||
LastLogonDate | datetime | NOT | 玩家最后登陆日期 | ||
lastLogonIp | varchar(32) | NOT | 玩家最后登陆IP | ||
logonTimes | int(11) | NOT | 玩家登陆总次数 | ||
mobile | char(11) | NOT | 玩家注册手机 | ||
age | tinyint(3) | 玩家年龄 |
- 游戏信息表
表名 | GameInfo | ||||
字段名 | 类型 | 是否为空 | 默认值 | 主、外键 | 备注 |
userId | int(11) | NOT | FK | 玩家标识ID,是用户ID 表的外键 | |
score | int | NOT | 0 | 玩家分数(金币数) | |
winCount | int | NOT | 0 | 游戏胜利局数 | |
LostCount | int | NOT | 0 | 游戏输局局数 | |
fleeCount | int | NOT | 0 | 游戏逃跑局数 | |
playTimeCount | int | NOT | 0 | 玩家游戏的总时长 | |
allPlayTimes | int | NOT | 0 | 玩家游戏的总次数 |
- 游戏日志表
表名 | GameLog | ||||
字段名 | 类型 | 是否为空 | 默认值 | 主、外键 | 备注 |
userId | int(11) | NOT | FK | 玩家标识ID,是用户ID 表的外键 | |
roomId | int | NOT | 房间号 | ||
kindId | int | NOT | 玩家进入的房间类型标识号码 | ||
InScore | int | NOT | 玩家进入房间开始游戏时的分值(金币数) | ||
goal | int | NOT | 游戏得分,+表示赢钱,-表示输钱 | ||
WinCount | int | NOT | 0 | 在房间中游戏胜利局数 | |
LostCount | int | NOT | 0 | 在房间中游戏失败局数 | |
clientIP | varchar(32) | NOT | 0 | 玩家登入房间的IP地址 | |
logonTime | datetime | NOT | 玩家进入房间的时间 | ||
leftTime | datetime | NOT | 玩家离开房间的时间 |
- 游戏列表信息
表名 | GameList | ||||
字段名 | 类型 | 是否为空 | 默认值 | 主、外键 | 备注 |
typeId | int | NOT | PK | 类型标识ID,是用户ID 表的外键 | |
typeName | varchar(64) | NOT | 类型名字,例如扑克类,麻将类等 | ||
enable | int | NOT | 1 | 控制游戏大厅是否显示该类型,默认为 1,即为默认显示 |
- 游戏种类信息
表名 | GameLog | ||||
字段名 | 类型 | 是否为空 | 默认值 | 主、外键 | 备注 |
kindID | int | NOT | PK | 游戏的唯一标识 | |
kindName | varchar(64) | NOT | 游戏名称, 比如德州扑克,斗地主等 | ||
typeID | int | NOT | 外键(GameTypeInfo的主键),该游戏所属类型 | ||
processName | int | NOT | 客户端进程名称 | ||
minVersion | decimal(8,2) | NOT | 客户端进程的最低版本,若是登录时发现客户端版本低于这个值,要求其更新 | ||
enable | int | NOT | 1 | 控制游戏大厅是否显示该游戏,默认为1。 |
- 附件一 作业答案
7.6 设计游戏用户信息表
回顾下Jack 要实现的用户登陆模块,具体需求如下:
- 游戏玩家访问游戏客户端,通过客户端界面输入用户名和密码
- 在游戏玩家点击”确认”后,客户端连接至数据库服务器对用户名和密码进行确认,
如果验证成功,则 玩家可以进入大厅,如果失败,则不允许进入!
俗话说得好,要开车,先修路!
请帮Jack 设计好相应的数据库表,并插入少量样例数据!
mysql> create database game_db; #创建游戏数据库game_db mysql> use game_db; #选择数据库game_db mysql> create table users ( id int(11) unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键', username varchar(64) NOT NULL UNIQUE COMMENT '用户名', password varchar(32) NOT NULL COMMENT '密码', nickname varchar(64) DEFAULT NULL COMMENT '昵称', mobile char(11) DEFAULT NULL COMMENT '手机号码', age tinyint(3) unsigned DEFAULT 18 COMMENT '年龄', idno char(18) DEFAULT NULL COMMENT '身份证号码' ); |
作用1: 验证用户的身份
作用2: 用来保存用户的基本信息
更高逼格的设计: 分成两个表,用户信息表和用户验证表
基于以下三方面的原因:
面向对象方面考虑 - 用户信息和用户验证是两种”对象”
性能方面考虑 - 登陆验证的时候列较少,查询速度快。
安全方面考虑 - 防止在查询用户信息时,把密码也直接查询出来,会容易被攻击和进行恶 意操作。
mysql> create database game_db; #创建游戏数据库game_db mysql> use game_db; #选择数据库game_db mysql> create table users ( id int(11) unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键', username varchar(64) NOT NULL UNIQUE COMMENT '用户名', nickname varchar(64) DEFAULT NULL COMMENT '昵称', mobile char(11) DEFAULT NULL COMMENT '手机号码', age tinyint(3) unsigned DEFAULT 18 COMMENT '年龄', idno char(18) DEFAULT NULL COMMENT '身份证号码' ); create table user_auths( userid int(11) unsigned NOT NULL COMMENT '外键,对应users表中的id', username varchar(64) NOT NULL UNIQUE COMMENT '用户名', password varchar(32) NOT NULL COMMENT '密码', FOREIGN KEY(userid) REFERENCES users(id) ); mysql> insert into users (username, nickname, mobile, age, idno) values('martin', '程咬金', '18684518289', 39, '430623871234567898'); mysql> insert into user_auths values(1, 'martin', '123456qweQWE'); #密码明文,不安全 mysql> insert into user_auths values(1, 'martin', md5('123456qweQWE')); #使用内部函数md5 加密,一亿年马老师都无法破解 |