[haxe][flixel] SkeletonSprite extends FlxTypedGroup<FlxObject> rather than FlxObject.

Replace direct bounds calculation with BoundsProvider interface for better
performance and correctness. This makes it easier to get the correct bounds.

BREAKING CHANGES:
- SkeletonSprite extends FlxTypedGroup<FlxObject> rather than FlxObject. This was necessary because the FlxObject bounding/hitbox is always connected to its position and size and cannot be offset.
- Removed getAnimationBounds() method. Replace it with the appropriate BoundsProvider implementation based on your use case, or create your own.
- Removed setBoundingBox(). Use BoundsProvider features.
- hitTest() now uses the assigned BoundsProvider instead of direct calculation. For accurate hit testing, use CurrentPoseBoundsProvider and call calculateBounds() each frame or on click.

New features:
- Uses BoundsProvider as starling.
- SkeletonSprite constructor now accepts a third optional parameter for BoundsProvider. SetupPoseBoundsProvider is used by default.
- Added calculateBounds() to recalculate bounds on demand.
This commit is contained in:
Davide Tantillo 2025-10-08 12:42:51 +02:00
parent 196df9c386
commit 5990697d6e
18 changed files with 408 additions and 157 deletions

View File

@ -0,0 +1,5 @@
quad.png
size:100,100
filter:Linear,Linear
image
bounds:0,0,100,100

View File

