import { _decorator, Component, sp, AssetManager, Asset, assetManager } from 'cc'; import { GEvent } from '../event/GEvent'; import { ensureSpineAssetResetEnums, patchSpineSkeletonResetEnums } from './spineRuntimePatch'; const { ccclass, property } = _decorator; @ccclass('LangSpine') export class LangSpine extends Component { @property path: string = ""; @property key: string = ""; @property animation: string = ""; langSp: sp.Skeleton = null; curentSpineData: sp.SkeletonData = null; private isLoading: boolean = false; private abortController: { aborted: boolean } = null; /** 每次发起加载自增;用于 finally 里判断是否仍是最新请求,避免并发把 isLoading 错清 */ private loadGeneration = 0; protected onDestroy(): void { if (this.abortController) { this.abortController.aborted = true; } this.releaseLoadedSpine(); } onLoad() { patchSpineSkeletonResetEnums(); this.langSp = this.getComponent(sp.Skeleton); } onEnable() { this.updateLang(); GEvent.Ins.on(GEvent.EVENT_LANG_CHANGE, this.updateLang, this); } onDisable() { GEvent.Ins.off(GEvent.EVENT_LANG_CHANGE, this.updateLang, this); if (this.abortController) { this.abortController.aborted = true; } } async updateLang() { if (!this.node.isValid || this.langSp == null) return; if (this.abortController) { this.abortController.aborted = true; } this.abortController = { aborted: false }; const currentAbort = this.abortController; const generation = ++this.loadGeneration; this.isLoading = true; try { await gg.lang.whenReady(); if (typeof sp.loadWasmModuleSpine === 'function') { await sp.loadWasmModuleSpine(); } let bundleName = 'lang_' + gg.lang.Current; let path = this.path; let key = this.key; if (key == "") key = this.node.name; let loop = this.langSp.loop; let bundle = await this.loadBundle(bundleName); if (currentAbort.aborted || !this.node.isValid) { return; } if (!bundle) { return; } let spineAsset = await this.loadRes(bundle, path + key, sp.SkeletonData) as sp.SkeletonData; if (currentAbort.aborted || !this.node.isValid || !spineAsset) { if (spineAsset) spineAsset.decRef(); return; } if (this.curentSpineData == spineAsset) { this.prepareSkeletonDataForReuse(spineAsset); ensureSpineAssetResetEnums(spineAsset); this.applySkeletonData(spineAsset); this.playSpineAnimation(loop); return; } let oldres = this.curentSpineData; this.curentSpineData = spineAsset; spineAsset.addRef(); this.prepareSkeletonDataForReuse(spineAsset); ensureSpineAssetResetEnums(spineAsset); this.applySkeletonData(spineAsset); this.playSpineAnimation(loop); if (oldres && oldres != spineAsset) { this.scheduleOnce(() => { if (oldres.refCount > 0) { oldres.decRef(); } }, 0.1); } } catch (error: any) { const msg = error && (error.message ?? String(error)); console.error('[LangSpine]', this.path, this.key, msg); } finally { if (generation === this.loadGeneration) { this.isLoading = false; } } } setKey(key: string) { this.key = key; this.updateLang(); } setPath(path: string) { this.path = path; this.updateLang(); } private prepareSkeletonDataForReuse(data: sp.SkeletonData) { if (data && typeof (data as any).reset === 'function') { try { (data as any).reset(); } catch { /* ignore */ } } } /** * sp.Skeleton 的 skeletonData setter 在「新值与当前引用相同」时会直接 return,不跑 _updateSkeletonData。 * 第二次进界面 bundle 缓存常为同一 SkeletonData,必须先置 null 再赋值,否则会不播动画且无报错。 */ private applySkeletonData(spineAsset: sp.SkeletonData) { const sk = this.langSp; if (!sk) return; sk.clearTrack(0); if (sk.skeletonData === spineAsset) { sk.skeletonData = null; } sk.skeletonData = spineAsset; } private playSpineAnimation(loop: boolean) { const run = () => { if (!this.node.isValid || !this.langSp || !this.animation) return; this.langSp.setAnimation(0, this.animation, loop); }; run(); this.scheduleOnce(run, 0); } private async loadBundle(bundleName) { return new Promise((resolve, reject) => { let bundle = assetManager.getBundle(bundleName); if (bundle) { resolve(bundle); return; } assetManager.loadBundle(bundleName, (err, bundle) => { if (err) { console.log(`加载Bundle:${bundleName}`, err?.message ?? String(err)); resolve(null); } else { resolve(bundle); } }) }) } /** * 不使用 bundle 内缓存:通过 loadAny + cacheAsset:false(不能往 bundle.load 第 3 个参数塞对象,那是 onProgress)。 * 与 updateLang 里的 addRef / onDestroy 的 release 成对。 */ private async loadRes(bundle: AssetManager.Bundle, assetPath: string, type = Asset) { return new Promise((resolve) => { assetManager.loadAny( { path: assetPath, bundle: bundle.name, type }, { cacheAsset: false }, (err: Error | null, asset: Asset) => { if (err) { console.log(`加载Bundle:${bundle.name},path:${assetPath}`, err?.message ?? String(err)); resolve(null); } else { resolve(asset); } }, ); }); } private releaseLoadedSpine() { if (this.langSp && this.langSp.isValid) { this.langSp.clearTrack(0); this.langSp.skeletonData = null; } const data = this.curentSpineData; this.curentSpineData = null; if (!data || !data.isValid) { return; } if (data.refCount > 0) { data.decRef(); } assetManager.releaseAsset(data); } }