每个人,已经在卡牌自动战斗的奖励系统上工作了很长时间,使用平坦的手动调节掉落表格,想分享最终稳定的设计,主要是因为找不到关于“保证vs加权”交互的具体写作。以下代码是简化的,实际运行的代码,但这是真实的逻辑。

最困难的部分:

  • 保证奖励不会与加权的同一滚动中发生冲突——“箱子总是掉落1瓶药水”的规则不应该与稀有度权重竞争,但仍需要尊严的独特规则
  • 让表格引用子表格(一个箱子从“药水”中取出一半,另一半从“装备”中取出)而不会将整个东西变成番茄酱或在坏的引用上无限循环
  • 构建“选择1个N”草稿模式,其中所有N选项来自同一个池子,没有重复,但每个选项仍然尊严其自己的条件(玩家等级,标签等)

保证掉落先行,每个都由其自己的独立机会(0-1)完全分离于加权滚动:

List<RewardResult> results = new List<RewardResult>(); // 保证掉落—永远不会接触稀有度权重
foreach (var guaranteed in table.guaranteedDrops)
{
    if (rng.NextDouble() <= guaranteed.chance)
    {
        results.Add(new RewardResult { item = guaranteed.item, wasGuaranteed = true });
    }
}
// 加权滚动填充请求的剩余数量
int remaining = request.amount - results.Count;
for (int i = 0; i < remaining; i++)
    results.Add(RollWeighted(table, excludedIDs, depth: 0));

顺序很重要——如果你先滚动加权,后滚动保证,一个“独特”的保证项目可以与加权滚动已经选中的项目冲突,现在你正在编写你不需要的冲突解决代码。

嵌套表格只是另一个加权项。当一个被选中时,滚动递归进入子表格而不是直接返回一个项目——限制了深度以防止游戏被坏的引用(表格A指向表格B指回表格A)挂起来:

RewardResult RollWeighted(LootTable table, HashSet<string> excludedIDs, int depth)
{
    if (depth >= maxNestedDepth) // 默认8
        throw new InvalidOperationException($"嵌套表格深度超出'{table.name}' — 检查循环引用。");
    int index = selector.Select(rng); // 见加权选择器
    var entry = table.entries[index];
    if (entry.isNestedTable)
        return RollWeighted(entry.subTable, excludedIDs, depth + 1);
    return new RewardResult { item = entry.item, rollIndex = index };
}

草稿模式排除已选中的项目而不是重新滚动冲突——更简单,且在给定种子下保持可预测(如果您想要重新播放或调试特定草稿):

List<RewardResult> RollDraft(LootTable table, int selectCount, int playerLevel)
{
    var excluded = new HashSet<string>();
    var picks = new List<RewardResult>();
    for (int i = 0; i < selectCount; i++)
    {
        var eligible = table.entries.Where(e => !excluded.Contains(e.item.itemID) && e.item.requiredPlayerLevel <= playerLevel).ToList();
        var picked = WeightedPickFrom(eligible, rng);
        excluded.Add(picked.item.itemID);
        picks.Add(picked);
    }
    return picks;
}

实际滚动使用一次建造的累积权重数组,然后二分搜索每次拉取——O(n)建造,O(log n)每次选择,100+条目表格成本基本上没有什么。

public class WeightedSelector
{
    float[] _cumulative;
    float _total;

    public void Build(IReadOnlyList<float> weights)
    {
        _cumulative = new float[weights.Count];
        float running = 0f;
        for (int i = 0; i < weights.Count; i++)
        {
            running += weights[i];
            _cumulative[i] = running;
        }
        _total = running;
    }

    public int Select(System.Random rng)
    {
        float roll = (float)(rng.NextDouble() * _total);
        int index = Array.BinarySearch(_cumulative, roll);
        return index < 0 ? ~index : index; // 二分搜索返回位元补数
    }
}

最终打包了整个东西,因为我已经重建了这个exact模式三次跨越不同项目——它作为CraftRewards上unity Asset Store。如果嵌套表格模式可以为别人节省重建时间,我很乐意深入讨论递归保护或草稿排除逻辑。