DYNAMIC EXPLAINER / ARTICLE FLOW

把章节变成可单步观察的过程

01 / 12
STEP 01 / CODE / LOGIC

先说结论

随机系统不是只有 Random.Range。普通随机决定等概率抽取,加权随机决定不同概率,可复现随机保证相同 seed 得到相同序列,保底和洗牌袋则主动限制连续坏结果。

金币权重 50 -> 区间 [0, 50)
药水权重 30 -> 区间 [50, 80)
武器权重 20 -> 区间 [80, 100)

分类:游戏逻辑常用模式 / 算法

先说结论

随机系统不是只有 Random.Range。普通随机决定等概率抽取,加权随机决定不同概率,可复现随机保证相同 seed 得到相同序列,保底和洗牌袋则主动限制连续坏结果。

把权重映射到一条数轴

金币权重 50 -> 区间 [0, 50)
药水权重 30 -> 区间 [50, 80)
武器权重 20 -> 区间 [80, 100)

随机数为 67 时落入药水区间;为 92 时落入武器区间。连续抽 100 次不保证刚好得到 50、30、20 次,那只是长期概率。若玩法要求每 10 抽必出武器,就已经不是单纯加权随机,而是带状态的保底规则。

核心思想

随机系统的核心不是“随便抽一个”,而是:

在不确定性、体验、公平性、可复现之间做取舍。

同样是随机,常见实现可以分成多种形态:

普通随机
可复现随机
加权随机
稳定随机
保底随机
洗牌袋随机

它们解决的问题不一样。

普通随机

普通随机就是直接取一个随机值。

整数区间抽取可以直接使用 Random.Range

return clampedMax <= clampedMin ? clampedMin : Random.Range(clampedMin, clampedMax + 1);

适合:

掉落数量
飘字偏移
临时视觉变化
不要求复现的小随机

普通随机的问题是:它可能产生极端结果。

例如 10% 概率不是“每 10 次必出 1 次”,而是:

每一次独立抽。

所以玩家可能连续 30 次不出,也可能连续 3 次都出。

这在数学上正常,但在体验上可能很糟。

加权随机

加权随机适合从一组候选里按权重抽一个。

一个典型的加权抽取流程如下:

int totalWeight = 0;
for (int index = 0; index < entries.Count; index++)
{
    totalWeight += Mathf.Max(0, entries[index].Weight);
}

int rolled = Random.Range(0, totalWeight);
int cursor = 0;
for (int index = 0; index < entries.Count; index++)
{
    cursor += Mathf.Max(0, entries[index].Weight);
    if (rolled < cursor)
    {
        return entries[index];
    }
}

可以这样理解。

配置:

金币:10
药水:30
装备:60

总权重:

100

随机数范围:

0 ~ 99

区间:

0  ~ 9   -> 金币
10 ~ 39  -> 药水
40 ~ 99  -> 装备

所以装备不是写死成 60%,而是因为它占了 100 份权重里的 60 份。

适合:

掉落表
随机事件库
怪物刷新池
技能随机候选
宝箱奖励

可复现随机

有些随机必须可以复现,尤其是战斗、GM 测试、回归验证。

需要测试与回放时,可以先把随机源抽象成接口:

public interface IRandomSource
{
    bool Roll(float probability, AttributeAttackRollType rollType);
    float RollVariance(float min, float max);
}

运行时可以用默认随机:

public sealed class RuntimeRandomSource : IRandomSource

测试时可以用固定 seed:

public sealed class SeededRandomSource : IRandomSource
{
    private readonly Random _random;

    public SeededRandomSource(int seed)
    {
        _random = new Random(seed);
    }
}

固定 seed 的特点是:

同一个 seed,会产生同一串随机结果。

这对调试很重要。

如果玩家说:

这次为什么没暴击?

有 seed 和随机轨迹,就能回放当时那次判定,而不是只能回答“随机的”。

稳定随机

稳定随机不是从随机数发生器里取“下一个随机数”,而是:

把一些稳定输入 hash 成 0~1 的值。

稳定随机可以把一组稳定输入映射成 0~1

float roll = StableHash01(groupId, instanceId, purpose, version, index) * totalWeight;

它内部把这些输入混合成一个 hash:

groupId
instanceId
purpose
version
index

再转成 0~1

这种随机特别适合:

看起来有随机差异,但同一轮里不要乱跳。

例如怪物围绕玩家站位。

如果每帧都 Random.value,位置会一直抖。

用稳定随机后:

同一个怪物、同一轮意图,得到固定偏移。
版本变了,才重新随机。

它的体验是:

有变化,但稳定。

保底随机

保底机制解决的是:

玩家长期倒霉。

例如 10% 概率出稀有奖励,纯随机允许玩家 100 次都不出。

保底可以这样写:

failCount++;

float chance = baseChance + failCount * bonusPerFail;

if (Random.value < chance || failCount >= maxFailCount)
{
    failCount = 0;
    GiveRareReward();
}

这里有两层保护:

失败越多,概率越高。
达到最大失败次数,直接保底。

保底不是让随机更“真实”,而是让体验更可控。

适合:

抽卡
稀有掉落
关键材料
日常奖励
长线养成系统

洗牌袋随机

洗牌袋的思路是:

先把结果按数量放进袋子。
打乱袋子。
每次从袋子里拿一个。
拿空后重新装袋。

例如希望 10 次里大致出现:

普通 7 次
稀有 2 次
史诗 1 次

就先建立一个袋子:

普通 普通 普通 普通 普通 普通 普通 稀有 稀有 史诗

然后打乱:

普通 稀有 普通 普通 史诗 普通 普通 稀有 普通 普通

之后按顺序抽。

这样仍然随机,但短期分布更可控,不容易出现极端连黑或极端连出。

适合:

卡牌抽取
关卡事件轮换
随机 BGM
随机敌人组合
不希望短时间重复的内容

为什么纯随机有时体验不好

纯随机的问题是:

数学公平,不等于玩家体感公平。

例如 20% 暴击率,玩家容易理解成:

大概 5 次里出 1 次。

但纯随机实际可能是:

连续 15 次不暴击。
连续 4 次都暴击。

这不是程序错了,而是独立随机本来就可能这样。

所以游戏里经常会加入:

保底
伪随机分布
洗牌袋
权重衰减
最近结果去重

目的不是让随机更随机,而是让随机更像玩家心里期待的随机。

怎么选

普通随机:简单、不需要复现、不怕极端。
加权随机:配置驱动,按权重抽池子。
可复现随机:战斗验证、GM 测试、回归测试。
稳定随机:看起来随机,但同输入下固定。
保底随机:限制玩家长期倒霉。
洗牌袋随机:控制短期分布,避免连续重复或极端结果。

常见坑

第一,概率配置和权重配置混用。

概率通常要求总和有意义。
权重只关心相对大小。

例如:

10 / 30 / 60
1 / 3 / 6

这两组权重抽出来的比例是一样的。

第二,运行时随机和测试随机混在一起。

战斗、回放、测试最好通过接口传入随机源,方便 seed 复现。

第三,每帧使用普通随机导致抖动。

站位、偏移、个体差异更适合稳定随机。

第四,只看数学概率,不看玩家体感。

稀有掉落、抽卡、关键材料,通常需要保底或洗牌袋。

最重要的收获

随机系统不是只有概率,而是在不同场景里选择合适的不确定性。

理解随机系统时,可以抓住三个核心模型:

累计区间:把权重映射为可命中的连续范围。
随机源接口:隔离取值方式,支持 seed 复现。
稳定 Hash:同样的输入得到同样的伪随机值。