《游戏系统设计七》重现游戏的抽奖系统

1,605 阅读6分钟

小知识,大挑战!本文正在参与“  程序员必备小知识

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

image.png

有粉丝私聊我做个大生意,让我在王者荣耀中随机模式卡出自己想用的影响,我想这个粉丝一定不知道这种随机都是服务器控制的,不是客户端可以操作的,如果想黑到服务器有点难度,如果再能找到对战的局修改数据可真是太牛逼了,我只想说我不行,这1000+ 我挣不到。

抽奖也几乎是每个游戏的标配,也是氪金的最重要的地方,现在仍然记得当初在一款游戏的时候,对抽奖欲罢不能,现在的我写了好多次的抽奖,具体的实现逻辑早都熟于心,所以也没有那么多的执念。今天写一下抽奖。

1、抽奖的需求:

抽奖的实现每个游戏大都不一样,具体抽奖如何实现是跟着具体的策划需求,策划的需求大部分分为几类:

1、抽奖分免费次数和付费抽取,有的游戏会多久cd时间送一次免费次数,或者签到多少次送一次抽奖次数

2、抽奖分十连抽(多次抽取)和单抽

3、抽奖分为是否必出某个奖励和不要求必出,比如十连抽必出某个牛逼的英雄

4、多次抽取的时候是否奖励池全随机,可出现重复的奖励,还是奖励池内随机十个

5、抽奖有的还分为是否有保底次数,是否首抽的判断逻辑,保底必出英雄,首抽送固定的英雄

image.png

2、抽奖的业务逻辑整理

协议定义:我之前接触的项目是用protobuf通信的,所以我就选择protobuf定义通信格式,你可以根据自己的项目进行定义,不重要。

C 表示客户端发送给服务器的消息

S 表示服务器返回的消息

message C_DRAW{
   required int32  rewardType = 1;  // 抽奖类型 1单抽 2 10连抽
}
message S_DRAW{
   required int32  rewardType = 1;  // 抽奖类型 1单抽 2 10连抽
   required string  rewardContent = 2;  // 抽奖结果
   optional string  specialReward = 3;  // 比如十连抽送的奖励
}

协议解释:

界面的逻辑很简单,客户端发送抽奖类型到服务器,服务器进行业务处理。

服务器的逻辑分为以下几步

1、检查客户端发送的抽奖类型是否合法,如果不是定义1 和 2 ,则报错,因为有人传递非法的数据。

2、检查对应抽奖类型的次数是否足够,如果次数不足则报错

3、检查对应的抽奖类型需要的资源是否足够,如果资源不足则报错给客户端。

4、获取对应的抽奖类型的基础数据配置,检查下是否异常,异常则报错

5、执行对应的抽奖逻辑

6、将抽取的资源加入到玩家身上

7、将抽取的资源组装成消息传递给客户端,进行页面展示。

从上面的逻辑可以看到,在客户端做任何操作都不会影响最终的抽奖结果,大生意做不成了!

3、代码实现

1、基础数据的配置

image.png

解释:

id 是表示数据类型,

cost 是代表抽奖的消耗资源配置

data 是抽奖池,A_50 A代表一种资源,比如A= 2_100 表示2号资源给100个。下划线最后的一个值50 是权重

2、基础数据的解析 注意点:

1.注意数据的格式

2.注意保护基础数据完全

import javafx.util.Pair;
​
import java.util.*;
​
/**
 * 奖励池的配置
 * @author 香菜
 */
public class RewardPoolConfig {
    Map<Integer, ConfigPair> rewardConfigMap;
    public RewardPoolConfig config;
​
    public RewardPoolConfig() {
        readConfig();
    }
    private void readConfig() {
        //  TODO readFromSource();
        //  根据自己项目的不同进行
        rewardConfigMap = new HashMap<>();
        //  模拟下
        rewardConfigMap.put(1, new ConfigPair("A_1", "A_50#B_50#C_50#D_50#E_50#F_50#G_50#H_50"));
        rewardConfigMap.put(2, new ConfigPair("B_1", "A_50#B_50#C_50#D_50#E_50#F_50#G_50#H_50"));
    }
​
    public RewardPoolConfig getInstance() {
        if (config == null) {
            config = new RewardPoolConfig();
        }
        return config;
    }
​
    public Map<Integer, ConfigPair> getRewardConfigMap() {
        return rewardConfigMap;
    }
​
    /**
     * 单个奖励池的解析
     */
    static class ConfigPair {
        //  消耗的资源
        String cost;
        //  权重列表
        List<Pair<String, Integer>> weightList;
​
        public ConfigPair(String cost, String data) {
            this.cost = cost;
            String[] split = data.split("#");
            List<Pair<String, Integer>> tmpList = new ArrayList<>();
            for (String itemPair : split) {
                int i = itemPair.lastIndexOf("_");
                String reward = itemPair.substring(0, i);
                String weightStr = itemPair.substring(i + 1);
                tmpList.add(new Pair<>(reward, Integer.valueOf(weightStr)));
            }
            weightList = Collections.unmodifiableList(tmpList);
        }
​
        public List<Pair<String, Integer>> getWeightList() {
            return weightList;
        }
​
        public String getCost() {
            return cost;
        }
    }
​
    public static void main(String[] args) {
        ConfigPair a_1 = new ConfigPair("A_1", "A_50#B_50#C_50#D_50#E_50#F_50#G_50#H_50");
        System.out.println();
    }
}

