← 返回全部文章

QuickTile Studio:从第一次提交到 Asset Store 上架包

发布于:
unitytoolingcandyrush

QuickTile Studio 是一个 Unity 编辑器工具,它把一个材质变成无缝、可平铺的纹理,并导出 为可直接用作 sprite 的 PNG。它以 1.0.0 版本在 Unity Asset Store 上架。

仓库创建于 2026 年 1 月 13 日。到 1 月 18 日,它已经有了重新组织过的包结构、一个演示 场景、一份更新日志和一份 PDF 手册。以下数据来自 git 历史和商店页面:

仓库创建2026-01-13(UTC)
包结构重组与 PDF 手册2026-01-18
提交次数35 次,含初始提交
发布的 C# 代码20 个文件,3,017 行(13 个运行时文件,7 个编辑器文件)
演示纹理从 43.9 MB 降到 4.6 MB
商店页面1.0.0,Unity 6000.3.2f1,兼容 Built-in 和 URP,不兼容 HDRP,5.3 MB,发布日期 2026 年 2 月 20 日

它从哪里来

CandyRush 需要重复铺开却看不出接缝的地面纹理。2025 年 10 月 29 日,一个 "Seamless Tiling Preview" 组件进入了游戏仓库:2x2 网格中的四个 quad,UV 互为镜像, 一台正交相机对准它们,再加一个 Randomize 按钮。之后它经过两个拉取请求逐步成长: 拉取请求 #113(11 月 14 日)加入了旋转、八个锁定开关、材质平铺与偏移控制,以及导出为 sprite 的捕获功能,导入设置为 Repeat 环绕模式。#150(12 月 13 日)加入了滑块实时更新、 每次捕获旁保存一份 JSON 设置文件,以及一个生成有机斑块形状的第二个工具。这些提交由 AI 编程代理在我的指导下编写;需求由我制定,结果由我审查。

到了 1 月,这个工具里已经没有任何 CandyRush 专属的东西。我判断一个内部工具能否成为 产品,用的就是这个测试:把游戏删掉,看工具本身是否仍然说得通。这个工具说得通。

技术原理:先镜像,再捕获 2x2

镜像是图像编辑器里做无缝平铺的老办法,这里用 UV 来实现。四个 quad 共用一个材质:

  • 左上:正常 UV
  • 右上:水平翻转
  • 左下:垂直翻转
  • 右下:两个轴都翻转

每条内部边都与自己的镜像相接,所以跨边的像素一致。外侧的边同样一致:左上 quad 的左边 和右上 quad 的右边采样的是源纹理的同一列。因此捕获到的 2x2 区块正好是图案的一个完整 周期,用普通的 Repeat 环绕模式就能平铺。这就是捕获时直接以 wrapMode = Repeat 写出 PNG、无需任何后处理的原因。

核心代码,来自 QuickTileGenerator:

// Base UV corners (vertex order: BL, BR, TL, TR)
Vector2 bl = new(0, 0), br = new(1, 0), tl = new(0, 1), tr = new(1, 1);

// Apply mirroring by swapping UV corners
if (sx < 0) { (bl, br) = (br, bl); (tl, tr) = (tr, tl); }
if (sy < 0) { (bl, tl) = (tl, bl); (br, tr) = (tr, br); }

// Apply UV rotation around center (0.5, 0.5)
if (rot != 0f)
{
    bl = RotateUV(bl, rot); br = RotateUV(br, rot);
    tl = RotateUV(tl, rot); tr = RotateUV(tr, rot);
}

历史记录下的两个决定

产品是 2x2 区块,而不是中心 quad

第一天里有一个精简的概念验证(提交信息:"PoC my brain hurts")。它以一个中心 quad 作为唯一的真实来源,周围环绕一圈圈镜像的邻居(3x3、5x5、7x7),相机只对准中心 quad。

预览看起来是无缝的。但捕获结果不会是:单个 quad 的边之所以对得上,只是因为它的邻居 被翻转了。单独捕获时,它四条边都有接缝。大约六小时后,一条提交信息写道:"2x2 necessary for seamless tiling. at some point I knew this and forgot."(无缝平铺必须用 2x2。我以前知道,后来忘了。)CandyRush 里的工具从 10 月起就一直用 2x2。这个概念验证 在 1 月 15 日被删除。

我的收获是:验证输出所依赖的性质,而不是预览所展示的性质。

旋转采样,而不是旋转几何体

在 CandyRush #113 中,旋转是作用在父 GameObject 上的,这在预览里能在任意角度保持镜像 对称。但在一台固定的、与坐标轴对齐的捕获相机下,几何体一旦旋转,捕获到的正方形一般 就不再是图案的一个周期。

QuickTile Studio 改为在镜像之后,绕每个 quad 的中心旋转 UV。共享的边仍然采样相同的 UV 点,2x2 周期在任何角度下都与捕获正方形对齐。代码注释直接写明了这一要求:旋转被 "baked into texture sampling rather than world-space, which is required for correct tiling across independent chunks"(烘焙进纹理采样而不是世界空间,这是独立区块之间正确 平铺的必要条件)。

把"看起来对"变成一个分数

镜像保证了像素对齐,但不保证结果好看。只要有明显的特征在折线处反射到自己身上,人眼 就会找到折线,也就是 2x2 中心的十字。面对五个连续参数(平铺 X 和 Y、偏移 X 和 Y、 旋转)再加一组材质,靠反复点 Randomize 直到看着顺眼,是不可扩展的。

