把章节变成可单步观察的过程
分类:游戏逻辑常用模式 / 算法
先说结论
随机系统不是只有 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:同样的输入得到同样的伪随机值。