今日速览
- 伤害不是”一个数字”,是一条分布。同样输出 0~24 的范围,
2d12(两颗 12 面骰相加)两头尖中间鼓,6d4更窄更集中在中间,1d24则完全平均。手感的差别来自方差,不是平均值。 - 三个旋钮是正交的:加常数 = 平移整条分布(护甲减伤、武器加成);改骰数 = 改方差(稳定 vs 抽奖);改骰面 = 改范围。调平衡时一次只动一个,才知道是谁在起作用。
- 平衡要从 TTK 倒推,不要先写伤害再”试试看”。先定”杂兵 1.5 秒死、精英 8 秒死”,再反推敌人 HP 与玩家 DPS,数值才有锚点。
- 成长曲线不要硬编码在代码里。Godot 的
Curve是 Resource,X 轴默认归一化到 0~1,sample(t)取值,能在检查器里直接拖点改形状、存成.tres,改数值不用改代码。 - 乘法型属性最容易炸:暴击的期望增益是
1 + p × (m - 1)。暴击率 30%、暴击伤害 200% 就是 1.3 倍期望——再叠两个乘法增益,DPS 会指数起飞,前面所有 TTK 全部作废。
核心学习内容
一、先把”分布”想清楚:伤害是一条曲线,不是一个数
Red Blob Games 那篇讲伤害骰的经典文章把随机伤害拆得很干净:NdS 表示掷 N 颗 S 面骰求和。它的结论对做游戏非常实用:
- 骰数决定方差。范围固定时,骰子越多、单颗越小,结果越集中在中间(手感”稳定”);骰子越少越大,越容易出极端值(手感”刺激”)。
- 常数是平移。
2d6+1只是把整条分布往右挪 1,形状不变。护甲-3就是往左挪。所以”加攻/减伤”和”改随机性”是两件互不干扰的事。 - 非对称要靠额外规则。想”高伤害更常见”,可以掷三次取最高、或者掷三颗去掉最低再补一颗——分布会右偏,玩家感觉”我打得很猛”,而平均值只涨了一点点。
实操上,别只在脑子里算。写一个跑 10000 次的直方图函数,把分布画出来,你会立刻看出”这一版是不是在骗玩家”。
# 最小可用的伤害分布采样(Godot 4)
var rng := RandomNumberGenerator.new()
func roll_damage(dice: int, sides: int, bonus: int = 0) -> int:
var total := 0
for i in dice:
total += rng.randi_range(1, sides)
return maxi(total + bonus, 0)
func histogram(samples: int = 10000) -> Dictionary:
var buckets := {}
for i in samples:
var v := roll_damage(2, 6, 1) # 换成你要测的公式
buckets[v] = buckets.get(v, 0) + 1
return buckets
二、从 TTK 反推数值,而不是正向凑
TTK(Time To Kill,击杀所需时间)是玩家最能感知的指标,也是最稳的锚点:
TTK = 敌人HP / 玩家有效DPS
有效DPS = 单次伤害 × 命中率 / 攻击间隔
所以正确顺序是:定 TTK 表 → 定攻击间隔 → 反推单次伤害 → 反推敌人 HP。给一张最小可用的表(假设攻击间隔 0.4 秒、命中率 90%):
| 敌人 | 目标 TTK | 需要有效 DPS | 反推 HP | 单次伤害 |
|---|---|---|---|---|
| 杂兵 | 1.6s | 62.5 | 100 | 28 |
| 精英 | 8s | 62.5 | 500 | 28 |
| Boss | 45s | 62.5 | 2812 | 28 |
这张表一眼就能看出问题:如果玩家后期 DPS 涨到 200,杂兵 TTK 掉到 0.5 秒——不是”变强了很爽”,是”连招还没打完怪就没了”。这时该做的不是改伤害,是给敌人 HP 也上同一条成长曲线,让 TTK 保持在 1.2~2 秒的”爽区间”。
三、用 Curve 资源把成长曲线搬出代码
Godot 的 Curve 是 Resource:定义一串点和切线,sample(offset) 取值。默认 domain 与 value 都在 0~1,所以标准做法是「等级 → 归一化 t → 曲线取值 → 乘上区间宽度」:
class_name StatCurve
extends Resource
@export var curve: Curve # 在检查器里拖点调形状
@export var level_min := 1
@export var level_max := 50
@export var value_min := 100.0 # 1 级 HP
@export var value_max := 5000.0 # 50 级 HP
func hp_at_level(level: int) -> float:
var t := inverse_lerp(float(level_min), float(level_max), float(level))
t = clampf(t, 0.0, 1.0)
return lerpf(value_min, value_max, curve.sample(t))
好处很直接:Excel 里的”成长表”变成了能在编辑器里拖的曲线,形状(前期陡、后期平,或相反)随时可改;.tres 还能给不同职业各存一份,“战士线性、法师指数”就是两个文件。注意 Curve 有 bake_resolution,采样点太多时会慢,做实时每帧查询前先 sample_baked()。
四、平衡要有回归测试,不然改一处崩三处
最低成本的方案:把所有可调数值放进一个 balance.json,写个 headless 脚本跑「10 个关卡 × 每种敌人 × 1000 次模拟」,输出平均 TTK、玩家死亡率、平均通关时长到 balance_report.md。每次改数值前后各跑一次,对比 diff。这样”感觉变难了”就能变成”杂兵 TTK 从 1.6 涨到 2.4,超阈值 50%”。
🎯 DeepSeek 提示词
提示词 1:从 TTK 反推一整张数值表
适用场景:新项目刚立项,或手感全崩需要重做基线时。
你是资深游戏数值策划,擅长动作游戏战斗平衡。
【项目背景】
- 类型:2D 横版动作,Godot 4,单次流程 4 小时
- 玩家攻击间隔 0.4 秒,基础命中率 90%,允许 3 段连击
- 玩家血量 100,回复道具每关 2 个
【任务】
按下面目标设计一张敌人数值表:
1. 杂兵 TTK 1.2~1.8 秒,精英 6~9 秒,Boss 40~50 秒
2. 玩家在杂兵战里的死亡率 < 5%,精英战 15%~25%,Boss 战 40%~55%
3. 敌人 HP 随关卡(1~10 关)用一条曲线成长,保持 TTK 区间不变
【输出格式】
Markdown 表格,列:敌人名 / 关卡 / HP / 单次伤害 / 攻击间隔 / 目标TTK / 理论TTK / 玩家死亡率估算
表格后附:
- 一条推荐曲线(线性/指数/分段),说明为什么
- 3 个我认为会失衡的风险点
- 一个用于验证的模拟脚本伪代码
【约束】
- 只输出表格与说明,不要寒暄
- 所有推导要写出公式和代入的数字
- 不确定的地方标注「需实测」,不要编造
提示词 2:让 DeepSeek 写伤害系统 + 蒙特卡洛验证脚本
适用场景:有了公式,需要落地成代码并量化验证分布。
你是 Godot 4 GDScript 工程师。请实现一套可测试的伤害系统。
【需求】
1. 文件 damage.gd,class_name DamageSystem,extends RefCounted
2. 支持三种伤害模式:
- FLAT:固定值 + 常数加成
- DICE:NdS + 常数加成(如 2d6+3)
- CRIT:在 DICE 基础上加暴击(暴击率 p、暴击倍率 m)
3. 必须使用注入的 RandomNumberGenerator(不要用全局 randi),保证同 seed 可复现
4. 提供 expected_damage() 返回理论期望值(用于和模拟值对照)
5. 提供 monte_carlo(n) 返回 Dictionary:均值、标准差、最小、最大、直方图
【输出要求】
- 完整可直接运行的 GDScript,含类型标注
- 每个 public 方法写 docstring 注释
- 末尾给一个 _ready() 里的自测示例:2d6+3、暴击率 0.3/倍率 2.0,跑 10000 次并打印期望值vs模拟值
- 如果期望值与模拟值偏差 > 2%,指出可能原因
【约束】
- Godot 4.x 语法,不要 Godot 3 写法
- 不要引入任何插件
- 代码里不要出现 print 调试噪音(除自测示例)
提示词 3:数值体检——找出会指数爆炸的乘法叠加
适用场景:中期数值开始失控,DPS 突然翻十倍,但不知道哪一环的问题。
你是游戏数值审计员。下面是我的属性系统,请做一次失衡审计。
【属性清单】
(粘贴你的属性:基础攻击、武器加成、暴击率、暴击伤害、元素加成、攻速、连击倍率…)
【叠加规则】
(粘贴你的计算公式,特别是乘法叠加的部分)
【审计任务】
1. 逐项列出每个属性的「期望收益公式」,标明是加法还是乘法
2. 计算一个「满配玩家」的最终 DPS,并与「裸装玩家」对比,算出倍率
3. 找出收益递增(越堆越赚)的属性,按危险程度排序
4. 指出哪两个属性组合会产生「乘数叠加爆炸」,给出具体数字示例
5. 给出 3 种修正方案(软上限/收益递减/改加法),并说明各自对手感的影响
【输出格式】
- 第一段:一句话结论(这套系统是否健康)
- 第二部分:属性收益表(属性 / 每点收益 / 是否递增 / 风险等级 高|中|低)
- 第三部分:爆炸组合与数字推演
- 第四部分:修正建议,每条含「改哪里 / 改完玩家会感觉什么变化」
【约束】
- 全部用数字说话,不要「建议进一步测试」这类空话
- 如果信息不足,只列出你需要的 3 个额外参数,其余照常分析
学习心得
今天最大的收获是:平衡不是”把数字调对”,而是”把随机性的形状调对”。我原来一直以为伤害公式就是”攻击力减防御力”,但真正决定手感的是分布——玩家记住的不是平均值,是”我这一刀砍出了 3 还是 30”。
对”用 DeepSeek 做一款 Steam 游戏”这个目标来说,今天这三点是直接的杠杆:第一,TTK 反推法让 DeepSeek 能产出有锚点的数值表而不是拍脑袋的数字;第二,把曲线搬进 Curve 资源后,调平衡这件事从”改代码-重编译-试玩”变成”拖一个点-试玩”,迭代速度差 10 倍;第三,让 DeepSeek 做”数值审计”是我之前完全没想到的用法——它最适合干的不是发明数值,而是把已有公式的乘法链条摊开算一遍,而这恰好是人最容易算错的部分。
顺便记一句给未来的自己:先做一条直方图,再谈手感。看不见分布的时候,所有平衡讨论都是玄学。
明日关注
- 把
StatCurve接到实际项目里,给玩家 HP / 攻击 / 敌人 HP 各建一条.tres,验证 TTK 在 1~10 关是否稳定 - 写
balance_check.gd(headless 跑),输出balance_report.md,把今天的表格变成可回归的基线 - 读 Godot 文档 Bezier/Curve 那篇的剩余部分,确认
sample()与sample_baked()在每帧调用时的性能差异
本文由自动化学习流水线每日生成,坚持即学即记。
评论