4. (C++)利用redis或MySQL实现射击游戏排名系统

一、MySQL实现方法

假设我们要设计一款排名系统,那必然要涉及到两大类数据:武器数据和非武器的通用数据,它他通常有一个共用的属性:那就是主键唯一的,例如玩家的数字编号,通常在MySQL中是自增的无符号整数字段。

非武器的通用数据可以理解为跟武器没有任何关联的数据,例如玩家ID,昵称,签名,注册时间,登录时间等等,在MySQL中就类似如下:

武器数据这里每一个武器对应的种类就相当于MYSQL中的一个字段,大致类似如下:

这个表单数据通常会只会增加并不会出现减少的现象,如果用MYSQL来做排名系统的话,当玩家数量越来越多成几何增长的时候,它会暴露出两个问题:

1、效率过低:大量的select与update语句就可能会显得非常臃肿;

2、不易扩展:如果需要实时增加新的武器数据类型(字段),可能会不方面

这时候如果用redis来做数据存储就会显示格外合适,仿佛天生就是用来干这个事情的!

二、用Redis实现方法

1、存储前字符串编码转换

    通常不建议在REDIS中直接存储明文字符串,建议采用网页编码来存储,转换函数源码如下:

// 十六进制字符表
const UCHAR			g_szHexTable[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };

//
// 获取网页编码
//
int getUrlCode(const UCHAR *pszBuff, const int nSize, char *pszOutput, const int nMaxSize)
{
	int		i = 0;
	char	*pszTemp = NULL;

	if (pszBuff == NULL || nSize == 0)
		return 0;

	if (pszOutput == NULL || nMaxSize == 0)
		return 0;

	memset(pszOutput, 0x00, nMaxSize);
	pszTemp = pszOutput;

	for (int i = 0; i < nSize; i++)
	{
		pszTemp[0] = '%';
		pszTemp[1] = '%';
		pszTemp[2] = g_szHexTable[(pszBuff[i] >> 4) & 0x0F];
		pszTemp[3] = g_szHexTable[pszBuff[i] & 0x0F];
		pszTemp += 4;
	}

	return nSize * 4;
}

// 测试函数
void test_url_code()
{
	UCHAR	szInput[] = "我要打10个!";
	char	szOutput[128] = { 0x00 };

	getUrlCode(szInput, strlen((char *)szInput), szOutput, 128);
	printf("%s\n", szOutput);
}

2、用户表操作

    我们可以采用Redis Hset 命令用于为哈希表中的字段赋值;如果哈希表不存在,一个新的哈希表被创建并进行 HSET 操作;如果字段已经存在于哈希表中,旧值将被覆;

HSET KEY_NAME FIELD VALUE 

KEY_NAME: 键值
FIELD:    字段
VALUE:    值(字符传、数字、浮点)

我们在KEY_NAME这里键值的时候,用一个组合方法来区分:

// KEY_NAME = type : id
#define STATS_ALL		"a"		// 总榜 a:1
#define STATS_YEAR		"y"		// 年榜 y2022:1
#define STATS_MONTH		"m"		// 月榜 m202205:1
#define STATS_DAY		"d"		// 日榜 d20220501:1
#define STATS_SESSION	"s"		// 季榜 s1_2022:1
#define STATS_USER		"user"	// 用户信息 user:1

    那么SQL中修改的语句修改用户名的时候,像这样的:

UPDATE dbuser SET name = 'aa' WHERE index = 1;
UPDATE dbuser SET signature = '我要打10个!' WHERE index = 1;
UPDATE dbuser SET regdate = '2023/10/1' WHERE index = 1;

    而在Redis中应该像这样:

// 注意这里的KEY是组合出来的

// 设置玩家ID=1的游戏昵称为"aa"
HSET user:1 name %%61%%61

// 设置玩家ID=1的游戏昵称为"我要打10个!"
HSET user:1 signature %%CE%%D2%%D2%%AA%%B4%%F2%%31%%30%%B8%%F6%%A3%%A1

