游戏中会有很多数值时刻发生着变化,比如角色的攻击力、移速等会因为各种 buff 发生改变,这些 buff 的来源也可能有很多,比如自身技能、敌人技能或者队友给你加的 buff 等等,这些 buff 还可以叠加,持续时间结束之后需要移除。这些对基础数值的修改称为 modifier

为了完成 modifier 的管理,饥荒 mod 为我们提供了一个思路。


SourceModifierList

饥荒中提供了一个类 SourceModifierList 用来管理这样的数值修改,先来看看怎么用的。

饥荒中所有需要战斗的实体都依赖于一个组件 combatcombat 为角色提供了攻击力,基本公式为:攻击力 = 基础攻击 * 攻击系数。

攻击系数就是一个 SourceModifierList

-- self 是 combat 组件,self.inst 是 combat 挂载的对象
self.externaldamagemultipliers = SourceModifierList(self.inst)

默认的修改值为 1,修改方法是所有 modifier 相乘(即基础攻击力 * 1)。

添加 modifier

角色吃下电羊果冻后,使角色攻击力乘以 1.5 倍,这时就需要添加一个 modifier

-- food 是电羊果冻,用于移除时的索引
inst.components.combat.externaldamagemultipliers:AddModifier(food, 1.5)

移除 modifier

电羊果冻持续时间结束后,移除对应的 modifier,角色攻击力就会恢复正常

inst.components.combat.externaldamagemultipliers:RemoveModifier(food)

计算攻击力

计算攻击力时,只需要调用 Get() 方法

function Combat:CalcDamage()
    ...
    return basedamage * externaldamagemultipliers:Get()
end

实现

以下代码都只保留关键结构,省略了很多细节

SourceModifierList 的实现也不复杂,维护了这几个私有变量:

  • _modifiers:内部用一个字典保存每个来源(source)对应的 modifier,当然一个 source 可能对应多个 modifier,所以每个 source 又对应一个字典,字典中再用字符串 key 标识不同的 modifier。
  • _modifier:保存最终计算出的修改值
  • _fn:计算修改值的函数,默认为两两相乘。也就是最终的 _modifier 是由 _modifiers 中的所有数值相乘得到的。
  • _base:基值,默认为 1。如果 _fn 是加法就应该设为 0。
SourceModifierList = Class(function(self, inst, base_value, fn)
    self.inst = inst

    -- Private members
    self._modifiers = {}
    if base_value ~= nil then
        self._modifier = base_value
        self._base = base_value
    else
        self._modifier = 1
        self._base = 1
    end

    self._fn = fn or SourceModifierList.multiply
end)

SourceModifierList.multiply = function(a, b)
    return a * b
end

SourceModifierList.additive = function(a, b)
    return a + b
end

SourceModifierList.boolean = function(a, b)
    return a or b
end

SetModifier 时,把 modifier 存入字典,然后重新计算 _modifier 结果。

function SourceModifierList:SetModifier(source, modifier, key)
    if key == nil then
        key = "key"
    end

    self._modifiers[source] = {
        [key] = modifier
    },
    RecalculateModifier(self)
end

重新计算(RecalculateModifier)也很简单,调用 _fn 遍历所有的 modifier 计算一遍即可。

local function RecalculateModifier(inst)
    local m = inst._base
    for source, modifiers in pairs(inst._modifiers) do
        for k, v in pairs(modifiers) do
            m = inst._fn(m, v)
        end
    end
    inst._modifier = m
end

RemoveModifier 把对应项从 _modifiers 字典移除,再重新计算 _modifier

-- Key is optional if you want to remove the entire source
function SourceModifierList:RemoveModifier(source, key)
    local modifiers = self._modifiers[source]
    if key ~= nil then
        modifiers[key] = nil
        if next(modifiers) ~= nil then
            --this source still has other keys
            RecalculateModifier(self)
            return
        end
    end

    --remove the entire source
    self._modifiers[source] = nil
    RecalculateModifier(self)
end

计算 _modifier 值是在添加和移除 modifier 时执行的,因此调用 Get() 时,只是简单地返回 _modifier 而已:

function SourceModifierList:Get()
    return self._modifier
end

以上就是 SourceModifier 的基本原理,完全可以应用在其他游戏中。

饥荒中还实现了更多实用的功能,比如

  • 计算所有相同 source 的 modifier 值
  • 计算所有相同 key 的 modifier 值
  • source 对象销毁时,自动调用 RemoveModifier

这些可以根据游戏实际需求选择实现。