656 lines
23 KiB
JavaScript
656 lines
23 KiB
JavaScript
// ============================================================
|
||
// SpineMgr —— Spine 动画管理器(独立文件)
|
||
// 集成到 gameabc Canvas 2D 绘制循环,零侵入 gamemain.js
|
||
// 在 index.html 中置于 gameabc.min.js 之后、gamemain.js 之前加载
|
||
// ============================================================
|
||
(function(){
|
||
|
||
// gameabc 的全局 logmessage 默认走 mode 0,是空操作(见 gameabc.min.js 的实现),
|
||
// 诊断日志必须直接走 console 才能在浏览器与真机 WebView 里看到。
|
||
function spineLog(msg) {
|
||
if (typeof console !== "undefined" && console.log) {
|
||
console.log(msg);
|
||
}
|
||
}
|
||
function spineWarn(msg) {
|
||
if (typeof console !== "undefined" && console.warn) {
|
||
console.warn(msg);
|
||
}
|
||
}
|
||
|
||
var SpineMgr = {
|
||
|
||
// --- 内部状态 ---
|
||
_entries: {}, // {id: EntryObject}
|
||
_assetManager: null,
|
||
_backend: null, // 渲染后端(SpineBackendWebGL / SpineBackendCanvas)
|
||
_lastTime: 0, // 上一帧时间戳(ms)
|
||
_inited: false,
|
||
_loadedPaths: {}, // 已请求加载的资源路径,避免重复请求
|
||
_pendingCmds: {}, // {id: [{method, args}]} 等待实例就绪后执行的命令队列
|
||
_scaleMap: {}, // {spineId: scale} 主流 SkeletonJson.scale 方案,_autoLoad 时注入
|
||
_skelDataCache: {}, // {"file.json_scale1": SkeletonData} 同文件共享解析结果,避免重复 readSkeletonData
|
||
|
||
// 默认资源根路径(可通过 init 覆盖)
|
||
_basePath: "assets/spine/",
|
||
|
||
// ----------------------------------------------------------
|
||
// init(basePath) 手动初始化(可选)
|
||
// 如不调用,load 时会自动以默认路径 "assets/spine/" 初始化
|
||
// ----------------------------------------------------------
|
||
init: function(basePath) {
|
||
if (!window.spine) {
|
||
spineWarn("[SpineMgr] spine 运行时未加载,请检查 index.html 的探测脚本");
|
||
return;
|
||
}
|
||
this._backend = this._selectBackend();
|
||
if (!this._backend) {
|
||
spineWarn("[SpineMgr] 无可用渲染后端");
|
||
return;
|
||
}
|
||
spineLog("[SpineMgr] 渲染后端: " + this._backend.name);
|
||
this._basePath = basePath || this._basePath;
|
||
// 挂接 context lost 恢复回调(仅 WebGL 后端有此机制)
|
||
var self = this;
|
||
if (this._backend.name === "webgl") {
|
||
this._backend._onContextRestored = function() {
|
||
self._rebuildAfterContextRestored();
|
||
};
|
||
}
|
||
// 后端初始化失败必须显式记录并让 _inited 保持 false,但不能让异常向上抛:
|
||
// init 由文件末尾的预加载块经 _ensureInit() 调用,异常会中断整个 IIFE,
|
||
// 导致后面的 gameenddraw 拦截根本不安装 —— Spine 从此永不绘制且无迹可循。
|
||
// _inited 保持 false 时,各 API 走既有的 if (!this._inited) return 静默路径。
|
||
try {
|
||
this._assetManager = this._backend.createAssetManager(this._basePath);
|
||
} catch (err) {
|
||
spineWarn("[SpineMgr] 后端初始化失败: " + err.message);
|
||
this._backend = null;
|
||
return;
|
||
}
|
||
this._lastTime = Date.now();
|
||
this._inited = true;
|
||
},
|
||
|
||
// (内部) 选定渲染后端。index.html 的探测脚本已决定加载哪套 spine 运行时,
|
||
// 此处按运行时实际提供的能力对应上后端实现。
|
||
_selectBackend: function() {
|
||
if (window.SpineBackendWebGL && SpineBackendWebGL.isAvailable()) {
|
||
return SpineBackendWebGL;
|
||
}
|
||
if (window.SpineBackendCanvas && SpineBackendCanvas.isAvailable()) {
|
||
return SpineBackendCanvas;
|
||
}
|
||
return null;
|
||
},
|
||
|
||
// (内部) 确保已初始化
|
||
_ensureInit: function() {
|
||
if (!this._inited) {
|
||
this.init(this._basePath);
|
||
}
|
||
},
|
||
|
||
// ----------------------------------------------------------
|
||
// (内部) _preloadAll()
|
||
// 按 gameabc_face.spineAssets 清单,把全部 Spine 资源注入当前 AssetManager 并发起加载。
|
||
// ⚠️ 必须整批注入,不能只补"当前已存在的 entry":
|
||
// 规避 file:// 下 XHR CORS 用的 rawDataUris 是 Downloader 实例私有的
|
||
// (AssetManagerBase 构造时默认 new Downloader()),换一个新 AssetManager
|
||
// 就等于换了一份空 rawDataUris。若只补已存在的 entry,context 恢复之后
|
||
// 首次触发的新特效(如天胡)会走真实 XHR 被 CORS 拦掉,_tryBuild 里 require()
|
||
// 抛异常 → ready 恒 false → 每帧重试且永不显示。
|
||
// 返回本次注册的资源组数。
|
||
// ----------------------------------------------------------
|
||
_preloadAll: function() {
|
||
var list = gameabc_face.spineAssets;
|
||
if (!list || list.length === 0) return 0;
|
||
var textData = gameabc_face.spineTextData || {};
|
||
for (var i = 0; i < list.length; i++) {
|
||
var name = list[i];
|
||
var jsonKey = name + ".json";
|
||
var atlasKey = name + ".atlas";
|
||
// 将嵌入文本数据注册为 rawDataURI,Spine Downloader 会优先从内存读取
|
||
if (textData[jsonKey]) {
|
||
this._assetManager.setRawDataURI(jsonKey, "data:," + textData[jsonKey]);
|
||
}
|
||
if (textData[atlasKey]) {
|
||
this._assetManager.setRawDataURI(atlasKey, "data:," + textData[atlasKey]);
|
||
}
|
||
this._assetManager.loadText(jsonKey);
|
||
this._loadedPaths[jsonKey] = true;
|
||
this._assetManager.loadTextureAtlas(atlasKey);
|
||
this._loadedPaths[atlasKey] = true;
|
||
}
|
||
spineLog("[SpineMgr] 预加载 " + list.length + " 组 Spine 资源" +
|
||
(Object.keys(textData).length > 0 ? "(使用嵌入数据)" : "(使用网络请求)"));
|
||
return list.length;
|
||
},
|
||
|
||
// (内部) 自动加载:根据 id 约定文件名 id.json / id.atlas
|
||
// 主流 SkeletonJson.scale 方案:从 _scaleMap 读取目标缩放,传入 load() 在解析时烘焙
|
||
_autoLoad: function(id) {
|
||
if (this._entries[id]) return;
|
||
var scale = this._scaleMap[id] || 1;
|
||
this.load(id, id + ".json", id + ".atlas", { scale: scale });
|
||
},
|
||
|
||
// (内部) 将命令加入等待队列
|
||
_queueCmd: function(id, method, args) {
|
||
if (!this._pendingCmds[id]) this._pendingCmds[id] = [];
|
||
this._pendingCmds[id].push({method: method, args: args});
|
||
},
|
||
|
||
// (内部) 实例就绪后执行队列中的命令
|
||
_flushCmds: function(id) {
|
||
var cmds = this._pendingCmds[id];
|
||
if (!cmds || cmds.length === 0) return;
|
||
delete this._pendingCmds[id];
|
||
for (var i = 0; i < cmds.length; i++) {
|
||
var cmd = cmds[i];
|
||
this[cmd.method].apply(this, [id].concat(cmd.args));
|
||
}
|
||
},
|
||
|
||
// ----------------------------------------------------------
|
||
// load(id, jsonFile, atlasFile, option)
|
||
// 加载一组 Spine 资源, id 为自定义标识
|
||
// option: {x, y, scale, skin, animation, loop, mixDuration}
|
||
// ----------------------------------------------------------
|
||
load: function(id, jsonFile, atlasFile, option) {
|
||
this._ensureInit();
|
||
if (!this._inited) return;
|
||
var opt = option || {};
|
||
// 只加载尚未请求过的资源(预加载过的会跳过)
|
||
if (!this._loadedPaths[jsonFile]) {
|
||
this._assetManager.loadText(jsonFile);
|
||
this._loadedPaths[jsonFile] = true;
|
||
}
|
||
if (!this._loadedPaths[atlasFile]) {
|
||
this._assetManager.loadTextureAtlas(atlasFile);
|
||
this._loadedPaths[atlasFile] = true;
|
||
}
|
||
this._entries[id] = {
|
||
jsonFile: jsonFile,
|
||
atlasFile: atlasFile,
|
||
skeleton: null,
|
||
state: null,
|
||
x: opt.x || 0,
|
||
y: opt.y || 0,
|
||
scale: opt.scale || 1,
|
||
skin: opt.skin || "default",
|
||
defAnim: opt.animation || null,
|
||
defLoop: opt.loop !== undefined ? opt.loop : true,
|
||
mixDur: opt.mixDuration || 0.2,
|
||
visible: true,
|
||
ready: false,
|
||
_hideOnComplete: false, // 播放完成后是否自动隐藏
|
||
_hideAfterCompletes: 0, // 剩余多少次 complete 后触发隐藏
|
||
_buildFailLogged: false // 构建失败日志只记一次(_tryBuild 每帧调用)
|
||
};
|
||
},
|
||
|
||
// ----------------------------------------------------------
|
||
// ensureEntry(id, jsonFile, atlasFile, option)
|
||
// 确保 id 对应的 entry 存在并达到 option 指定的状态。
|
||
// entry 生命周期归 SpineMgr 管,调用方无需掌握"不在就 load、在就 setScale"的内部规则,
|
||
// 也无需从 id 推导文件名(entryId 与资源文件名不一定同名,如 ting_0 -> ting.json)。
|
||
// ----------------------------------------------------------
|
||
ensureEntry: function(id, jsonFile, atlasFile, option) {
|
||
var e = this._entries[id];
|
||
if (!e) {
|
||
this.load(id, jsonFile, atlasFile, option);
|
||
return;
|
||
}
|
||
if (option && option.scale !== undefined) {
|
||
this.setScale(id, option.scale);
|
||
}
|
||
},
|
||
|
||
// ----------------------------------------------------------
|
||
// isReady() 后端是否已就绪、能真正播放 Spine
|
||
// 探测阶段的 canvas 兜底只覆盖"WebGL 不可用";若探测通过但 GL 初始化失败
|
||
// (叠加层 DOM 缺失 / 并发 context 耗尽 / shader 编译失败),此时 canvas 运行时
|
||
// 并未加载、回不去,调用方应据此走各自既有的帧动画降级路径,
|
||
// 而不是让 Spine 静默失效却以为播成功了。
|
||
// ----------------------------------------------------------
|
||
isReady: function() {
|
||
return !!this._inited;
|
||
},
|
||
|
||
// ----------------------------------------------------------
|
||
// (内部) 资源加载完成后实例化骨骼
|
||
// 同一 jsonFile+scale 的 SkeletonData 只解析一次,多个 entry 共享
|
||
// ----------------------------------------------------------
|
||
_buildEntry: function(entry) {
|
||
spineLog("[SpineMgr] _buildEntry: " + entry._id + " skelJson.scale=" + entry.scale);
|
||
var cacheKey = entry.jsonFile + "_s" + entry.scale;
|
||
var skelData = this._skelDataCache[cacheKey];
|
||
if (!skelData) {
|
||
var atlas = this._assetManager.require(entry.atlasFile);
|
||
var loader = new spine.AtlasAttachmentLoader(atlas);
|
||
var skelJson = new spine.SkeletonJson(loader);
|
||
skelJson.scale = entry.scale;
|
||
skelData = skelJson.readSkeletonData(
|
||
this._assetManager.require(entry.jsonFile)
|
||
);
|
||
this._skelDataCache[cacheKey] = skelData;
|
||
spineLog("[SpineMgr] SkeletonData 解析并缓存: " + cacheKey);
|
||
} else {
|
||
spineLog("[SpineMgr] SkeletonData 命中缓存: " + cacheKey);
|
||
}
|
||
entry.skeleton = new spine.Skeleton(skelData);
|
||
|
||
// 设置皮肤
|
||
if (entry.skin && entry.skin !== "default") {
|
||
entry.skeleton.setSkinByName(entry.skin);
|
||
}
|
||
entry.skeleton.setToSetupPose();
|
||
|
||
// AnimationState
|
||
var stateData = new spine.AnimationStateData(skelData);
|
||
stateData.defaultMix = entry.mixDur;
|
||
entry.state = new spine.AnimationState(stateData);
|
||
|
||
// 绑定事件回调 —— 转发到 Spine_Event.js
|
||
entry.state.addListener({
|
||
complete: function(trackEntry) {
|
||
// 自动隐藏:playOnce / playQueue 设置的计数器
|
||
if (entry._hideOnComplete && !trackEntry.loop) {
|
||
entry._hideAfterCompletes--;
|
||
if (entry._hideAfterCompletes <= 0) {
|
||
entry.visible = false;
|
||
entry._hideOnComplete = false;
|
||
}
|
||
}
|
||
if (gameabc_face.spine_onComplete) {
|
||
gameabc_face.spine_onComplete(entry._id, trackEntry.animation.name, trackEntry.trackIndex);
|
||
}
|
||
},
|
||
event: function(trackEntry, event) {
|
||
if (gameabc_face.spine_onEvent) {
|
||
gameabc_face.spine_onEvent(entry._id, event.data.name, event.intValue, event.floatValue, event.stringValue);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 默认动画
|
||
if (entry.defAnim) {
|
||
entry.state.setAnimation(0, entry.defAnim, entry.defLoop);
|
||
}
|
||
entry.ready = true;
|
||
},
|
||
|
||
// ----------------------------------------------------------
|
||
// (内部) 每帧调用:确保所有资源就绪
|
||
// ----------------------------------------------------------
|
||
_tryBuild: function() {
|
||
if (!this._assetManager.isLoadingComplete()) return false;
|
||
for (var id in this._entries) {
|
||
var e = this._entries[id];
|
||
if (!e.ready) {
|
||
e._id = id;
|
||
try {
|
||
this._buildEntry(e);
|
||
spineLog("[SpineMgr] " + id + " 构建完成");
|
||
this._flushCmds(id);
|
||
} catch(err) {
|
||
// 重试本身必须保留(资源可能只是还没到齐),但日志只记一次:
|
||
// 本方法每帧调用,持续失败时会以约 60 条/秒刷屏并淹没其他日志。
|
||
if (!e._buildFailLogged) {
|
||
e._buildFailLogged = true;
|
||
spineWarn("[SpineMgr] " + id + " 构建失败(仅记一次): " + err.message);
|
||
}
|
||
e.ready = false;
|
||
}
|
||
}
|
||
}
|
||
return true;
|
||
},
|
||
|
||
// ----------------------------------------------------------
|
||
// (内部) WebGL context 恢复后重建全部 GPU 资源
|
||
// context lost 会让 AssetManager 中缓存的 GLTexture 与 shader 一并作废,
|
||
// 必须重新加载资源并重建所有 entry,否则表现为"特效再也不显示"。
|
||
// ----------------------------------------------------------
|
||
_rebuildAfterContextRestored: function() {
|
||
spineLog("[SpineMgr] 重建 Spine 资源(context restored)");
|
||
|
||
// 记录当前 entry 的声明式状态,重建后按原样恢复
|
||
var saved = [];
|
||
for (var id in this._entries) {
|
||
var e = this._entries[id];
|
||
saved.push({
|
||
id: id,
|
||
jsonFile: e.jsonFile,
|
||
atlasFile: e.atlasFile,
|
||
x: e.x,
|
||
y: e.y,
|
||
scale: e.scale,
|
||
skin: e.skin,
|
||
mixDur: e.mixDur
|
||
});
|
||
}
|
||
|
||
// 丢弃全部缓存:SkeletonData 持有已作废的 GLTexture 引用,不能复用
|
||
this._entries = {};
|
||
this._pendingCmds = {};
|
||
this._skelDataCache = {};
|
||
this._loadedPaths = {};
|
||
|
||
// 先释放旧 AssetManager 再换新的:GLTexture 构造时会 context.addRestorable(this),
|
||
// 只有 dispose() 才 removeRestorable。本后端刻意复用同一个 ManagedWebGLRenderingContext
|
||
// (见 SpineBackendWebGL._ensureGL 注释),若不释放,每恢复一次就把 23 页图集的
|
||
// GLTexture 永久留在 restorables 里(解码位图约 137MB)—— 而 context lost 的诱因
|
||
// 正是内存压力,泄漏会把 lost/restore 变成越恢复越糟的死循环。
|
||
// context 已丢失时 gl.deleteTexture 等按规范是 no-op,安全;仍加 try/catch 防御,
|
||
// 释放失败不能挡住后面的重建。
|
||
if (this._assetManager) {
|
||
try {
|
||
this._assetManager.dispose();
|
||
} catch (errDispose) {
|
||
spineWarn("[SpineMgr] 释放旧 AssetManager 失败(已忽略): " + errDispose.message);
|
||
}
|
||
this._assetManager = null;
|
||
}
|
||
|
||
// 本方法在 webglcontextrestored 事件回调中执行,异常会逸出到事件循环,
|
||
// 故与 init 同样显式捕获:重建失败就停在不可用状态并留下日志。
|
||
try {
|
||
this._assetManager = this._backend.createAssetManager(this._basePath);
|
||
} catch (err) {
|
||
spineWarn("[SpineMgr] context 恢复后重建 AssetManager 失败: " + err.message);
|
||
this._inited = false;
|
||
return;
|
||
}
|
||
|
||
// 整批重注入 rawDataURI 并重新发起加载:新 AssetManager = 新 Downloader = 空 rawDataUris,
|
||
// 只补 saved 里的资源会让恢复后首次触发的新特效在 file:// 下被 CORS 拦死(见 _preloadAll 注释)。
|
||
// _preloadAll 会把全部资源写进 _loadedPaths,故下面按 saved 重建 entry 时
|
||
// load() 的 if (!this._loadedPaths[...]) 判断会正确跳过重复请求。
|
||
this._preloadAll();
|
||
|
||
for (var i = 0; i < saved.length; i++) {
|
||
var s = saved[i];
|
||
this.load(s.id, s.jsonFile, s.atlasFile, {
|
||
x: s.x, y: s.y, scale: s.scale, skin: s.skin, mixDuration: s.mixDur
|
||
});
|
||
// 重建后默认不可见:正在播的动画已随 context 丢失,
|
||
// 强行续播会出现半截动画,交由下一次业务触发重新播放。
|
||
this._entries[s.id].visible = false;
|
||
}
|
||
},
|
||
|
||
// ----------------------------------------------------------
|
||
// updateAndDraw(ctx) 每帧自动调用
|
||
// ctx: gameabc_face.dc (Canvas 2D Context)
|
||
// ----------------------------------------------------------
|
||
updateAndDraw: function(ctx) {
|
||
if (!this._inited) return;
|
||
this._tryBuild();
|
||
|
||
var now = Date.now();
|
||
var dt = (now - this._lastTime) / 1000;
|
||
this._lastTime = now;
|
||
if (dt <= 0 || dt > 0.5) dt = 1/30;
|
||
|
||
// 先推进动画状态:即使本帧无法绘制(后端未就绪 / context lost),
|
||
// 动画时间轴与 complete 事件也必须照常推进,否则回调链会卡死。
|
||
var visibleList = [];
|
||
for (var id in this._entries) {
|
||
var e = this._entries[id];
|
||
if (!e.ready || !e.visible) continue;
|
||
|
||
e.state.update(dt);
|
||
e.state.apply(e.skeleton);
|
||
// 骨骼位置由后端决定:Canvas 后端归零(用 ctx.translate 定位),
|
||
// WebGL 后端写入 x/-y(位移并入骨骼,使所有 entry 共用同一 MVP)。
|
||
// 必须在 updateWorldTransform 之前设置,否则不生效于本帧。
|
||
this._backend.placeEntry(e);
|
||
e.skeleton.updateWorldTransform(spine.Physics.update);
|
||
visibleList.push(e);
|
||
}
|
||
|
||
if (!this._backend.beginFrame(visibleList.length)) return;
|
||
// endFrame 必须配对执行:WebGL 后端的 PolygonBatcher 只在 endFrame 里 end(),
|
||
// 若 drawEntry 抛出而漏了 end(),batcher.isDrawing 会永久停在 true,
|
||
// 下一帧 begin() 即抛 "PolygonBatch is already drawing" 并冒到 gameenddraw —— Spine 永久黑屏。
|
||
try {
|
||
for (var i = 0; i < visibleList.length; i++) {
|
||
this._backend.drawEntry(visibleList[i]);
|
||
}
|
||
} finally {
|
||
this._backend.endFrame();
|
||
}
|
||
},
|
||
|
||
// ==========================================================
|
||
// 公开控制 API
|
||
// ==========================================================
|
||
|
||
setAnimation: function(id, animName, loop, track) {
|
||
var e = this._entries[id];
|
||
if (!e) {
|
||
this._autoLoad(id);
|
||
this._queueCmd(id, "setAnimation", [animName, loop, track]);
|
||
return null;
|
||
}
|
||
e.visible = true;
|
||
if (!e.ready) {
|
||
this._queueCmd(id, "setAnimation", [animName, loop, track]);
|
||
return null;
|
||
}
|
||
return e.state.setAnimation(track || 0, animName, loop !== false);
|
||
},
|
||
|
||
addAnimation: function(id, animName, loop, delay, track) {
|
||
var e = this._entries[id];
|
||
if (!e) {
|
||
this._autoLoad(id);
|
||
this._queueCmd(id, "addAnimation", [animName, loop, delay, track]);
|
||
return null;
|
||
}
|
||
if (!e.ready) {
|
||
this._queueCmd(id, "addAnimation", [animName, loop, delay, track]);
|
||
return null;
|
||
}
|
||
return e.state.addAnimation(track || 0, animName, loop !== false, delay || 0);
|
||
},
|
||
|
||
setPosition: function(id, x, y) {
|
||
var e = this._entries[id];
|
||
if (!e) {
|
||
this._autoLoad(id);
|
||
e = this._entries[id];
|
||
}
|
||
e.x = x; e.y = y;
|
||
},
|
||
|
||
setScale: function(id, sx, sy) {
|
||
// 主流方案:scale 通过 SkeletonJson.scale 在 load 时烘焙,运行时 setScale 更新 _scaleMap
|
||
// 并在 entry 未就绪时作为 option 重建,保持接口向后兼容
|
||
this._scaleMap[id] = sx;
|
||
var e = this._entries[id];
|
||
if (!e) {
|
||
this._autoLoad(id); // 会使用 _scaleMap[id] = sx
|
||
return;
|
||
}
|
||
if (e.scale === sx) return; // 已是目标 scale,无需重建
|
||
// 目标 scale 与已烘焙 scale 不同 → 清除旧 entry 并以新 scale 重建。
|
||
// 文件名必须取自 entry(load 时记录的权威值):entryId 与资源文件名不一定同名
|
||
// (如 entryId=ting_0 对应 ting.json),按 id 推导会去加载不存在的文件。
|
||
var savedX = e.x, savedY = e.y;
|
||
var savedJson = e.jsonFile, savedAtlas = e.atlasFile;
|
||
this.remove(id);
|
||
this.load(id, savedJson, savedAtlas, { scale: sx, x: savedX, y: savedY });
|
||
},
|
||
|
||
setFlip: function(id, flipX, flipY) {
|
||
var e = this._entries[id];
|
||
if (!e) {
|
||
this._autoLoad(id);
|
||
this._queueCmd(id, "setFlip", [flipX, flipY]);
|
||
return;
|
||
}
|
||
if (!e.skeleton) {
|
||
this._queueCmd(id, "setFlip", [flipX, flipY]);
|
||
return;
|
||
}
|
||
e.skeleton.scaleX = Math.abs(e.skeleton.scaleX) * (flipX ? -1 : 1);
|
||
e.skeleton.scaleY = Math.abs(e.skeleton.scaleY) * (flipY ? -1 : 1);
|
||
},
|
||
|
||
setVisible: function(id, visible) {
|
||
var e = this._entries[id];
|
||
if (!e) {
|
||
this._autoLoad(id);
|
||
e = this._entries[id];
|
||
}
|
||
e.visible = !!visible;
|
||
},
|
||
|
||
setSkin: function(id, skinName) {
|
||
var e = this._entries[id];
|
||
if (!e) {
|
||
this._autoLoad(id);
|
||
this._queueCmd(id, "setSkin", [skinName]);
|
||
return;
|
||
}
|
||
if (!e.ready) {
|
||
this._queueCmd(id, "setSkin", [skinName]);
|
||
return;
|
||
}
|
||
e.skeleton.setSkinByName(skinName);
|
||
e.skeleton.setSlotsToSetupPose();
|
||
},
|
||
|
||
getAnimations: function(id) {
|
||
var e = this._entries[id];
|
||
if (!e) { this._autoLoad(id); return []; }
|
||
if (!e.ready) return [];
|
||
var anims = e.skeleton.data.animations;
|
||
var names = [];
|
||
for (var i = 0; i < anims.length; i++) names.push(anims[i].name);
|
||
return names;
|
||
},
|
||
|
||
getSkins: function(id) {
|
||
var e = this._entries[id];
|
||
if (!e) { this._autoLoad(id); return []; }
|
||
if (!e.ready) return [];
|
||
var skins = e.skeleton.data.skins;
|
||
var names = [];
|
||
for (var i = 0; i < skins.length; i++) names.push(skins[i].name);
|
||
return names;
|
||
},
|
||
|
||
playOnce: function(id, animName, track) {
|
||
this.setVisible(id, true);
|
||
// 重播前重置骨骼到初始姿态,消除上次末帧残留导致的混合过渡(斜向位移)
|
||
var e = this._entries[id];
|
||
if (e && e.ready) {
|
||
e.state.clearTracks();
|
||
e.skeleton.setToSetupPose();
|
||
}
|
||
this.setAnimation(id, animName, false, track);
|
||
e = this._entries[id];
|
||
if (e) {
|
||
e._hideOnComplete = true;
|
||
e._hideAfterCompletes = 1;
|
||
}
|
||
},
|
||
|
||
playQueue: function(id, animList, hideOnComplete) {
|
||
if (!animList || animList.length === 0) return;
|
||
this.setVisible(id, true);
|
||
// 重播前重置骨骼到初始姿态,消除上次末帧残留导致的混合过渡
|
||
var e = this._entries[id];
|
||
if (e && e.ready) {
|
||
e.state.clearTracks();
|
||
e.skeleton.setToSetupPose();
|
||
}
|
||
this.setAnimation(id, animList[0], false, 0);
|
||
for (var i = 1; i < animList.length; i++) {
|
||
this.addAnimation(id, animList[i], false, 0, 0);
|
||
}
|
||
var e = this._entries[id];
|
||
if (e) {
|
||
e._hideOnComplete = hideOnComplete !== false;
|
||
e._hideAfterCompletes = animList.length;
|
||
}
|
||
},
|
||
|
||
stop: function(id) {
|
||
var e = this._entries[id];
|
||
if (!e) return;
|
||
e.visible = false;
|
||
e._hideOnComplete = false;
|
||
e._hideAfterCompletes = 0;
|
||
delete this._pendingCmds[id];
|
||
if (e.state) {
|
||
e.state.clearTracks();
|
||
}
|
||
if (e.skeleton) {
|
||
e.skeleton.setToSetupPose();
|
||
}
|
||
},
|
||
|
||
stopAll: function() {
|
||
for (var id in this._entries) {
|
||
this.stop(id);
|
||
}
|
||
},
|
||
|
||
remove: function(id) {
|
||
delete this._entries[id];
|
||
delete this._pendingCmds[id];
|
||
},
|
||
|
||
removeAll: function() {
|
||
this._entries = {};
|
||
this._pendingCmds = {};
|
||
this._skelDataCache = {};
|
||
}
|
||
};
|
||
|
||
// 挂载到 gameabc_face 上
|
||
gameabc_face.spineMgr = SpineMgr;
|
||
|
||
// ★ 自动预加载:读取 gameabc_face.spineAssets 清单,在引擎启动前预加载所有 Spine 资源
|
||
// 如果存在 spineTextData(嵌入文本数据),则通过 setRawDataURI 注入,
|
||
// 彻底避免 file:// 协议下 XHR CORS 拦截问题
|
||
if (gameabc_face.spineAssets && gameabc_face.spineAssets.length > 0) {
|
||
SpineMgr._ensureInit();
|
||
if (!SpineMgr._inited) {
|
||
// 后端未就绪:跳过预加载,但必须继续往下走完 gameenddraw 拦截的安装。
|
||
// 此处不能 return —— 预加载块在 IIFE 函数体内,return 会退出整个 IIFE,
|
||
// 反而导致拦截装不上,正是本守卫要避免的后果。
|
||
// 回归锁:codes/test/node/spineMgr.gameEndDrawGuard.test.js
|
||
spineWarn("[SpineMgr] 后端未就绪,跳过 Spine 资源预加载");
|
||
} else {
|
||
// 与 context 恢复后的重注入共用同一份实现,避免两处清单分叉
|
||
SpineMgr._preloadAll();
|
||
}
|
||
}
|
||
|
||
// ★ 用 defineProperty 拦截 gameenddraw 赋值
|
||
// 无论 gamemain.js 或其他代码如何定义 gameenddraw,
|
||
// Spine 渲染都会自动追加在用户逻辑之后
|
||
var _userEndDraw = gameabc_face.gameenddraw || null;
|
||
var _wrappedEndDraw = function(gameid, spid, times, timelong) {
|
||
if (typeof _userEndDraw === "function") {
|
||
_userEndDraw(gameid, spid, times, timelong);
|
||
}
|
||
var ctx = gameabc_face.dc;
|
||
if (ctx && SpineMgr._inited) {
|
||
SpineMgr.updateAndDraw(ctx);
|
||
}
|
||
};
|
||
Object.defineProperty(gameabc_face, "gameenddraw", {
|
||
configurable: true,
|
||
get: function() { return _wrappedEndDraw; },
|
||
set: function(fn) { _userEndDraw = fn; }
|
||
});
|
||
|
||
})();
|