// 设置玩家ID=1的注册时间为"2023/10/1"
HSET user:1 regdate %%32%%30%%32%%33%%2F%%31%%30%%2F%%31

// 设置玩家ID=1的经验值时间为100
HSET user:1 exp 100


// 上面redis命令也可以优化成下面的一句执行完
HMSET user:1 name %%61%%61 signature %%CE%%D2%%D2%%AA%%B4%%F2%%31%%30%%B8%%F6%%A3%%A1 regdate %%32%%30%%32%%33%%2F%%31%%30%%2F%%31 exp 100

C++代码片段如下:

	wsprintf(szCommand, /*HSET %s:%u %s %s*/XorStr<0xAB, 17, 0x72ACBFF2>("\xE3\xFF\xE8\xFA\x8F\x95\xC2\x88\x96\xC1\x95\x93\xC4\x98\x9C\xC9" + 0x72ACBFF2).s, STATS_USER, pInfo->m_nDbUid, g_szUserInfo[nPos], szValue);

	reply = (redisReply *)redisCommand(pRedis, szCommand);
	if (NULL == reply) goto STEP_END;
	if (REDIS_REPLY_ERROR == reply->type)goto STEP_END;
	bRet = TRUE;

STEP_END:
	if (NULL != reply)
	{
		freeReplyObject(reply);
		reply = NULL;
	}

3、武器数据操作

    在这方面数据操作就比较容易实现了,而且能随时扩充字段个数,不像SQL那样固定的个数;另外武器的字段的数值操作都是自加来累计,而不会产生添加或删除操作,比如当网关服务器提交一条玩家ID=1,武器=2,累计杀敌总数自加3的命令时,通常对应的SQL语句如下:

// 修改总榜数据
UPDATE dbweapon_all SET kill = kill + 3 where index = 1 AND wid = 2;

// 修改年榜数据
UPDATE dbweapon_2023 SET kill = kill + 3 where index = 1 AND wid = 2;

// 修改赛季榜数据
UPDATE dbweapon_s1_2023 SET kill = kill + 3 where index = 1 AND wid = 2;

    而在Redis中应该像这样:

// HINCRBY命令原型
// 参考地址:https://www.runoob.com/redis/hashes-hincrby.html
HINCRBY KEY_NAME FIELD_NAME INCR_BY_NUMBER

// redis执行3条命令 
HINCRBY a:1 1:kill 3
HINCRBY y2023:1 1:kill 3
HINCRBY s1_2023:1 1:kill 3

键值KEY_NAME注释:
a:1       代表总榜
y2023:1   外表年榜
s1_2023:1 代表2023第一赛季榜

字段FIELD_NAME注释:
1:kill    武器编号为1,类型为kill

字段INCR_BY_NUMBER代表自加或自减数值

4、MySQL修改积分查询排名

    通常来说需要写一个复杂的MySQL语句来计算玩家的积分,大致如下:

// 修改积分
UPDATE SET db_user exp = exp + 3.0 WHERE index = 1;

// 查询积分
SELECT exp FROM db_user WHERE index = 1;

// 查询排名
SELECT COUNT(*) + 1 AS rank 
FROM db_user 
WHERE exp >= (SELECT exp FROM db_user WHERE index = 1);

而在redis中本身就有了现成的,通常在网关中处理武器数据时,会同时修改该榜单的积分值

5、Redis Zincrby 修改积分

    Redis Zincrby 命令对有序集合中指定成员的分数加上增量 increment,可以通过传递一个负数值 increment ,让分数减去相应的值,比如 ZINCRBY key -5 member ,就是让 member 的 score 值减去 5 ;当 key 不存在,或分数不是 key 的成员时, ZINCRBY key increment member 等同于 ZADD key increment member;当 key 不是有序集类型时,返回一个错误;分数值可以是整数值或双精度浮点数。

命令原型如下:

ZINCRBY key increment member

key        同上面的武器榜单
increment  自增或自减的积分
member     玩家id


// 假设1号武器杀敌一次累计3分,那对应命令如下:
ZINCRBY a:1 3.0 1
ZINCRBY y2023:1 3.0 1
ZINCRBY s1_2023:1 3.0 1

