今日速览

  • 电脑生成不了「真随机」,Godot 底层用的是 PCG 系列伪随机数生成器(PRNG)——同一种子必然得到同一串随机数。
  • Godot 有两条路:全局函数(randi()/randf())图省事,RandomNumberGenerator 类可实例化、各自带种子和状态,适合多人联机、回放、地牢生成。
  • 想复现结果就 seed(12345);想要「每次都不一样」就 randomize()。这是程序化关卡「可分享种子」的基础。
  • 加权随机用 randf() 分区间,或 RandomNumberGenerator.rand_weighted() 按相对权重取索引——让稀有真正稀有。
  • 随机抽卡容易「欧非两极」,用洗牌袋(Shuffle Bag):抽完不放回,抽空再重填洗牌,分布更均匀、体验更稳。

核心学习内容

1. 种子(Seed)与状态(State):随机的「可复现」开关

Godot 默认每次运行都用随机种子,所以结果不可复现。只要在初始化时 seed(12345),同一串随机数就会被稳定重放;也可以把字符串哈希成种子,例如 seed("Hello world".hash()),这样「种子」就能当作玩家可输入、可分享的短码——roguelike 常见的「种子号」正是这么来的。

RandomNumberGenerator 类还多了一个 state 属性。种子的作用是重置整条序列,而状态可以在不改种子的前提下回到序列中的某一点。用法很简单:先设 seed,再存一份 state,之后把 state 写回去,就能重放同一段结果:

var rng = RandomNumberGenerator.new()
rng.seed = 12345          # 改种子会重置状态,所以一定要先设种子
var saved = rng.state     # 存档点
print(rng.randi())        # 产生一个数
rng.state = saved         # 回到存档点
print(rng.randi())        # 会打印同一个数

这条特性对多人联机、录像回放、撤销/回退都关键:只要双方种子与调用顺序一致,就能得到同一个世界。

2. 四个常用随机函数,各司其职

  • randi() → 0 到 2³²−1 的整数,取模后才是你要的范围:randi() % 50(0–49)、randi() % 51 + 10(10–60)。
  • randf() → 0.0 到 1.0 的浮点,用来做概率判断(见下一节)。
  • randf_range(from, to) → 指定区间的浮点,例如 randf_range(-4, 6.5),适合抖动、散射、随机偏移。
  • randi_range(from, to) → 指定区间的整数,例如 randi_range(-10, 10),比手写取模更安全、更直观。
  • randfn(mean, deviation) → 正态分布,默认均值 0、标准差 1,适合「大多数普通、偶尔极端」的数值(如伤害浮动)。

取数组随机元素有 Array.pick_random();取字典则先 .values() 再索引。想避免「连续重复」时,不要只靠 while 重抽(会「乒乓」),应该上洗牌袋。

3. 加权随机:让稀有真正稀有

最简单的方式是拿 randf() 切区间:

func get_item_rarity() -> String:
    var r = randf()
    if r < 0.80:
        return "Common"     # 80%
    elif r < 0.95:
        return "Uncommon"   # 15%
    else:
        return "Rare"       # 5%

如果候选很多,用实例版 RandomNumberGenerator.rand_weighted() 更清爽:传入一组相对权重数组,它会返回对应索引。[0.5, 1, 1, 2] 表示索引 3 的出现概率是索引 1/2 的两倍,是索引 0 的四倍。

var fruits = ["apple", "orange", "pear", "banana"]
var weights = [0.5, 1, 1, 2]
var rng = RandomNumberGenerator.new()
print(fruits[rng.rand_weighted(weights)])

小坑:数组里的值一旦被引用,重抽时记得 .duplicate(),避免改动污染原数据。

4. 洗牌袋(Shuffle Bag):把「欧非两极」熨平

真正的随机容易被玩家感知成「不公平」——连出三张同卡,玩家会骂系统坏了。洗牌袋的做法是:选过一个就从池子里拿走;池子空了再重填并洗牌。于是一轮之内每种元素恰好出现一次,分布均匀,又不会像固定循环那样死板。

var _fruits = ["apple", "orange", "pear", "banana"]
var _full = []

func _ready():
    _full = _fruits.duplicate()
    _fruits.shuffle()

func get_fruit():
    if _fruits.is_empty():
        _fruits = _full.duplicate()
        _fruits.shuffle()
    return _fruits.pop_front()

掉落表、事件池、BGM 随机播放、敌人刷新,凡是「希望平均但又要随机」的地方,都优先用洗牌袋。

