在旅途中,所经历的每一次挫折和伤痛都将成为你成长道路上宝贵的积累。
前言
装备系统完成后的第二天,小白悠哉着在工作上喝着水,品味着劳动后的休闲时光。不一会,眼角余光出现一抹蓝格子衬衫的身影。小白刚想装作专注工作的样子,却被李叔一巴掌扶住肩膀弄得有些不知所措。
“小白,我给你找了个简单的任务。” 李叔满脸笑容,声音里透着一股颇有些"别样"的亲切。
“啊,什么任务?” 小白虽然有些犹豫,但还是装出一副积极乐观的样子。他知道拒绝似乎也不是个选项。
“我们需要一个简单的竞技场模块。就是要有排名、积分,还有挑战和防守机制。相关文档我已经发到了你的飞书上,别担心,很简单的” 李叔热切地解释着,仿佛这就是世界上最有趣的事情。
小白勉强笑了笑,内心却在默默祈祷这个所谓的“简单任务”别让自己栽倒。正好刚学了redis,就用他来实现这个排名吧。
需求
- 每个大区都有一个竞技场玩法,竞技场拥有1000个排名,最初全部都是机器人,玩家默认无排名
- 竞技场的名次通过玩家挑战积分进行排序,分数越高,排名越高
- 战斗分为挑战和防守,挑战/防守胜利-增加自身分数,反之减少自身分数
分析
数据存储
这里使用redis进行数据的存储,当然也可使用mysql会进行存储,根据项目实际情况进行数据库的选择
- 建立redis的key,用于存储玩家的积分:由于每个大区id(zone_id)的不同,进行生成唯一的key,比如:GAME_ZONE_SIMPLE_ARENA<zone_id>
- 数据结构的选择:名次根据积分进行排序且玩家有一个唯一的标识id(uid),则可以使用有序集合进行存储玩家的积分,减少取出再排序的工作量(虽然使用容器封装好的api即可,但我们速度更快)。同一积分可以根据时间戳进行一个倍增再存储,取出来时在进行还原即可。
分布式战斗锁
进行排名战斗,为避免多个人打同一个人,造成脏数据,需要增加战斗锁,保证同一时间只能有一个人打另一个人。分布式战斗锁不需要兼容太多方面,很简单,仅需注意两点:
- 锁的唯一性
- 锁超时机制
redis的set命令可以完美解决这两个问题,示例如下:
127.0.0.1:6379> SET rank_1 1 EX 5 NX
OK
127.0.0.1:6379> SET rank_1 1 EX 5 NX
(nil)
127.0.0.1:6379> SET rank_1 1 EX 5 NX
(nil)
127.0.0.1:6379> SET rank_1 1 EX 5 NX
OK
127.0.0.1:6379> keys *
(empty list or set)
由上面redis String数据结构的set命令中,不难看出
NX:代表仅在不存在插入key的情况下才进行插入EX 5:代表该key在5秒之后过期
分布式锁的key设计至关重要,简单竞技场比较简单,锁排名即可,比如要攻打第一名,则key可设计为rank_1。具体key的设计还是需要根据具体需要来制定,我这里只是举一个例子。
功能相关协议
- 查询排行榜
- 点击查看玩家详情
- 传参
- uid 玩家唯一id,用于查询玩家详细信息和阵容
- 传参
- 战斗
- 传参
- rank 目标排名
- 传参
开发
redis数据库不需要特地去进行创建,制定好key名,在使用的时候进行插入会自行进行创建key并向key中插入指定内容,不了解可以点击右方链接了解一下redis的相关使用:一文带你搞懂redis使用过程(持续更新中)
玩家基础数据
public class SimpleArenaBean implements Serializable {
private long uid;
private int rank;
private long score;
public SimpleArenaBean(long uid, int rank, long score) {
this.uid = uid;
this.rank = rank;
this.score = score;
}
public long getUid() {
return uid;
}
public void setUid(long uid) {
this.uid = uid;
}
public int getRank() {
return rank;
}
public void setRank(int rank) {
this.rank = rank;
}
public long getScore() {
return score;
}
public void setScore(long score) {
this.score = score;
}
}
java中redis的封装使用
java中redis的封装代码点击右方链接了解使用方式: 利用 Jedis 打造高效 Redis 数据库:Java 开发者必备指南
积分更新和查询
当玩家拥有同一积分,为保证先到达这个积分的人,排名更高,需要进行下面两个方面
- 入库需要将积分结合时间戳进行倍增
- 查询需要将积分进行转化得到真实的积分
public enum SimpleArenaMgr {
Instance;
private static final String RANK_KEY = "_GAME_ZONE_SIMPLE_ARENA_1";
private static final Double RANKINGS_DEFINE_OFFSET = Math.pow(10,14);
private static final long MAX_MILLIS = (long) (Math.pow(10,13) - 1);
private static final int DEFAULT_MIN_SCORE = 1000;
public List<SimpleArenaBean> query(int min,int max){
List<SimpleArenaBean> ret = new ArrayList<>();
int rank = 1;
try(RedisSession session = GlobalRedisMgr.Instance.getSession()){
List<Tuple> tuples = session.zrevrangeWithScores(RANK_KEY, min, max);
for (Tuple tuple : tuples) {
double tempScore = tuple.getScore();
long uid = Long.parseLong(tuple.getElement());
long score = (long) (tempScore / RANKINGS_DEFINE_OFFSET);
ret.add(new SimpleArenaBean(uid,rank,score));
rank++;
}
}
return ret;
}
public void updateScore(long uid,long score,long millis){
try(RedisSession session = GlobalRedisMgr.Instance.getSession()) {
double tempScore = score;
tempScore = (tempScore * RANKINGS_DEFINE_OFFSET) + MAX_MILLIS - millis;
session.zadd(RANK_KEY,tempScore, String.valueOf(uid));
}
}
}
不难从上述代码中看出,更新时积分的倍增和查询时真实积分的转换通过RANKINGS_DEFINE_OFFSET和RANKINGS_DEFINE_OFFSET来实现
RANKINGS_DEFINE_OFFSET是一个15为的数值,用于和积分进行倍增MAX_MILLS设计为是一个13位数的最大值(9999..9),这是由于millis(毫秒)是一个13位的数值,两者进行 差值计算得出的值 加上积分和RANKINGS_DEFINE_OFFSET即可得出一个唯一的积分- 由于
RANKING_DEFINE_OFFSET和MAX_MILLIS有2位的差距,所以查询积分时,直接当入库积分与RANKINGS_DEFINE_OFFSET得到一个商就是真实积分
战斗模块
一般来说,战斗会被封装成一个公共的模块,方便多场景战斗的处理。这里先简单写一个方法介绍,之后我会写一篇关于战斗系统封装的文章。 下面例子不对战斗部分进行详细解释,仅关注于玩法的战斗部分业务。
public void battle(long uid, int rank, long millis){
// 查询对应排名的uid
long targetUid = 0;
long targetScore = 0;
boolean isSuccessLock = false;
boolean isWin = false;
try(RedisSession session = GlobalRedisMgr.Instance.getSession()){
int index = rank - 1;
List<Tuple> tuples = session.zrevrangeWithScores(RANK_KEY, index, index);
if(tuples.isEmpty()){
// todo 说明目标是机器人,根据配置找到机器人的阵容
}else {
Tuple tuple = tuples.get(0);
targetUid = Long.parseLong(tuple.getElement());
targetScore = (long) tuple.getScore();
// todo 根据target_uid查找玩家阵容 rightBattleSquad
}
// todo 根据uid获取自身阵容 leftBattleSquad
try{
isSuccessLock = tryLock(session, rank, millis);
if(!isSuccessLock){
Debug.err("上锁失败,目标正处于战斗");
return;
}
// todo 调用战斗模块的方法传入`leftBattleSquad`和`rightBattleSquad`进行战斗,获得战斗结果,根据战斗结果进行积分的变换
if(isWin){
long addValue = 0;
long delValue = 0;
// todo 根据配置计算出自身增加积分(add_value),目标及减少积分(delValue,目标不为机器人时进行减少)
session.zincrby(RANK_KEY,addValue, String.valueOf(uid));
if(targetUid > 0){
session.zincrby(RANK_KEY,delValue,String.valueOf(targetUid));
}
}else {
long addValue = 0;
long delValue = 0;
// todo 根据配置计算出目标增加积分(add_value),自身及减少积分(delValue,目标不为机器人时进行减少)
session.zincrby(RANK_KEY,delValue, String.valueOf(uid));
if(targetUid > 0){
session.zincrby(RANK_KEY,addValue,String.valueOf(targetUid));
}
}
// todo 根据业务做其他方面处理
}finally {
if(isSuccessLock){
unlock(session,rank);
}
}
}
}
实际还需要根据业务技能调整,思路并无太大区别。
最后
希望对大家有所帮助,以上内容就到这里,感谢各位看官老爷们的观看,如果觉得写得好,给个赞支持一下哈!!!