C++代码片段如下:

	// 总榜记录
	wsprintf(szCommand, /*ZINCRBY %s %s %u*/XorStr<0x09, 17, 0x5A9256F5>("\x53\x43\x45\x4F\x5F\x4C\x56\x30\x34\x61\x33\x31\x66\x36\x32\x6D" + 0x5A9256F5).s, STATS_ALL, szScore, nDbUid);

	bRet = redisGetReply(m_pRedis, (void **)&reply);
	if (bRet != REDIS_OK)
		goto STEP_END;

	if (reply)
	{
		freeReplyObject(reply);
		reply = NULL;
	}

6、Redis Zscore 查询积分

    Redis 有序集合(sorted set),Redis Zscore 命令返回有序集中,成员的分数值。 如果成员元素不是有序集 key 的成员,或 key 不存在,返回 nil 。

命令原型如下:

ZSCORE key member

key        同上面的武器榜单
member     玩家id


// 查询不同榜单的积分命令:
ZSCORE a:1 1
ZSCORE y2023:1 1
ZSCORE s1_2023:1 1

C++操作代码片段如下:

	// 获取玩家积分(插件中只获取总榜记录)
	wsprintf(szCommand, /*ZSCORE %s %u*/XorStr<0x40, 13, 0xF87B9E21>("\x1A\x12\x01\x0C\x16\x00\x66\x62\x3B\x69\x6F\x3E" + 0xF87B9E21).s, STATS_ALL, nDbUid);
	reply = (redisReply *)redisCommand(pRedis, szCommand);
	if (NULL == reply) goto STEP_END;
	if (REDIS_REPLY_ERROR == reply->type)goto STEP_END;
	if (REDIS_REPLY_STRING == reply->type)
	{
		fScore = strtod(reply->str, NULL);
	}
	if (NULL != reply)
	{
		freeReplyObject(reply);
		reply = NULL;
	}

7、Redis Zrevrank 查询排名

    Redis 有序集合(sorted set),Redis Zrevrank 命令返回有序集中成员的排名。其中有序集成员按分数值递减(从大到小)排序;排名以 0 为底,也就是说, 分数值最大的成员排名为 0 ;使用 ZRANK 命令可以获得成员按分数值递增(从小到大)排列的排名。

命令原型如下:

Zrevrank key member

key        同上面的武器榜单
member     玩家id


// 查询不同榜单的排名命令:
ZREVRANK a:1 1
ZREVRANK y2023:1 1
ZREVRANK s1_2023:1 1

C++操作代码片段如下:

	// 获取在玩家排名(插件中只获取总榜记录)
	wsprintf(szCommand, /*ZREVRANK %s %u*/XorStr<0x01, 15, 0x9CCD219A>("\x5B\x50\x46\x52\x57\x47\x49\x43\x29\x2F\x78\x2C\x28\x7B" + 0x9CCD219A).s, STATS_ALL, nDbUid);
	reply = (redisReply *)redisCommand(pRedis, szCommand);
	if (NULL == reply) goto STEP_END;
	if (REDIS_REPLY_ERROR == reply->type)goto STEP_END;
	if (REDIS_REPLY_NIL == reply->type)
	{
		nRank = pServer->m_nMaxPlayers;
		bRet = TRUE;
	}
	if (REDIS_REPLY_INTEGER == reply->type)
	{
		nRank = (reply->integer & 0xFFFFFFFF) + 1;
		bRet = TRUE;
	}

	if (NULL != reply)
	{
		freeReplyObject(reply);
		reply = NULL;
	}

三、总结避坑

1、尽量不要使用固定字符串做KEY

在编写排名的时候,一般武器数据的KEY中不会用到像"kill"这种字符类型,通常用一个数字代替方面扩展时,只需要修改enum宏即可,武器 + 数据类型 来拼接 %u:%u 的KEY_NAME,类似下面这种:

