[cocos2d-objc] Added IK example. Also added SkeletonAnimation preUpdateWorldTransformListener and postUpdateWorldTransformsListener which allow modifying a skeleton pre- and post-calculation of the world transformations. See #1532.

This commit is contained in:
badlogic 2019-10-28 15:34:53 +01:00
parent 656b08a32e
commit 015de8ed4c
14 changed files with 4499 additions and 1568 deletions

View File

@ -53,9 +53,12 @@
### Cocos2d-Objc
* Added mix-and-match example to demonstrate the new Skin API.
* Added `IKExample`.
* Added `SkeletonAnimation preUpdateWorldTransformsListener` and `SkeletonAnimation postUpdateWorldTransformsListener`. When set, these callbacks will be invokved before and after the skeleton's `updateWorldTransforms()` method is called. See the `IKExample` how it can be used.
### SFML
* Added mix-and-match example to demonstrate the new Skin API.
* Added `IKExample`.
## C++
* **Breaking Changes**
@ -88,6 +91,7 @@
* Updated to cocos2d-x 3.17.1
* Added mix-and-match example to demonstrate the new Skin API.
* Exmaple project requires Visual Studio 2019 on Windows
* Added `IKExample`.
* Added `SkeletonAnimation::setPreUpdateWorldTransformsListener()` and `SkeletonAnimation::setPreUpdateWorldTransformsListener()`. When set, these callbacks will be invokved before and after the skeleton's `updateWorldTransforms()` method is called. See the `IKExample` how it can be used.
### SFML

View File

@ -64,7 +64,7 @@ cp -f ../raptor/export/raptor-pro.json "$ROOT/spine-cocos2d-objc/Resources/"
cp -f ../raptor/export/raptor.atlas "$ROOT/spine-cocos2d-objc/Resources/"
cp -f ../raptor/export/raptor.png "$ROOT/spine-cocos2d-objc/Resources/"
cp -f ../spineboy/export/spineboy-ess.json "$ROOT/spine-cocos2d-objc/Resources/"
cp -f ../spineboy/export/spineboy-pro.json "$ROOT/spine-cocos2d-objc/Resources/"
cp -f ../spineboy/export/spineboy.atlas "$ROOT/spine-cocos2d-objc/Resources/"
cp -f ../spineboy/export/spineboy.png "$ROOT/spine-cocos2d-objc/Resources/"

View File

@ -1,7 +1,7 @@
#import "cocos2d.h"
#import "AppDelegate.h"
#import "SpineboyExample.h"
#import "IKExample.h"
@implementation AppController
@ -24,7 +24,7 @@
CCSetupShowDebugStats: @YES,
}];
[[CCDirector sharedDirector] runWithScene:[SpineboyExample scene]];
[[CCDirector sharedDirector] runWithScene:[IKExample scene]];
return YES;
}

View File

@ -49,6 +49,8 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -28,7 +28,7 @@
*****************************************************************************/
#import "CoinExample.h"
#import "SpineBoyExample.h"
#import "IKExample.h"
@implementation CoinExample
@ -66,7 +66,7 @@
else if (skeletonNode.timeScale == 1)
skeletonNode.timeScale = 0.3f;
else
[[CCDirector sharedDirector] replaceScene:[SpineboyExample scene]];
[[CCDirector sharedDirector] replaceScene:[IKExample scene]];
}
#endif

View File

@ -0,0 +1,40 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated May 1, 2019. Replaces all prior versions.
*
* Copyright (c) 2013-2019, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
* NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS
* INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#import "cocos2d.h"
#import <spine/spine-cocos2d-objc.h>
@interface IKExample : CCNode {
SkeletonAnimation* skeletonNode;
CGPoint position;
}
+ (CCScene*) scene;
@end

View File

