前言
博客首页如果只有文章列表的话总显得有些冷清,所以我将奸商罗德岛超级总工程师——可露希尔抓请了过来()
本文记录从游戏客户端资源中提取她的 Spine 骨骼动画、最终以看板娘形式挂在博客首页的完整过程
环境:
- Windows10 22H2
- Node.js v22.23.1
概念准备
Spine
Spine 是 Esoteric Software 出品的2D骨骼动画系统:先用皮肤/骨骼/关键帧搭建动画,再导出为运行时文件。它产出的是一组配套文件,业内叫 Spine 三件套:
.skel
骨架数据:骨骼层级、插槽、关键帧动画(二进制或 JSON).png
纹理图集:所有子图拼合后的实际像素.atlas
图集描述:子图在纹理中的布局与 UV 坐标
三者缺一不可,渲染时由 Spine 运行时解析.skel的动作、按.atlas的坐标从.png上裁出对应部位进行摆位
AssetBundle 与 LZHAM
AssetBundle(AB) 是Unity的资源打包格式,把纹理、网格动画序列化进单一文件,游戏运行时按需加载
明日方舟新版AB使用 LZHAM 无损压缩算法,导致AssetStudio 这类通用 Unity 解包工具无法解码——这是很多人第一步就卡住的原因。要解它必须用针对性的专用工具——Ark-Unpacker
通道分离
明日方舟的Spine纹理把RGB与Alpha拆成两张独立图片存储。直接拼接会得到一张发灰发黑的贴图,直接应用会导致模型出现类似花屏的效果。必须把Alpha通道图与RGB通道图合并还原为 RGBA 单图,Spine才能正确渲染
ark-waifu 与 pnpm patch
有了三件套,我们还需要一个“引擎”在网页加载模型,我们选择Arkwaifu
它是一个 npm 看板娘挂载库,基于 PixiJS + Spine Runtime,负责加载三件套、驱动待机/触控动画与气泡台词
pnpm patch 是 pnpm 的依赖补丁机制:它把某个依赖的源码复制出来让你直接编辑,再用 pnpm patch-commit 把改动固化成 patches/*.patch 文件,之后每次安装都会自动重放。除此之外还有使用CDN来加载组件的方式,但是受限于网络,我就直接使用patch来改动源码了
源文件提取
获取资源
直接从官网下载apk,7z解包,在如下路径找到.ab
arknights-hg-2761\assets\AB\Android\prefabs\shop\shopkeeper\live_007_closre.ab
解包
从Ark-Unpacker下载最新release,是一个exe,把它跟.ab放到同一个目录下,双击使用

我们选择2,写入.ab路径,输出目录留空自动生成,类型全选itamj

完成后进入生成的目录,alpha通道的png即在根目录,json后续不用到,其余的文件在更内部的子目录,包括RGBonly的png和.skel以及.atlas

还没完,png需要通道合并,注意需要把所有文件放在同一目录下,Unpacker才能合并

然后在Unpacker中选择3,就能在工作目录根目录找到Combined_xxx文件夹,完整合并的png在内,最后把三件套拿出来,就算是完成啦

资源部署
将三件套与配置文件放入博客静态目录:
public/models/closure/
├── live_007_closre.skel
├── live_007_closre.atlas
├── live_007_closre.png
├── manifest.json
└── tips.json
其中manifest.json和tips.json这两个配置文件需要自己写入,arkwaifu的README中有示例,可以照着改
manifest.json
声明模型文件名、动画动作映射与渲染参数:
{
"id": "closure-shop",
"name": "可露希尔",
"type": "ark-spine",
"version": "spine-3.8",
"files": {
"skel": "live_007_closre.skel",
"atlas": "live_007_closre.atlas",
"textures": ["live_007_closre.png"]
},
"actions": {
"idle": "Idle",
"touch": "Introduce"
},
"scale": 1.2,
"position": { "x": 0.5, "y": 1.2 }
}
files指向三件套actions把库的抽象动作映射到.skel中的真实动画名scale负责调整角色在画布中的比例与站位
tips.json
状态台词,可以同时包含模型状态台词和网页DOM事件规则,不写入模型manifest,这里贴出示例,博士们自由发挥
{
"version": 1,
"lines": {
"load": ["博士,我准备好了。"],
"relax": ["先在这里休息一下。"],
"sit": ["这个位置看起来不错。"],
"stand": ["休息结束,继续工作吧。"],
"click": ["嗯?博士有什么安排吗?"],
"error": ["资源好像没有加载成功。"]
},
"rules": [
{
"event": "load",
"text": ["欢迎来到这个博客页面。"]
},
{
"selector": "a",
"event": "mouseenter",
"text": ["要打开这个链接吗?"],
"action": "touch"
},
{
"selector": ".ark-waifu-sit-target",
"event": "settle",
"text": ["这里可以坐一会儿。"],
"action": "sit",
"delayMs": 3000,
"cooldownMs": 6000
}
]
}
安装依赖与补丁
还没有将arkwaifu安装到博客中呢
pnpm add ark-waifu
pnpm patch ark-waifu
pnpm patch会返回一个编辑目录,比如node_modules/.pnpm_patches/ark-waifu@1.0.1,改完源码后执行
pnpm patch-commit node_modules/.pnpm_patches/ark-waifu@1.0.1
补丁一共有两处修改:
-
动画互斥,网页内能触发动作的地方太多了,导致鼠标一移动就开始触发各种事件,但是上一个动作还没做完下一个就来了,导致人物开始抽搐(),为此我引入
状态守卫函数,同一时刻只允许一个临时动画,播放完才恢复待机:playTemporary(t) { if (this.currentTemporaryAction === t) return; // 正在播同一个动作,直接丢弃 this.context.hasAction(t) && (this.currentTemporaryAction = t, this.context.play(t, { loop: !1, onComplete: () => { this.currentTemporaryAction = null, this.sitting || this.playBaseAction(); }})); } -
关闭按钮,在根节点创建
ark-waifu-close按钮,点击后销毁实例并派发ark-waifu:close自定义事件,交给上层组件处理:this.closeButton = document.createElement("div"); this.closeButton.className = "ark-waifu-close"; this.closeButton.textContent = "\u00d7"; this.closeButton.addEventListener("click", () => { this.destroy(); this.root.dispatchEvent(new CustomEvent("ark-waifu:close", { bubbles: true })); });
事件状态不存储,所以刷新又会回来的
组件挂载
src/components/ark-waifu/ArkWaifu.tsx:
import { useEffect, useRef, useState } from 'react';
import { mountArkWaifu } from 'ark-waifu';
import type { MountedArkWaifu } from 'ark-waifu';
export function ArkWaifu() {
const mountedRef = useRef<MountedArkWaifu | null>(null);
const [visible, setVisible] = useState(true);
useEffect(() => {
if (!visible) return;
let cancelled = false;
const init = async () => {
try {
const mounted = mountArkWaifu({
manifestUrl: '/models/closure/manifest.json',
tipsUrl: '/models/closure/tips.json',
draggable: false, // 禁止拖拽
clickAction: 'touch', // 点击触发 Introduce 动画
defaultAction: 'idle',
width: 400,
height: 480,
zIndex: 9999,
maxDpr: 1.5, // 限制高 DPI 渲染开销
fpsLimit: 30, // 帧率上限,控 CPU
pauseWhenHidden: true, // 页面隐藏时暂停渲染
pauseWhenOffscreen: true, // 滚动出屏时暂停渲染
});
mountedRef.current = mounted;
await mounted.ready;
if (cancelled) mounted.widget.destroy();
} catch (error) {
console.error('Ark-Waifu mount failed', error);
}
};
void init();
const handleClose = () => setVisible(false);
document.addEventListener('ark-waifu:close', handleClose);
return () => {
cancelled = true;
document.removeEventListener('ark-waifu:close', handleClose);
mountedRef.current?.widget.destroy();
mountedRef.current = null;
};
}, [visible]);
if (!visible) return null;
return <div />;
}
mounted.ready等待资源加载完成,React卸载时destory()释放WebGL上下文- 监听补丁派发的
ark-waifu:close事件,触发setVisible(false),不持久化
在首页挂载
src/pages/index.astro:
---
import { ArkWaifu } from '@/components/ark-waifu/ArkWaifu';
---
<Layout>
...
<ArkWaifu client:only="react" />
</Layout>
client:only="react"声明该组件跳过服务端渲染,只在客户端由 React渲染,目的在于减小初始负载
样式定制
第一次想调气泡样式时我发现在tailwind.css里写了半天,页面上一点反应都没有,原来ark-waifu不是用css来控制样式的,而是用js的内联样式,优先级更高,那我只好!important了,在游戏内商店界面能看到的可露希尔的文字框,我尝试仿制了一下
.ark-waifu-theme .ark-waifu-bubble {
right: 84px !important;
bottom: 34% !important;
width: 250px !important;
height: 84px !important;
border: 1px solid #f9fb67 !important;
border-radius: 2px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
text-align: center !important;
padding: 13px 14px !important;
color: #f9fb67 !important;
background: rgba(18, 20, 18, 0.78) !important;
font-size: 13px !important;
line-height: 1.5 !important;
}
.ark-waifu-theme .ark-waifu-bubble::before {
content: "FROM";
position: absolute;
top: 0;
left: 0;
padding: 0 3px;
color: #111 !important;
background: #f9fb67 !important;
font-family: Bahnschrift, "DIN Alternate", "DIN Condensed", "Archivo Narrow", "Oswald", "Helvetica Neue", Arial, sans-serif;
font-size: 8px;
font-weight: 700;
line-height: 11px;
}
结语
至此,你的Spine看板娘就部署完成了~欢迎各位前来调戏()
游戏资源版权归鹰角网络所有,本文方法仅限个人学习与收藏,勿用于商用。
喜欢的话,留下你的评论吧~