// push数据类别				// 命令     字符	积分
enum EnumPushType{					
	EPT_UNKNOW = '@',		// 未知		64 @	+0
	EPT_KILL,				// 杀敌		65 A	+2
	EPT_SHOT,				// 射击		66 B	+0
	EPT_HEADSHOT,			// 爆头		67 C	+1
	EPT_HIT,				// 击中		68 D	+0

	EPT_DAMAGE,				// 伤害		69 E	+0
	EPT_DEATH,				// 死亡		70 F	-2
	EPT_FIRSTKILL,			// 首杀		71 G	+1
	EPT_FIRSTDEATH,			// 首死		72 H	+0
	EPT_BOMB_DEFUSION,		// 拆除C4	73 I	+2

	EPT_BOMB_PLANTING,		// 安装C4	74 J	+2
	EPT_TIME_ONLINE,		// 在线		75 K	+0  每秒+0.002分(每小时7.2分)

	EPT_KILL_WORLD,			// 摔死		76 L	+0
	EPT_KILL_SELF,			// 自杀次数	77 M	+0

	EPT_MAX_PLAYER,			// 最大玩家 78 N	+0
	EPT_RANK,				// 当前排名 79 O	+0
	EPT_SCORE,				// 当前积分 80 P	+0

	// 身体伤害
	EPT_DMAGE_NONE,			// 击中空枪 81 Q	+0
	EPT_DMAGE_HEAD,			// 击中头部 82 R	+0
	EPT_DMAGE_CHEST,		// 击中胸部 83 S	+0
	EPT_DMAGE_STOMACH,		// 击中胃部 84 T	+0
	EPT_DMAGE_LEFTARM,		// 击中左臂 85 U	+0
	EPT_DMAGE_RIGHTARM,		// 击中右臂 86 V	+0
	EPT_DMAGE_LEFTEG,		// 击中左脚 87 W	+0
	EPT_DMAGE_RIGHTEG,		// 击中右脚 88 X	+0
	EPT_DMAGE_SHIELD,		// 击中盾牌 89 Y	+0

	// 武器+BKILL
	EPT_BKILL,				// 被击杀	90 Z	+0
	EPT_BHEAD,				// 被爆头	91 [	+0
	
	// 击中次数
	EPT_HIT_NONE,			// 击中空枪 92 \	+0
	EPT_HIT_HEAD,			// 击中头部 93 ]	+0
	EPT_HIT_CHEST,			// 击中胸部 94 ^	+0
	EPT_HIT_STOMACH,		// 击中胃部 95 _	+0
	EPT_HIT_LEFTARM,		// 击中左臂 96 `	+0
	EPT_HIT_RIGHTARM,		// 击中右臂 97 a	+0
	EPT_HIT_LEFTEG,			// 击中左脚 98 b	+0
	EPT_HIT_RIGHTEG,		// 击中右脚 99 c	+0
	EPT_HIT_SHIELD,			// 击中盾牌 100 d	+0

	// 混战参数 add by MT 2023-09-30
	EPT_ROUND,				// 总回合
	EPT_RWIN_T,				// 回合:T杀完胜利
	EPT_RWIN_BOOM,			// 回合:T爆炸胜利
	EPT_RWIN_CT,			// 回合:CT杀完胜利
	EPT_RWIN_DEFUSE,		// 回合:CT爆炸胜利
	EPT_RWIN_SAVED,			// 回合:CT时间结束胜利
	EPT_RWIN_RESCUE,		// 回合:CT解救人质胜利
	EPT_RWIN_NOT_RESCUE,	// 回合:CT未解救人质胜利
	EPT_RWIN_TYPE1,			// 回合:保留胜利1
	EPT_RWIN_TYPE2,			// 回合:保留胜利2

	EPT_RLOSE,				// 失败回合
	EPT_REVEN,				// 平局回合

	// 比赛参数
	EPT_SESSION,			// 总场次
	EPT_SWIN,				// 胜利场次
	EPT_SLOSE,				// 失败场次
	EPT_SEVEN,				// 平局场次

	EPT_MVP,				// 最佳次数
	EPT_RWS,				// 每局贡献评分(伤害占比)

