上一篇写的是角色怎么从 Clear-only 定型成三时代单角色。这篇换一条轴:卡看起来像什么,以及这套外观在尖塔运行时里归谁管。
木之本樱的牌不完全走原版「卡框 + 横向肖像」管道。透明牌用 Clear 全脸图;库洛 / 小樱 / 符咒共用 Classic 全脸渲染器。全脸图好看,但工程问题立刻变硬——因为尖塔的 NCard 是池化节点:同一张 UI 壳子会先后显示你的牌、原版牌、别的 Mod 的牌。
这篇文章记录这套视觉系统真正想守住的边界。重点不是某个 PNG 路径,而是:
每一次改视觉,都是在借节点;借走的状态,必须由同一个所有者还回去。
为什么不能只换肖像
原版卡面大致是:固定卡框、中间肖像、费用图标、标题与描述由原生控件渲染。很多 Mod 只要换 PortraitPath,再偶尔改卡框材质,就能过关。
全脸卡不是这条路。整张可见卡面是一张带透明度、外框、缎带和内区的成品图;费用、标题、描述仍要叠在原生控件上,选中高光、不可打出能量、附魔页签、异常状态覆盖层也还活着。于是渲染器同时要做三件事:
- 画出自家全脸;
- 把原生控件挪到自家几何里;
- 在节点被拿去显示别人的牌时,把一切改动撤干净。
早期 Clear Card 原型里,卡框分层预览已经暗示了这一点:图负责视觉身份,可读信息仍由 Mod UI / 原版控件承担。后来 Classic 全脸加入后,问题从「一种皮肤」变成「两种布局共享一套生命周期」。

