小知识,大挑战!本文正在参与“ 程序员必备小知识
本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金
有粉丝私聊我做个大生意,让我在王者荣耀中随机模式卡出自己想用的影响,我想这个粉丝一定不知道这种随机都是服务器控制的,不是客户端可以操作的,如果想黑到服务器有点难度,如果再能找到对战的局修改数据可真是太牛逼了,我只想说我不行,这1000+ 我挣不到。
抽奖也几乎是每个游戏的标配,也是氪金的最重要的地方,现在仍然记得当初在一款游戏的时候,对抽奖欲罢不能,现在的我写了好多次的抽奖,具体的实现逻辑早都熟于心,所以也没有那么多的执念。今天写一下抽奖。
1、抽奖的需求:
抽奖的实现每个游戏大都不一样,具体抽奖如何实现是跟着具体的策划需求,策划的需求大部分分为几类:
1、抽奖分免费次数和付费抽取,有的游戏会多久cd时间送一次免费次数,或者签到多少次送一次抽奖次数
2、抽奖分十连抽(多次抽取)和单抽
3、抽奖分为是否必出某个奖励和不要求必出,比如十连抽必出某个牛逼的英雄
4、多次抽取的时候是否奖励池全随机,可出现重复的奖励,还是奖励池内随机十个
5、抽奖有的还分为是否有保底次数,是否首抽的判断逻辑,保底必出英雄,首抽送固定的英雄
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、基础数据的配置
解释:
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 处理好就可以了