You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
227 lines
6.9 KiB
227 lines
6.9 KiB
|
|
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<AssetManager.Bundle>((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<Asset>((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);
|
|
}
|
|
}
|
|
|