TEdit 遮罩系统重构指南:跨工具解耦的 TileMaskSettings 架构与液体遮罩实现解析

原创2026-10-03 14:16:551,843 阅读
文章标签:桌面应用游戏开发

TEdit 遮罩系统重构指南:跨工具解耦的 TileMaskSettings 架构与液体遮罩实现解析

本指南围绕 TEdit(Terraria 地图编辑器)规划中的 Masking System Overhaul(对应 issue #1274、#900,见 docs/todo/masking-system.md)展开,讲解其核心目标——把「遮罩」从当前编辑图层中解耦出来,使其成为独立作用于「哪些瓦片会被影响」的过滤器;并结合仓库中已落地的 TileMaskSettings 实现(src/TEdit.Editor/TileMaskSettings.cs)讲解液体遮罩、属性匹配语义、掩码求值原理与 UI 重构方向。读完本文,你将掌握 TEdit 遮罩系统的数据模型、四种匹配模式的求值规则,以及如何在画笔、铅笔、桶填充、橡皮擦乃至导线模式下利用遮罩精确控制编辑范围。

一、背景与总体目标:从「图层耦合」到「解耦遮罩」

1.1 当前行为的痛点

在原需求文档中明确指出了现有遮罩系统的核心问题:遮罩与当前正在编辑的图层强耦合——编辑瓦片(Tile)时只有瓦片遮罩生效,编辑墙体(Wall)时只有墙体遮罩生效,即「遮罩只作用于与自身同类的图层」。

这种设计的局限在于:当用户想「在墙体编辑操作中只影响特定类型的瓦片」时无从下手,遮罩能力被操作类型锁死。

1.2 新行为的语义转变

需求文档给出了两项关键定义(原文引用):

  • Masks apply regardless of which layers are being edited
  • Mask is a filter on which tiles are affected, not what operation runs

翻译成实现语言即是:

  • 遮罩不再绑定操作类型:例如设置瓦片遮罩为「Stone」,那么墙体编辑只会作用于「Stone」瓦片所在的位置;设置墙体遮罩为「Dirt Wall」,则放置执行器(Actuator)的操作只会发生在 Dirt Wall 瓦片上。
  • 遮罩是「受影响范围」过滤器,而非「运行什么操作」的开关:操作(放置、擦除、涂色)由工具决定,遮罩只负责筛选目标瓦片。

从仓库源码看,这一「全局门控」模式已经在编辑核心 WorldEditor 中体现:在 src/TEdit.Editor/WorldEditor.cs 的 SetPixel 入口处,有一段注释为 Global mask gate: if any enabled mask fails, skip this tile entirely 的判断——if (!MaskSettings.Passes(curTile)) return;。也就是说,无论当前 PaintMode 是瓦片、墙体还是涂色,遮罩评估都先于操作执行,这正是「解耦遮罩」思想的代码落点。

1.3 配套的 UI 重构要求

文档同时要求重构遮罩选择器 UI:不再维护一套独立的遮罩选择控件,而是复用统一的瓦片选择器(Unified Tile Picker)控件,其交互规格详见配套需求文档 docs/todo/pattern-selection.md。该控件提供:

  • 紧凑模式(Compact Mode):24×24 复合预览图,将瓦片、墙体、液体、瓦片涂色、墙体涂色、涂层、执行器状态全部合成渲染;带「擦除」红叉、「空/禁用」变暗虚线边框、执行器角标等状态指示。
  • 完整模式(Full Mode):每个属性一个独立的下拉选择器(瓦片类型支持搜索/过滤、涂色下拉、液体下拉等),且每个属性有独立的启用/禁用开关与擦除开关。
  • 两种模式是同一个 TilePicker UserControl 的 DisplayMode 属性切换,数据模型为 TilePickerSlot。

掩码选择器将采用该控件的紧凑模式展示,点击后展开为完整模式,并为每个属性追加一个**匹配模式(Match Mode)**下拉框,另加一个 **Invert Mask(反转遮罩)**开关。

二、液体遮罩(Liquid Masking,#900)

液体遮罩是本轮重构新增的核心能力,需求要点如下(原文):

  • Add liquid type to the mask options: Water, Lava, Honey, Shimmer
  • Mask modes for liquid: Match (only affect tiles with this liquid), Ignore (skip tiles with this liquid)
  • Works with all tools: brush, pencil, bucket fill, eraser

2.1 液体类型的枚举定义

需求中提到的四种液体,在仓库的 LiquidType 枚举中均有明确定义(src/TEdit.Terraria/LiquidType.cs):

液体 枚举值
None(无液体) 0x0
Water(水) 0x01
Lava(岩浆) 0x02
Honey(蜂蜜) 0x03
Shimmer(微光,1.4.4 引入) 0x08

2.2 液体在遮罩数据模型中的落点

在 TileMaskSettings 中,液体遮罩由两组属性承载:

  • 液体类型遮罩:LiquidTypeMaskMode(匹配模式)+ LiquidTypeMaskValue(目标液体类型,默认 LiquidType.Water);
  • 液体水平遮罩:LiquidLevelMaskMode + LiquidLevelMaskValue(byte,0–255)。

液体水平并非需求文档原文提到的新增项,但仓库实现已将其纳入(src/TEdit.Editor/LiquidLevelMaskMode.cs 定义了四种比较模式),形成了「类型 + 液位」的双重液体过滤能力:

模式 语义
Ignore 不对液体水平过滤(默认)
GreaterThan 仅作用于液体水平大于目标值的瓦片
LessThan 仅作用于液体水平小于目标值的瓦片
Equal 仅作用于液体水平等于目标值的瓦片

2.3 工具适用范围

需求文档明确液体遮罩需要「Works with all tools: brush, pencil, bucket fill, eraser」。从架构上看,这一要求由「全局遮罩门控」天然满足:由于 Passes(Tile) 在 SetPixel 入口统一执行,任何经由该路径的工具(画笔、铅笔、桶填充、橡皮擦,以及后续的导线放置)都会自动受到液体遮罩约束,无需每个工具单独实现液体过滤逻辑。

三、遮罩属性体系:统一数据模型与匹配模式

3.1 需求文档中的属性表

需求文档给出了遮罩属性与匹配模式的原始定义(原文表格,完整保留):

Property Match Mode Description
Tile type Exact / Any / Empty Filter by tile ID
Tile paint Exact / Any / None Filter by tile paint color
Wall type Exact / Any / Empty Filter by wall ID
Wall paint Exact / Any / None Filter by wall paint color
Liquid Exact / Any / None Filter by liquid type
Actuator On / Off / Any Filter by actuator state
Coating Exact / Any / None Filter by coating type

并给出三种匹配模式的语义定义(原文):

  • Exact: only affect tiles matching this value
  • Any: don't filter on this property
  • Empty/None: only affect tiles where this property is unset

3.2 源码中的枚举对应关系

需求文档的「Exact / Any / Empty / None / On / Off」在仓库实现中被收敛为四个枚举值(src/TEdit.Editor/MaskMode.cs):

枚举值 说明 与需求文档的对应
Off 关闭该属性的遮罩过滤 Any(不过滤)
Empty 仅作用于该属性为「空」的瓦片 Empty / None
Match 仅作用于与该属性值精确匹配的瓦片 Exact / On
NotMatching 仅作用于与该属性值不匹配的瓦片(即反转) 需求中的 Invert Mask 开关

3.3 完整属性清单(需求 + 实现)

需求文档表格列出的 7 类属性,在 src/TEdit.Editor/TileMaskSettings.cs 中被实现为「模式 + 值」成对出现的 Reactive 属性。结合源码,实际的遮罩属性全集如下(MaskMode 均可取上表四值之一):

类别 属性(模式 / 值) 值语义
瓦片类型 TileMaskMode / TileMaskValue 瓦片 ID;-1 表示「空/未激活」
墙体类型 WallMaskMode / WallMaskValue 墙体 ID;0 表示「无墙体」
砖块样式 BrickStyleMaskMode / BrickStyleMaskValue BrickStyle 枚举(如 Full 等)
执行器 ActuatorMaskMode 瓦片是否含执行器
执行器未激活 ActuatorInActiveMaskMode 瓦片是否处于未激活(InActive)状态
瓦片涂色 TilePaintMaskMode / TilePaintMaskValue 涂色 ID;0 为无涂色
墙体涂色 WallPaintMaskMode / WallPaintMaskValue 墙体涂色 ID;0 为无涂色
瓦片 Echo 涂层 TileEchoMaskMode 瓦片是否隐形(InvisibleBlock)
瓦片 Illuminant 涂层 TileIlluminantMaskMode 瓦片是否发光(FullBrightBlock)
墙体 Echo 涂层 WallEchoMaskMode 墙体是否隐形(InvisibleWall)
墙体 Illuminant 涂层 WallIlluminantMaskMode 墙体是否发光(FullBrightWall)
红色导线 WireRedMaskMode 瓦片是否含红色导线
蓝色导线 WireBlueMaskMode 瓦片是否含蓝色导线
绿色导线 WireGreenMaskMode 瓦片是否含绿色导线
黄色导线 WireYellowMaskMode 瓦片是否含黄色导线
液体类型 LiquidTypeMaskMode / LiquidTypeMaskValue LiquidType 枚举
液体水平 LiquidLevelMaskMode / LiquidLevelMaskValue 液位 0–255,比较模式见上文

可以看到,仓库实现比需求文档的原始列表多出砖块样式、执行器未激活、Echo/Illuminant 涂层、四种导线、液体水平等属性——说明遮罩数据模型在落地过程中已经按同一套「模式 + 值」范式做了横向扩展,四色导线属性也为「导线模式扩展」需求(见下文第六节)提前铺好了数据基础。

四、遮罩求值原理:Passes() 与 AND 语义

4.1 统一的求值入口

TileMaskSettings.Passes(Tile)(src/TEdit.Editor/TileMaskSettings.cs)是全部遮罩的统一求值函数,其文档注释明确了关键语义:

Evaluates all active masks against the given tile. Returns true if the tile passes all masks. All masks are AND'd: if any enabled mask fails, the tile is rejected.

即所有已启用的遮罩之间是 AND 关系:任何一个属性不满足,瓦片即被拒绝。这是保证「遮罩是可预测的范围过滤器」的基础——不会出现某个属性放行、另一个属性拦截的歧义。

4.2 快速路径:AllMasksOff

在 Passes 之上还有一个性能优化属性 AllMasksOff(同上文件 L62-L79),它枚举全部 17 个遮罩模式并检查是否全部处于 Off(液体水平处于 Ignore)。若全部关闭,Passes 直接短路返回 true,调用方(如整图替换逻辑)也可以据此跳过整轮遍历:

  • SetPixel 中通过 Passes 逐瓦片过滤(src/TEdit.Editor/WorldEditor.cs);
  • ReplaceAll(整图替换,src/TEdit.Editor/WorldEditor.cs)在入口处检查 if (MaskSettings.AllMasksOff) return false;——所有遮罩关闭时没有可匹配的目标,直接返回失败,避免无意义的全图遍历;遍历循环内同样以 if (!MaskSettings.Passes(curTile)) continue; 过滤,且支持仅在当前选区(selectionOnly)内执行。

4.3 各类属性的求值细节

Passes 内部将属性分为四类求值模式,均有独立的私有求值器:

  • 瓦片类型(PassesTileMask):处理特殊的 -1(空/未激活)情形——Match 且值为 -1 时匹配「非激活瓦片」,Match 且值 ≥ 0 时要求 IsActive && Type == value,Empty 时匹配非激活瓦片,NotMatching 时取反;
  • 整数型属性(PassesIntMask,静态):墙体、涂色等 ID 值统一走此路径,Match 要求相等、Empty 要求等于空值(墙体为 0、涂色为 0)、NotMatching 要求不等;
  • 布尔型属性(PassesBoolMask,静态):执行器、涂层、导线等「有/无」属性,Match 要求为真、Empty 要求为假、NotMatching 取反;
  • 液体类:PassesLiquidTypeMask 处理液体类型(Empty 时匹配 LiquidAmount == 0 或 LiquidType == None);PassesLiquidLevelMask 处理液位比较(Equal/GreaterThan/LessThan)。

4.4 一键取样与清空

数据模型还提供两个实用方法:

  • SetFromTile(Tile)(L140-L178):把目标瓦片的所有属性一次性「取样」为遮罩条件——将所有模式置为 Match(布尔属性按瓦片实际状态置 Match 或 Empty),值取自该瓦片,最终把 MaskPreset 置为 ExactMatch。这对应「用吸管工具取样一个瓦片,然后用它作为全图匹配条件」的典型工作流(PickerTool 即位于 src/TEdit/Editor/Tools/PickerTool.cs)。
  • ClearAll()(L183-L210):将所有模式复位为 Off/Ignore、值复位为默认值,并把 MaskPreset 置为 Off。

4.5 预设机制

MaskPreset 枚举(src/TEdit.Editor/MaskPreset.cs)为遮罩组合提供命名状态:Off(关闭)、Custom(自定义组合)、ExactMatch(与某瓦片完全一致,由 SetFromTile 写入),供 UI 快速切换/恢复遮罩配置,默认定义见 src/TEdit.Editor/ToolDefaultData.cs。

4.6 测试印证

仓库测试 src/TEdit.Tests/Editor/CannonballSlopeTests.cs 展示了 WorldEditor 的构造方式:new WorldEditor(picker, mask, world, selection, undo, null),其中 mask = new TileMaskSettings() 作为独立依赖注入,印证了「遮罩是独立于工具/操作的核心组件」这一架构定位。

五、Mask Picker UI 重构方向

5.1 复用统一瓦片选择器

需求要求遮罩选择器直接复用统一瓦片选择器控件(详细交互见 docs/todo/pattern-selection.md):

  • 以 Compact Mode(24×24 复合预览) 展示当前遮罩组合的合成效果;
  • 点击后展开为 Full Mode,每个属性显示独立的匹配模式下拉框(Exact / Any / Empty);
  • 追加 Invert Mask 开关——「影响除匹配之外的所有瓦片」,对应源码中的 MaskMode.NotMatching(即 Passes 中所有 NotMatching 分支取反的语义)。

5.2 现有实现的分区布局参考

仓库当前的面板 src/TEdit/View/Sidebar/Controls/MaskOptionsPanelControl.xaml 已经按照「属性分区」组织遮罩编辑界面,可作为重构后的布局基准:

  • Type 分区:瓦片/墙体两个并排区域,各含匹配模式下拉(MaskModeEnum 资源,带图标模板)与 TabbedPickerControl 分页选择器(支持搜索/过滤,ModIdThreshold 用于区分原版与 Mod 内容);
  • Brick Style 分区:砖块样式模式 + 样式值;
  • Paint 分区:瓦片涂色 / 墙体涂色两组「模式 + 颜色下拉」;
  • Coating 分区:Echo(隐形)与 Illuminant(发光)分别作用于瓦片/墙体;
  • Extras 分区:Actuator(执行器)与 Inactive(未激活);
  • Wires 分区:红/蓝/绿/黄四色导线的开关匹配;
  • Liquid 分区:液体类型(模式 + LiquidType 下拉)与液体水平(模式下拉 + 0–255 滑块,IsSnapToTickEnabled 步进 1)。

所有值均通过 TwoWay 绑定到 MaskSettings 对应属性,模式下拉绑定 MaskModeEnum、值下拉绑定 WorldConfiguration 提供的瓦片/墙体/涂色数据源(如 TileBricksMask、WallPropertiesMask、PaintProperties)。

六、导线模式扩展(Wire Mode Extension)

需求文档最后要求将解耦遮罩扩展到导线编辑模式:

  • Extend decoupled masking to wire editing mode
  • Example: place actuators only on tiles matching a specific tile ID
  • Example: place wire only on tiles with a specific wall type
  • All mask properties available in wire mode, same as other tool modes

这一需求的实现基础在数据层已经完备:

  • TileMaskSettings 中已有四色导线遮罩(WireRedMaskMode / WireBlueMaskMode / WireGreenMaskMode / WireYellowMaskMode),可分别按「含/不含某色导线」过滤;
  • 与需求示例对应:放置执行器时以 TileMaskMode = Match 限定瓦片 ID;放置导线时以 WallMaskMode 限定墙体类型;
  • 由于 Passes(Tile) 是 SetPixel 路径上的全局门控,导线放置(同样经由编辑核心)天然继承全部遮罩属性,无需为导线模式单独复制一份遮罩逻辑——这正是「All mask properties available in wire mode, same as other tool modes」的架构保证。

七、与其他子系统的关系

  • 统一瓦片选择器 / 图案填充:遮罩选择器与图案调色板共用同一个 TilePicker 控件与 TilePickerSlot 数据模型(见 docs/todo/pattern-selection.md),三者(主编辑栏、图案槽、遮罩选择器)行为一致;
  • 整体规划:该重构属于 TEdit 发布路线图中的「Masking System Overhaul」(#2168),与统一瓦片选择器(#2169)、Morph 工具(#533)、tModLoader 支持(#1075)等并列,完整路线见 docs/todo/README.md;其中 #1274 已拆分为多个子任务(#2168、#2169、#2170、#2171)并分别跟进。

八、总结与使用要点

  1. 遮罩 = 范围过滤器:遮罩决定「哪些瓦片会被影响」,操作类型由工具决定,两者解耦;
  2. 四值匹配模式:Off(不过滤)/ Match(精确匹配,对应需求的 Exact/On)/ Empty(匹配属性为空,对应 Empty/None)/ NotMatching(反转,对应 Invert Mask 开关);
  3. AND 语义:多个遮罩同时启用时需全部通过,Passes(Tile) 是唯一求值入口,AllMasksOff 提供零开销快速路径;
  4. 液体遮罩:类型(Water/Lava/Honey/Shimmer)+ 液位(Equal/GreaterThan/LessThan)双维度过滤,适用于画笔、铅笔、桶填充、橡皮擦等全部工具;
  5. 导线模式:四色导线遮罩数据已就绪,配合瓦片/墙体遮罩可实现「只在特定瓦片/墙体上放线或执行器」的精确布线;
  6. UI 演进:最终形态是复用统一瓦片选择器(紧凑 24×24 预览 + 完整模式),每个属性带匹配模式下拉与反转开关;当前 MaskOptionsPanelControl.xaml 的分区布局可视为过渡形态。

若要在自己的实现中复刻这套遮罩架构,推荐直接参照 src/TEdit.Editor/TileMaskSettings.cs 的「模式 + 值」成对属性、统一 Passes 求值入口与编辑器核心的全局门控模式,这是保证遮罩在所有工具间行为一致、可测试、可扩展的最小骨架。

登录后查看全文
Terraria-Map-Editor