From bbb07886cf746d6c3d5cdc550ff375861171cc15 Mon Sep 17 00:00:00 2001 From: Davide Tantillo Date: Tue, 24 Feb 2026 11:07:08 +0100 Subject: [PATCH] [ts][pixi-v7][pixi-v8] Add ticker properties to provide custom ticker. See #3031. --- spine-ts/spine-pixi-v7/src/Spine.ts | 40 +++++++++++++++++++++------ spine-ts/spine-pixi-v8/src/Spine.ts | 42 ++++++++++++++++++++++------- 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/spine-ts/spine-pixi-v7/src/Spine.ts b/spine-ts/spine-pixi-v7/src/Spine.ts index 51a21fe75..af4bbc705 100644 --- a/spine-ts/spine-pixi-v7/src/Spine.ts +++ b/spine-ts/spine-pixi-v7/src/Spine.ts @@ -85,6 +85,9 @@ export interface SpineFromOptions { /** Set {@link AtlasAttachmentLoader.allowMissingRegions} property on the AtlasAttachmentLoader. */ allowMissingRegions?: boolean; + + /** The ticker to use when {@link autoUpdate} is `true`. Defaults to {@link Ticker.shared}. */ + ticker?: Ticker, }; export interface SpineOptions { @@ -99,6 +102,9 @@ export interface SpineOptions { /** See {@link SpineFromOptions.boundsProvider}. */ boundsProvider?: SpineBoundsProvider, + + /** See {@link SpineFromOptions.ticker}. */ + ticker?: Ticker, } /** @@ -282,19 +288,37 @@ export class Spine extends Container { afterUpdateWorldTransforms: (object: Spine) => void = () => { }; private _autoUpdate: boolean = false; + private _ticker: Ticker = Ticker.shared; + public get autoUpdate (): boolean { return this._autoUpdate; } - /** When `true`, the Spine AnimationState and the Skeleton will be automatically updated using the {@link Ticker.shared} instance. */ + /** When `true`, the Spine AnimationState and the Skeleton will be automatically updated using the {@link ticker}. */ public set autoUpdate (value: boolean) { if (value && !this._autoUpdate) { - Ticker.shared.add(this.internalUpdate, this); + this._ticker.add(this.internalUpdate, this); } else if (!value && this._autoUpdate) { - Ticker.shared.remove(this.internalUpdate, this); + this._ticker.remove(this.internalUpdate, this); } this._autoUpdate = value; } + /** The ticker to use when {@link autoUpdate} is `true`. Defaults to {@link Ticker.shared}. */ + public get ticker (): Ticker { + return this._ticker; + } + /** Sets the ticker to use when {@link autoUpdate} is `true`. If `autoUpdate` is already `true`, the update callback will be moved from the old ticker to the new one. */ + public set ticker (value: Ticker) { + if (this._ticker === value) return; + + if (this._autoUpdate) { + this._ticker.remove(this.internalUpdate, this); + value.add(this.internalUpdate, this); + } + + this._ticker = value; + } + private meshesCache = new Map(); private static vectorAux: Vector2 = new Vector2(); @@ -339,9 +363,10 @@ export class Spine extends Container { else if ("skeleton" in options) options = new.target.createOptions(options); - const { autoUpdate = true, boundsProvider, darkTint, skeletonData } = options; + const { autoUpdate = true, boundsProvider, darkTint, skeletonData, ticker } = options; this.skeleton = new Skeleton(skeletonData); this.state = new AnimationState(new AnimationStateData(skeletonData)); + if (ticker) this._ticker = ticker; this.autoUpdate = autoUpdate; this.boundsProvider = boundsProvider; @@ -360,8 +385,7 @@ export class Spine extends Container { protected internalUpdate (_deltaFrame: number, deltaSeconds?: number): void { this.hasNeverUpdated = false; - // Because reasons, pixi uses deltaFrames at 60fps. We ignore the default deltaFrames and use the deltaSeconds from pixi ticker. - const delta = deltaSeconds ?? Ticker.shared.deltaMS / 1000; + const delta = deltaSeconds ?? this._ticker.deltaMS / 1000; this.state.update(delta); this.state.apply(this.skeleton); this.beforeUpdateWorldTransforms(this); @@ -863,7 +887,7 @@ export class Spine extends Container { * @param options - Options to configure the Spine game object. See {@link SpineFromOptions} * @returns {SpineOptions} The configuration ready to be passed to the Spine constructor */ - public static createOptions ({ skeleton, atlas, scale = 1, darkTint, autoUpdate = true, boundsProvider, allowMissingRegions }: SpineFromOptions): SpineOptions { + public static createOptions ({ skeleton, atlas, scale = 1, darkTint, autoUpdate = true, boundsProvider, allowMissingRegions, ticker }: SpineFromOptions): SpineOptions { const cacheKey = `${skeleton}-${atlas}-${scale}`; let skeletonData = Spine.skeletonCache[cacheKey]; @@ -877,7 +901,7 @@ export class Spine extends Container { skeletonData = parser.readSkeletonData(skeletonAsset); Spine.skeletonCache[cacheKey] = skeletonData; } - return { skeletonData, darkTint, autoUpdate, boundsProvider }; + return { skeletonData, darkTint, autoUpdate, boundsProvider, ticker }; } /** diff --git a/spine-ts/spine-pixi-v8/src/Spine.ts b/spine-ts/spine-pixi-v8/src/Spine.ts index f782763b5..8633f2c8c 100644 --- a/spine-ts/spine-pixi-v8/src/Spine.ts +++ b/spine-ts/spine-pixi-v8/src/Spine.ts @@ -95,6 +95,9 @@ export interface SpineFromOptions { /** Set {@link AtlasAttachmentLoader.allowMissingRegions} property on the AtlasAttachmentLoader. */ allowMissingRegions?: boolean; + + /** The ticker to use when {@link autoUpdate} is `true`. Defaults to {@link Ticker.shared}. */ + ticker?: Ticker, }; const vectorAux = new Vector2(); @@ -247,6 +250,9 @@ export interface SpineOptions extends ContainerOptions { /** See {@link SpineFromOptions.boundsProvider}. */ boundsProvider?: SpineBoundsProvider, + + /** See {@link SpineFromOptions.ticker}. */ + ticker?: Ticker, } /** @@ -358,21 +364,38 @@ export class Spine extends ViewContainer { } private _autoUpdate = false; + private _ticker: Ticker = Ticker.shared; public get autoUpdate (): boolean { return this._autoUpdate; } - /** When `true`, the Spine AnimationState and the Skeleton will be automatically updated using the {@link Ticker.shared} instance. */ + /** When `true`, the Spine AnimationState and the Skeleton will be automatically updated using the {@link ticker}. */ public set autoUpdate (value: boolean) { if (value && !this._autoUpdate) { - Ticker.shared.add(this.internalUpdate, this); + this._ticker.add(this.internalUpdate, this); } else if (!value && this._autoUpdate) { - Ticker.shared.remove(this.internalUpdate, this); + this._ticker.remove(this.internalUpdate, this); } this._autoUpdate = value; } + /** The ticker to use when {@link autoUpdate} is `true`. Defaults to {@link Ticker.shared}. */ + public get ticker (): Ticker { + return this._ticker; + } + /** Sets the ticker to use when {@link autoUpdate} is `true`. If `autoUpdate` is already `true`, the update callback will be moved from the old ticker to the new one. */ + public set ticker (value: Ticker) { + if (this._ticker === value) return; + + if (this._autoUpdate) { + this._ticker.remove(this.internalUpdate, this); + value.add(this.internalUpdate, this); + } + + this._ticker = value; + } + private _boundsProvider?: SpineBoundsProvider; /** The bounds provider to use. If undefined the bounds will be dynamic, calculated when requested and based on the current frame. */ public get boundsProvider (): SpineBoundsProvider | undefined { @@ -397,9 +420,10 @@ export class Spine extends ViewContainer { this.allowChildren = true; - const { autoUpdate, boundsProvider, darkTint, skeletonData } = options; + const { autoUpdate, boundsProvider, darkTint, skeletonData, ticker } = options; this.skeleton = new Skeleton(skeletonData); this.state = new AnimationState(new AnimationStateData(skeletonData)); + if (ticker) this._ticker = ticker; this.autoUpdate = autoUpdate ?? true; this._boundsProvider = boundsProvider; @@ -419,9 +443,7 @@ export class Spine extends ViewContainer { } protected internalUpdate (ticker?: Ticker, deltaSeconds?: number): void { - // Because reasons, pixi uses deltaFrames at 60fps. - // We ignore the default deltaFrames and use the deltaSeconds from pixi ticker. - this._updateAndApplyState(deltaSeconds ?? Ticker.shared.deltaMS / 1000); + this._updateAndApplyState(deltaSeconds ?? this._ticker.deltaMS / 1000); } override get bounds () { @@ -1021,7 +1043,7 @@ export class Spine extends ViewContainer { public override destroy (options: DestroyOptions = false) { super.destroy(options); - Ticker.shared.remove(this.internalUpdate, this); + this._ticker.remove(this.internalUpdate, this); this.state.clearListeners(); this.debug = undefined; (this.skeleton as unknown) = null; @@ -1066,7 +1088,7 @@ export class Spine extends ViewContainer { * @param options - Options to configure the Spine game object. See {@link SpineFromOptions} * @returns {SpineOptions} The configuration ready to be passed to the Spine constructor */ - static createOptions ({ skeleton, atlas, scale = 1, darkTint, autoUpdate = true, boundsProvider, allowMissingRegions = false }: SpineFromOptions): SpineOptions { + static createOptions ({ skeleton, atlas, scale = 1, darkTint, autoUpdate = true, boundsProvider, allowMissingRegions = false, ticker }: SpineFromOptions): SpineOptions { const cacheKey = `${skeleton}-${atlas}-${scale}`; if (Cache.has(cacheKey)) { @@ -1075,6 +1097,7 @@ export class Spine extends ViewContainer { darkTint, autoUpdate, boundsProvider, + ticker, }; } @@ -1097,6 +1120,7 @@ export class Spine extends ViewContainer { darkTint, autoUpdate, boundsProvider, + ticker, }; }