在各种游戏的设计中,无论是抽卡还是强化,只要涉及到概率,都在掉落机制的范畴之内。游戏掉落对游戏体验十分重要,也是玩家和策划都关注的话题。玩家们会以欧皇和非酋来调侃难以预料的掉落结果。本文介绍和探讨游戏中掉落机制的实现。
游戏中的概率
养成中的抽卡、强化,战斗中的暴击……
玩家的迷信
游戏设计中有一个斯金纳箱原理。根据斯金纳(B.F. Skinner)的研究,小鼠会狂热地不断按下能触发随机食物奖励的按钮,而对能给稳定奖励的按钮兴趣一般。真正能带来趣味的概率控制,应该既准确实现策划意图中的概率,又存在不确定性使得玩家难以预测和掌握规律,但有着多抽一定多得的基本规律。
之所以出现欧皇和非酋,是因为原始的掉落是纯随机实现,多次掉落均是独立事件,并无关联。现实中一些沉迷中的赌徒和彩民,往往从玄学的角度将互为独立事件的多次博弈,强行理解成非独立事件,煞有其事地分析运气走势,并因此陷入宗教般的狂热。而在游戏设计中,真正的随机概率却会让非酋们理智地弃游上岸,而简单的保底和水位也会让玩家觉得索然无味、缺乏刺激。
欧皇和非酋
简单掉落的多次重复,势必会导致欧皇和非酋的出现。天选之子可能每一次出手都是特等奖。假设高产出的概率是 20%,那么在十连抽中有十次高产出和十次低产出的概率分别是:
在10万次参与中,会有一人次遇到10次高产出;同时会有1万人次什么都没得到。通过概率获得的内容不可控,会导致不同玩家的体验期望值差异极大,从而变成了拼人品的玄学游戏,会打击到整体玩家的积极性。游戏设计中引入概率,本意是增加娱乐性。因此我们需要可控的概率设计。
游戏概率的控制方式
PRD
在 Dota 中,暴击是伪概率,使用了 PRD(Pseudo Random Distribution)算法实现1,这一点广为人知。第一次攻击时的概率比较低,只要没有发生暴击,下一次攻击时发生暴击的概率就会持续增加,直到发生暴击。玩家在游戏面板中看到的暴击率,实际是整体的暴击期望概率值。相当于是动态可变概率。这种每次实际概率不同的机制,也被玩家分析利用2。
保底
为了控制十连抽这种玩法的产出,保底是一种简单的修正办法,也可以理解为一种简单的伪概率实现,或理解成赠送了一张100%中奖的奖券。算是一种对非酋的定向扶贫了。
卡池/队列
另一种控制产出的方式,是建立一个产出队列或池,计算产出时从中取走一个结果,并在适当时机补充内容。
在扑克游戏中,每个玩家抓到 Joker 的概率都是相等的。如果让一个人来逐张翻开一副扑克牌,那么在 54 次内必定翻到 Joker。但玩家也会体会到最多间隔 106 次会有一次必中,可以使用两副扑克牌。按副加入扑克牌,保证了产出的准确概率;而加入多副扑克牌,则使得产出分布更具有不可预料性。卡池的设计在削除非酋的同时,也消除了欧皇。可以结合其他机制,引入一些提前洗牌的判断,让玩家抽起来更刺激。
以扑克来举例,也更容易理解上述的概率极端情况。
- 普通掉落即洗牌后抽一张扑克,无论结果如何,都重新洗牌。这样也许你一个晚上都抽不到 Joker。
- 伪概率是用一副Joker 数量相同但总张是 80 的扑克,每当你抽到 Joker 即进行洗牌。
- 而卡池的方式是,直到抽完54张才进行洗牌。
基于卡池的设计和配置表中的权重结构,也可以非常简单地构建任何掉落系统。比如某种玩法是全服共享同一个产出队列/池,可以精确控制产出,类似商场的抽奖活动;或者游戏中的转盘抽奖,已抽到的奖励将会从奖池中移除,类似“盒子里的巧克力糖”,吃一粒少一粒,但吃完就能品尝到所有味道。
策划的补偿
策划可能再进一部在玩法上,为非酋做一些补偿。比如低级物品可以分解并兑换高级物品,算是掉落算法之外的保底。
实现
数值表的结构
游戏中每一种触发概率计算的行为,都对应着游戏策划的数值表中的一行掉落配置。一般会配置成一个表示概率的数字,或者权重数据。实际上指定概率也可以表示成一个产出权重和一个无产出权重。所以一般我们可以设计成 “产出与权重” 的结构。
使用权重可以非常方便地处理掉落。尽管按百分比显示的掉率是一个非常重要的数据,但相比起来使用权重可以更方便地管理游戏数据、实现掉落相关功能。
单次掉落的实现
将配置表中的单个产出解析为 Entity:
@Data
public class Entity<T> {
private T item;
private int weight;
}
处理掉落时,将权重计算总和,在此范围随机一个数字即可。
public static <E> Entity<E> pickOne(Entity<E>... entities) {
int total = 0;
int[] ranges = new int[entities.length];
for(i = 0; i < entities.length; i++){
Entity<E> e = entities[i];
total += entity.getWeight();
ranges[i] = total;
}
if(total > 0){
int random = RandomUtils.nextInt(total);
for(int i = 0; i < ranges.length; i++){
int range = ranges[i];
if(random < range){
return entities[i];
}
}
}
return null;
}
卡池控制
“盒子里的巧克力糖”伪代码实现:
List<Entity> pool = Arrays.toList(new Entity("1", 50), new Entity("2", 30), new Entity("3", 20));
Entity a = pickOne(pool.toArrays());
Entity b = pickOne(pool.stream().filter(e->!e.getItem().equals(a)).collect(Collections::toList).toArrays());
产出队列
伪代码实现
List<Entity> queue = Arrays.toList(new Entity("1", 50), new Entity("2", 30), new Entity("3", 20));
Random random = new Random(seed);
queue.shuffle(random);
Entity e = queue.get(times);
结语
概率 10% 跟抽十次中一次并不能划等号,抽十次中一次只是一种特例。使用权重结构、卡池与队列的设计,既能控制产出又能保持不确定性。实际中,可以进一步结合真概率、伪概率,达到更合理的平衡性。
参考
-
Random Distribution. Dota2. ↩
-
JFF比JF更强壮. dota2中关于暴击/眩晕伪随机概率的玄学及其应用. JFF的摸鱼碎碎念. 2022. ↩
评论表单