Files
erqiwang_youle/client/js/gameabc-framework/spine/SpineMgr.js
T

468 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 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}]} 等待实例就绪后执行的命令队列
_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) {
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
// 主流 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 后触发隐藏
};
},
// ----------------------------------------------------------
// (内部) 资源加载完成后实例化骨骼
// 同一 jsonFile+scale 的 SkeletonData 只解析一次,多个 entry 共享
// ----------------------------------------------------------
_buildEntry: function(entry) {
logmessage("[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;
logmessage("[SpineMgr] SkeletonData 解析并缓存: " + cacheKey);
} else {
logmessage("[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);
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(scale 已烘焙进 SkeletonJson.scale)
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) {
// 主流方案: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,下次 _autoLoad 将以新 scale 重建
var savedX = e.x, savedY = e.y;
this.remove(id);
this.load(id, id + ".json", id + ".atlas", { 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();
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; }
});
})();