	// 每回合杀敌统计
	EPT_KILL_0,				// 0K 酱油局
	EPT_KILL_1,				// 1K
	EPT_KILL_2,				// 2K
	EPT_KILL_3,				// 3K
	EPT_KILL_4,				// 3K
	EPT_KILL_5,				// 5K
	EPT_KILL_6,				// 6K
	EPT_KILL_7,				// 7K
	EPT_KILL_8,				// 8K
	EPT_KILL_9,				// 9K
	EPT_KILL_10,			// 10K
	EPT_KILL_11,			// 11K
	EPT_KILL_12,			// 12K
	EPT_KILL_13,			// 13K
	EPT_KILL_14,			// 14K
	EPT_KILL_15,			// 15K
	EPT_KILL_16,			// 16K

	// 残局统计
	EPT_1V1,				// 1v1
	EPT_1V2,				// 1v2
	EPT_1V3,				// 1v3
	EPT_1V4,				// 1v4
	EPT_1V5,				// 1v5
	EPT_1V6,				// 1v6
	EPT_1V7,				// 1v7
	EPT_1V8,				// 1v8
	EPT_1V9,				// 1v9
	EPT_1V10,				// 1v10
	EPT_1V11,				// 1v11
	EPT_1V12,				// 1v12
	EPT_1V13,				// 1v13
	EPT_1V14,				// 1v14
	EPT_1V15,				// 1v15
	EPT_1V16,				// 1v16

	// 残局信息
	EPT_1ROUND,				// 残局场次
	EPT_1RWIN,				// 残局胜利

	EPT_ASSIST,				// 助攻次数
	EPT_ADR,				// 场均实际伤害占比累计

	// 穿墙信息
	EPT_WALL_HIT,			// 穿墙累计命中次数
	EPT_WALL_DAMAGE,		// 穿墙累计射击伤害
	EPT_WALL_HEAD,			// 穿墙累计爆头次数
	EPT_WALL_KILL,			// 穿墙累计击杀次数

	EPT_BWALL_HIT,			// 被穿墙累计命中次数
	EPT_BWALL_DAMAGE,		// 被穿墙累计射击伤害
	EPT_BWALL_HEAD,			// 被穿墙累计爆头次数
	EPT_BWALL_KILL,			// 被穿墙累计击杀次数
};

 这一个宏再对应一个double数组来计算得分,如此只需要一个这样的函数作为网关总接口即可:

// 提交排名数据
UINT redis_PushCommand(REDIS_SERVER *pServer, const UINT nDataType, const UINT nWeapon, UINT nDbUid, UINT nValue);


2、尽量用事务方式读写

最后注意一下用事务处理的方法操作,可以加快读写效率,普通的10倍以上,示例代码如下:

	// 新版采用事务方式读取,加快速度
	for (i = 0; i < MAX_WEAPONS; i++)
	{
		nWeapon = i + EPT_UNKNOW;
		for (j = 0; j < MAX_OPT_COUNT; j++)
		{
			wsprintf(szCommand, /*HGET %s:%u %u:%u*/XorStr<0x18, 17, 0xF6DE906E>("\x50\x5E\x5F\x4F\x3C\x38\x6D\x25\x05\x54\x02\x06\x51\x1F\x03\x52" + 0xF6DE906E).s, STATS_ALL, nDbUid, , EPT_UNKNOW + j, nWeapon);
			redisAppendCommand(pRedis, szCommand); // 事务读取
		}
	}

    // 遍历所有武器数据
	for (i = 0; i < MAX_WEAPONS; i++)
	{
		// 遍历所有参数
		for (j = 0; j < MAX_OPT_COUNT; j++)
		{
			nRet = redisGetReply(pRedis, (void **)&reply);
			if (nRet != REDIS_OK)
				continue;

			if (reply->type == REDIS_REPLY_STRING)
			{
				sData.m_nValue[i][j] = atoi(reply->str);
			}
			if (reply->type == REDIS_REPLY_INTEGER)
			{
				sData.m_nValue[i][j] = reply->integer & 0xFFFFFFFF;
			}
		}
	}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

汪宁宇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值