模板工程升级修改
This commit is contained in:
@@ -5,12 +5,25 @@
|
||||
// ============================================================
|
||||
(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,
|
||||
_renderer: null, // spine.SkeletonRenderer (Canvas 2D)
|
||||
_backend: null, // 渲染后端(SpineBackendWebGL / SpineBackendCanvas)
|
||||
_lastTime: 0, // 上一帧时间戳(ms)
|
||||
_inited: false,
|
||||
_loadedPaths: {}, // 已请求加载的资源路径,避免重复请求
|
||||
@@ -27,15 +40,50 @@ var SpineMgr = {
|
||||
// ----------------------------------------------------------
|
||||
init: function(basePath) {
|
||||
if (!window.spine) {
|
||||
logmessage("[SpineMgr] spine-canvas.js 未加载,请检查引用");
|
||||
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;
|
||||
this._assetManager = new spine.AssetManager(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) {
|
||||
@@ -43,6 +91,42 @@ var SpineMgr = {
|
||||
}
|
||||
},
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// (内部) _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) {
|
||||
@@ -101,16 +185,45 @@ var SpineMgr = {
|
||||
visible: true,
|
||||
ready: false,
|
||||
_hideOnComplete: false, // 播放完成后是否自动隐藏
|
||||
_hideAfterCompletes: 0 // 剩余多少次 complete 后触发隐藏
|
||||
_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) {
|
||||
logmessage("[SpineMgr] _buildEntry: " + entry._id + " skelJson.scale=" + entry.scale);
|
||||
spineLog("[SpineMgr] _buildEntry: " + entry._id + " skelJson.scale=" + entry.scale);
|
||||
var cacheKey = entry.jsonFile + "_s" + entry.scale;
|
||||
var skelData = this._skelDataCache[cacheKey];
|
||||
if (!skelData) {
|
||||
@@ -122,9 +235,9 @@ var SpineMgr = {
|
||||
this._assetManager.require(entry.jsonFile)
|
||||
);
|
||||
this._skelDataCache[cacheKey] = skelData;
|
||||
logmessage("[SpineMgr] SkeletonData 解析并缓存: " + cacheKey);
|
||||
spineLog("[SpineMgr] SkeletonData 解析并缓存: " + cacheKey);
|
||||
} else {
|
||||
logmessage("[SpineMgr] SkeletonData 命中缓存: " + cacheKey);
|
||||
spineLog("[SpineMgr] SkeletonData 命中缓存: " + cacheKey);
|
||||
}
|
||||
entry.skeleton = new spine.Skeleton(skelData);
|
||||
|
||||
@@ -179,10 +292,15 @@ var SpineMgr = {
|
||||
e._id = id;
|
||||
try {
|
||||
this._buildEntry(e);
|
||||
logmessage("[SpineMgr] " + id + " 构建完成");
|
||||
spineLog("[SpineMgr] " + id + " 构建完成");
|
||||
this._flushCmds(id);
|
||||
} catch(err) {
|
||||
logmessage("[SpineMgr] " + id + " 构建失败: " + err.message);
|
||||
// 重试本身必须保留(资源可能只是还没到齐),但日志只记一次:
|
||||
// 本方法每帧调用,持续失败时会以约 60 条/秒刷屏并淹没其他日志。
|
||||
if (!e._buildFailLogged) {
|
||||
e._buildFailLogged = true;
|
||||
spineWarn("[SpineMgr] " + id + " 构建失败(仅记一次): " + err.message);
|
||||
}
|
||||
e.ready = false;
|
||||
}
|
||||
}
|
||||
@@ -190,6 +308,79 @@ var SpineMgr = {
|
||||
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)
|
||||
@@ -203,29 +394,33 @@ var SpineMgr = {
|
||||
this._lastTime = now;
|
||||
if (dt <= 0 || dt > 0.5) dt = 1/30;
|
||||
|
||||
if (!this._renderer) {
|
||||
this._renderer = new spine.SkeletonRenderer(ctx);
|
||||
this._renderer.triangleRendering = true; // ★ 启用三角形渲染以支持 Mesh 网格附件
|
||||
}
|
||||
// 确保 renderer 用的是当前 ctx (gameabc可能重建)
|
||||
this._renderer.ctx = ctx;
|
||||
|
||||
// 先推进动画状态:即使本帧无法绘制(后端未就绪 / 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);
|
||||
// 骨骼位置归零,由 ctx.translate 控制实际位置
|
||||
e.skeleton.x = 0;
|
||||
e.skeleton.y = 0;
|
||||
// 骨骼位置由后端决定:Canvas 后端归零(用 ctx.translate 定位),
|
||||
// WebGL 后端写入 x/-y(位移并入骨骼,使所有 entry 共用同一 MVP)。
|
||||
// 必须在 updateWorldTransform 之前设置,否则不生效于本帧。
|
||||
this._backend.placeEntry(e);
|
||||
e.skeleton.updateWorldTransform(spine.Physics.update);
|
||||
visibleList.push(e);
|
||||
}
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(e.x, e.y);
|
||||
ctx.scale(1, -1); // ★ Spine Y-up → Canvas Y-down(scale 已烘焙进 SkeletonJson.scale)
|
||||
this._renderer.draw(e.skeleton);
|
||||
ctx.restore();
|
||||
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();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -281,10 +476,13 @@ var SpineMgr = {
|
||||
return;
|
||||
}
|
||||
if (e.scale === sx) return; // 已是目标 scale,无需重建
|
||||
// 目标 scale 与已烘焙 scale 不同 → 清除旧 entry,下次 _autoLoad 将以新 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, id + ".json", id + ".atlas", { scale: sx, x: savedX, y: savedY });
|
||||
this.load(id, savedJson, savedAtlas, { scale: sx, x: savedX, y: savedY });
|
||||
},
|
||||
|
||||
setFlip: function(id, flipX, flipY) {
|
||||
@@ -423,26 +621,16 @@ gameabc_face.spineMgr = SpineMgr;
|
||||
// 彻底避免 file:// 协议下 XHR CORS 拦截问题
|
||||
if (gameabc_face.spineAssets && gameabc_face.spineAssets.length > 0) {
|
||||
SpineMgr._ensureInit();
|
||||
var list = gameabc_face.spineAssets;
|
||||
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]) {
|
||||
SpineMgr._assetManager.setRawDataURI(jsonKey, "data:," + textData[jsonKey]);
|
||||
}
|
||||
if (textData[atlasKey]) {
|
||||
SpineMgr._assetManager.setRawDataURI(atlasKey, "data:," + textData[atlasKey]);
|
||||
}
|
||||
SpineMgr._assetManager.loadText(jsonKey);
|
||||
SpineMgr._loadedPaths[jsonKey] = true;
|
||||
SpineMgr._assetManager.loadTextureAtlas(atlasKey);
|
||||
SpineMgr._loadedPaths[atlasKey] = true;
|
||||
if (!SpineMgr._inited) {
|
||||
// 后端未就绪:跳过预加载,但必须继续往下走完 gameenddraw 拦截的安装。
|
||||
// 此处不能 return —— 预加载块在 IIFE 函数体内,return 会退出整个 IIFE,
|
||||
// 反而导致拦截装不上,正是本守卫要避免的后果。
|
||||
// 回归锁:codes/test/node/spineMgr.gameEndDrawGuard.test.js
|
||||
spineWarn("[SpineMgr] 后端未就绪,跳过 Spine 资源预加载");
|
||||
} else {
|
||||
// 与 context 恢复后的重注入共用同一份实现,避免两处清单分叉
|
||||
SpineMgr._preloadAll();
|
||||
}
|
||||
logmessage("[SpineMgr] 预加载 " + list.length + " 组 Spine 资源" +
|
||||
(Object.keys(textData).length > 0 ? "(使用嵌入数据)" : "(使用网络请求)"));
|
||||
}
|
||||
|
||||
// ★ 用 defineProperty 拦截 gameenddraw 赋值
|
||||
|
||||
Reference in New Issue
Block a user