所以这个包带了两个扫描器。Random Sampling 探索整个参数范围。Guided Refinement 在当前 数值周围的小邻域内搜索:如果邻域在评估预算之内,就按里程表顺序穷举;否则随机采样。

每个候选只渲染一次,再由三个启发式打分,它们都关注折线:

启发式衡量什么
Fold Line Variance沿折线方向的对比度
Fold Gradient垂直于折线方向的亮度梯度
Fold Edge Detection折线周围一条带状区域内的边缘强度

让它在编辑器里真正好用的细节:

  • 可复现。 候选生成器接收一个种子(默认 12345),所以相同的扫描设置会产生相同的 候选。
  • 不卡编辑器。 扫描是一个由 EditorApplication.update 驱动的 IEnumerable, 每个 tick 评估五个候选。编辑器保持可交互,扫描中途可以取消,完成或取消后会恢复 生成器原来的参数。
  • 一次渲染,多个分数。 每个候选以扫描分辨率(默认 256 px)渲染,所有启用的 启发式对同一份像素打分。每个启发式各自保留一个 top-K 列表,因此一次扫描就能比较 不同启发式。
  • 不丢结果。 超过接受阈值(默认 0.7)的候选会保存为 JSON,可选同时保存图像。
  • 平滑回放。 在 Guided Refinement 中,采样得到的候选会被排序,让检视面板最上面的 滑块变化得最少。看扫描运行时,滑块是在平滑扫过,而不是来回跳。
  • 可扩展。 启发式通过反射发现,所以新增一个只需要写一个类:
[TileHeuristic(DefaultEnabled = true, Order = 3)]
public sealed class FoldEdgeDetectionHeuristic : ITileQualityHeuristic
{
    public string Name => "Fold Edge Detection";
    public TileScanScore Evaluate(Color32[] px, int w, int h) { /* ... */ }
}

扫描逻辑在运行时程序集中,窗口在编辑器程序集中,各有自己的 .asmdef,因此打分代码 不依赖 UnityEditor。

坦白说明局限:这些启发式只是感知的代理指标。我没有做过量化研究来衡量它们与人工判断的 一致程度。工具也是这样呈现它们的:它们给候选排序,交给人来审阅。

为了发布砍掉了什么

通往一个可发布包的最快路径,是删东西:

  • 斑块生成器。 它在第一次提交时被带了过来(一个 794 行的 FeatureGenerator), 当天就被移除。它是另一个产品。
  • 托管的 WebGL 版本。 曾考虑在我的网站上放一个浏览器演示,这个想法记在一个草稿 笔记文件里,然后被推迟。1 月 15 日,这些草稿笔记被一个 TODO.md 取代。
  • 对 URP 的硬依赖。 包默认使用 Built-in 渲染管线,URP 材质以附带的 .unitypackage 形式提供,由 URP 用户自行导入。一个包覆盖两种管线,而不强制依赖 其中任何一个。
  • 生成器本身的体积。 第一次提交时的生成器有 697 行;发布的 QuickTileGenerator 是 226 行。

打包,1 月 14 日至 18 日

最后一段是产品工作,而不是功能工作:

  • 1 月 14 日一次 "to prep for asset store"(为上架 Asset Store 做准备)的重命名
  • 一个包含写实和风格化两套材质的演示场景
  • 重组到单一的 Assets/QuickTileStudio 文件夹,在 IrsikSoftware.QuickTileStudio 命名空间下拆分 Runtime 和 Editor 程序集(涉及 111 个文件)
  • 演示纹理重新导出,从 43.9 MB 降到 4.6 MB,缩小约 90%;包的大部分体积来自演示资源, 而不是代码
  • 一份 Keep a Changelog 格式的更新日志,以及在 1 月 18 日导出为 PDF 的手册

商店页面以 1.0.0 版本上线,发布日期为 2026 年 2 月 20 日。

一个相关的小工具:LogSmith

同一个月,我为 LogSmith 打上了首批公开 发布标签。它是一个 Unity 日志包,通过 UPM git URL 安装。

0.1.0 于 1 月 3 日 02:08 UTC 打上标签。54 分钟后,0.2.0 作为一次有意的破坏性变更发布: 静态入口类从 LogSmith 改名为 Log。一个与自己命名空间同名的类,会在名称冲突时迫使 调用方写成 LogSmith.LogSmith。这个版本附带了一张查找替换迁移表:

LogSmith.GetLogger -> Log.GetLogger
LogSmith.Logger    -> Log.Logger
LogSmith.Router    -> Log.Router

做破坏性重命名,成本最低的时机是在还没有人依赖这个名字之前。

可推广的经验

删掉宿主来检验内部工具。 如果没有游戏它仍然说得通,它就可以成为产品。

验证输出的不变量,而不是预览的。 中心 quad 的预览看起来无缝,捕获出来却会有接缝。 真正重要的性质是 2x2 周期。

把构造无法保证的部分自动化,并把代理指标明确标为代理指标。 镜像解决了像素连续性。 扫描器处理的是感知问题,它们为人排序候选,而不是声称自己能做判断。

尽早砍范围。 第二个生成器、WebGL 演示和渲染管线依赖都往后放了,包也就发布了。