Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 23 additions & 36 deletions Extensions/Spine/JsExtension.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ module.exports = {
.addDefaultBehavior('OpacityCapability::OpacityBehavior')
.addDefaultBehavior('AnimatableCapability::AnimatableBehavior')
.setIncludeFile('Extensions/Spine/spineruntimeobject.js')
.addIncludeFile('Extensions/Spine/spine-pixi-v7/spine-pixi-v7-pre.js')
.addIncludeFile('Extensions/Spine/spine-pixi-v7/spine-pixi-v7.js')
.addIncludeFile('Extensions/Spine/spineruntimeobject-pixi-renderer.js')
.addIncludeFile('Extensions/Spine/pixi-spine/pixi-spine.js')
.addIncludeFile('Extensions/Spine/managers/pixi-spine-atlas-manager.js')
.addIncludeFile('Extensions/Spine/managers/pixi-spine-manager.js')
.setCategory('Advanced')
Expand Down Expand Up @@ -280,7 +281,7 @@ module.exports = {
const { PIXI, RenderedInstance, gd } = objectsRenderingService;

class RenderedSpineInstance extends RenderedInstance {
/** @type {pixi_spine.Spine | null} */
/** @type {spine.Spine | null} */
_spine = null;
/** @type {PIXI.Sprite} */
_placeholder;
Expand Down Expand Up @@ -467,7 +468,8 @@ module.exports = {
// if custom size is set it will be reinitialized in update method
spine.scale.set(1, 1);
spine.state.setAnimation(0, source, shouldLoop);
spine.state.tracks[0].trackTime = 0;
const newTrack = spine.state.tracks[0];
if (newTrack) newTrack.trackTime = 0;
spine.update(0);
spine.autoUpdate = false;
}
Expand Down Expand Up @@ -497,49 +499,34 @@ module.exports = {
gd.SpineObjectConfiguration
);
this._pixiResourcesLoader
.getSpineData(this._project, object.getSpineResourceName())
.then((spineDataOrLoadingError) => {
.createSpine(this._project, object.getSpineResourceName())
.then((spineInstance) => {
if (this._wasDestroyed) return;
if (this._spine) this._pixiObject.removeChild(this._spine);

if (!spineDataOrLoadingError.skeleton) {
console.error(
'Unable to load Spine (' +
(spineDataOrLoadingError.loadingErrorReason ||
'Unknown reason') +
')',
spineDataOrLoadingError.loadingError
);
if (!spineInstance) {
this._spine = null;
this._placeholder.alpha = 255;
return;
}

try {
this._spine = new PIXI.Spine(spineDataOrLoadingError.skeleton);

// Apply the default skin if configured.
const skinName = object.getSkinName();
if (skinName && this._spine) {
try {
this._spine.skeleton.setSkinByName(skinName);
this._spine.skeleton.setSlotsToSetupPose();
} catch (skinError) {
console.warn(
'Unable to set skin "' + skinName + '":',
skinError
);
}
this._spine = spineInstance;

const skinName = object.getSkinName();
if (skinName) {
try {
spineInstance.skeleton.setSkinByName(skinName);
spineInstance.skeleton.setSlotsToSetupPose();
} catch (skinError) {
console.warn(
'Unable to set skin "' + skinName + '":',
skinError
);
}

this._pixiObject.addChild(this._spine);
this._placeholder.alpha = 0;
} catch (error) {
console.error('Exception while loading Spine.', error);
this._spine = null;
this._placeholder.alpha = 255;
return;
}

this._pixiObject.addChild(spineInstance);
this._placeholder.alpha = 0;
});
}
}
Expand Down
155 changes: 53 additions & 102 deletions Extensions/Spine/managers/pixi-spine-atlas-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,25 @@
* This project is released under the MIT License.
*/
namespace gdjs {
/**
* The callback called when a text that was requested is loaded (or an error occurred).
* @category Resources > Spine
*/
export type SpineAtlasManagerRequestCallback = (
error: Error | null,
content?: pixi_spine.TextureAtlas
) => void;

const atlasKinds: ResourceKind[] = ['atlas'];

/**
* AtlasManager loads atlas files with pixi loader, using the "atlas" resources
* registered in the game resources and process them to Pixi TextureAtlas.
* SpineAtlasManager loads `.atlas` files via the official `@esotericsoftware/spine-pixi-v7`
* Pixi atlas loader, sharing texture pages with the engine's ImageManager.
*
* The loader is auto-registered by the `spine-pixi-v7` IIFE bundle. We simply prepare
* the asset metadata (`data.images`) so that PIXI.Assets binds atlas pages to the
* already-loaded base textures instead of fetching them again.
*
* Contrary to audio/fonts, text files are loaded asynchronously, when requested.
* You should properly handle errors, and give the developer/player a way to know
* that loading failed.
* @category Resources > Spine
*/
export class SpineAtlasManager implements gdjs.ResourceManager {
private _imageManager: ImageManager;
private _resourceLoader: ResourceLoader;
private _loadedSpineAtlases =
new gdjs.ResourceCache<pixi_spine.TextureAtlas>();
new gdjs.ResourceCache<spine.TextureAtlas>();
private _loadingSpineAtlases = new gdjs.ResourceCache<
Promise<pixi_spine.TextureAtlas>
Promise<spine.TextureAtlas>
>();

/**
Expand All @@ -49,97 +41,68 @@ namespace gdjs {
return atlasKinds;
}

async processResource(resourceName: string): Promise<void> {
// Do nothing because pixi-spine parses resources by itself.
async processResource(_resourceName: string): Promise<void> {
// The spine-pixi-v7 atlas loader parses the resource itself.
}

async loadResource(resourceName: string): Promise<void> {
await this.getOrLoad(resourceName);
}

/**
* Returns promisified loaded atlas resource if it is available, loads it otherwise.
*
* @param resourceName The name of resource to load.
* Returns a cached promise resolving to the loaded atlas, loading it if needed.
*/
getOrLoad(resourceName: string): Promise<pixi_spine.TextureAtlas> {
getOrLoad(resourceName: string): Promise<spine.TextureAtlas> {
const resource = this._getAtlasResource(resourceName);

if (!resource) {
return Promise.reject(
`Unable to find atlas for resource '${resourceName}'.`
new Error(`Unable to find atlas for resource '${resourceName}'.`)
);
}

let loadingPromise = this._loadingSpineAtlases.get(resource);

if (!loadingPromise) {
loadingPromise = new Promise<pixi_spine.TextureAtlas>(
(resolve, reject) => {
const onLoad: SpineAtlasManagerRequestCallback = (
error,
content
) => {
if (error) {
return reject(
`Error while preloading a spine atlas resource: ${error}`
);
}
if (!content) {
return reject(
`Cannot reach texture atlas for resource '${resourceName}'.`
);
}

resolve(content);
};

this.load(resource, onLoad);
}
);
const cachedAtlas = this._loadedSpineAtlases.get(resource);
if (cachedAtlas) {
return Promise.resolve(cachedAtlas);
}

this._loadingSpineAtlases.set(resource, loadingPromise);
const inflight = this._loadingSpineAtlases.get(resource);
if (inflight) {
return inflight;
}

const loadingPromise = this._load(resource).then((atlas) => {
this._loadedSpineAtlases.set(resource, atlas);
return atlas;
});
this._loadingSpineAtlases.set(resource, loadingPromise);
return loadingPromise;
}

/**
* Load specified atlas resource and pass it to callback once it is loaded.
*
* @param resource The data of resource to load.
* @param callback The callback to pass atlas to it once it is loaded.
*/
load(
resource: ResourceData,
callback: SpineAtlasManagerRequestCallback
): void {
private async _load(resource: ResourceData): Promise<spine.TextureAtlas> {
const game = this._resourceLoader.getRuntimeGame();
const embeddedResourcesNames = game.getEmbeddedResourcesNames(
resource.name
);

if (!embeddedResourcesNames.length)
return callback(
new Error(`${resource.name} do not have image metadata!`)
);
if (!embeddedResourcesNames.length) {
throw new Error(`${resource.name} does not have image metadata!`);
}

const images = embeddedResourcesNames.reduce<{
[key: string]: PIXI.Texture;
[key: string]: PIXI.BaseTexture;
}>((imagesMap, embeddedResourceName) => {
const mappedResourceName = game.resolveEmbeddedResource(
resource.name,
embeddedResourceName
);
// The v7 atlas loader expects BaseTexture instances when sharing pages
// with already-loaded textures.
imagesMap[embeddedResourceName] =
this._imageManager.getOrLoadPIXITexture(mappedResourceName);

this._imageManager.getOrLoadPIXITexture(mappedResourceName)
.baseTexture;
return imagesMap;
}, {});
const onLoad = (atlas: pixi_spine.TextureAtlas) => {
this._loadedSpineAtlases.set(resource, atlas);
callback(null, atlas);
};

const url = this._resourceLoader.getFullUrl(resource.file);
const alias = url;

Expand All @@ -150,45 +113,33 @@ namespace gdjs {
: 'anonymous',
});
PIXI.Assets.add({ alias, src: url, data: { images } });
PIXI.Assets.load<pixi_spine.TextureAtlas | string>(alias).then(
(atlas) => {
/**
* Ideally atlas of TextureAtlas should be passed here
* but there is known issue in case of preloaded images (see https://github.com/pixijs/spine/issues/537)
*
* Here covered all possible ways to make it work fine if issue is fixed in pixi-spine or after migration to spine-pixi
*/
if (typeof atlas === 'string') {
new pixi_spine.TextureAtlas(
atlas,
(textureName, textureCb) =>
//@ts-ignore
textureCb(images[textureName].baseTexture),
onLoad
);
} else {
onLoad(atlas);
}
}
);
return PIXI.Assets.load<spine.TextureAtlas>(alias);
}

/**
* Check if the given atlas resource was loaded (preloaded or loaded with `load`).
* Check if the given atlas resource was loaded.
* @param resourceName The name of the atlas resource.
* @returns true if the content of the atlas resource is loaded, false otherwise.
*/
isLoaded(resourceName: string): boolean {
return !!this._loadedSpineAtlases.getFromName(resourceName);
}

/**
* Get the Pixi TextureAtlas for the given resource that is already loaded (preloaded or loaded with `load`).
* If the resource is not loaded, `null` will be returned.
* @param resourceName The name of the atlas resource.
* @returns the TextureAtlas of the atlas if loaded, `null` otherwise.
* Returns the alias used to register the atlas in PIXI.Assets,
* or null if the resource is not loaded.
*/
getAtlasTexture(resourceName: string): pixi_spine.TextureAtlas | null {
getAtlasAlias(resourceName: string): string | null {
const resource = this._getAtlasResource(resourceName);
if (!resource) return null;
return this._loadedSpineAtlases.get(resource)
? this._resourceLoader.getFullUrl(resource.file)
: null;
}

/**
* Returns the loaded TextureAtlas for the given resource, if available.
*/
getAtlasTexture(resourceName: string): spine.TextureAtlas | null {
return this._loadedSpineAtlases.getFromName(resourceName);
}

Expand All @@ -198,9 +149,9 @@ namespace gdjs {
? resource
: null;
}

/**
* To be called when the game is disposed.
* Clear the Spine atlases loaded in this manager.
*/
dispose(): void {
this._loadedSpineAtlases.clear();
Expand All @@ -220,7 +171,7 @@ namespace gdjs {
resourceData.name
);
if (loadingSpineAtlas) {
loadingSpineAtlas.then((atl) => atl.dispose());
loadingSpineAtlas.then((atlas) => atlas.dispose()).catch(() => {});
this._loadingSpineAtlases.delete(resourceData);
}
}
Expand Down
Loading