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

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

前言

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

粗劣的实现

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

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
本文并不打算详解其中的奥妙,这是愚蠢的复读机行为。直接上代码:

-- 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,先来看看获取随机掉落的代码:

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:

return {
    ["equipment/weapon/sword"] = 0.3,
    ["equipment/weapon/knife"] = 0.3,
    ["equipment/weapon/katana"] = 0.3,
    ["skill/flash"] = 0.1
}

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

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的需求也仅此而已,期待日后能接触到更主流的设计。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值