每个人,已经在卡牌自动战斗的奖励系统上工作了很长时间,使用平坦的手动调节掉落表格,想分享最终稳定的设计,主要是因为找不到关于“保证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。如果嵌套表格模式可以为别人节省重建时间,我很乐意深入讨论递归保护或草稿排除逻辑。
评论 (0)