1、上面的加载数据的数据源要根据具体项目的设计,从恰当的数据源读入,比如有的是用xml,Excel,或者数据库,Json等

2、可以根据需求进行扩展,比如有其他的十连抽必送,可增加字段,这里只是写核心代码。

3、权重随机算法

import com.google.common.collect.Lists;
import javafx.util.Pair;
​
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
​
/**
* 权重随机
* @author 香菜
* @param <K>
* @param <V>
*/
public class WeightRandom<K, V extends Number> {
   private TreeMap<Double, K> weightMap = new TreeMap<>();
   public WeightRandom(List<Pair<K, V>> list) throws Exception {
       if (list == null || list.size() == 0) {
           throw new Exception("list is null");
       }
       for (Pair<K, V> pair : list) {
           if (pair.getValue().doubleValue() <= 0) {
               throw new Exception("weight is wrong");
           }
           double lastWeight = this.weightMap.size() == 0 ? 0 : this.weightMap.lastKey().doubleValue();//统一转为double
           this.weightMap.put(pair.getValue().doubleValue() + lastWeight, pair.getKey());//权重累加
       }
   }
​
   public K random() {
       double randomWeight = this.weightMap.lastKey() * Math.random();
       SortedMap<Double, K> tailMap = this.weightMap.tailMap(randomWeight, false);
       return this.weightMap.get(tailMap.firstKey());
   }
​
   public static void main(String[] args) throws Exception {
       List<Pair<String, Integer>> weighList = Lists.newArrayList();
       weighList.add(new Pair<>("AAA", 10));
       weighList.add(new Pair<>("BBB", 10));
       weighList.add(new Pair<>("CCC", 10));
       weighList.add(new Pair<>("DDD", 10));
       String random = new WeightRandom<>(weighList).random();
       System.out.println(random);
   }
​
}
4、调用逻辑
import javafx.util.Pair;
import java.util.List;
import java.util.Map;
​
/**
* 模拟抽奖
* @author 香菜
*/
public class Aain {
​
   public static void main(String[] args) throws Exception {
       int rewardType = 1;
       Map<Integer, RewardPoolConfig.ConfigPair> rewardConfigMap = RewardPoolConfig.getInstance().getRewardConfigMap();
       //1、检查客户端发送的抽奖类型是否合法,如果不是定义1 和 2 ,则报错,因为有人传递非法的数据。
       if (!rewardConfigMap.containsKey(rewardType)) {
           //  非法的抽奖类型
           return;
       }
       //2、检查对应抽奖类型的次数是否足够,如果次数不足则报错
       //  TODO checkPlayerData();
       RewardPoolConfig.ConfigPair configPair = rewardConfigMap.get(rewardType);
       //3、检查对应的抽奖类型需要的资源是否足够,如果资源不足则报错给客户端。
       String cost = configPair.getCost();
       //  TODO checkResource();
       //4、获取对应的抽奖类型的基础数据配置,检查下是否异常,异常则报错
       //  在最开始的时候检测了,这不忽略
       //5、执行对应的抽奖逻辑
       List<Pair<String, Integer>> weightList = configPair.getWeightList();
       String reward = new WeightRandom<>(weightList).random();
       //6、将抽取的资源加入到玩家身上
       //  TODO addResource(reward);
       //7、将抽取的资源组装成消息传递给客户端,进行页面展示。
       //  sendResultToClient(reward)
       System.out.println(reward);
   }
​
}

5、其他功能完成

5.1 十连抽,只要改动抽奖循环就可以了

5.2 十连抽额外送,只要在基础数据配一下送的道具,最后加资源到玩家身上的时候直接加上

5.3 十连抽不重复,只要在抽完一次直接从池子里移除道具

5.4 免费次数抽完进行cd ,则直接记录玩家的抽取时间就可以了,下次判断时间间隔是否大于cd时间就可以了

4、总结:

抽奖的逻辑是相对来说比较简单,但是在项目中会和其他模块进行关联,比如任务系统的进度提升,比如要求不能出重复的武将,这些五花八门的要求只要在前置处理,把最后的Weigh 处理好就可以了