《DFQ》开发随录——随机掉落

  欢迎参与讨论,转载请注明出处。

前言

  随机掉落可谓时下RPG的流行设定,DFQ自然也不例外。而掉落业务自然也有其值得细说之处,不然也就不会有本文了(笑)。接下来将一步步引申出随机掉落的实现演进。

粗劣的实现

  在以往的开发生涯中,对于掉落业务,我采取了很粗劣的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
local random = math.random() -- 0-1
local pool -- drop pool
if (random < 0.1) then
pool = pools.normal
elseif (random < 0.3) then
pool = pools.rare
else
pool = pools.other
end
local index = math.random(1, #pool) -- Choice one.
local item = pool[index] -- Get an item.

  这种实现的槽点可谓数不胜数:掉落池的选取可谓暴力代码,而池中的道具也只能通过塞入相同的多份来扩充概率,对于概率的控制度很生硬。哪怕是将掉落池采取与道具相同的做法(将pools做成list)以去除暴力代码,对于概率控制度的问题依旧没有解决。且进行了两次取随机数,从概率而言并不纯粹。实际效果而言也导致了经常重复掉落,并不可取。

Alias Method

  那么如果选择将多个掉落池合而为一,使之只有一个list呢?
  如此确实能让概率纯粹了,但是对于道具概率的控制度依然很差。这个问题可以通过构建道具概率表({a = 0.1, b = 0.5, ...})以生成掉落池({a, b, b, b,...})解决。但这样生成的掉落池未免也太大了(最后可能会达上千个元素),这太不环保了,那怎么办呢?
  长达廿二年的人生经验告诉我:我们做的绝大多数事情都是前人做过的,遇到不会的问题看看前人是怎么做的就对了。果不其然,这就遇上了个合适的算法:Alias Method
  本文并不打算详解其中的奥妙,这是愚蠢的复读机行为。直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
-- items: A list of probability of item.
function Alias(items)
local len = #items
local alias = {}
local probs = {}
local small = {}
local large = {}
for n=1, len do
items[n] = items[n] * len
local tab = items[n] < 1 and small or large
table.insert(tab, n)
end
while (#small > 0 and #large > 0) do
local less = table.pop(small) -- Remove the first element of list and return it.
local more = table.pop(large)
probs[less] = items[less]
alias[less] = more
items[more] = items[more] - (1 - items[less])
local tab = items[more] < 1 and small or large
table.insert(tab, more)
end
while (#small > 0) do
probs[table.pop(small)] = 1
end
while (#large > 0) do
probs[table.pop(large)] = 1
end
return alias, probs
end

  算法的代码量并不多,也就三十多行,输入参数items为道具的的概率list({0.1, 0.1, 0.5, ...}),即代表需要配套的paths来表示对应的道具标识({"stone", "potion", "gold", ...})。至于返回值alias, probs,先来看看获取随机掉落的代码:

1
2
3
local index = math.random(1, #paths) --- 1-n
index = math.random() < probs[index] and index or alias[index]
local path = paths[index] --- Item's path.

  以上代码很好理解,首先随机获取一个道具的索引,根据索引获取到probs[index]的值,与随机数(0-1)比较,由此可见probs存放的是一种运算后的概率值。若是随机数大于概率值,索引则改为alias[index],由此可见alias存放的是一种与原索引相对应的新索引,而新的索引自然会有对应的道具。
  如此我们便可理解这套算法的做法了:为每个道具设置一个概率值以及相对应的另一个道具,随机到一个道具后,仍需二次随机进行二选一。这么做很好理解,就是将一些高概率的道具填充到一些低概率的道具里
example

  如图所示的第二项紫色的占比(概率)为1,表示不需要进行二次随机了,如此即可保证整个掉落池的概率是可以平分干净的(多出的部分就作为1概率项)。不得不说这种做法十分绝妙,完美解决了先前做法中掉落池元素过大的问题,美中不足在于需要进行二次随机,相对破坏了概率的纯粹性,但由于只是二选一,实际上效果是可接受的。

掉落池的维护

  虽说Alias Method方案的掉落池配置变得相当容易,只需如此这般填写概率值即可,再分别生成items与paths:

1
2
3
4
5
6
return {
["equipment/weapon/sword"] = 0.3,
["equipment/weapon/knife"] = 0.3,
["equipment/weapon/katana"] = 0.3,
["skill/flash"] = 0.1
}

  然而实际上掉落项的种类与数量都相当的多,并且会时常更改。所以这般直接的配置是无法满足需求的,于是演进为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
return {
skill = {
prob = 0,
item = {
flash = 1
}
},
["equipment/weapon"] = {
prob = 0.9,
item = {
sword = 0,
knife = 0,
katana = 0
}
}
}

  新配置明显就方便了不少,若是概率填写为0则表示剩余总概率的平均值sword=0 => 0.9/3 => 0.3),且填写的概率是相对于本层的(skill的总概率为0.1,故flash=1 => 0.1)。算是基于原配置进行了一波封装,可维护性大幅提升,如此便可面对变化频繁的需求了。

后记

  本文所展示的掉落业务只是基础,在业界会有复杂度远超于此的需求(与时间、职业等因素挂钩,掉落池数量等),但DFQ的需求也仅此而已,期待日后能接触到更主流的设计。