phaser
Version:
A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.
1,586 lines (1,392 loc) • 51 kB
JavaScript
/**
* @author Richard Davey <rich@photonstorm.com>
* @copyright 2022 Photon Storm Ltd.
* @license {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
*/
var AngleBetween = require('../../../../src/math/angle/Between');
var Clamp = require('../../../../src/math/Clamp');
var Class = require('../../../../src/utils/Class');
var ComponentsComputedSize = require('../../../../src/gameobjects/components/ComputedSize');
var ComponentsDepth = require('../../../../src/gameobjects/components/Depth');
var ComponentsFlip = require('../../../../src/gameobjects/components/Flip');
var ComponentsScrollFactor = require('../../../../src/gameobjects/components/ScrollFactor');
var ComponentsTransform = require('../../../../src/gameobjects/components/Transform');
var ComponentsVisible = require('../../../../src/gameobjects/components/Visible');
var CounterClockwise = require('../../../../src/math/angle/CounterClockwise');
var DegToRad = require('../../../../src/math/DegToRad');
var GameObject = require('../../../../src/gameobjects/GameObject');
var RadToDeg = require('../../../../src/math/RadToDeg');
var SpineEvents = require('../events/');
var SpineGameObjectRender = require('./SpineGameObjectRender');
/**
* @classdesc
* A Spine Game Object is a Phaser level object that can be added to your Phaser Scenes. It encapsulates
* a Spine Skeleton with Spine Animation Data and Animation State, with helper methods to allow you to
* easily change the skin, slot attachment, bone positions and more.
*
* Spine Game Objects can be created via the Game Object Factory, Game Object Creator, or directly.
* You can only create them if the Spine plugin has been loaded into Phaser.
*
* The quickest way is the Game Object Factory:
*
* ```javascript
* let jelly = this.add.spine(512, 550, 'jelly', 'jelly-think', true);
* ```
*
* Here we are creating a new Spine Game Object positioned at 512 x 550. It's using the `jelly`
* Spine data, which has previously been loaded into your Scene. The `jelly-think` argument is
* an optional animation to start playing on the skeleton. The final argument `true` sets the
* animation to loop. Look at the documentation for further details on each of these options.
*
* For more control, you can use the Game Object Creator, passing in a Spine Game Object
* Configuration object:
*
* ```javascript
* let jelly = this.make.spine({
* x: 512, y: 550, key: 'jelly',
* scale: 1.5,
* skinName: 'square_Green',
* animationName: 'jelly-think', loop: true,
* slotName: 'hat', attachmentName: 'images/La_14'
* });
* ```
*
* Here, you've got the ability to specify extra details, such as the slot name, attachments or
* overall scale.
*
* If you wish to instantiate a Spine Game Object directly you can do so, but in order for it to
* update and render, it must be added to the display and update lists of your Scene:
*
* ```javascript
* let jelly = new SpineGameObject(this, this.spine, 512, 550, 'jelly', 'jelly-think', true);
* this.sys.displayList.add(jelly);
* this.sys.updateList.add(jelly);
* ```
*
* It's possible to enable Spine Game Objects for input, but you should be aware that it will use
* the bounds of the skeletons current pose to create the hit area from. Ensure that your setup
* post in the Spine Editor does _not_ have everything turned off, or the runtimes will be unable
* to get an accurate bounds. You can make use of the `InputPlugin.enableDebug` method to view the
* input shape being created. If it's not suitable, provide your own shape to the `setInteractive` method.
*
* Due to the way Spine handles scaling, it's not recommended to enable a Spine Game Object for
* physics directly. Instead, you should look at creating a proxy body and syncing the Spine Game
* Object position with it. See the examples for further details.
*
* If your Spine Game Object has black outlines around the different parts of the texture when it
* renders then you have exported the files from Spine with pre-multiplied alpha enabled, but have
* forgotten to set that flag when loading the Spine data. Please see the loader docs for more details.
*
* @class SpineGameObject
* @extends Phaser.GameObjects.GameObject
* @constructor
* @since 3.19.0
*
* @param {Phaser.Scene} scene - A reference to the Scene that this Game Object belongs to.
* @param {SpinePlugin} pluginManager - A reference to the Phaser Spine Plugin.
* @param {number} x - The horizontal position of this Game Object in the world.
* @param {number} y - The vertical position of this Game Object in the world.
* @param {string} [key] - The key of the Spine Skeleton this Game Object will use, as stored in the Spine Plugin.
* @param {string} [animationName] - The name of the animation to set on this Skeleton.
* @param {boolean} [loop=false] - Should the animation playback be looped or not?
*/
var SpineGameObject = new Class({
Extends: GameObject,
Mixins: [
ComponentsComputedSize,
ComponentsDepth,
ComponentsFlip,
ComponentsScrollFactor,
ComponentsTransform,
ComponentsVisible,
SpineGameObjectRender
],
initialize:
function SpineGameObject (scene, plugin, x, y, key, animationName, loop)
{
GameObject.call(this, scene, 'Spine');
/**
* A reference to the Spine Plugin.
*
* @name SpineGameObject#plugin
* @type {SpinePlugin}
* @since 3.19.0
*/
this.plugin = plugin;
/**
* The Spine Skeleton this Game Object is using.
*
* @name SpineGameObject#skeleton
* @type {spine.Skeleton}
* @since 3.19.0
*/
this.skeleton = null;
/**
* The Spine Skeleton Data associated with the Skeleton this Game Object is using.
*
* @name SpineGameObject#skeletonData
* @type {spine.SkeletonData}
* @since 3.19.0
*/
this.skeletonData = null;
/**
* The Spine Animation State this Game Object is using.
*
* @name SpineGameObject#state
* @type {spine.AnimationState}
* @since 3.19.0
*/
this.state = null;
/**
* The Spine Animation State Data associated with the Animation State this Game Object is using.
*
* @name SpineGameObject#stateData
* @type {spine.AnimationStateData}
* @since 3.19.0
*/
this.stateData = null;
/**
* A reference to the root bone of the Skeleton.
*
* @name SpineGameObject#root
* @type {spine.Bone}
* @since 3.19.0
*/
this.root = null;
/**
* This object holds the calculated bounds of the current
* pose, as set when a new Skeleton is applied.
*
* @name SpineGameObject#bounds
* @type {any}
* @since 3.19.0
*/
this.bounds = null;
/**
* A Game Object level flag that allows you to enable debug drawing
* to the Skeleton Debug Renderer by toggling it.
*
* @name SpineGameObject#drawDebug
* @type {boolean}
* @since 3.19.0
*/
this.drawDebug = false;
/**
* The factor to scale the Animation update time by.
*
* @name SpineGameObject#timeScale
* @type {number}
* @since 3.19.0
*/
this.timeScale = 1;
/**
* The calculated Display Origin of this Game Object.
*
* @name SpineGameObject#displayOriginX
* @type {number}
* @since 3.19.0
*/
this.displayOriginX = 0;
/**
* The calculated Display Origin of this Game Object.
*
* @name SpineGameObject#displayOriginY
* @type {number}
* @since 3.19.0
*/
this.displayOriginY = 0;
/**
* A flag that stores if the texture associated with the current
* Skin being used by this Game Object, has its alpha pre-multiplied
* into it, or not.
*
* @name SpineGameObject#preMultipliedAlpha
* @type {boolean}
* @since 3.19.0
*/
this.preMultipliedAlpha = false;
/**
* A default Blend Mode. You cannot change the blend mode of a
* Spine Game Object.
*
* @name SpineGameObject#blendMode
* @type {number}
* @readonly
* @since 3.19.0
*/
this.blendMode = -1;
this.setPosition(x, y);
if (key)
{
this.setSkeleton(key, animationName, loop);
}
},
/**
* Returns `true` if this Spine Game Object both has a skeleton and
* also passes the render tests for the given Camera.
*
* @method SpineGameObject#willRender
* @since 3.19.0
*
* @param {Phaser.Cameras.Scene2D.Camera} camera - The Camera that is rendering the Game Object.
* @param {SpineContainer} [container] - If this Spine object is in a Spine Container, this is a reference to it.
*
* @return {boolean} `true` if this Game Object should be rendered, otherwise `false`.
*/
willRender: function (camera, container)
{
var GameObjectRenderMask = 15;
var result = (!this.skeleton || !(GameObjectRenderMask !== this.renderFlags || (this.cameraFilter !== 0 && (this.cameraFilter & camera.id))));
if (!container && !result && this.parentContainer)
{
var plugin = this.plugin;
var sceneRenderer = plugin.sceneRenderer;
if (plugin.gl && sceneRenderer.batcher.isDrawing)
{
sceneRenderer.end();
plugin.renderer.pipelines.rebind();
}
}
return result;
},
/**
* Set the Alpha level for the whole Skeleton of this Game Object.
*
* The alpha controls the opacity of the Game Object as it renders.
*
* Alpha values are provided as a float between 0, fully transparent, and 1, fully opaque.
*
* To set the alpha of a specific attachment, use the `setSlotAlpha` method instead.
*
* @method SpineGameObject#setAlpha
* @since 3.19.0
*
* @param {number} [value=1] - The alpha value used for the whole Skeleton.
*
* @return {this} This Game Object instance.
*/
setAlpha: function (value)
{
if (value === undefined) { value = 1; }
this.alpha = value;
return this;
},
/**
* Set the Alpha level for the given attachment slot.
*
* The alpha controls the opacity of the slot as it renders.
*
* Alpha values are provided as a float between 0, fully transparent, and 1, fully opaque.
*
* To set the alpha for the whole skeleton, use the `setAlpha` method instead.
*
* @method SpineGameObject#setSlotAlpha
* @since 3.80.0
*
* @param {string} slotName - The name of the slot to find.
* @param {number} [value=1] - The alpha value used for the slot.
*
* @return {this} This Game Object instance.
*/
setSlotAlpha: function (value, slotName)
{
if (value === undefined) { value = 1; }
var slot = this.findSlot(slotName);
if (slot)
{
slot.color.a = Clamp(value, 0, 1);
}
return this;
},
/**
* The alpha value of the Skeleton.
*
* A value between 0 and 1.
*
* This is a global value, impacting the entire Skeleton, not just a region of it.
*
* @name SpineGameObject#alpha
* @type {number}
* @since 3.19.0
*/
alpha: {
get: function ()
{
return this.skeleton.color.a;
},
set: function (value)
{
var v = Clamp(value, 0, 1);
if (this.skeleton)
{
this.skeleton.color.a = v;
}
if (v === 0)
{
this.renderFlags &= ~2;
}
else
{
this.renderFlags |= 2;
}
}
},
/**
* The amount of red used when rendering the Skeleton.
*
* A value between 0 and 1.
*
* This is a global value, impacting the entire Skeleton, not just a region of it.
*
* @name SpineGameObject#red
* @type {number}
* @since 3.19.0
*/
red: {
get: function ()
{
return this.skeleton.color.r;
},
set: function (value)
{
var v = Clamp(value, 0, 1);
if (this.skeleton)
{
this.skeleton.color.r = v;
}
}
},
/**
* The amount of green used when rendering the Skeleton.
*
* A value between 0 and 1.
*
* This is a global value, impacting the entire Skeleton, not just a region of it.
*
* @name SpineGameObject#green
* @type {number}
* @since 3.19.0
*/
green: {
get: function ()
{
return this.skeleton.color.g;
},
set: function (value)
{
var v = Clamp(value, 0, 1);
if (this.skeleton)
{
this.skeleton.color.g = v;
}
}
},
/**
* The amount of blue used when rendering the Skeleton.
*
* A value between 0 and 1.
*
* This is a global value, impacting the entire Skeleton, not just a region of it.
*
* @name SpineGameObject#blue
* @type {number}
* @since 3.19.0
*/
blue: {
get: function ()
{
return this.skeleton.color.b;
},
set: function (value)
{
var v = Clamp(value, 0, 1);
if (this.skeleton)
{
this.skeleton.color.b = v;
}
}
},
/**
* Sets the color on the given attachment slot. Or, if no slot is given, on the whole skeleton.
*
* @method SpineGameObject#setColor
* @since 3.19.0
*
* @param {integer} [color=0xffffff] - The color being applied to the Skeleton or named Slot. Set to white to disable any previously set color.
* @param {string} [slotName] - The name of the slot to set the color on. If not give, will be set on the whole skeleton.
*
* @return {this} This Game Object instance.
*/
setColor: function (color, slotName)
{
if (color === undefined) { color = 0xffffff; }
var red = (color >> 16 & 0xFF) / 255;
var green = (color >> 8 & 0xFF) / 255;
var blue = (color & 0xFF) / 255;
var alpha = (color > 16777215) ? (color >>> 24) / 255 : null;
var target = this.skeleton;
if (slotName)
{
var slot = this.findSlot(slotName);
if (slot)
{
target = slot;
}
}
target.color.r = red;
target.color.g = green;
target.color.b = blue;
if (alpha !== null)
{
target.color.a = alpha;
}
return this;
},
/**
* Sets this Game Object to use the given Skeleton based on the Atlas Data Key and a provided JSON object
* that contains the Skeleton data.
*
* @method SpineGameObject#setSkeletonFromJSON
* @since 3.19.0
*
* @param {string} atlasDataKey - The key of the Spine data to use for this Skeleton.
* @param {object} skeletonJSON - The JSON data for the Skeleton.
* @param {string} [animationName] - Optional name of the animation to set on the Skeleton.
* @param {boolean} [loop=false] - Should the animation, if set, loop or not?
*
* @return {this} This Game Object.
*/
setSkeletonFromJSON: function (atlasDataKey, skeletonJSON, animationName, loop)
{
return this.setSkeleton(atlasDataKey, skeletonJSON, animationName, loop);
},
/**
* Sets this Game Object to use the given Skeleton based on its cache key.
*
* Typically, once set, the Skeleton doesn't change. Instead, you change the skin,
* or slot attachment, or any other property to adjust it.
*
* @method SpineGameObject#setSkeleton
* @since 3.19.0
*
* @param {string} atlasDataKey - The key of the Spine data to use for this Skeleton.
* @param {string} [animationName] - Optional name of the animation to set on the Skeleton.
* @param {boolean} [loop=false] - Should the animation, if set, loop or not?
* @param {object} [skeletonJSON] - The JSON data for the Skeleton.
*
* @return {this} This Game Object.
*/
setSkeleton: function (atlasDataKey, animationName, loop, skeletonJSON)
{
if (this.state)
{
this.state.clearListeners();
this.state.clearListenerNotifications();
}
var data = this.plugin.createSkeleton(atlasDataKey, skeletonJSON);
this.skeletonData = data.skeletonData;
this.preMultipliedAlpha = data.preMultipliedAlpha;
var skeleton = data.skeleton;
skeleton.setSkin();
skeleton.setToSetupPose();
this.skeleton = skeleton;
// AnimationState
data = this.plugin.createAnimationState(skeleton);
if (this.state)
{
this.state.clearListeners();
this.state.clearListenerNotifications();
}
this.state = data.state;
this.stateData = data.stateData;
this.state.addListener({
event: this.onEvent.bind(this),
complete: this.onComplete.bind(this),
start: this.onStart.bind(this),
end: this.onEnd.bind(this),
dispose: this.onDispose.bind(this),
interrupted: this.onInterrupted.bind(this)
});
if (animationName)
{
this.setAnimation(0, animationName, loop);
}
this.root = this.getRootBone();
if (this.root)
{
// +90 degrees to account for the difference in Spine vs. Phaser rotation
this.root.rotation = RadToDeg(CounterClockwise(this.rotation)) + 90;
}
this.state.apply(skeleton);
skeleton.updateCache();
return this.updateSize();
},
/**
* Internal event handler that emits the Spine onComplete event via this Game Object.
*
* @method SpineGameObject#onComplete
* @fires SpinePluginEvents#COMPLETE
* @private
* @since 3.19.0
*
* @param {any} entry - The event data from Spine.
*/
onComplete: function (entry)
{
this.emit(SpineEvents.COMPLETE, entry);
},
/**
* Internal event handler that emits the Spine onDispose event via this Game Object.
*
* @method SpineGameObject#onDispose
* @fires SpinePluginEvents#DISPOSE
* @private
* @since 3.19.0
*
* @param {any} entry - The event data from Spine.
*/
onDispose: function (entry)
{
this.emit(SpineEvents.DISPOSE, entry);
},
/**
* Internal event handler that emits the Spine onEnd event via this Game Object.
*
* @method SpineGameObject#onEnd
* @fires SpinePluginEvents#END
* @private
* @since 3.19.0
*
* @param {any} entry - The event data from Spine.
*/
onEnd: function (entry)
{
this.emit(SpineEvents.END, entry);
},
/**
* Internal event handler that emits the Spine Event event via this Game Object.
*
* @method SpineGameObject#onEvent
* @fires SpinePluginEvents#EVENT
* @private
* @since 3.19.0
*
* @param {any} entry - The event data from Spine.
* @param {spine.Event} event - The Spine event.
*/
onEvent: function (entry, event)
{
this.emit(SpineEvents.EVENT, entry, event);
},
/**
* Internal event handler that emits the Spine onInterrupted event via this Game Object.
*
* @method SpineGameObject#onInterrupted
* @fires SpinePluginEvents#INTERRUPTED
* @private
* @since 3.19.0
*
* @param {any} entry - The event data from Spine.
*/
onInterrupted: function (entry)
{
this.emit(SpineEvents.INTERRUPTED, entry);
},
/**
* Internal event handler that emits the Spine onStart event via this Game Object.
*
* @method SpineGameObject#onStart
* @fires SpinePluginEvents#START
* @private
* @since 3.19.0
*
* @param {any} entry - The event data from Spine.
*/
onStart: function (entry)
{
this.emit(SpineEvents.START, entry);
},
/**
* Refreshes the data about the current Skeleton.
*
* This will reset the rotation, position and size of the Skeleton to match this Game Object.
*
* Call this method if you need to access the Skeleton data directly, and it may have changed
* recently.
*
* @method SpineGameObject#refresh
* @since 3.19.0
*
* @return {this} This Game Object.
*/
refresh: function ()
{
if (this.root)
{
// +90 degrees to account for the difference in Spine vs. Phaser rotation
this.root.rotation = RadToDeg(CounterClockwise(this.rotation)) + 90;
}
this.updateSize();
this.skeleton.updateCache();
return this;
},
/**
* Sets the size of this Game Object.
*
* If no arguments are given it uses the current skeleton data dimensions.
*
* You can use this method to set a fixed size of this Game Object, such as for input detection,
* when the skeleton data doesn't match what is required in-game.
*
* @method SpineGameObject#setSize
* @since 3.19.0
*
* @param {number} [width] - The width of the Skeleton. If not given it defaults to the Skeleton Data width.
* @param {number} [height] - The height of the Skeleton. If not given it defaults to the Skeleton Data height.
* @param {number} [offsetX=0] - The horizontal offset of the Skeleton from its x and y coordinate.
* @param {number} [offsetY=0] - The vertical offset of the Skeleton from its x and y coordinate.
*
* @return {this} This Game Object.
*/
setSize: function (width, height, offsetX, offsetY)
{
var skeleton = this.skeleton;
if (width === undefined) { width = skeleton.data.width; }
if (height === undefined) { height = skeleton.data.height; }
if (offsetX === undefined) { offsetX = 0; }
if (offsetY === undefined) { offsetY = 0; }
this.width = width;
this.height = height;
this.displayOriginX = skeleton.x - offsetX;
this.displayOriginY = skeleton.y - offsetY;
return this;
},
/**
* Sets the offset of this Game Object from the Skeleton position.
*
* You can use this method to adjust how the position of this Game Object relates to the Skeleton it is using.
*
* @method SpineGameObject#setOffset
* @since 3.19.0
*
* @param {number} [offsetX=0] - The horizontal offset of the Skeleton from its x and y coordinate.
* @param {number} [offsetY=0] - The vertical offset of the Skeleton from its x and y coordinate.
*
* @return {this} This Game Object.
*/
setOffset: function (offsetX, offsetY)
{
var skeleton = this.skeleton;
if (offsetX === undefined) { offsetX = 0; }
if (offsetY === undefined) { offsetY = 0; }
this.displayOriginX = skeleton.x - offsetX;
this.displayOriginY = skeleton.y - offsetY;
return this;
},
/**
* Internal method that syncs all of the Game Object position and scale data to the Skeleton.
* It then syncs the skeleton bounds back to this Game Object.
*
* This method is called automatically as needed internally, however, it's also exposed should
* you require overriding the size settings.
*
* @method SpineGameObject#updateSize
* @since 3.19.0
*
* @return {this} This Game Object.
*/
updateSize: function ()
{
var skeleton = this.skeleton;
var renderer = this.plugin.renderer;
var height = renderer.height;
var oldScaleX = this.scaleX;
var oldScaleY = this.scaleY;
skeleton.x = this.x;
skeleton.y = height - this.y;
skeleton.scaleX = 1;
skeleton.scaleY = 1;
skeleton.updateWorldTransform();
var bounds = this.getBounds();
this.width = bounds.size.x;
this.height = bounds.size.y;
this.displayOriginX = this.x - bounds.offset.x;
this.displayOriginY = this.y - (height - (this.height + bounds.offset.y));
skeleton.scaleX = oldScaleX;
skeleton.scaleY = oldScaleY;
skeleton.updateWorldTransform();
return this;
},
/**
* The horizontal scale of this Game Object, as applied to the Skeleton it is using.
*
* @name SpineGameObject#scaleX
* @type {number}
* @default 1
* @since 3.19.0
*/
scaleX: {
get: function ()
{
return this._scaleX;
},
set: function (value)
{
this._scaleX = value;
this.refresh();
}
},
/**
* The vertical scale of this Game Object, as applied to the Skeleton it is using.
*
* @name SpineGameObject#scaleY
* @type {number}
* @default 1
* @since 3.19.0
*/
scaleY: {
get: function ()
{
return this._scaleY;
},
set: function (value)
{
this._scaleY = value;
this.refresh();
}
},
/**
* Returns an array containing the names of all the bones in the Skeleton Data.
*
* @method SpineGameObject#getBoneList
* @since 3.19.0
*
* @return {string[]} An array containing the names of all the bones in the Skeleton Data.
*/
getBoneList: function ()
{
var output = [];
var skeletonData = this.skeletonData;
if (skeletonData)
{
for (var i = 0; i < skeletonData.bones.length; i++)
{
output.push(skeletonData.bones[i].name);
}
}
return output;
},
/**
* Returns an array containing the names of all the skins in the Skeleton Data.
*
* @method SpineGameObject#getSkinList
* @since 3.19.0
*
* @return {string[]} An array containing the names of all the skins in the Skeleton Data.
*/
getSkinList: function ()
{
var output = [];
var skeletonData = this.skeletonData;
if (skeletonData)
{
for (var i = 0; i < skeletonData.skins.length; i++)
{
output.push(skeletonData.skins[i].name);
}
}
return output;
},
/**
* Returns an array containing the names of all the slots in the Skeleton.
*
* @method SpineGameObject#getSlotList
* @since 3.19.0
*
* @return {string[]} An array containing the names of all the slots in the Skeleton.
*/
getSlotList: function ()
{
var output = [];
var skeleton = this.skeleton;
for (var i = 0; i < skeleton.slots.length; i++)
{
output.push(skeleton.slots[i].data.name);
}
return output;
},
/**
* Returns an array containing the names of all the animations in the Skeleton Data.
*
* @method SpineGameObject#getAnimationList
* @since 3.19.0
*
* @return {string[]} An array containing the names of all the animations in the Skeleton Data.
*/
getAnimationList: function ()
{
var output = [];
var skeletonData = this.skeletonData;
if (skeletonData)
{
for (var i = 0; i < skeletonData.animations.length; i++)
{
output.push(skeletonData.animations[i].name);
}
}
return output;
},
/**
* Returns the current animation being played on the given track, if any.
*
* @method SpineGameObject#getCurrentAnimation
* @since 3.19.0
*
* @param {integer} [trackIndex=0] - The track to return the current animation on.
*
* @return {?spine.Animation} The current Animation on the given track, or `undefined` if there is no current animation.
*/
getCurrentAnimation: function (trackIndex)
{
if (trackIndex === undefined) { trackIndex = 0; }
var current = this.state.getCurrent(trackIndex);
if (current)
{
return current.animation;
}
},
/**
* Sets the current animation for a track, discarding any queued animations.
* If the formerly current track entry was never applied to a skeleton, it is replaced (not mixed from).
*
* Animations are referenced by a unique string-based key, as defined in the Spine software.
*
* @method SpineGameObject#play
* @fires SpinePluginEvents#START
* @since 3.19.0
*
* @param {string} animationName - The string-based key of the animation to play.
* @param {boolean} [loop=false] - Should the animation be looped when played?
* @param {boolean} [ignoreIfPlaying=false] - If this animation is already playing then ignore this call.
*
* @return {this} This Game Object. If you need the TrackEntry, see `setAnimation` instead.
*/
play: function (animationName, loop, ignoreIfPlaying)
{
this.setAnimation(0, animationName, loop, ignoreIfPlaying);
this.preUpdate(0, 16.6);
return this;
},
/**
* Sets the current animation for a track, discarding any queued animations.
* If the formerly current track entry was never applied to a skeleton, it is replaced (not mixed from).
*
* Animations are referenced by a unique string-based key, as defined in the Spine software.
*
* @method SpineGameObject#setAnimation
* @fires SpinePluginEvents#START
* @since 3.19.0
*
* @param {integer} trackIndex - The track index to play the animation on.
* @param {string} animationName - The string-based key of the animation to play.
* @param {boolean} [loop=false] - Should the animation be looped when played?
* @param {boolean} [ignoreIfPlaying=false] - If the animation specified by the track index is already playing then ignore this call.
*
* @return {spine.TrackEntry} A track entry to allow further customization of animation playback.
*/
setAnimation: function (trackIndex, animationName, loop, ignoreIfPlaying)
{
if (loop === undefined) { loop = false; }
if (ignoreIfPlaying === undefined) { ignoreIfPlaying = false; }
if (ignoreIfPlaying && this.state)
{
var currentTrack = this.state.getCurrent(trackIndex);
if (currentTrack && currentTrack.animation.name === animationName && !currentTrack.isComplete())
{
return;
}
}
if (this.findAnimation(animationName))
{
return this.state.setAnimation(trackIndex, animationName, loop);
}
},
/**
* Adds an animation to be played after the current or last queued animation for a track.
* If the track is empty, it is equivalent to calling setAnimation.
*
* Animations are referenced by a unique string-based key, as defined in the Spine software.
*
* The delay is a float. If > 0, sets delay. If <= 0, the delay set is the duration of the previous
* track entry minus any mix duration (from the AnimationStateData) plus the specified delay
* (ie the mix ends at (delay = 0) or before (delay < 0) the previous track entry duration).
* If the previous entry is looping, its next loop completion is used instead of its duration.
*
* @method SpineGameObject#addAnimation
* @since 3.19.0
*
* @param {integer} trackIndex - The track index to add the animation to.
* @param {string} animationName - The string-based key of the animation to add.
* @param {boolean} [loop=false] - Should the animation be looped when played?
* @param {integer} [delay=0] - A delay, in ms, before which this animation will start when played.
*
* @return {spine.TrackEntry} A track entry to allow further customization of animation playback.
*/
addAnimation: function (trackIndex, animationName, loop, delay)
{
if (loop === undefined) { loop = false; }
if (delay === undefined) { delay = 0; }
return this.state.addAnimation(trackIndex, animationName, loop, delay);
},
/**
* Sets an empty animation for a track, discarding any queued animations, and sets the track
* entry's mixDuration. An empty animation has no timelines and serves as a placeholder for mixing in or out.
*
* Mixing out is done by setting an empty animation with a mix duration using either setEmptyAnimation,
* setEmptyAnimations, or addEmptyAnimation. Mixing to an empty animation causes the previous animation to be
* applied less and less over the mix duration. Properties keyed in the previous animation transition to
* the value from lower tracks or to the setup pose value if no lower tracks key the property.
* A mix duration of 0 still mixes out over one frame.
*
* Mixing in is done by first setting an empty animation, then adding an animation using addAnimation
* and on the returned track entry, set the mixDuration. Mixing from an empty animation causes the new
* animation to be applied more and more over the mix duration. Properties keyed in the new animation
* transition from the value from lower tracks or from the setup pose value if no lower tracks key the
* property to the value keyed in the new animation.
*
* @method SpineGameObject#setEmptyAnimation
* @since 3.19.0
*
* @param {integer} trackIndex - The track index to add the animation to.
* @param {integer} [mixDuration] - Seconds for mixing from the previous animation to this animation. Defaults to the value provided by AnimationStateData getMix based on the animation before this animation (if any).
*
* @return {spine.TrackEntry} The returned Track Entry.
*/
setEmptyAnimation: function (trackIndex, mixDuration)
{
return this.state.setEmptyAnimation(trackIndex, mixDuration);
},
/**
* Removes all animations from the track, leaving skeletons in their current pose.
*
* It may be desired to use setEmptyAnimation to mix the skeletons back to the setup pose,
* rather than leaving them in their current pose.
*
* @method SpineGameObject#clearTrack
* @since 3.19.0
*
* @param {integer} trackIndex - The track index to add the animation to.
*
* @return {this} This Game Object.
*/
clearTrack: function (trackIndex)
{
this.state.clearTrack(trackIndex);
return this;
},
/**
* Removes all animations from all tracks, leaving skeletons in their current pose.
*
* It may be desired to use setEmptyAnimation to mix the skeletons back to the setup pose,
* rather than leaving them in their current pose.
*
* @method SpineGameObject#clearTracks
* @since 3.19.0
*
* @return {this} This Game Object.
*/
clearTracks: function ()
{
this.state.clearTracks();
return this;
},
/**
* Sets the skin used to look up attachments before looking in the defaultSkin.
*
* Attachments from the new skin are attached if the corresponding attachment from the
* old skin was attached. If there was no old skin, each slot's setup mode attachment is
* attached from the new skin.
*
* After changing the skin, the visible attachments can be reset to those attached in the
* setup pose by calling setSlotsToSetupPose. Also, often apply is called before the next time
* the skeleton is rendered to allow any attachment keys in the current animation(s) to hide
* or show attachments from the new skin.
*
* @method SpineGameObject#setSkinByName
* @since 3.19.0
*
* @param {string} skinName - The name of the skin to set.
*
* @return {this} This Game Object.
*/
setSkinByName: function (skinName)
{
var skeleton = this.skeleton;
skeleton.setSkinByName(skinName);
skeleton.setSlotsToSetupPose();
this.state.apply(skeleton);
return this;
},
/**
* Sets the skin used to look up attachments before looking in the defaultSkin.
*
* Attachments from the new skin are attached if the corresponding attachment from the
* old skin was attached. If there was no old skin, each slot's setup mode attachment is
* attached from the new skin.
*
* After changing the skin, the visible attachments can be reset to those attached in the
* setup pose by calling setSlotsToSetupPose. Also, often apply is called before the next time
* the skeleton is rendered to allow any attachment keys in the current animation(s) to hide
* or show attachments from the new skin.
*
* @method SpineGameObject#setSkin
* @since 3.19.0
*
* @param {?spine.Skin} newSkin - The Skin to set. May be `null`.
*
* @return {this} This Game Object.
*/
setSkin: function (newSkin)
{
var skeleton = this.skeleton;
skeleton.setSkin(newSkin);
skeleton.setSlotsToSetupPose();
this.state.apply(skeleton);
return this;
},
/**
* Sets the mix duration when changing from the specified animation to the other.
*
* @method SpineGameObject#setMix
* @since 3.19.0
*
* @param {string} fromName - The animation to mix from.
* @param {string} toName - The animation to mix to.
* @param {number} [duration] - Seconds for mixing from the previous animation to this animation. Defaults to the value provided by AnimationStateData getMix based on the animation before this animation (if any).
*
* @return {this} This Game Object.
*/
setMix: function (fromName, toName, duration)
{
this.stateData.setMix(fromName, toName, duration);
return this;
},
/**
* Finds an attachment by looking in the skin and defaultSkin using the slot
* index and attachment name. First the skin is checked and if the attachment was not found,
* the default skin is checked.
*
* @method SpineGameObject#getAttachment
* @since 3.19.0
*
* @param {integer} slotIndex - The slot index to search.
* @param {string} attachmentName - The attachment name to look for.
*
* @return {?spine.Attachment} The Attachment, if found. May be null.
*/
getAttachment: function (slotIndex, attachmentName)
{
return this.skeleton.getAttachment(slotIndex, attachmentName);
},
/**
* Finds an attachment by looking in the skin and defaultSkin using the slot name and attachment name.
*
* @method SpineGameObject#getAttachmentByName
* @since 3.19.0
*
* @param {string} slotName - The slot name to search.
* @param {string} attachmentName - The attachment name to look for.
*
* @return {?spine.Attachment} The Attachment, if found. May be null.
*/
getAttachmentByName: function (slotName, attachmentName)
{
return this.skeleton.getAttachmentByName(slotName, attachmentName);
},
/**
* A convenience method to set an attachment by finding the slot with findSlot,
* finding the attachment with getAttachment, then setting the slot's attachment.
*
* @method SpineGameObject#setAttachment
* @since 3.19.0
*
* @param {string} slotName - The slot name to add the attachment to.
* @param {string} attachmentName - The attachment name to add.
*
* @return {this} This Game Object.
*/
setAttachment: function (slotName, attachmentName)
{
if (Array.isArray(slotName) && Array.isArray(attachmentName) && slotName.length === attachmentName.length)
{
for (var i = 0; i < slotName.length; i++)
{
this.skeleton.setAttachment(slotName[i], attachmentName[i]);
}
}
else
{
this.skeleton.setAttachment(slotName, attachmentName);
}
return this;
},
/**
* Sets the bones, constraints, slots, and draw order to their setup pose values.
*
* @method SpineGameObject#setToSetupPose
* @since 3.19.0
*
* @return {this} This Game Object.
*/
setToSetupPose: function ()
{
this.skeleton.setToSetupPose();
return this;
},
/**
* Sets the slots and draw order to their setup pose values.
*
* @method SpineGameObject#setSlotsToSetupPose
* @since 3.19.0
*
* @return {this} This Game Object.
*/
setSlotsToSetupPose: function ()
{
this.skeleton.setSlotsToSetupPose();
return this;
},
/**
* Sets the bones and constraints to their setup pose values.
*
* @method SpineGameObject#setBonesToSetupPose
* @since 3.19.0
*
* @return {this} This Game Object.
*/
setBonesToSetupPose: function ()
{
this.skeleton.setBonesToSetupPose();
return this;
},
/**
* Gets the root bone, or null.
*
* @method SpineGameObject#getRootBone
* @since 3.19.0
*
* @return {spine.Bone} The root bone, or null.
*/
getRootBone: function ()
{
return this.skeleton.getRootBone();
},
/**
* Takes a Bone object and a position in world space and rotates the Bone so it is angled
* towards the given position. You can set an optional angle offset, should the bone be
* designed at a specific angle already. You can also set a minimum and maximum range for the angle.
*
* @method SpineGameObject#angleBoneToXY
* @since 3.19.0
*
* @param {spine.Bone} bone - The bone to rotate towards the world position.
* @param {number} worldX - The world x coordinate to rotate the bone towards.
* @param {number} worldY - The world y coordinate to rotate the bone towards.
* @param {number} [offset=0] - An offset to add to the rotation angle.
* @param {number} [minAngle=0] - The minimum range of the rotation angle.
* @param {number} [maxAngle=360] - The maximum range of the rotation angle.
*
* @return {this} This Game Object.
*/
angleBoneToXY: function (bone, worldX, worldY, offset, minAngle, maxAngle)
{
if (offset === undefined) { offset = 0; }
if (minAngle === undefined) { minAngle = 0; }
if (maxAngle === undefined) { maxAngle = 360; }
var renderer = this.plugin.renderer;
var height = renderer.height;
var angle = CounterClockwise(AngleBetween(bone.worldX, height - bone.worldY, worldX, worldY) + DegToRad(offset));
bone.rotation = Clamp(RadToDeg(angle), minAngle, maxAngle);
return this;
},
/**
* Finds a bone by comparing each bone's name. It is more efficient to cache the results
* of this method than to call it multiple times.
*
* @method SpineGameObject#findBone
* @since 3.19.0
*
* @param {string} boneName - The name of the bone to find.
*
* @return {spine.Bone} The bone, or null.
*/
findBone: function (boneName)
{
return this.skeleton.findBone(boneName);
},
/**
* Finds the index of a bone by comparing each bone's name. It is more efficient to cache the results
* of this method than to call it multiple times.
*
* @method SpineGameObject#findBoneIndex
* @since 3.19.0
*
* @param {string} boneName - The name of the bone to find.
*
* @return {integer} The bone index. Or -1 if the bone was not found.
*/
findBoneIndex: function (boneName)
{
return this.skeleton.findBoneIndex(boneName);
},
/**
* Finds a slot by comparing each slot's name. It is more efficient to cache the results
* of this method than to call it multiple times.
*
* @method SpineGameObject#findSlot
* @since 3.19.0
*
* @param {string} slotName - The name of the slot to find.
*
* @return {spine.Slot} The Slot. May be null.
*/
findSlot: function (slotName)
{
return this.skeleton.findSlot(slotName);
},
/**
* Finds the index of a slot by comparing each slot's name. It is more efficient to cache the results
* of this method than to call it multiple times.
*
* @method SpineGameObject#findSlotIndex
* @since 3.19.0
*
* @param {string} slotName - The name of the slot to find.
*
* @return {integer} The slot index. Or -1 if the Slot was not found.
*/
findSlotIndex: function (slotName)
{
return this.skeleton.findSlotIndex(slotName);
},
/**
* Finds a skin by comparing each skin's name. It is more efficient to cache the results of
* this method than to call it multiple times.
*
* @method SpineGameObject#findSkin
* @since 3.19.0
*
* @param {string} skinName - The name of the skin to find.
*
* @return {spine.Skin} The Skin. May be null.
*/
findSkin: function (skinName)
{
return this.skeletonData.findSkin(skinName);
},
/**
* Finds an event by comparing each events's name. It is more efficient to cache the results
* of this method than to call it multiple times.
*
* @method SpineGameObject#findEvent
* @since 3.19.0
*
* @param {string} eventDataName - The name of the event to find.
*
* @return {spine.EventData} The Event Data. May be null.
*/
findEvent: function (eventDataName)
{
return this.skeletonData.findEvent(eventDataName);
},
/**
* Finds an animation by comparing each animation's name. It is more efficient to cache the results
* of this method than to call it multiple times.
*
* @method SpineGameObject#findAnimation
* @since 3.19.0
*
* @param {string} animationName - The name of the animation to find.
*
* @return {spine.Animation} The Animation. May be null.
*/
findAnimation: function (animationName)
{
return this.skeletonData.findAnimation(animationName);
},
/**
* Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results
* of this method than to call it multiple times.
*
* @method SpineGameObject#findIkConstraint
* @since 3.19.0
*
* @param {string} constraintName - The name of the constraint to find.
*
* @return {spine.IkConstraintData} The IK constraint. May be null.
*/
findIkConstraint: function (constraintName)
{
return this.skeletonData.findIkConstraint(constraintName);
},
/**
* Finds an transform constraint by comparing each transform constraint's name.
* It is more efficient to cache the results of this method than to call it multiple times.
*
* @method SpineGameObject#findTransformConstraint
* @since 3.19.0
*
* @param {string} constraintName - The name of the constraint to find.
*
* @return {spine.TransformConstraintData} The transform constraint. May be null.
*/
findTransformConstraint: function (constraintName)
{
return this.skeletonData.findTransformConstraint(constraintName);
},
/**
* Finds a path constraint by comparing each path constraint's name.
* It is more efficient to cache the results of this method than to call it multiple times.
*
* @method SpineGameObject#findPathConstraint
* @since 3.19.0
*
* @param {string} constraintName - The name of the constraint to find.
*
* @return {spine.PathConstraintData} The path constraint. May be null.
*/
findPathConstraint: function (constraintName)
{
return this.skeletonData.findPathConstraint(constraintName);
},
/**
* Finds the index of a path constraint by comparing each path constraint's name.
* It is more efficient to cache the results of this method than to call it multiple times.
*
* @method SpineGameObject#findPathConstraintIndex
* @since 3.19.0
*
* @param {string} constraintName - The name of the constraint to find.
*
* @return {integer} The constraint index. Or -1 if the constraint was not found.
*/
findPathConstraintIndex: function (constraintName)
{
return this.skeletonData.findPathConstraintIndex(constraintName);
},
/**
* Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose.
*
* The returned object contains two properties: `offset` and `size`:
*
* `offset` - The distance from the skeleton origin to the bottom left corner of the AABB.
* `size` - The width and height of the AABB.
*
* @method SpineGameObject#getBounds
* @since 3.19.0
*
* @return {any} The bounds object.
*/
getBounds: function ()
{
return this.plugin.getBounds(thi