EP1 把地基(框架层 + 启动流程)搭好了,但屏幕上还没有游戏。这一集(EP2)在地基上长出核心玩法——Block Blast 式的方块消除。

但延续这个系列的原则:重点不是"做个看起来能玩的",而是把规则写成可断言的纯逻辑,用一组测试把它钉死。能不能玩之前,先证明它对不对。

先剧透这套玩法跑起来真机长什么样(后面接上输入和 UI 后、Unity 里实拍,拖放落子、消行、计分都在跑):

真机可玩对局

玩法规则

8×8 网格。每轮发 3 个方块(各种形状),玩家把它们拖到网格的空位上;只要某一行或某一列被填满,就整行/整列消除得分;3 个方块都用完就补发新的 3 个;当剩下的方块在棋盘上没有任何位置放得下,判定死局、游戏结束。

把这套拆成几个纯逻辑件(都不依赖 Unity,可独立测):

  • BoardModel:网格占用 + 放置 / 消行列 / 死局判定
  • Piece / PieceLibrary:方块形状定义 + Block Blast 风格形状库(1~5 格直条、方块、L/T/S/Z…)
  • Scoring:计分公式
  • BlockGameController:把上面串成一局的循环

棋盘逻辑:放置、消除、死局

public class BoardModel
{
    public readonly int W, H;
    readonly int[,] _cell;   // 0 空;>0 为颜色 id

    public bool CanPlace(in Piece p, int ox, int oy)   // 全部格子在界内且为空
    {
        foreach (var c in p.cells)
        {
            int x = ox + c.x, y = oy + c.y;
            if (!InBounds(x, y) || _cell[x, y] != 0) return false;
        }
        return true;
    }

    // 同时找出所有填满的行和列,一次性清掉(先判定再清,避免边清边判)
    public (int lines, int cells) ClearFullLines()
    {
        var fullRows = new List<int>(); var fullCols = new List<int>();
        for (int y = 0; y < H; y++) { bool full = true; for (int x = 0; x < W; x++) if (_cell[x,y]==0){full=false;break;} if(full) fullRows.Add(y); }
        for (int x = 0; x < W; x++) { bool full = true; for (int y = 0; y < H; y++) if (_cell[x,y]==0){full=false;break;} if(full) fullCols.Add(x); }
        var cleared = new HashSet<(int,int)>();
        foreach (int y in fullRows) for (int x=0;x<W;x++) cleared.Add((x,y));
        foreach (int x in fullCols) for (int y=0;y<H;y++) cleared.Add((x,y));
        foreach (var (x,y) in cleared) _cell[x,y]=0;
        return (fullRows.Count + fullCols.Count, cleared.Count);
    }

    // 死局:给定可用方块,没有一个能放下
    public bool AnyPlaceable(IEnumerable<Piece> pieces)
    {
        foreach (var p in pieces) if (p.CellCount>0 && HasSpotFor(p)) return true;
        return false;
    }
}

一个容易写错的点:消除必须先把所有满行满列判定完,再一次性清。如果边判边清,清掉一行会影响列的判定,行列同时满的情况就会算错。用一个 HashSet 收集所有要清的格子再统一清,行列交叉的格子也不会被算两次。

计分:鼓励一次多消

public static class Scoring
{
    public static int ForPlacement(int cells) => cells;          // 放下给小分
    public static int ForClear(int lines)                        // 消除按行列数平方放大
        => lines <= 0 ? 0 : lines * lines * 10;                  // 1条=10, 2条=40, 3条=90, 4条=160
}

平方放大是故意的:一次消两条给的不是 20 而是 40,鼓励玩家攒着一次多消——这是 Block Blast 上瘾感的来源之一。

一局的循环

BlockGameController 把这些串起来:发 3 个 → 放一个(消行列、计分、标记已用)→ 三个用完补新的 → 每次放完检查剩余方块是否还有处可放,没有就 GameOver 并刷新最高分(写进 EP1 的 Property 存档)。

public PlaceResult TryPlace(int index, int ox, int oy)
{
    var p = Tray[index];
    if (p.CellCount == 0 || !Board.CanPlace(p, ox, oy)) return default;   // 放不下
    Board.Place(p, ox, oy);
    Tray[index] = /* 标记已用 */;
    var (lines, cells) = Board.ClearFullLines();
    Score.Value += Scoring.ForPlacement(p.CellCount) + Scoring.ForClear(lines);
    if (TrayAllUsed()) DealTray();
    if (!Board.AnyPlaceable(UnusedTray())) { GameOver = true; /* 刷新最高分 */ }
    // ...返回详细结果给视图做表现
}

验证:13 条断言把规则钉死

这是 EP2 的核心交付,不是"跑起来看着对",而是断言。

(一个工程细节:funplay 的 execute_code 内联编译器较老,不支持局部函数 / in 参数 / 元组命名这些新语法。所以把自测写成项目里一个正常的 .cs,由 Unity 的编译器编译,execute_code 只调一行 Ep2SelfTest.Run()——绕开老编译器。这条以后写 MCP 驱动的测试都用得上。)

PASS  A 单行消除 lines==1
PASS  A 消除后清空 filled==0
PASS  B 行列同时消除 lines==2
PASS  C 满盘 HasSpotFor(d2h)==false
PASS  C 满盘 AnyPlaceable==false 死局
PASS  C 空盘 AnyPlaceable([dot])==true
PASS  D ForClear(1)==10
PASS  D ForClear(2)==40
PASS  D ForClear(3)==90
PASS  D ForPlacement(5)==5
PASS  E NewGame tray==3 / score==0 / !GameOver
==== 13/13 PASS ====

覆盖了最容易错的几处:单行消除后棋盘真清空、行列同时填满算作 2 条(边清边判会算错的那种)、满盘时各种方块都判无处可放(死局)、计分公式的平方放大、控制器开局状态。规则钉死了。

顺手把画面立起来:AI 出图流水线接上玩法

光有逻辑没画面不像游戏。用 EP0 验证的那条 gpt-image-2 → 程序扣图 流水线,并发生成了一套 6 色糖果方块(同一段提示词、只换颜色词,保证风格统一),程序扣掉品红底成透明 PNG:

AI 生成的 6 色方块家族

导入 Unity 当 Sprite,按棋盘网格渲染出一个中局状态 + 底部 3 个待放方块(竖屏 720×1559):

渲染出的棋盘

这一张是这个系列第一张"像商业产品"的画面——方块是 AI 生成的、布局是代码摆的,没有一处占位色块。

这一集的产物与诚实的话

  • 5 个玩法逻辑文件(BoardModel / Piece / PieceLibrary / Scoring / BlockGameController)+ 自测,编译零错误。
  • 13 条断言全绿。
  • 一套 6 色 AI 方块 + 第一张棋盘渲染图。

诚实地讲:现在还不能用手拖——拖拽输入、落块动画、消除特效都还没做(那是 EP7 的 juice + 输入层)。这一集证明的是"规则正确 + 画面能渲染",不是"手感好玩"。手感要等表现层和数值调校。但核心循环的对错已经被钉死,后面所有东西(数据、经济、留存)都长在这套可验证的规则上。

下一篇 EP3:把方块库、难度曲线做成数据驱动(ScriptableObject / 配置),并把玩法状态接进 EP1 的存档——让"内容"和"代码"分家。

Logo

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

更多推荐