@ -0,0 +1,44 @@
{
"bones": [
{ "name": "root" },
{
"name": "pivot",
"parent": "root",
"scaleX": 1,
"scaleY": 1,
"rotation": 0,
"x": 20,
"y": -20
},
{
"name": "pivot2",
"parent": "pivot",
"scaleX": 1,
"scaleY": 1,
"rotation": 0,
"x": 20,
"y": -20
},
{
"name": "replaceMe",
"parent": "pivot2",
"scaleX": 1,
"scaleY": 1,
"rotation": 0,
"x": 20,
"y": -20
}
],
"slots": [
{ "name": "replaceMe", "bone": "replaceMe", "attachment": "image" }
],
"skins": [
{
"name": "default",
"attachments": {
"replaceMe": { "image": { "width": 100, "height": 100 } }
}
}
],
"animations": { "animation": {} }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -57,8 +57,7 @@ class BasicExample extends FlxState {
animationStateData.defaultMix = 0.25; animationStateData.defaultMix = 0.25;
skeletonSprite = new SkeletonSprite(skeletondata, animationStateData); skeletonSprite = new SkeletonSprite(skeletondata, animationStateData);
var animation = skeletonSprite.state.setAnimationByName(0, "walk", true).animation; // skeletonSprite.state.setAnimationByName(0, "walk", true);
skeletonSprite.setBoundingBox(animation);
skeletonSprite.screenCenter(); skeletonSprite.screenCenter();
add(skeletonSprite); add(skeletonSprite);

View File

@ -29,6 +29,7 @@
package flixelExamples; package flixelExamples;
import spine.boundsprovider.SkinsAndAnimationBoundsProvider;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import flixel.text.FlxText; import flixel.text.FlxText;
import spine.Skin; import spine.Skin;
@ -42,7 +43,7 @@ import spine.SkeletonData;
import spine.animation.AnimationStateData; import spine.animation.AnimationStateData;
import spine.atlas.TextureAtlas; import spine.atlas.TextureAtlas;
class AnimationBoundExample extends FlxState { class BoundsProviderExample extends FlxState {
var loadBinary = true; var loadBinary = true;
override public function create():Void { override public function create():Void {
@ -60,38 +61,34 @@ class AnimationBoundExample extends FlxState {
var animationStateData = new AnimationStateData(data); var animationStateData = new AnimationStateData(data);
animationStateData.defaultMix = 0.25; animationStateData.defaultMix = 0.25;
var skeletonSpriteClipping = new SkeletonSprite(data, animationStateData); var skeletonSpriteClipping = new SkeletonSprite(data, animationStateData, new SkinsAndAnimationBoundsProvider("portal", null, null, true));
var animationClipping = skeletonSpriteClipping.state.setAnimationByName(0, "portal", true).animation; skeletonSpriteClipping.state.setAnimationByName(0, "portal", true);
skeletonSpriteClipping.update(0);
skeletonSpriteClipping.setBoundingBox(animationClipping, true);
skeletonSpriteClipping.screenCenter(); skeletonSpriteClipping.screenCenter();
skeletonSpriteClipping.x = FlxG.width / 4 - skeletonSpriteClipping.width / 2; skeletonSpriteClipping.x = FlxG.width / 4;
add(skeletonSpriteClipping); add(skeletonSpriteClipping);
var textClipping = new FlxText(); var textClipping = new FlxText();
textClipping.text = "Animation bound with clipping"; textClipping.text = "Bounds with clipping";
textClipping.size = 12; textClipping.size = 12;
textClipping.x = skeletonSpriteClipping.x + skeletonSpriteClipping.width / 2 - textClipping.width / 2; textClipping.x = skeletonSpriteClipping.boundsX + skeletonSpriteClipping.width / 2 - textClipping.width / 2;
textClipping.y = skeletonSpriteClipping.y + skeletonSpriteClipping.height + 20; textClipping.y = skeletonSpriteClipping.boundsY + skeletonSpriteClipping.height + 20;
textClipping.setBorderStyle(FlxTextBorderStyle.OUTLINE, FlxColor.RED, 2); textClipping.setBorderStyle(FlxTextBorderStyle.OUTLINE, FlxColor.RED, 2);
add(textClipping); add(textClipping);
var skeletonSpriteNoClipping = new SkeletonSprite(data, animationStateData); var skeletonSpriteNoClipping = new SkeletonSprite(data, animationStateData, new SkinsAndAnimationBoundsProvider("portal"));
var animationClipping = skeletonSpriteNoClipping.state.setAnimationByName(0, "portal", true).animation; skeletonSpriteNoClipping.state.setAnimationByName(0, "portal", true);
skeletonSpriteNoClipping.update(0);
skeletonSpriteNoClipping.setBoundingBox(animationClipping, false);
skeletonSpriteNoClipping.screenCenter(); skeletonSpriteNoClipping.screenCenter();
skeletonSpriteNoClipping.x = FlxG.width / 4 * 3 - skeletonSpriteClipping.width / 2 - 50; skeletonSpriteNoClipping.x = FlxG.width / 4 * 3;
add(skeletonSpriteNoClipping); add(skeletonSpriteNoClipping);
var textNoClipping = new FlxText(); var textNoClipping = new FlxText();
textNoClipping.text = "Animation bound without clipping"; textNoClipping.text = "Bounds without clipping";
textNoClipping.size = 12; textNoClipping.size = 12;
textNoClipping.x = skeletonSpriteNoClipping.x + skeletonSpriteNoClipping.width / 2 - textNoClipping.width / 2; textNoClipping.x = skeletonSpriteNoClipping.boundsX + skeletonSpriteNoClipping.width / 2 - textNoClipping.width / 2;
textNoClipping.y = skeletonSpriteNoClipping.y + skeletonSpriteNoClipping.height + 20; textNoClipping.y = skeletonSpriteNoClipping.boundsY + skeletonSpriteNoClipping.height + 20;
textNoClipping.setBorderStyle(FlxTextBorderStyle.OUTLINE, FlxColor.RED, 2); textNoClipping.setBorderStyle(FlxTextBorderStyle.OUTLINE, FlxColor.RED, 2);
add(textNoClipping); add(textNoClipping);
var textInstruction = new FlxText(); var textInstruction = new FlxText();
textInstruction.text = "Red rectangle is the animation bound"; textInstruction.text = "Red rectangle is the Spine provider bounds";
textInstruction.size = 12; textInstruction.size = 12;
textInstruction.screenCenter(); textInstruction.screenCenter();
textInstruction.y = textNoClipping.y + 40; textInstruction.y = textNoClipping.y + 40;

View File

@ -46,7 +46,7 @@ class CloudPotExample extends FlxState {
override public function create():Void { override public function create():Void {
FlxG.cameras.bgColor = 0xffa1b2b0; FlxG.cameras.bgColor = 0xffa1b2b0;
var button = new FlxButton(0, 0, "Next scene", () -> FlxG.switchState(() -> new AnimationBoundExample())); var button = new FlxButton(0, 0, "Next scene", () -> FlxG.switchState(() -> new BoundsProviderExample()));
button.setPosition(FlxG.width * .75, FlxG.height / 10); button.setPosition(FlxG.width * .75, FlxG.height / 10);
add(button); add(button);

View File

@ -65,8 +65,7 @@ class ControlBonesExample extends FlxState {
var skeletonSprite = new SkeletonSprite(data, animationStateData); var skeletonSprite = new SkeletonSprite(data, animationStateData);
skeletonSprite.scaleX = .5; skeletonSprite.scaleX = .5;
skeletonSprite.scaleY = .5; skeletonSprite.scaleY = .5;
var animation = skeletonSprite.state.setAnimationByName(0, "idle", true).animation; skeletonSprite.state.setAnimationByName(0, "idle", true);
skeletonSprite.setBoundingBox(animation);
skeletonSprite.screenCenter(); skeletonSprite.screenCenter();
add(skeletonSprite); add(skeletonSprite);
@ -77,6 +76,8 @@ class ControlBonesExample extends FlxState {
"front-leg-ik-target", "front-leg-ik-target",
]; ];
// we need to update, to ensure scale is applied before getting bone values
skeletonSprite.update(0);
var radius = 6; var radius = 6;
for (boneName in controlBoneNames) { for (boneName in controlBoneNames) {
var bone = skeletonSprite.skeleton.findBone(boneName); var bone = skeletonSprite.skeleton.findBone(boneName);
@ -84,7 +85,7 @@ class ControlBonesExample extends FlxState {
skeletonSprite.skeletonToHaxeWorldCoordinates(point); skeletonSprite.skeletonToHaxeWorldCoordinates(point);
var control = new FlxSprite(); var control = new FlxSprite();
control.makeGraphic(radius * 2, radius * 2, FlxColor.TRANSPARENT, true); control.makeGraphic(radius * 2, radius * 2, FlxColor.TRANSPARENT, true);
FlxSpriteUtil.drawCircle(control, radius, radius, radius, 0xffff00ff); FlxSpriteUtil.drawCircle(control, -1, -1, -1, 0xffff00ff);
control.setPosition(point[0] - radius, point[1] - radius); control.setPosition(point[0] - radius, point[1] - radius);
controlBones.push(bone); controlBones.push(bone);
controls.push(control); controls.push(control);

View File

@ -68,9 +68,6 @@ class EventsExample extends FlxState {
skeletonSprite.state.setAnimationByName(0, "walk", true); skeletonSprite.state.setAnimationByName(0, "walk", true);
var trackEntry = skeletonSprite.state.addAnimationByName(0, "run", true, 3); var trackEntry = skeletonSprite.state.addAnimationByName(0, "run", true, 3);
skeletonSprite.setBoundingBox(trackEntry.animation);
skeletonSprite.setBoundingBox();
skeletonSprite.screenCenter(); skeletonSprite.screenCenter();
skeletonSprite.skeleton.setupPoseBones(); skeletonSprite.skeleton.setupPoseBones();
add(skeletonSprite); add(skeletonSprite);

View File

@ -30,6 +30,11 @@
package flixelExamples; package flixelExamples;
import flixel.ui.FlxButton; import flixel.ui.FlxButton;
import flixel.math.FlxPoint;
import flixel.util.FlxColor;
import flixel.util.FlxSpriteUtil;
import spine.boundsprovider.SetupPoseBoundsProvider;
import spine.boundsprovider.SkinsAndAnimationBoundsProvider;
import flixel.group.FlxSpriteGroup; import flixel.group.FlxSpriteGroup;
import flixel.FlxSprite; import flixel.FlxSprite;
import flixel.graphics.FlxGraphic; import flixel.graphics.FlxGraphic;
@ -56,6 +61,11 @@ class FlixelState extends FlxState {
var scale = 4; var scale = 4;
var speed:Float; var speed:Float;
var skeletonOrigin:FlxSprite;
var gameObjectOrigin:FlxSprite;
var radius = 3;
var rootPoint = [.0, .0];
override public function create():Void { override public function create():Void {
FlxG.cameras.bgColor = 0xffa1b2b0; FlxG.cameras.bgColor = 0xffa1b2b0;
@ -97,12 +107,37 @@ class FlixelState extends FlxState {
// loading spineboy // loading spineboy
var atlas = new TextureAtlas(Assets.getText("assets/spineboy.atlas"), new FlixelTextureLoader("assets/spineboy.atlas")); var atlas = new TextureAtlas(Assets.getText("assets/spineboy.atlas"), new FlixelTextureLoader("assets/spineboy.atlas"));
var skeletondata = SkeletonData.from(Assets.getText("assets/spineboy-pro.json"), atlas, 1 / scale); var skeletondata = SkeletonData.from(Assets.getText("assets/spineboy-pro.json"), atlas, 1);
// var atlas = new TextureAtlas(Assets.getText("assets/quad.atlas"), new FlixelTextureLoader("assets/quad.atlas"));
// var skeletondata = SkeletonData.from(Assets.getText("assets/quad.json"), atlas, 2);
var animationStateData = new AnimationStateData(skeletondata); var animationStateData = new AnimationStateData(skeletondata);
spineSprite = new SkeletonSprite(skeletondata, animationStateData); // spineSprite = new SkeletonSprite(skeletondata, animationStateData, new SetupPoseBoundsProvider());
spineSprite = new SkeletonSprite(skeletondata, animationStateData, new SkinsAndAnimationBoundsProvider("walk"));
spineSprite.scale = new FlxPoint(1 / scale, 1 / scale);
// positioning spineboy // positioning spineboy
spineSprite.setPosition(.5 * FlxG.width, .5 * FlxG.height); spineSprite.screenCenter();
spineSprite.y += spineSprite.height / 2;
var control3 = new FlxSprite();
control3.makeGraphic(radius * 2, radius * 2, FlxColor.TRANSPARENT, true);
FlxSpriteUtil.drawCircle(control3, radius, radius, radius, 0xff5500ff);
add(control3);
control3.setPosition(FlxG.width / 2, FlxG.height / 2);
skeletonOrigin = new FlxSprite();
skeletonOrigin.makeGraphic(radius * 2, radius * 2, FlxColor.TRANSPARENT, true);
FlxSpriteUtil.drawCircle(skeletonOrigin, radius, radius, radius, 0xff5500ff);
add(skeletonOrigin);
gameObjectOrigin = new FlxSprite();
gameObjectOrigin.makeGraphic(radius * 2, radius * 2, FlxColor.TRANSPARENT, true);
FlxSpriteUtil.drawCircle(gameObjectOrigin, radius, radius, radius, 0xffff9100);
add(gameObjectOrigin);
// setting mix times // setting mix times
animationStateData.defaultMix = 0.5; animationStateData.defaultMix = 0.5;
@ -119,7 +154,7 @@ class FlixelState extends FlxState {
// setting idle animation // setting idle animation
spineSprite.state.setAnimationByName(0, "idle", true); spineSprite.state.setAnimationByName(0, "idle", true);
// setting y offset function to move object body while jumping // // setting y offset function to move object body while jumping
var hip = spineSprite.skeleton.findBone("hip"); var hip = spineSprite.skeleton.findBone("hip");
var initialY = 0.; var initialY = 0.;
var initialOffsetY = 0.; var initialOffsetY = 0.;
@ -137,13 +172,19 @@ class FlixelState extends FlxState {
} }
}); });
var diff = .0; var diff = .0;
var tmpPoint = [.0, .0];
spineSprite.afterUpdateWorldTransforms = spineSprite -> { spineSprite.afterUpdateWorldTransforms = spineSprite -> {
if (jumping) { if (jumping) {
diff -= hip.pose.y; tmpPoint[1] = hip.applied.worldY;
spineSprite.offsetY -= diff; spineSprite.skeletonToHaxeWorldCoordinates(tmpPoint);
spineSprite.y += diff; diff -= (tmpPoint[1]);
spineSprite.offsetY += diff;
spineSprite.y -= diff;
} }
diff = hip.pose.y;
tmpPoint[1] = hip.applied.worldY;
spineSprite.skeletonToHaxeWorldCoordinates(tmpPoint);
diff = tmpPoint[1];
} }
// adding spineboy to the stage // adding spineboy to the stage
@ -152,7 +193,7 @@ class FlixelState extends FlxState {
// FlxG.debugger.visible = !FlxG.debugger.visible; // FlxG.debugger.visible = !FlxG.debugger.visible;
// debug ui // debug ui
// FlxG.debugger.visible = true; // FlxG.debugger.visible = true;
// FlxG.debugger.drawDebug = true; FlxG.debugger.drawDebug = true;
// FlxG.log.redirectTraces = true; // FlxG.log.redirectTraces = true;
// FlxG.debugger.track(spineSprite); // FlxG.debugger.track(spineSprite);
@ -160,12 +201,14 @@ class FlixelState extends FlxState {
// FlxG.watch.add(spineSprite, "offsetY"); // FlxG.watch.add(spineSprite, "offsetY");
// FlxG.watch.add(spineSprite, "y"); // FlxG.watch.add(spineSprite, "y");
// FlxG.watch.add(this, "jumping"); // FlxG.watch.add(this, "jumping");
super.create(); super.create();
} }
var justSetIdle = true; var justSetIdle = true;
override public function update(elapsed:Float):Void { override public function update(elapsed:Float):Void {
// spineSprite.calculateBounds();
if (FlxG.overlap(spineSprite, group)) { if (FlxG.overlap(spineSprite, group)) {
myText.text = "Overlapping"; myText.text = "Overlapping";
} else { } else {
@ -184,6 +227,17 @@ class FlixelState extends FlxState {
FlxG.debugger.visible = !FlxG.debugger.visible; FlxG.debugger.visible = !FlxG.debugger.visible;
} }
if (FlxG.keys.anyPressed([UP, DOWN])) {
if (FlxG.keys.anyPressed([UP])) {
if (spineSprite.flipY == true)
spineSprite.flipY = false;
}
if (FlxG.keys.anyPressed([DOWN])) {
if (spineSprite.flipY == false)
spineSprite.flipY = true;
}
}
if (FlxG.keys.anyPressed([RIGHT, LEFT])) { if (FlxG.keys.anyPressed([RIGHT, LEFT])) {
justSetIdle = false; justSetIdle = false;
var flipped = false; var flipped = false;
@ -217,6 +271,13 @@ class FlixelState extends FlxState {
spineSprite.state.setAnimationByName(0, "idle", true); spineSprite.state.setAnimationByName(0, "idle", true);
} }
var rootBone = spineSprite.skeleton.findBone("root");
rootPoint[0] = rootBone.applied.worldX;
rootPoint[1] = rootBone.applied.worldY;
spineSprite.skeletonToHaxeWorldCoordinates(rootPoint);
skeletonOrigin.setPosition(rootPoint[0] - radius, rootPoint[1] - radius);
gameObjectOrigin.setPosition(spineSprite.x - radius, spineSprite.y - radius);
super.update(elapsed); super.update(elapsed);
} }
} }

View File

@ -29,6 +29,7 @@
package flixelExamples; package flixelExamples;
import spine.boundsprovider.SkinsAndAnimationBoundsProvider;
import spine.Skin; import spine.Skin;
import flixel.ui.FlxButton; import flixel.ui.FlxButton;
import flixel.FlxG; import flixel.FlxG;
@ -59,7 +60,7 @@ class MixAndMatchExample extends FlxState {
var animationStateData = new AnimationStateData(data); var animationStateData = new AnimationStateData(data);
animationStateData.defaultMix = 0.25; animationStateData.defaultMix = 0.25;
skeletonSprite = new SkeletonSprite(data, animationStateData); skeletonSprite = new SkeletonSprite(data, animationStateData, new SkinsAndAnimationBoundsProvider("dance", ["full-skins/boy"]));
var customSkin = new Skin("custom"); var customSkin = new Skin("custom");
var skinBase = data.findSkin("skin-base"); var skinBase = data.findSkin("skin-base");
customSkin.addSkin(skinBase); customSkin.addSkin(skinBase);
@ -74,8 +75,7 @@ class MixAndMatchExample extends FlxState {
skeletonSprite.skeleton.skin = customSkin; skeletonSprite.skeleton.skin = customSkin;
skeletonSprite.state.update(0); skeletonSprite.state.update(0);
var animation = skeletonSprite.state.setAnimationByName(0, "dance", true).animation; skeletonSprite.state.setAnimationByName(0, "dance", true);
skeletonSprite.setBoundingBox(animation);
skeletonSprite.screenCenter(); skeletonSprite.screenCenter();
add(skeletonSprite); add(skeletonSprite);

View File

@ -59,7 +59,6 @@ class SequenceExample extends FlxState {
skeletonSprite = new SkeletonSprite(skeletondata, animationStateData); skeletonSprite = new SkeletonSprite(skeletondata, animationStateData);
var animation = skeletonSprite.state.setAnimationByName(0, "flying", true).animation; var animation = skeletonSprite.state.setAnimationByName(0, "flying", true).animation;
skeletonSprite.setBoundingBox(animation);
skeletonSprite.screenCenter(); skeletonSprite.screenCenter();
add(skeletonSprite); add(skeletonSprite);
super.create(); super.create();

View File

@ -57,7 +57,6 @@ class TankExample extends FlxState {
var skeletonSprite = new SkeletonSprite(data, animationStateData); var skeletonSprite = new SkeletonSprite(data, animationStateData);
var animation = skeletonSprite.state.setAnimationByName(0, "drive", true).animation; var animation = skeletonSprite.state.setAnimationByName(0, "drive", true).animation;
skeletonSprite.setBoundingBox(animation);
skeletonSprite.screenCenter(); skeletonSprite.screenCenter();
add(skeletonSprite); add(skeletonSprite);

View File

@ -57,7 +57,6 @@ class VineExample extends FlxState {
var skeletonSprite = new SkeletonSprite(data, animationStateData); var skeletonSprite = new SkeletonSprite(data, animationStateData);
var animation = skeletonSprite.state.setAnimationByName(0, "grow", true).animation; var animation = skeletonSprite.state.setAnimationByName(0, "grow", true).animation;
skeletonSprite.setBoundingBox(animation);
skeletonSprite.screenCenter(); skeletonSprite.screenCenter();
add(skeletonSprite); add(skeletonSprite);

View File

@ -2,9 +2,9 @@
<project> <project>
<meta title="spine-haxe-example" package="spine" version="4.2.0" company="Esoteric Software" /> <meta title="spine-haxe-example" package="spine" version="4.2.0" company="Esoteric Software" />
<app main="Main" path="export" file="SpineHaxeExample" /> <!-- <app main="Main" path="export" file="SpineHaxeExample" /> -->
<!-- <app main="MainStarling" path="export" file="SpineHaxeExample" /> --> <!-- <app main="MainStarling" path="export" file="SpineHaxeExample" /> -->
<!-- <app main="MainFlixel" path="export" file="SpineHaxeExample" /> --> <app main="MainFlixel" path="export" file="SpineHaxeExample" />
<window allow-high-dpi="true" /> <window allow-high-dpi="true" />
<haxelib name="openfl" /> <haxelib name="openfl" />

View File

@ -45,8 +45,8 @@ class SetupPoseBoundsProvider extends BoundsProvider {
} }
public function calculateBounds(gameObject:BoundsGameObject, out:BoundsRectangle):BoundsRectangle { public function calculateBounds(gameObject:BoundsGameObject, out:BoundsRectangle):BoundsRectangle {
var skeleton = gameObject.skeleton; var prevSkeleton = gameObject.skeleton;
if (skeleton == null) { if (prevSkeleton == null) {
zeroRectangle(out); zeroRectangle(out);
return out; return out;
} }
@ -54,7 +54,9 @@ class SetupPoseBoundsProvider extends BoundsProvider {
// Make a copy of skeleton as this might be called while // Make a copy of skeleton as this might be called while
// the skeleton in the GameObject has already been heavily modified. We can not // the skeleton in the GameObject has already been heavily modified. We can not
// reconstruct that state. // reconstruct that state.
var skeleton = new Skeleton(skeleton.data); var skeleton = new Skeleton(prevSkeleton.data);
skeleton.scaleX = prevSkeleton.scaleX;
skeleton.scaleY = prevSkeleton.scaleY * Bone.yDir;
skeleton.setupPose(); skeleton.setupPose();
skeleton.updateWorldTransform(Physics.update); skeleton.updateWorldTransform(Physics.update);
var newBounds = skeleton.getBounds(clipping ? new SkeletonClipping() : null); var newBounds = skeleton.getBounds(clipping ? new SkeletonClipping() : null);

View File

@ -56,9 +56,9 @@ class SkinsAndAnimationBoundsProvider extends BoundsProvider {
} }
public function calculateBounds(gameObject:BoundsGameObject, out:BoundsRectangle):BoundsRectangle { public function calculateBounds(gameObject:BoundsGameObject, out:BoundsRectangle):BoundsRectangle {
var skeleton = gameObject.skeleton; var prevSkeleton = gameObject.skeleton;
var state = gameObject.state; var state = gameObject.state;
if (skeleton == null || state == null) { if (prevSkeleton == null || state == null) {
zeroRectangle(out); zeroRectangle(out);
return out; return out;
} }
@ -67,7 +67,9 @@ class SkinsAndAnimationBoundsProvider extends BoundsProvider {
// the skeleton in the GameObject has already been heavily modified. We can not // the skeleton in the GameObject has already been heavily modified. We can not
// reconstruct that state. // reconstruct that state.
var animationState = new AnimationState(state.data); var animationState = new AnimationState(state.data);
var skeleton = new Skeleton(skeleton.data); var skeleton = new Skeleton(prevSkeleton.data);
skeleton.scaleX = prevSkeleton.scaleX;
skeleton.scaleY = prevSkeleton.scaleY * Bone.yDir;
var clipper = clipping ? new SkeletonClipping() : null; var clipper = clipping ? new SkeletonClipping() : null;
var data = skeleton.data; var data = skeleton.data;
if (skins.length > 0) { if (skins.length > 0) {

View File

@ -29,42 +29,35 @@
package spine.flixel; package spine.flixel;
import flixel.util.FlxDirectionFlags;
import flixel.math.FlxRect;
import flixel.FlxCamera;
import flixel.FlxBasic;
import flixel.util.FlxAxes;
import flixel.group.FlxGroup.FlxTypedGroup;
import spine.boundsprovider.SetupPoseBoundsProvider;
import spine.boundsprovider.BoundsProvider;
import openfl.geom.Point; import openfl.geom.Point;
import flixel.math.FlxPoint; import flixel.math.FlxPoint;
import flixel.math.FlxMatrix; import flixel.math.FlxMatrix;
import spine.animation.MixDirection;
import spine.animation.MixBlend;
import spine.animation.Animation;
import spine.TextureRegion; import spine.TextureRegion;
import haxe.extern.EitherType;
import spine.attachments.Attachment;
import flixel.util.typeLimit.OneOfTwo;
import flixel.FlxCamera;
import flixel.math.FlxRect;
import flixel.FlxG; import flixel.FlxG;
import flixel.FlxObject; import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.FlxStrip;
import flixel.group.FlxSpriteGroup;
import flixel.graphics.FlxGraphic;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import openfl.Vector; import openfl.Vector;
import openfl.display.BlendMode;
import spine.Bone; import spine.Bone;
import spine.Rectangle;
import spine.Skeleton; import spine.Skeleton;
import spine.SkeletonData; import spine.SkeletonData;
import spine.Slot; import spine.Slot;
import spine.animation.AnimationState; import spine.animation.AnimationState;
import spine.animation.AnimationStateData; import spine.animation.AnimationStateData;
import spine.atlas.TextureAtlasRegion;
import spine.attachments.MeshAttachment; import spine.attachments.MeshAttachment;
import spine.attachments.RegionAttachment; import spine.attachments.RegionAttachment;
import spine.attachments.ClippingAttachment; import spine.attachments.ClippingAttachment;
import spine.flixel.SkeletonMesh; import spine.flixel.SkeletonMesh;
/** A FlxObject that draws a skeleton. The animation state and skeleton must be updated each frame. */ /** A FlxObject that draws a skeleton. The animation state and skeleton must be updated each frame. */
class SkeletonSprite extends FlxObject { class SkeletonSprite extends FlxTypedGroup<FlxObject> {
public var skeleton(default, null):Skeleton; public var skeleton(default, null):Skeleton;
public var state(default, null):AnimationState; public var state(default, null):AnimationState;
public var stateData(default, null):AnimationStateData; public var stateData(default, null):AnimationStateData;
@ -81,6 +74,18 @@ class SkeletonSprite extends FlxObject {
public var flipY(default, set):Bool = false; public var flipY(default, set):Bool = false;
public var antialiasing:Bool = true; public var antialiasing:Bool = true;
public var boundsProvider:BoundsProvider;
public var angle(default, set) = 0.;
public var x(default, set) = 0.;
public var y(default, set) = 0.;
public var width(get, set):Float;
public var height(get, set):Float;
public var boundsX(get, never):Float;
public var boundsY(get, never):Float;
@:isVar
public var scale(never, set):FlxPoint;
@:isVar @:isVar
public var scaleX(get, set):Float = 1; public var scaleX(get, set):Float = 1;
@:isVar @:isVar
@ -92,103 +97,50 @@ class SkeletonSprite extends FlxObject {
private var _tempMatrix = new FlxMatrix(); private var _tempMatrix = new FlxMatrix();
private var _tempPoint = new Point(); private var _tempPoint = new Point();
private var _tempPointFlip = [.0, .0];
private var __bounds = new openfl.geom.Rectangle();
private var __objectBounds = new FlxObject();
private static var QUAD_INDICES:Array<Int> = [0, 1, 2, 2, 3, 0]; private static var QUAD_INDICES:Array<Int> = [0, 1, 2, 2, 3, 0];
/** Creates an uninitialized SkeletonSprite. The renderer, skeleton, and animation state must be set before use. */ /** Creates an uninitialized SkeletonSprite. The renderer, skeleton, and animation state must be set before use. */
public function new(skeletonData:SkeletonData, animationStateData:AnimationStateData = null) { public function new(skeletonData:SkeletonData, animationStateData:AnimationStateData = null, ?boundsProvider:BoundsProvider) {
super(0, 0); super(1);
Bone.yDown = true; Bone.yDown = true;
skeleton = new Skeleton(skeletonData); skeleton = new Skeleton(skeletonData);
skeleton.updateWorldTransform(Physics.update); skeleton.updateWorldTransform(Physics.update);
state = new AnimationState(animationStateData != null ? animationStateData : new AnimationStateData(skeletonData)); state = new AnimationState(animationStateData != null ? animationStateData : new AnimationStateData(skeletonData));
setBoundingBox(); // setBoundingBox();
this.boundsProvider = boundsProvider ?? new SetupPoseBoundsProvider();
this.calculateBounds();
add(__objectBounds);
} }
public function setBoundingBox(?animation:Animation, ?clip:Bool = true) { // TODO: this changes the scale
var bounds = animation == null ? skeleton.getBounds() : getAnimationBounds(animation, clip); // public function setSize(width:Float, height:Float):Void {
if (bounds.width > 0 && bounds.height > 0) { // this.width = width;
width = bounds.width; // this.height = height;
height = bounds.height; // }
offsetX = -bounds.x; // ============================================================
offsetY = -bounds.y; // DEBUG METHODS (if FLX_DEBUG)
} // ============================================================
}
public function getAnimationBounds(animation:Animation, clip:Bool = true):Rectangle {
var clipper = clip ? SkeletonSprite.clipper : null;
skeleton.setupPose();
var steps = 100, time = 0.;
var stepTime = animation.duration != 0 ? animation.duration / steps : 0;
var minX = 100000000.,
maxX = -100000000.,
minY = 100000000.,
maxY = -100000000.;
for (i in 0...steps) {
animation.apply(skeleton, time, time, false, [], 1, MixBlend.setup, MixDirection.mixIn, false);
skeleton.updateWorldTransform(Physics.update);
var boundsSkel = skeleton.getBounds(clipper);
if (!Math.isNaN(boundsSkel.x) && !Math.isNaN(boundsSkel.y) && !Math.isNaN(boundsSkel.width) && !Math.isNaN(boundsSkel.height)) {
minX = Math.min(boundsSkel.x, minX);
minY = Math.min(boundsSkel.y, minY);
maxX = Math.max(boundsSkel.x + boundsSkel.width, maxX);
maxY = Math.max(boundsSkel.y + boundsSkel.height, maxY);
} else
throw new SpineException("Animation bounds are invalid: " + animation.name);
time += stepTime;
}
var bounds = new Rectangle();
bounds.x = minX;
bounds.y = minY;
bounds.width = maxX - minX;
bounds.height = maxY - minY;
return bounds;
}
override public function destroy():Void {
state.clearListeners();
state = null;
skeleton = null;
_tempVertices = null;
_quadTriangles = null;
_tempMatrix = null;
_tempPoint = null;
if (_meshes != null) {
for (mesh in _meshes)
mesh.destroy();
_meshes = null;
}
super.destroy();
}
override public function update(elapsed:Float):Void {
super.update(elapsed);
state.update(elapsed);
state.apply(skeleton);
this.beforeUpdateWorldTransforms(this);
skeleton.update(elapsed);
skeleton.updateWorldTransform(Physics.update);
this.afterUpdateWorldTransforms(this);
}
override public function draw():Void {
if (alpha == 0)
return;
renderMeshes();
#if FLX_DEBUG #if FLX_DEBUG
if (FlxG.debugger.drawDebug) public function drawDebug():Void {
drawDebug(); __objectBounds.drawDebug();
}
public function drawDebugOnCamera(camera:FlxCamera):Void {
__objectBounds.drawDebugOnCamera(camera);
}
#end #end
// ============================================================
// SKELETON SPRITE METHODS
// ============================================================
public function calculateBounds() {
this.boundsProvider.calculateBounds(this, __bounds);
__objectBounds.setPosition(x + __bounds.x, y + __bounds.y);
__objectBounds.setSize(__bounds.width, __bounds.height);
} }
function renderMeshes():Void { function renderMeshes():Void {
@ -320,7 +272,7 @@ class SkeletonSprite extends FlxObject {
private function getTransformMatrix():FlxMatrix { private function getTransformMatrix():FlxMatrix {
_tempMatrix.identity(); _tempMatrix.identity();
// scale is connected to the skeleton scale - no need to rescale // scale is connected to the skeleton scale - no need to rescale
_tempMatrix.scale(1, 1); // _tempMatrix.scale(1, 1);
_tempMatrix.rotate(angle * Math.PI / 180); _tempMatrix.rotate(angle * Math.PI / 180);
_tempMatrix.translate(x + offsetX, y + offsetY); _tempMatrix.translate(x + offsetX, y + offsetY);
return _tempMatrix; return _tempMatrix;
@ -375,18 +327,24 @@ class SkeletonSprite extends FlxObject {
} }
function set_flipX(value:Bool):Bool { function set_flipX(value:Bool):Bool {
if (value != flipX) if (value != flipX) {
skeleton.scaleX = -skeleton.scaleX; skeleton.scaleX = -skeleton.scaleX;
this.calculateBounds();
}
return flipX = value; return flipX = value;
} }
function set_flipY(value:Bool):Bool { function set_flipY(value:Bool):Bool {
if (value != flipY) if (value != flipY) {
skeleton.scaleY = -skeleton.scaleY; skeleton.scaleY = -skeleton.scaleY * Bone.yDir;
this.calculateBounds();
}
return flipY = value; return flipY = value;
} }
function set_scale(value:FlxPoint):FlxPoint { function set_scale(value:FlxPoint):FlxPoint {
scaleX = value.x;
scaleY = value.y;
return value; return value;
} }
@ -395,15 +353,199 @@ class SkeletonSprite extends FlxObject {
} }
function set_scaleX(value:Float):Float { function set_scaleX(value:Float):Float {
return skeleton.scaleX = value; skeleton.scaleX = value;
this.calculateBounds();
return value;
} }
function get_scaleY():Float { function get_scaleY():Float {
return skeleton.scaleY; return skeleton.scaleY * Bone.yDir;
} }
function set_scaleY(value:Float):Float { function set_scaleY(value:Float):Float {
return skeleton.scaleY = value; skeleton.scaleY = value;
this.calculateBounds();
return value;
}
function set_angle(value:Float):Float {
__objectBounds.angle = value;
return angle = value;
}
function set_x(value:Float):Float {
__objectBounds.x = __bounds.x + value;
return x = value;
}
function set_y(value:Float):Float {
__objectBounds.y = __bounds.y + value;
return y = value;
}
function get_height():Float {
return __bounds.height;
}
function get_width():Float {
return __bounds.width;
}
function set_width(value:Float):Float {
var scale = value / __bounds.width;
scaleX *= scale;
return __bounds.width;
}
function set_height(value:Float):Float {
var scale = value / __bounds.height;
scaleY *= scale;
return __bounds.height;
}
function get_boundsX():Float {
return __objectBounds.x;
}
function get_boundsY():Float {
return __objectBounds.y;
}
// ============================================================
// OVERRIDE METHODS FROM FlxBasic
// ============================================================
override public function update(elapsed:Float):Void {
super.update(elapsed);
state.update(elapsed);
state.apply(skeleton);
this.beforeUpdateWorldTransforms(this);
skeleton.update(elapsed);
skeleton.updateWorldTransform(Physics.update);
this.afterUpdateWorldTransforms(this);
}
override public function draw():Void {
if (alpha == 0)
return;
renderMeshes();
#if FLX_DEBUG
if (FlxG.debugger.drawDebug)
__objectBounds.drawDebug();
#end
}
override public function destroy():Void {
state.clearListeners();
state = null;
skeleton = null;
_tempVertices = null;
_quadTriangles = null;
_tempMatrix = null;
_tempPoint = null;
if (_meshes != null) {
for (mesh in _meshes)
mesh.destroy();
_meshes = null;
}
super.destroy();
}
// ============================================================
// OVERLAP/COLLISION METHODS
// ============================================================
public function overlaps(objectOrGroup:FlxBasic, inScreenSpace:Bool = false, ?camera:FlxCamera):Bool {
return __objectBounds.overlaps(objectOrGroup, inScreenSpace, camera);
}
public function overlapsAt(x:Float, y:Float, objectOrGroup:FlxBasic, inScreenSpace = false, ?camera:FlxCamera):Bool {
return __objectBounds.overlapsAt(x, y, objectOrGroup, inScreenSpace, camera);
}
public function overlapsPoint(point:FlxPoint, inScreenSpace = false, ?camera:FlxCamera):Bool {
return __objectBounds.overlapsPoint(point, inScreenSpace, camera);
}
// ============================================================
// BOUNDS/POSITION METHODS
// ============================================================
public function inWorldBounds():Bool {
return __objectBounds.inWorldBounds();
}
public function getScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint {
return __objectBounds.getScreenPosition(result, camera);
}
public function getPosition(?result:FlxPoint):FlxPoint {
return __objectBounds.getPosition(result);
}
public function getMidpoint(?point:FlxPoint):FlxPoint {
return __objectBounds.getMidpoint(point);
}
public function getHitbox(?rect:FlxRect):FlxRect {
return __objectBounds.getHitbox(rect);
}
public function getRotatedBounds(?newRect:FlxRect):FlxRect {
return __objectBounds.getRotatedBounds(newRect);
}
// ============================================================
// STATE METHODS
// ============================================================
public function reset(x:Float, y:Float):Void {
__objectBounds.reset(x, y);
}
public function isOnScreen(?camera:FlxCamera):Bool {
return __objectBounds.isOnScreen(camera);
}
public function isPixelPerfectRender(?camera:FlxCamera):Bool {
return __objectBounds.isPixelPerfectRender(camera);
}
public function isTouching(direction:FlxDirectionFlags):Bool {
return __objectBounds.isTouching(direction);
}
public function justTouched(direction:FlxDirectionFlags):Bool {
return __objectBounds.justTouched(direction);
}
// ============================================================
// UTILITY METHODS
// ============================================================
public inline function screenCenter(axes:FlxAxes = XY):SkeletonSprite {
if (axes.x)
x = (FlxG.width - __bounds.width) / 2 - __bounds.x;
if (axes.y)
y = (FlxG.height - __bounds.height) / 2 - __bounds.y;
return this;
}
public function setPosition(x = 0.0, y = 0.0):Void {
this.x = x;
this.y = y;
}
public function setSize(width:Float, height:Float):Void {
this.width = width;
this.height = height;
} }
} }

View File

@ -46,9 +46,13 @@
// Create the spine display object // Create the spine display object
const stretchyman = spine.Spine.from({skeleton: "stretchymanData", atlas: "stretchymanAtlas", const stretchyman = spine.Spine.from({skeleton: "stretchymanData", atlas: "stretchymanAtlas",
scale: 0.75, scale: 1,
// scale: 0.75,
}); });
stretchyman.skeleton.scaleX = .5;
stretchyman.skeleton.scaleY = 1;
// Set the default mix time to use when transitioning // Set the default mix time to use when transitioning
// from one animation to the next. // from one animation to the next.
stretchyman.state.data.defaultMix = 0.2; stretchyman.state.data.defaultMix = 0.2;