From 42626d7ff7531dab7b3dc5072052685bbd087553 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 28 Apr 2023 13:32:17 +0200 Subject: [PATCH] [phaser] Clean-up and inline documentation. --- spine-ts/spine-phaser/src/SpineGameObject.ts | 46 +++++++- spine-ts/spine-phaser/src/SpinePlugin.ts | 106 +++++++++++-------- spine-ts/spine-phaser/src/keys.ts | 1 - 3 files changed, 103 insertions(+), 50 deletions(-) diff --git a/spine-ts/spine-phaser/src/SpineGameObject.ts b/spine-ts/spine-phaser/src/SpineGameObject.ts index 37cd5084a..856476391 100644 --- a/spine-ts/spine-phaser/src/SpineGameObject.ts +++ b/spine-ts/spine-phaser/src/SpineGameObject.ts @@ -9,10 +9,13 @@ class BaseSpineGameObject extends Phaser.GameObjects.GameObject { } } +/** A bounds provider calculates the bounding box for a skeleton, which is then assigned as the size of the SpineGameObject. */ export interface SpineGameObjectBoundsProvider { + // Returns the bounding box for the skeleton, in skeleton space. calculateBounds (gameObject: SpineGameObject): { x: number, y: number, width: number, height: number }; } +/** A bounds provider that calculates the bounding box from the setup pose. */ export class SetupPoseBoundsProvider implements SpineGameObjectBoundsProvider { calculateBounds (gameObject: SpineGameObject) { if (!gameObject.skeleton) return { x: 0, y: 0, width: 0, height: 0 }; @@ -26,9 +29,14 @@ export class SetupPoseBoundsProvider implements SpineGameObjectBoundsProvider { } } +/** A bounds provider that calculates the bounding box by taking the maximumg bounding box for a combination of skins and specific animation. */ export class SkinsAndAnimationBoundsProvider implements SpineGameObjectBoundsProvider { + /** + * @param animation The animation to use for calculating the bounds. If null, the setup pose is used. + * @param skins The skins to use for calculating the bounds. If empty, the default skin is used. + * @param timeStep The time step to use for calculating the bounds. A smaller time step means more precision, but slower calculation. + */ constructor (private animation: string, private skins: string[] = [], private timeStep: number = 0.05) { - } calculateBounds (gameObject: SpineGameObject): { x: number; y: number; width: number; height: number; } { @@ -75,13 +83,34 @@ export class SkinsAndAnimationBoundsProvider implements SpineGameObjectBoundsPro } } +/** + * A SpineGameObject is a Phaser {@link GameObject} that can be added to a Phaser Scene and render a Spine skeleton. + * + * The Spine GameObject is a thin wrapper around a Spine {@link Skeleton}, {@link AnimationState} and {@link AnimationStateData}. It is responsible for: + * - updating the animation state + * - applying the animation state to the skeleton's bones, slots, attachments, and draw order. + * - updating the skeleton's bone world transforms + * - rendering the skeleton + * + * See the {@link SpinePlugin} class for more information on how to create a `SpineGameObject`. + * + * The skeleton, animation state, and animation state data can be accessed via the repsective fields. They can be manually updated via {@link updatePose}. + * + * To modify the bone hierarchy before the world transforms are computed, a callback can be set via the {@link beforeUpdateWorldTransforms} field. + * + * To modify the bone hierarchy after the world transforms are computed, a callback can be set via the {@link afterUpdateWorldTransforms} field. + * + * The class also features methods to convert between the skeleton coordinate system and the Phaser coordinate system. + * + * See {@link skeletonToPhaserWorldCoordinates}, {@link phaserWorldCoordinatesToSkeleton}, and {@link phaserWorldCoordinatesToBoneLocal.} + */ export class SpineGameObject extends ComputedSizeMixin(DepthMixin(FlipMixin(ScrollFactorMixin(TransformMixin(VisibleMixin(AlphaMixin(BaseSpineGameObject))))))) { blendMode = -1; skeleton: Skeleton; animationStateData: AnimationStateData; animationState: AnimationState; - beforeUpdateWorldTransforms: (object: SpineGameObject) => void = () => {}; - afterUpdateWorldTransforms: (object: SpineGameObject) => void = () => {}; + beforeUpdateWorldTransforms: (object: SpineGameObject) => void = () => { }; + afterUpdateWorldTransforms: (object: SpineGameObject) => void = () => { }; private premultipliedAlpha = false; private _displayOriginX = 0; private _displayOriginY = 0; @@ -97,7 +126,7 @@ export class SpineGameObject extends ComputedSizeMixin(DepthMixin(FlipMixin(Scro this.animationStateData = new AnimationStateData(this.skeleton.data); this.animationState = new AnimationState(this.animationStateData); this.skeleton.updateWorldTransform(); - this.updateSize(); + this.updateSize(); } public get displayOriginX () { @@ -145,6 +174,7 @@ export class SpineGameObject extends ComputedSizeMixin(DepthMixin(FlipMixin(Scro this.displayOriginY = -bounds.y; } + /** Converts a point from the skeleton coordinate system to the Phaser world coordinate system. */ skeletonToPhaserWorldCoordinates (point: { x: number, y: number }) { let transform = this.getWorldTransformMatrix(); let a = transform.a, b = transform.b, c = transform.c, d = transform.d, tx = transform.tx, ty = transform.ty; @@ -154,6 +184,7 @@ export class SpineGameObject extends ComputedSizeMixin(DepthMixin(FlipMixin(Scro point.y = x * b + y * d + ty; } + /** Converts a point from the Phaser world coordinate system to the skeleton coordinate system. */ phaserWorldCoordinatesToSkeleton (point: { x: number, y: number }) { let transform = this.getWorldTransformMatrix(); transform = transform.invert(); @@ -164,6 +195,7 @@ export class SpineGameObject extends ComputedSizeMixin(DepthMixin(FlipMixin(Scro point.y = x * b + y * d + ty; } + /** Converts a point from the Phaser world coordinate system to the bone's local coordinate system. */ phaserWorldCoordinatesToBone (point: { x: number, y: number }, bone: Bone) { this.phaserWorldCoordinatesToSkeleton(point); if (bone.parent) { @@ -173,7 +205,11 @@ export class SpineGameObject extends ComputedSizeMixin(DepthMixin(FlipMixin(Scro } } - updatePose(delta: number) { + /** + * Updates the {@link AnimationState}, applies it to the {@link Skeleton}, then updates the world transforms of all bones. + * @param delta The time delta in milliseconds + */ + updatePose (delta: number) { this.animationState.update(delta / 1000); this.animationState.apply(this.skeleton); this.beforeUpdateWorldTransforms(this); diff --git a/spine-ts/spine-phaser/src/SpinePlugin.ts b/spine-ts/spine-phaser/src/SpinePlugin.ts index 614c453f9..6f4a819fc 100644 --- a/spine-ts/spine-phaser/src/SpinePlugin.ts +++ b/spine-ts/spine-phaser/src/SpinePlugin.ts @@ -27,54 +27,69 @@ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -import phaser from "phaser"; -import { SPINE_ATLAS_CACHE_KEY, SPINE_CONTAINER_TYPE, SPINE_GAME_OBJECT_TYPE, SPINE_ATLAS_TEXTURE_CACHE_KEY, SPINE_SKELETON_DATA_FILE_TYPE, SPINE_ATLAS_FILE_TYPE, SPINE_SKELETON_FILE_CACHE_KEY as SPINE_SKELETON_DATA_CACHE_KEY } from "./keys"; -import { AtlasAttachmentLoader, Bone, GLTexture, SceneRenderer, Skeleton, SkeletonBinary, SkeletonData, SkeletonJson, TextureAtlas } from "@esotericsoftware/spine-webgl" +import Phaser from "phaser"; +import { SPINE_ATLAS_CACHE_KEY, SPINE_CONTAINER_TYPE, SPINE_GAME_OBJECT_TYPE, SPINE_SKELETON_DATA_FILE_TYPE, SPINE_ATLAS_FILE_TYPE, SPINE_SKELETON_FILE_CACHE_KEY as SPINE_SKELETON_DATA_CACHE_KEY } from "./keys"; +import { AtlasAttachmentLoader, GLTexture, SceneRenderer, Skeleton, SkeletonBinary, SkeletonData, SkeletonJson, TextureAtlas } from "@esotericsoftware/spine-webgl" import { SpineGameObject, SpineGameObjectBoundsProvider } from "./SpineGameObject"; import { CanvasTexture, SkeletonRenderer } from "@esotericsoftware/spine-canvas"; +/** + * Configuration object used when creating {@link SpineGameObject} instances via a scene's + * {@link GameObjectCreator} (`Scene.make`). + */ export interface SpineGameObjectConfig extends Phaser.Types.GameObjects.GameObjectConfig { + /** The x-position of the object, optional, default: 0 */ x?: number, + /** The y-position of the object, optional, default: 0 */ y?: number, + /** The skeleton data key */ dataKey: string, + /** The atlas key */ atlasKey: string + /** The bounds provider, optional, default: `SetupPoseBoundsProvider` */ boundsProvider?: SpineGameObjectBoundsProvider } +/** + * {@link ScenePlugin} implementation adding Spine Runtime capabilities to a scene. + * + * The scene's {@link LoaderPlugin} (`Scene.load`) gets these additional functions: + * * `spineBinary(key: string, url: string, xhrSettings?: XHRSettingsObject)`: loads a skeleton binary `.skel` file from the `url`. + * * `spineJson(key: string, url: string, xhrSettings?: XHRSettingsObject)`: loads a skeleton binary `.skel` file from the `url`. + * * `spineAtlas(key: string, url: string, premultipliedAlpha: boolean = true, xhrSettings?: XHRSettingsObject)`: loads a texture atlas `.atlas` file from the `url` as well as its correponding texture atlas page images. + * + * The scene's {@link GameObjectFactory} (`Scene.add`) gets these additional functions: + * * `spine(x: number, y: number, dataKey: string, atlasKey: string, boundsProvider: SpineGameObjectBoundsProvider = SetupPoseBoundsProvider())`: + * creates a new {@link SpineGameObject} from the data and atlas at position `(x, y)`, using the {@link BoundsProvider} to calculate its bounding box. The object is automatically added to the scene. + * + * The scene's {@link GameObjectCreator} (`Scene.make`) gets these additional functions: + * * `spine(config: SpineGameObjectConfig)`: creates a new {@link SpineGameObject} from the given configuration object. + * + * The plugin has additional public methods to work with Spine Runtime core API objects: + * * `getAtlas(atlasKey: string)`: returns the {@link TextureAtlas} instance for the given atlas key. + * * `getSkeletonData(skeletonDataKey: string)`: returns the {@link SkeletonData} instance for the given skeleton data key. + * * `createSkeleton(skeletonDataKey: string, atlasKey: string, premultipliedAlpha: boolean = true)`: creates a new {@link Skeleton} instance from the given skeleton data and atlas key. + * * `isPremultipliedAlpha(atlasKey: string)`: returns `true` if the atlas with the given key has premultiplied alpha. + */ export class SpinePlugin extends Phaser.Plugins.ScenePlugin { game: Phaser.Game; - isWebGL: boolean; - gl: WebGLRenderingContext | null; - textureManager: Phaser.Textures.TextureManager; - phaserRenderer: Phaser.Renderer.Canvas.CanvasRenderer | Phaser.Renderer.WebGL.WebGLRenderer | null; + private isWebGL: boolean; + private gl: WebGLRenderingContext | null; webGLRenderer: SceneRenderer | null; canvasRenderer: SkeletonRenderer | null; - skeletonDataCache: Phaser.Cache.BaseCache; - atlasCache: Phaser.Cache.BaseCache; + private skeletonDataCache: Phaser.Cache.BaseCache; + private atlasCache: Phaser.Cache.BaseCache; constructor (scene: Phaser.Scene, pluginManager: Phaser.Plugins.PluginManager, pluginKey: string) { super(scene, pluginManager, pluginKey); - var game = this.game = pluginManager.game; + this.game = pluginManager.game; this.isWebGL = this.game.config.renderType === 2; this.gl = this.isWebGL ? (this.game.renderer as Phaser.Renderer.WebGL.WebGLRenderer).gl : null; - this.textureManager = this.game.textures; - this.phaserRenderer = this.game.renderer; this.webGLRenderer = null; this.canvasRenderer = null; this.skeletonDataCache = this.game.cache.addCustom(SPINE_SKELETON_DATA_CACHE_KEY); this.atlasCache = this.game.cache.addCustom(SPINE_ATLAS_CACHE_KEY); - if (!this.phaserRenderer) { - this.phaserRenderer = { - width: game.scale.width, - height: game.scale.height, - preRender: () => { }, - postRender: () => { }, - render: () => { }, - destroy: () => { } - } as unknown as Phaser.Renderer.Canvas.CanvasRenderer; - } - let skeletonJsonFileCallback = function (this: any, key: string, url: string, xhrSettings: Phaser.Types.Loader.XHRSettingsObject) { @@ -84,7 +99,6 @@ export class SpinePlugin extends Phaser.Plugins.ScenePlugin { }; pluginManager.registerFileType("spineJson", skeletonJsonFileCallback, scene); - let skeletonBinaryFileCallback = function (this: any, key: string, url: string, xhrSettings: Phaser.Types.Loader.XHRSettingsObject) { @@ -129,7 +143,7 @@ export class SpinePlugin extends Phaser.Plugins.ScenePlugin { Skeleton.yDown = true; if (this.isWebGL) { if (!this.webGLRenderer) { - this.webGLRenderer = new SceneRenderer((this.phaserRenderer! as Phaser.Renderer.WebGL.WebGLRenderer).canvas, this.gl!, true); + this.webGLRenderer = new SceneRenderer((this.game.renderer! as Phaser.Renderer.WebGL.WebGLRenderer).canvas, this.gl!, true); } this.onResize(); this.game.scale.on(Phaser.Scale.Events.RESIZE, this.onResize, this); @@ -146,7 +160,7 @@ export class SpinePlugin extends Phaser.Plugins.ScenePlugin { } onResize () { - var phaserRenderer = this.phaserRenderer; + var phaserRenderer = this.game.renderer; var sceneRenderer = this.webGLRenderer; if (phaserRenderer && sceneRenderer) { @@ -177,17 +191,8 @@ export class SpinePlugin extends Phaser.Plugins.ScenePlugin { if (this.webGLRenderer) this.webGLRenderer.dispose(); } - isAtlasPremultiplied (atlasKey: string) { - let atlasFile = this.game.cache.text.get(atlasKey); - if (!atlasFile) return false; - return atlasFile.premultipliedAlpha; - } - - createSkeleton (dataKey: string, atlasKey: string) { - return new Skeleton(this.getSkeletonData(dataKey, atlasKey)); - } - - getAtlas(atlasKey: string) { + /** Returns the TextureAtlas instance for the given key */ + getAtlas (atlasKey: string) { let atlas: TextureAtlas; if (this.atlasCache.exists(atlasKey)) { atlas = this.atlasCache.get(atlasKey); @@ -198,11 +203,11 @@ export class SpinePlugin extends Phaser.Plugins.ScenePlugin { let gl = this.gl!; gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); for (let atlasPage of atlas.pages) { - atlasPage.setTexture(new GLTexture(gl, this.textureManager.get(atlasKey + "!" + atlasPage.name).getSourceImage() as HTMLImageElement | ImageBitmap, false)); + atlasPage.setTexture(new GLTexture(gl, this.game.textures.get(atlasKey + "!" + atlasPage.name).getSourceImage() as HTMLImageElement | ImageBitmap, false)); } } else { for (let atlasPage of atlas.pages) { - atlasPage.setTexture(new CanvasTexture(this.textureManager.get(atlasKey + "!" + atlasPage.name).getSourceImage() as HTMLImageElement | ImageBitmap)); + atlasPage.setTexture(new CanvasTexture(this.game.textures.get(atlasKey + "!" + atlasPage.name).getSourceImage() as HTMLImageElement | ImageBitmap)); } } this.atlasCache.add(atlasKey, atlas); @@ -210,10 +215,18 @@ export class SpinePlugin extends Phaser.Plugins.ScenePlugin { return atlas; } - getSkeletonData(dataKey: string, atlasKey: string) { + /** Returns whether the TextureAtlas uses premultiplied alpha */ + isAtlasPremultiplied (atlasKey: string) { + let atlasFile = this.game.cache.text.get(atlasKey); + if (!atlasFile) return false; + return atlasFile.premultipliedAlpha; + } + + /** Returns the SkeletonData instance for the given data and atlas key */ + getSkeletonData (dataKey: string, atlasKey: string) { const atlas = this.getAtlas(atlasKey) const combinedKey = dataKey + atlasKey; - let skeletonData: SkeletonData; + let skeletonData: SkeletonData; if (this.skeletonDataCache.exists(combinedKey)) { skeletonData = this.skeletonDataCache.get(combinedKey); } else { @@ -230,14 +243,19 @@ export class SpinePlugin extends Phaser.Plugins.ScenePlugin { } return skeletonData; } + + /** Creates a new Skeleton instance from the data and atlas. */ + createSkeleton (dataKey: string, atlasKey: string) { + return new Skeleton(this.getSkeletonData(dataKey, atlasKey)); + } } -export enum SpineSkeletonDataFileType { +enum SpineSkeletonDataFileType { json, binary } -export class SpineSkeletonDataFile extends Phaser.Loader.MultiFile { +class SpineSkeletonDataFile extends Phaser.Loader.MultiFile { constructor (loader: Phaser.Loader.LoaderPlugin, key: string, url: string, public fileType: SpineSkeletonDataFileType, xhrSettings: Phaser.Types.Loader.XHRSettingsObject) { let file = null; let isJson = fileType == SpineSkeletonDataFileType.json; @@ -268,7 +286,7 @@ export class SpineSkeletonDataFile extends Phaser.Loader.MultiFile { } } -export class SpineAtlasFile extends Phaser.Loader.MultiFile { +class SpineAtlasFile extends Phaser.Loader.MultiFile { constructor (loader: Phaser.Loader.LoaderPlugin, key: string, url: string, public premultipliedAlpha: boolean = true, xhrSettings: Phaser.Types.Loader.XHRSettingsObject) { super(loader, SPINE_ATLAS_FILE_TYPE, key, [ new Phaser.Loader.FileTypes.TextFile(loader, { diff --git a/spine-ts/spine-phaser/src/keys.ts b/spine-ts/spine-phaser/src/keys.ts index f13f0dda4..feaac7ebc 100644 --- a/spine-ts/spine-phaser/src/keys.ts +++ b/spine-ts/spine-phaser/src/keys.ts @@ -1,6 +1,5 @@ export const SPINE_SKELETON_FILE_CACHE_KEY = "esotericsoftware.spine.skeletonFile.cache"; export const SPINE_ATLAS_CACHE_KEY = "esotericsoftware.spine.atlas.cache"; -export const SPINE_ATLAS_TEXTURE_CACHE_KEY = "esotericsoftware.spine.atlas.texture.cache"; export const SPINE_LOADER_TYPE = "spine"; export const SPINE_SKELETON_DATA_FILE_TYPE = "spineSkeletonData"; export const SPINE_ATLAS_FILE_TYPE = "spineAtlasData";