🎯 DeepSeek 提示词

提示词 1:让 DeepSeek 写一个可复现的随机数管理器(适合地牢/关卡生成)

【角色】你是一位资深 Godot 4.x GDScript 游戏开发工程师。
【任务】为我的 2D roguelike 游戏写一个"可控随机管理器",作为 Autoload 单例使用。
【需求】
1. 用一个 RandomNumberGenerator 实例统一管理游戏内所有"程序化生成"相关的随机;
2. 提供 set_seed(value: int) 与 set_seed_from_string(s: String) 两个方法;
3. 提供 next_int(min, max)、next_float(min, max)、chance(p: float) 三个便捷封装;
4. 提供 get_seed() 与 get_state()/set_state(),便于存档时保存/恢复随机进度;
5. 额外提供一个独立的 rng 实例给"纯表现层随机"(粒子、抖动)用,避免它污染生成序列。
【输出格式】只给完整 .gd 文件,带中文注释,说明每段用途,并附 3 行使用示例。
【约束】Godot 4.x 语法;不要用全局 seed()/randi() 全局函数;注释解释"为什么表现层随机要分开"。

提示词 2:让 DeepSeek 实现加权随机掉落表(适合战利品/事件系统)

【角色】你是一位擅长数值设计的 Godot 工程师。
【任务】实现一个数据驱动的"加权随机抽取"工具类 WeightedPicker。
【输入】一个 Array[Dictionary],形如 [{id="sword", weight=1.0}, {id="potion", weight=5.0}, ...]。
【输出】pick(rng: RandomNumberGenerator) -> Dictionary,按相对权重返回其中一项;
       另外提供 pick_unique(n: int, rng) -> Array,一次抽 n 个不重复项。
【格式】完整 .gd 文件 + 1 个最小可运行示例(含示例数据)。
【约束】Godot 4.x;用 RandomNumberGenerator.rand_weighted() 实现;权重为 0 的项永不返回;
       非法输入(空数组、负权重)要给出明确报错或降级处理,并写明处理策略。

提示词 3:让 DeepSeek 用洗牌袋改写「随机 BGM/事件池」(适合手感与公平性调优)

【角色】你是 Godot 4.x 高级工程师,注重玩家体验。
【任务】把一段"每次都用 pick_random() 抽背景音乐 / 随机事件"的代码,重构为"洗牌袋(Shuffle Bag)"实现。
【要求】
1. 封装成可复用的 ShuffleBag 类:支持 add(item)、next()、reset()、remaining();
2. 池空自动重填并 shuffle(),且避免"新一轮第一个"与"上一轮最后一个"重复;
3. 支持设置"冷却/权重":最近出现过的项在一定次数内降低被抽中概率(可选开关)。
【输出格式】重构后的完整代码 + 一段对比说明:为什么洗牌袋比 pick_random 体验更稳。
【约束】Godot 4.x 语法;解释什么场景该用洗牌袋、什么场景用加权随机、什么场景两者结合。

学习心得

今天这节直接补上了「程序化生成」最容易被忽略的底座:随机不是不可控的黑盒。对「用 DeepSeek 做一款 Steam 游戏」这个目标来说,价值有三:

  1. 可控 = 可调试 = 可复现 bug。 只要随机走统一的种子管理器,玩家报「第三关卡死」时,我能用同一个种子号把那一局完整重放出来,而不是靠碰运气复现。
  2. 体验比概率更重要。 加权随机的「感知公平」、洗牌袋的「均匀分布」,是让玩家愿意反复开局的隐形手感;这部分 DeepSeek 能按提示词稳定生成,我只需把关数值与开关。
  3. 提示词要点:把「随机架构」当需求说清楚。 明确指出「生成随机与表现随机要分离」「用实例 RNG 而非全局函数」「要能存 state」——这几句约束一加,DeepSeek 产出的代码就从玩具变成能进版本库的工程件。

明日关注

  • 把「随机数管理器」真正接进项目:给现有的地牢/关卡生成器换上统一 RNG,并加一个「输入种子号重开」的调试按钮。
  • 给掉落系统换成 WeightedPicker + 保底机制,验证稀有物「感知公平」。
  • 检索 Godot 的 Noise(FastNoiseLite)与程序化地形生成,把「一维随机」扩展到「二维噪声地图」。

本文由自动化学习流水线每日生成,坚持即学即记。