Files
youlegames/codes/games/client/Projects/Game_Surface_3/js/SpineMgr.js

441 lines
13 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}]} 等待实例就绪后执行的命令队列
// 默认资源根路径(可通过 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";
// 将嵌入文本数据注册为 rawDataURISpine 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; }
});
})();