EP2 把核心玩法跑通了,但有个"原型味"很重的地方:发牌是写死的均匀随机——PieceLibrary.Random 等概率抽一个形状。这在原型里没问题,在商业游戏里是大问题。

EP0 讲过:成熟商业游戏的数值(经济曲线、难度参数等)通常都是服务端可下发的配置,策划改表、运营调参,都不用动代码、不用发版。这一集(EP3)就立这个规矩——让数值和代码分家,拿"难度曲线"开刀。

为什么数值必须数据化

写死 Random() 意味着:想让"越往后大块越多、越难"就得改代码、重编译、重新出包。等到有 200 关、要按留存数据反复调难度时,这是灾难。商业做法是把"可调的东西"抽成数据资产,代码只读它。

Block Blast 的难度主要由发什么方块决定:开局多给小块(好放、轻松上手),后期多给大块和难形状(制造压力)。把这条曲线做成数据。

难度配置:一个 ScriptableObject

[CreateAssetMenu(menuName = "BlockBlast/DifficultyConfig")]
public class DifficultyConfig : ScriptableObject
{
    [System.Serializable]
    public struct Tier { public int scoreThreshold; public float wSmall, wMed, wLarge; }

    public int colorCount = 6;
    public Tier[] tiers;        // 按 scoreThreshold 升序

    // 取 ≤ score 的最高档权重
    public void WeightsFor(int score, out float s, out float m, out float l)
    {
        s = 1f; m = 1f; l = 0.4f;
        if (tiers == null || tiers.Length == 0) return;
        for (int i = 0; i < tiers.Length; i++)
        {
            if (score >= tiers[i].scoreThreshold) { s = tiers[i].wSmall; m = tiers[i].wMed; l = tiers[i].wLarge; }
            else break;
        }
    }
}

配的 4 档(资产放在 Assets/Resources/DifficultyConfig.asset):

分数档 小块权重 中块权重 大块权重
0(开局) 3.0 2.0 0.3
300 2.0 2.5 0.8
1000 1.2 2.0 2.0
3000(后期) 0.8 1.8 3.0

难度档位配置(数据图)

光看配置表没意思,直接在真游戏里验:开局(分数低)托盘给的是小块——好放、轻松上手:

真机:开局托盘是小块

然后我用 MCP 把分数顶到 5000、按当前分数重发一手托盘(execute_code 调控制器的 DealTray),同一套发牌代码,托盘立刻变成清一色大块(两个 5 格 L + 一个 L 角)——压力一下就上来了:

真机:高分时托盘真的发大块

代码一行没改,改的只是"当前分数"喂给配置——这就是数据驱动在真机上的样子。下面把它怎么实现的拆开。

方块按尺寸分类 + 加权抽取

PieceLibrary 把形状按格子数分三档(小 ≤2 / 中 3~4 / 大 ≥5),按权重抽:

public static int SizeClass(in Piece p) => p.CellCount <= 2 ? 0 : (p.CellCount <= 4 ? 1 : 2);

public static Piece WeightedRandom(System.Random rng, float wSmall, float wMed, float wLarge, int colorCount)
{
    float total = wSmall + wMed + wLarge;
    int cls = total <= 0 ? rng.Next(3) : Pick(rng, wSmall, wMed, wLarge);  // 按权重选档
    var bucket = _byClass[cls];
    var src = bucket[rng.Next(bucket.Count)];
    return new Piece { name = src.name, colorId = 1 + rng.Next(colorCount), cells = src.cells };
}

控制器发牌时,从配置按当前分数取权重——代码不关心曲线长什么样,只问配置:

void DealTray()
{
    for (int i = 0; i < TraySize; i++)
    {
        _diff.WeightsFor(Score.Value, out float s, out float m, out float l);
        Tray.Add(PieceLibrary.WeightedRandom(_rng, s, m, l, _diff.colorCount));
    }
}

代码里没有任何一个难度数字——全在资产里。想让游戏更难/更易,改资产,不动代码。这就是"分家"。

验证:11 条断言,重点是分布真的漂移了

PASS  A dot→小 / d2h→小 / T→中 / d5h→大 / sq3→大     (尺寸分类)
PASS  B 分0→档0(wLarge=0.3) / 分700→档1(=0.8... 实为1) / 分5000→档2/3   (tier 选择)
       大块占比: 早期=5.8 %  后期=50.8 %
PASS  C 后期大块占比 > 早期
PASS  C 后期大块占比明显更高(>2x)
PASS  D NewGame 后 tray==3                              (控制器集成)
==== 11/11 PASS ====

最有说服力的是 C:用早期权重抽 4000 次,大块只占 5.8%;用后期权重抽 4000 次,大块占 50.8%。同一套抽取代码,只换了配置里的几个数字,难度分布就从"几乎全是小块"漂移到"一半是大块"。这正是数据驱动的意义——行为由数据决定,不由代码决定

这一集的产物与诚实的话

  • DifficultyConfig(ScriptableObject)+ 方块尺寸分类 + 加权抽取 + 控制器集成。
  • DifficultyConfig.asset(4 档曲线)放进 Resources,运行时加载。
  • 11 条断言全绿,含分布漂移实测。

诚实地讲:这一集只把"难度"这一类数值数据化了,而且是"按尺寸权重"这种粗粒度的难度(不是精确编排每一关的布局)。一个完整商业游戏还要把关卡布局、商城 SKU、主题、各种平衡曲线全部数据化——那是后面几集(经济、商城、主题)会继续铺的。但"数值/内容与代码分家"的这条缝,从这一集起立住了,后面所有可调的东西都顺着它走。

一个该数据化但还没做的:现在配置放在 Resources 里、随包走。真正的商业项目会让它服务端可下发(远程拉一份覆盖本地),运营不发版就能调难度——这个留到 EP8 远程配置一起做。

下一篇 EP4:经济系统——货币、道具,以及那条"金币随玩家进度缩放"的经济曲线(就是 EP0 拆解时讲的 系数 × milestone(进度))。开始从"玩法"走进"商业化"。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