左边是透明牌用的 Clear 全脸:粉金框、缎带、外缘透明,源图约 787×1717。右边是库洛 / 小樱 / 符咒共用的 Classic 全脸:更厚实的卡面语言,源图约 221×491,由同一套 Classic 渲染器绘制。两者都不是「换一张横向肖像」那么简单,但它们共享后面要讲的生命周期与几何合同。
先分清三个词
项目里最容易混的,是把「这是不是小樱的牌」和「这张牌此刻用哪套皮」当成一件事。现在刻意拆开:
| 概念 | 回答 | 当前取值 |
|---|---|---|
| 内容归属 | 这张模型算不算 Sakura 内容? | 由目录判定;先古特例可归 Sakura,却不走自定义全脸 |
| 视觉路线 | 走 Kinomoto 自定义管线,还是原版管线? | Kinomoto / Vanilla |
| 渲染布局 | 具体用哪套全脸渲染器? | Clear / Classic / None |
合并角色之后,库洛、小樱、透明、符咒都走 Kinomoto 路线;透明仍用 Clear 布局,其余源卡与符咒用 Classic 布局。公开的 Clear / Classic 家族路由已经退场——它们是布局,不是角色身份。
这条拆分有直接收益:选项牌可以借用 Clear 全脸外观,却不必因此进入透明牌奖励目录;先古牌可以仍是 Sakura 内容,却继续用原生肖像与 Layout.None。玩法词汇和皮肤词汇不再互相绑架。
整条链可以压成一张所有权图:路由决定走哪条皮,生命周期门面负责恢复与应用,Clear / Classic 只拥有全脸层,原生控件继续拥有费用、标题、附魔与异常,账本记下每一次借用。
池化节点:真正的对手
NCard 不会为每张牌永久占一个节点。战斗、奖励、图鉴、商店预览都会复用壳子。于是会出现这种时间线:
- 节点刚显示过一张 Clear 全脸;
- 下一秒被拿去显示铁甲战士的打击;
- 再下一秒显示另一张 Classic 全脸;
- 中途还可能夹着外部卡图动画 Mod 的 postfix。
如果 Clear 渲染器只「把自己画上去」,不记录它改过哪些属性,原版卡就会带着粉框偏移、错误高光、残留主题色,或者标题描边被删光。反过来,如果恢复时把主题覆盖「一律删除」,又会误伤其他角色或其他美化 Mod 装上的合法覆盖。
所以合同写成:
- 改之前捕获原状态;
- 只登记自己真正改过的属性;
- 恢复时按捕获值精确还回,包括「当时有没有 theme override」。
共享的 SakuraCardMutationLedger 就是这件事的账本。同一节点上,新渲染器开始前要先让旧渲染器还账;同一渲染器反复应用时,保留第一次捕获的基线,避免相对坐标被二次映射越推越偏。
有一条很具体、也很典型的坑:恢复 CustomMinimumSize 必须先于 Size。Godot 会按当前最小值钳制尺寸;先写小尺寸再降低最小值,控件不会缩回去。结果是原版池化卡继承了小樱标签高度,即使「捕获到的原生 Size」看起来是对的。
视觉与几何为什么要拆开
全脸卡有两套容易缠在一起的职责:
- 视觉生命周期:贴图、主题色、谁拥有当前 ledger 会话、资源失效怎么恢复;
- 几何生命周期:卡根尺寸、holder、手牌间距、网格单元格、悬停预览、选中高光层。
早期两者散落在 Clear / Classic 两套补丁里,同一表面会出现重复入口。收敛后的边界是:
| 所有者 | 负责 | 不负责 |
|---|---|---|
SakuraCardVisualLifecycle | 旧布局恢复、当前布局应用、renderer ledger、失效贴图恢复 | 手写几何公式、纹理像素生成 |
SakuraCardGeometryLifecycle | 根/holder/手牌/网格/悬停尺寸与高光几何 | 加载纹理、开启或提交 renderer ledger |
ClearCardLayout / ClassicCardLayout | 各自全脸与 holder 点缀 | 手牌目标 X、网格居中、全局间距策略 |
| Patch / Dispatcher | 接到原生 seam 后转发一次 | 家族分支、资源缓存、布局公式 |
补丁类应当很薄:一个原生缝只调用一个 lifecycle 入口。判断「当前该 Clear 还是 Classic」属于 lifecycle / family 路由,不属于 Harmony 适配器自己的 if 森林。
资源恢复也有硬边界:恢复路径可以修贴图,但不得顺手改几何或重跑整套渲染。_EnterTree 这类不稳定时机尤其不能借机抓布局快照。
全脸图与原生肖像:两套所有权
全脸卡最反直觉的一条,是:
玩家看见的脸,不等于
CardModel.Portrait那条原生肖像槽。
Clear / Classic 全脸由布局渲染器加载和绘制;原生三条肖像路径改走游戏自带的缺失肖像占位。可见全脸可以进入渲染器自己的 run asset 列表,但不要把那张被藏起来的原生肖像再塞进 run teardown 清单。
原因很实际:跑局结束会卸载 run 资源;同进程里稍后 NCard.Reload 仍可能复用缓存过的原生肖像包装。若同一张贴图既是模型肖像,又是 run 作用域资源,弃跑后再开小樱局,就容易在展牌时撞上已释放的 CompressedTexture2D。
例外也写死:Layout.None 的小樱先古牌,原生肖像就是可见内容,因此它的真肖像仍是 run asset。规则不是「小樱牌一律没有肖像」,而是「谁负责让玩家看见,谁就拥有那张可见资源」。
必须留给原版的东西
全脸渲染器很容易越权。项目里几条「看起来能改、其实不该动」的边界特别值钱。
不可打出能量图标是能量图标的子覆盖层,不是根坐标系里的另一枚费用图标。布局可以重排父级能量图标和费用数字,但不应借、移、换贴或直接隐藏那个子覆盖层;把父级费用盒再套到子覆盖层上,等于偏移算两次,数字和斜杠会对不上。
附魔页签与附魔 VFX继续由原生 NCard 拥有可见性与内容。全脸卡用的是左上原点,原版附魔控件却按居中本地坐标摆放;几何层只借 Position,再按原版卡尺寸做一次原点换算,并保留原生尺寸。更麻烦的是:附魔变更通知会在不走完整 UpdateVisuals 的情况下重置页签位置,所以需要一条只重映射页签的窄补丁,而不是整卡重渲——整卡重渲会把原点平移加第二次。
异常状态覆盖层也不能一藏了之。原版七种 profile 要按小樱卡面尺寸重适配遮罩、饰件与粒子锚点;统一非等比拉伸整棵 VFX 子树会直接穿帮。带 pivot 的饰件必须按「轴心投影」映射位置,只乘 Position 再改 Scale,肉眼上看就是一对号角往旁边歪。
这些细节听起来琐碎,但它们共同回答同一个问题:全脸卡是皮肤,不是把 NCard 整棵树收编成私有 UI。
手牌间距:混排比单排难
Clear 与 Classic 的可视外轮廓都比原版 300x422 更瘦高。全是小樱牌时,只要按自家 footprint 留缝即可;一手里同时出现小樱牌与原版牌时,再用「一律 +24」这种常数修正,边界会变成离群值。
现在的做法是:
- 先吃原版给出的目标 X 序列;
- 只对含小樱布局的相邻对求解中心距;
- 用角度、缩放后的半宽估算真实视觉边界;
- 整串算完后一次性重新居中;
- 只写 holder 的目标 X,不动 Y、旋转、缩放、拖拽与焦点。
同布局对要精确复现既有 Clear/Clear 或 Classic/Classic footprint;只有混排边界才几何求解。Clear 渲染器与 Classic 渲染器都不再各自写手牌间距——那是共享几何合同,不是两套皮肤私货。
高光也是规则文本
手牌高光不只是「能打出发蓝光」。小樱把一部分战斗规则编进颜色:
| 条件 | 高光 |
|---|---|
| 遗忘中的透明牌 | 红(状态指示,即使当前不可打出也保留) |
| 可打出,且额外效果将被激活 | 金 |
| 可打出,且无上述状态 | 原版蓝 |
| 不可打出,且非遗忘透明牌 | 不覆盖成小樱色 |
金光的门控必须跟原版一样看 CanPlay(),不能看「当前是不是已经金色」。池化 holder 可能还残留上一张牌的金色 modulate,而原生逻辑已经把不可打出牌藏起来了。遗忘红优先于金:它说的是牌的寿命状态,不是「这张现在能出额外效果」。
视觉层也不应自己复读魔力充能、锁定之类条件;是否显示额外效果激活,走统一的额外效果事务判断。否则高光会和结算各说各话。
和外部 Mod 共处时,所有权更重要
实战里很少只有 SakuraMod 在动卡图。卡图替换、卡图动画这类 postfix 会在 UpdateVisuals / _EnterTree 上再挂一层。小樱侧做过的选择是:
- 不读、不清、不改别人的私有贴图缓存;
- 只在能证明「当前模型与 Sakura 自有节点已有效」时,才吞掉特定的已释放
CompressedTexture2D异常; - 外部动画正常时不覆盖;外部资源失效时退回当前模型的有效静态卡图,而不是黑图或半初始化节点;
- 非资源异常继续上抛,避免把真正的逻辑错误藏进「视觉恢复」。
这也解释了为什么全脸渲染器要尽量在默认优先级先于后加载的动画 postfix 落定:恢复最终器只能修已应用的自有贴图,不能靠「我最后再画一次」硬抢时序。
一条明确失败过的路
有过一次看起来很合理的修复:给 Sakura 创建的视觉节点做共享登记,在 UpdateVisuals 前后按当前家族把「外来层」藏起来,指望靠可见性清扫堵住池化泄漏。
结果是行为更差,改动在提交前撤回。
失败原因也很清楚:它在治疗症状,而不是证明池化子树的恢复顺序与所有权生命周期;它在热路径上铺开前后缀清理,容易和布局补丁抢跑;泄漏本身也不只是「多了一层自己的 overlay」,还包含 Clear 占位图与 Classic 图互相污染。
后来的路线相反:先认清每个字段的所有者,再用 ledger 证明借还,而不是事后扫楼。
设计上想守住的几句话
如果把这套系统压成可复述的原则,大概是这些:
- 玩法目录决定内容归属,布局枚举决定皮肤,二者分开。
- 全脸图由布局渲染器拥有;原生肖像槽不要偷偷共用同一张 run 资源。
- 几何与贴图生命周期分家;补丁只转发,不决策。
- 能留给原版的控件就留给原版;只适配坐标系,不接管所有权。
- 池化节点上的每项变异都要可验证地还原;主题覆盖按原样装回,不要一律删除。
- 和其他美化 Mod 共处时,只做有证据的资源隔离,不做私有缓存清道夫。
上一篇说的是角色如何用三时代卡池表达自己;这篇说的是这些卡如何在尖塔的 UI 壳子里不互相污染地显形。两者其实是同一类问题:边界清楚了,系统才装得下更多内容。
至于某张全脸图怎么从原画裁到 787x1717、描述区遮罩如何兼顾中英文换行,那是资产管线的另一篇文章。这里先把更硬的那一层写清楚——没有所有权合同,再好看的卡面也只是下一次池化复用时的事故材料。