@ -0,0 +1,118 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated May 1, 2019. Replaces all prior versions.
*
* Copyright (c) 2013-2019, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
* NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS
* INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#import "IKExample.h"
#import "SpineboyExample.h"
// This example demonstrates how to set the position
// of a bone based on the touch position, which in
// turn will make an IK chain follow that bone
// smoothly.
@implementation IKExample
+ (CCScene*) scene {
CCScene *scene = [CCScene node];
[scene addChild:[IKExample node]];
return scene;
}
-(id) init {
self = [super init];
if (!self) return nil;
// Load the Spineboy skeleton and create a SkeletonAnimation node from it
// centered on the screen.
skeletonNode = [SkeletonAnimation skeletonWithFile:@"spineboy-pro.json" atlasFile:@"spineboy.atlas" scale:0.4];
CGSize windowSize = [[CCDirector sharedDirector] viewSize];
[skeletonNode setPosition:ccp(windowSize.width / 2, 20)];
[self addChild:skeletonNode];
self.userInteractionEnabled = YES;
self.contentSize = windowSize;
// Queue the "walk" animation on the first track.
[skeletonNode setAnimationForTrack:0 name:@"walk" loop:YES];
// Queue the "aim" animation on a higher track.
// It consists of a single frame that positions
// the back arm and gun such that they point at
// the "crosshair" bone. By setting this
// animation on a higher track, it overrides
// any changes to the back arm and gun made
// by the walk animation, allowing us to
// mix the two. The mouse position following
// is performed in the lambda below.
[skeletonNode setAnimationForTrack:1 name:@"aim" loop:YES];
// Position the "crosshair" bone at the mouse
// location.
//
// When setting the crosshair bone position
// to the mouse position, we need to translate
// from "skeleton space" to "local bone space".
// Note that the local bone space is calculated
// using the bone's parent worldToLocal() function!
//
// After updating the bone position based on the
// converted mouse location, we call updateWorldTransforms()
// again so the change of the IK target position is
// applied to the rest of the skeleton.
__weak IKExample* scene = self;
skeletonNode.postUpdateWorldTransformsListener = ^(SkeletonAnimation* node) {
if (scene != NULL) {
__strong IKExample* sceneStrong = scene;
spBone* crosshair = [node findBone:@"crosshair"]; // The bone should be cached
float localX = 0, localY = 0;
spBone_worldToLocal(crosshair->parent, sceneStrong->position.x, sceneStrong->position.y, &localX, &localY);
crosshair->x = localX;
crosshair->y = localY;
crosshair->appliedValid = FALSE;
spBone_updateWorldTransform(crosshair);
}
};
return self;
}
#if ( TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR )
- (void)touchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
position = [skeletonNode convertToNodeSpace:touch.locationInWorld];
printf("%f %f\n", position.x, position.y);
}
- (void)touchMoved:(UITouch *)touch withEvent:(UIEvent *)event {
position = [skeletonNode convertToNodeSpace:touch.locationInWorld];
printf("%f %f\n", position.x, position.y);
}
- (void)touchEnded:(UITouch *)touch withEvent:(UIEvent *)event {
[[CCDirector sharedDirector] replaceScene:[SpineboyExample scene]];
}
#endif
@end

View File

@ -42,7 +42,7 @@
self = [super init];
if (!self) return nil;
skeletonNode = [SkeletonAnimation skeletonWithFile:@"spineboy-ess.json" atlasFile:@"spineboy.atlas" scale:0.4];
skeletonNode = [SkeletonAnimation skeletonWithFile:@"spineboy-pro.json" atlasFile:@"spineboy.atlas" scale:0.4];
[skeletonNode setMixFrom:@"walk" to:@"jump" duration:0.2f];
[skeletonNode setMixFrom:@"jump" to:@"run" duration:0.2f];

View File

