小白后端游戏开发:简单竞技场

337 阅读6分钟

在旅途中,所经历的每一次挫折和伤痛都将成为你成长道路上宝贵的积累。

前言

装备系统完成后的第二天,小白悠哉着在工作上喝着水,品味着劳动后的休闲时光。不一会,眼角余光出现一抹蓝格子衬衫的身影。小白刚想装作专注工作的样子,却被李叔一巴掌扶住肩膀弄得有些不知所措。

“小白,我给你找了个简单的任务。” 李叔满脸笑容,声音里透着一股颇有些"别样"的亲切。

“啊,什么任务?” 小白虽然有些犹豫,但还是装出一副积极乐观的样子。他知道拒绝似乎也不是个选项。

“我们需要一个简单的竞技场模块。就是要有排名、积分,还有挑战和防守机制。相关文档我已经发到了你的飞书上,别担心,很简单的” 李叔热切地解释着,仿佛这就是世界上最有趣的事情。

小白勉强笑了笑,内心却在默默祈祷这个所谓的“简单任务”别让自己栽倒。正好刚学了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_OFFSETRANKINGS_DEFINE_OFFSET来实现

  • RANKINGS_DEFINE_OFFSET是一个15为的数值,用于和积分进行倍增
  • MAX_MILLS设计为是一个13位数的最大值(9999..9),这是由于millis(毫秒)是一个13位的数值,两者进行 差值计算得出的值 加上 积分RANKINGS_DEFINE_OFFSET 即可得出一个唯一的积分
  • 由于RANKING_DEFINE_OFFSETMAX_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);
                }
            }

        }
    }

实际还需要根据业务技能调整,思路并无太大区别。

最后

希望对大家有所帮助,以上内容就到这里,感谢各位看官老爷们的观看,如果觉得写得好,给个赞支持一下哈!!!