// ============================================================ // SpineMgr —— Spine 动画管理器(独立文件) // 集成到 gameabc Canvas 2D 绘制循环,零侵入 gamemain.js // 在 index.html 中置于 gameabc.min.js 之后、gamemain.js 之前加载 // ============================================================ (function(){ var SpineMgr = { // --- 内部状态 --- _entries: {}, // {id: EntryObject} _assetManager: null, _renderer: null, // spine.SkeletonRenderer (Canvas 2D) _lastTime: 0, // 上一帧时间戳(ms) _inited: false, _loadedPaths: {}, // 已请求加载的资源路径,避免重复请求 _pendingCmds: {}, // {id: [{method, args}]} 等待实例就绪后执行的命令队列 // 默认资源根路径(可通过 init 覆盖) _basePath: "assets/spine/", // ---------------------------------------------------------- // init(basePath) 手动初始化(可选) // 如不调用,load 时会自动以默认路径 "assets/spine/" 初始化 // ---------------------------------------------------------- init: function(basePath) { if (!window.spine) { logmessage("[SpineMgr] spine-canvas.js 未加载,请检查引用"); return; } this._basePath = basePath || this._basePath; this._assetManager = new spine.AssetManager(this._basePath); this._lastTime = Date.now(); this._inited = true; }, // (内部) 确保已初始化 _ensureInit: function() { if (!this._inited) { this.init(this._basePath); } }, // (内部) 自动加载:根据 id 约定文件名 id.json / id.atlas _autoLoad: function(id) { if (this._entries[id]) return; this.load(id, id + ".json", id + ".atlas", {}); }, // (内部) 将命令加入等待队列 _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 后触发隐藏 }; }, // ---------------------------------------------------------- // (内部) 资源加载完成后实例化骨骼 // ---------------------------------------------------------- _buildEntry: function(entry) { var atlas = this._assetManager.require(entry.atlasFile); var loader = new spine.AtlasAttachmentLoader(atlas); var skelJson = new spine.SkeletonJson(loader); skelJson.scale = entry.scale; var skelData = skelJson.readSkeletonData( this._assetManager.require(entry.jsonFile) ); 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); logmessage("[SpineMgr] " + id + " 构建完成"); this._flushCmds(id); } catch(err) { logmessage("[SpineMgr] " + id + " 构建失败: " + err.message); e.ready = false; } } } return true; }, // ---------------------------------------------------------- // 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; if (!this._renderer) { this._renderer = new spine.SkeletonRenderer(ctx); this._renderer.triangleRendering = true; // ★ 启用三角形渲染以支持 Mesh 网格附件 } // 确保 renderer 用的是当前 ctx (gameabc可能重建) this._renderer.ctx = ctx; 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; e.skeleton.updateWorldTransform(spine.Physics.update); ctx.save(); ctx.translate(e.x, e.y); ctx.scale(1, -1); // ★ Spine Y-up → Canvas Y-down this._renderer.draw(e.skeleton); ctx.restore(); } }, // ========================================================== // 公开控制 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) { var e = this._entries[id]; if (!e) { this._autoLoad(id); this._queueCmd(id, "setScale", [sx, sy]); return; } if (!e.skeleton) { this._queueCmd(id, "setScale", [sx, sy]); return; } e.skeleton.scaleX = sx; e.skeleton.scaleY = sy !== undefined ? sy : sx; }, 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); this.setAnimation(id, animName, false, track); var 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); 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 = {}; } }; // 挂载到 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(); 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; } logmessage("[SpineMgr] 预加载 " + list.length + " 组 Spine 资源" + (Object.keys(textData).length > 0 ? "(使用嵌入数据)" : "(使用网络请求)")); } // ★ 用 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; } }); })();