@ -8,7 +8,6 @@
/* Begin PBXBuildFile section */
43C3282F170B0C19004A9460 /* spine-cocos2d-objc.m in Sources */ = {isa = PBXBuildFile; fileRef = 43C3282D170B0C19004A9460 /* spine-cocos2d-objc.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; };
43C3286C170B0DA6004A9460 /* spineboy-ess.json in Resources */ = {isa = PBXBuildFile; fileRef = 43C32868170B0DA6004A9460 /* spineboy-ess.json */; };
43C3286E170B0DA6004A9460 /* spineboy.atlas in Resources */ = {isa = PBXBuildFile; fileRef = 43C3286A170B0DA6004A9460 /* spineboy.atlas */; };
43C3286F170B0DA6004A9460 /* spineboy.png in Resources */ = {isa = PBXBuildFile; fileRef = 43C3286B170B0DA6004A9460 /* spineboy.png */; };
43C3287D170B0DBE004A9460 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 43C32871170B0DBE004A9460 /* Default-568h@2x.png */; };
@ -34,6 +33,8 @@
765A2EF61D7D7A08003FB779 /* goblins.atlas in Resources */ = {isa = PBXBuildFile; fileRef = 765A2EF41D7D7A08003FB779 /* goblins.atlas */; };
765A2EF71D7D7A08003FB779 /* goblins.png in Resources */ = {isa = PBXBuildFile; fileRef = 765A2EF51D7D7A08003FB779 /* goblins.png */; };
76BF7E071E66ED9C00485998 /* GLUtils.c in Sources */ = {isa = PBXBuildFile; fileRef = 76BF7E051E66ED9C00485998 /* GLUtils.c */; };
76C893BC23672757009D8DC8 /* IKExample.m in Sources */ = {isa = PBXBuildFile; fileRef = 76C893B623672757009D8DC8 /* IKExample.m */; };
76C893BF236728A4009D8DC8 /* spineboy-pro.json in Resources */ = {isa = PBXBuildFile; fileRef = 76C893BE236728A4009D8DC8 /* spineboy-pro.json */; };
76EE4E461EB36DE6000254F4 /* Array.c in Sources */ = {isa = PBXBuildFile; fileRef = 76EE4E421EB36DE6000254F4 /* Array.c */; };
76EE4E471EB36DE6000254F4 /* ClippingAttachment.c in Sources */ = {isa = PBXBuildFile; fileRef = 76EE4E431EB36DE6000254F4 /* ClippingAttachment.c */; };
76EE4E481EB36DE6000254F4 /* SkeletonClipping.c in Sources */ = {isa = PBXBuildFile; fileRef = 76EE4E441EB36DE6000254F4 /* SkeletonClipping.c */; };
@ -138,7 +139,6 @@
/* Begin PBXFileReference section */
43C3282D170B0C19004A9460 /* spine-cocos2d-objc.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "spine-cocos2d-objc.m"; path = "src/spine/spine-cocos2d-objc.m"; sourceTree = "<group>"; };
43C3282E170B0C19004A9460 /* spine-cocos2d-objc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "spine-cocos2d-objc.h"; path = "src/spine/spine-cocos2d-objc.h"; sourceTree = "<group>"; };
43C32868170B0DA6004A9460 /* spineboy-ess.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = "spineboy-ess.json"; path = "Resources/spineboy-ess.json"; sourceTree = "<group>"; };
43C3286A170B0DA6004A9460 /* spineboy.atlas */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = spineboy.atlas; path = Resources/spineboy.atlas; sourceTree = "<group>"; };
43C3286B170B0DA6004A9460 /* spineboy.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = spineboy.png; path = Resources/spineboy.png; sourceTree = "<group>"; };
43C32871170B0DBE004A9460 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "Resources-ios/Default-568h@2x.png"; sourceTree = "<group>"; };
@ -171,6 +171,9 @@
765A2EF51D7D7A08003FB779 /* goblins.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = goblins.png; path = Resources/goblins.png; sourceTree = "<group>"; };
76BF7E051E66ED9C00485998 /* GLUtils.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = GLUtils.c; path = src/spine/GLUtils.c; sourceTree = "<group>"; };
76BF7E061E66ED9C00485998 /* GLUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GLUtils.h; path = src/spine/GLUtils.h; sourceTree = "<group>"; };
76C893B623672757009D8DC8 /* IKExample.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = IKExample.m; path = example/IKExample.m; sourceTree = "<group>"; };
76C893BB23672757009D8DC8 /* IKExample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = IKExample.h; path = example/IKExample.h; sourceTree = "<group>"; };
76C893BE236728A4009D8DC8 /* spineboy-pro.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = "spineboy-pro.json"; path = "Resources/spineboy-pro.json"; sourceTree = "<group>"; };
76EE4E421EB36DE6000254F4 /* Array.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = Array.c; path = "../spine-c/spine-c/src/spine/Array.c"; sourceTree = "<group>"; };
76EE4E431EB36DE6000254F4 /* ClippingAttachment.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = ClippingAttachment.c; path = "../spine-c/spine-c/src/spine/ClippingAttachment.c"; sourceTree = "<group>"; };
76EE4E441EB36DE6000254F4 /* SkeletonClipping.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = SkeletonClipping.c; path = "../spine-c/spine-c/src/spine/SkeletonClipping.c"; sourceTree = "<group>"; };
@ -282,6 +285,8 @@
43C32823170B0BC7004A9460 /* spine-cocos2d-objc */,
43F7FF8C1927F96700CA4038 /* SpineboyExample.h */,
43F7FF8D1927F96700CA4038 /* SpineboyExample.m */,
76C893BB23672757009D8DC8 /* IKExample.h */,
76C893B623672757009D8DC8 /* IKExample.m */,
43F7FF8A1927F96700CA4038 /* GoblinsExample.h */,
43F7FF8B1927F96700CA4038 /* GoblinsExample.m */,
76F5BDA81D2BDE67005917E5 /* RaptorExample.h */,
@ -364,6 +369,7 @@
43C32867170B0C7F004A9460 /* Resources */ = {
isa = PBXGroup;
children = (
76C893BE236728A4009D8DC8 /* spineboy-pro.json */,
76EE4E4E1EB36E53000254F4 /* coin.atlas */,
76EE4E4F1EB36E53000254F4 /* coin-pro.json */,
76EE4E501EB36E53000254F4 /* coin.png */,
@ -376,7 +382,6 @@
76F5BDA01D2BDE1C005917E5 /* tank-pro.json */,
76F5BDA11D2BDE1C005917E5 /* tank.png */,
43F7010D1927FBC700CA4038 /* goblins-pro.json */,
43C32868170B0DA6004A9460 /* spineboy-ess.json */,
43C3286A170B0DA6004A9460 /* spineboy.atlas */,
43C3286B170B0DA6004A9460 /* spineboy.png */,
);
@ -484,6 +489,7 @@
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
English,
en,
);
mainGroup = 9A5D248C170A94DA0030D4DD;
@ -540,7 +546,6 @@
files = (
765A2EF61D7D7A08003FB779 /* goblins.atlas in Resources */,
76F5BDA51D2BDE1C005917E5 /* tank.atlas in Resources */,
43C3286C170B0DA6004A9460 /* spineboy-ess.json in Resources */,
43C3286E170B0DA6004A9460 /* spineboy.atlas in Resources */,
43C3286F170B0DA6004A9460 /* spineboy.png in Resources */,
43C3287D170B0DBE004A9460 /* Default-568h@2x.png in Resources */,
@ -557,6 +562,7 @@
76EE4E511EB36E53000254F4 /* coin.atlas in Resources */,
76F5BDA21D2BDE1C005917E5 /* raptor.atlas in Resources */,
43C32883170B0DBE004A9460 /* Icon-Small.png in Resources */,
76C893BF236728A4009D8DC8 /* spineboy-pro.json in Resources */,
43C32884170B0DBE004A9460 /* Icon-Small@2x.png in Resources */,
43C32885170B0DBE004A9460 /* Icon.png in Resources */,
76F5BDA71D2BDE1C005917E5 /* tank.png in Resources */,
@ -607,6 +613,7 @@
76F28D311DEC810300CDE54D /* Slot.c in Sources */,
43C32A06170B0F93004A9460 /* main.m in Sources */,
76F28D351DEC810300CDE54D /* VertexAttachment.c in Sources */,
76C893BC23672757009D8DC8 /* IKExample.m in Sources */,
76FAC1961E3FA15E001CCC8C /* Color.c in Sources */,
76F28D331DEC810300CDE54D /* TransformConstraint.c in Sources */,
43C32A09170B10FF004A9460 /* AppDelegate.m in Sources */,

View File

@ -39,6 +39,7 @@ typedef void(^spEndListener)(spTrackEntry* entry);
typedef void(^spDisposeListener)(spTrackEntry* entry);
typedef void(^spCompleteListener)(spTrackEntry* entry);
typedef void(^spEventListener)(spTrackEntry* entry, spEvent* event);
typedef void(^spUpdateWorldTransformsListener)(SkeletonAnimation* node);
/** Draws an animated skeleton, providing an AnimationState for applying one or more animations and queuing animations to be
* played later. */
@ -90,5 +91,7 @@ typedef void(^spEventListener)(spTrackEntry* entry, spEvent* event);
@property (nonatomic, copy) spDisposeListener disposeListener;
@property (nonatomic, copy) spCompleteListener completeListener;
@property (nonatomic, copy) spEventListener eventListener;
@property (nonatomic, copy) spUpdateWorldTransformsListener preUpdateWorldTransformsListener;
@property (nonatomic, copy) spUpdateWorldTransformsListener postUpdateWorldTransformsListener;
@end

View File

@ -80,6 +80,8 @@ static _TrackEntryListeners* getListeners (spTrackEntry* entry) {
@synthesize endListener = _endListener;
@synthesize completeListener = _completeListener;
@synthesize eventListener = _eventListener;
@synthesize preUpdateWorldTransformsListener = _preUpdateWorldTransformsListener;
@synthesize postUpdateWorldTransformsListener = _postUpdateWorldTransformsListener;
+ (id) skeletonWithData:(spSkeletonData*)skeletonData ownsSkeletonData:(bool)ownsSkeletonData {
return [[[self alloc] initWithData:skeletonData ownsSkeletonData:ownsSkeletonData] autorelease];
@ -141,6 +143,8 @@ static _TrackEntryListeners* getListeners (spTrackEntry* entry) {
[_disposeListener release];
[_completeListener release];
[_eventListener release];
[_preUpdateWorldTransformsListener release];
[_postUpdateWorldTransformsListener release];
[super dealloc];
}
@ -150,7 +154,9 @@ static _TrackEntryListeners* getListeners (spTrackEntry* entry) {
spSkeleton_update(_skeleton, deltaTime);
spAnimationState_update(_state, deltaTime);
spAnimationState_apply(_state, _skeleton);
if (_preUpdateWorldTransformsListener) _preUpdateWorldTransformsListener(self);
spSkeleton_updateWorldTransform(_skeleton);
if (_postUpdateWorldTransformsListener) _postUpdateWorldTransformsListener(self);
}
- (void) setAnimationStateData:(spAnimationStateData*)stateData {

View File

@ -33,6 +33,10 @@
USING_NS_CC;
using namespace spine;
// This example demonstrates how to set the position
// of a bone based on the touch position, which in
// turn will make an IK chain follow that bone
// smoothly.
Scene* IKExample::scene () {
Scene *scene = Scene::create();
scene->addChild(IKExample::create());
@ -88,7 +92,7 @@ bool IKExample::init () {
// again so the change of the IK target position is
// applied to the rest of the skeleton.
skeletonNode->setPostUpdateWorldTransformsListener([this] (SkeletonAnimation* node) -> void {
Bone* crosshair = node->findBone("crosshair");
Bone* crosshair = node->findBone("crosshair"); // The bone should be cached
float localX = 0, localY = 0;
crosshair->getParent()->worldToLocal(position.x, position.y, localX, localY);
crosshair->setX(localX);