diff --git a/CHANGELOG.md b/CHANGELOG.md index a5c3cb9b7..d444ca35f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,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 `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 * Added mix-and-match example to demonstrate the new Skin API. diff --git a/spine-cocos2dx/example/Classes/AppDelegate.cpp b/spine-cocos2dx/example/Classes/AppDelegate.cpp index dcade70a0..1bc2b0a9b 100644 --- a/spine-cocos2dx/example/Classes/AppDelegate.cpp +++ b/spine-cocos2dx/example/Classes/AppDelegate.cpp @@ -32,11 +32,8 @@ #include #include -#include "RaptorExample.h" -#include "BatchingExample.h" -#include "CoinExample.h" -#include "SkeletonRendererSeparatorExample.h" -#include "MixAndMatchExample.h" +#include "IKExample.h" +#include #include #include "AppMacros.h" #include @@ -44,6 +41,8 @@ USING_NS_CC; using namespace std; +using namespace spine; + DebugExtension debugExtension(SpineExtension::getInstance()); AppDelegate::AppDelegate () { @@ -112,7 +111,7 @@ bool AppDelegate::applicationDidFinishLaunching () { // create a scene. it's an autorelease object //auto scene = RaptorExample::scene(); - auto scene = BatchingExample::scene(); + auto scene = IKExample::scene(); // run director->runWithScene(scene); diff --git a/spine-cocos2dx/example/Classes/BatchingExample.cpp b/spine-cocos2dx/example/Classes/BatchingExample.cpp index c85940888..666307dec 100644 --- a/spine-cocos2dx/example/Classes/BatchingExample.cpp +++ b/spine-cocos2dx/example/Classes/BatchingExample.cpp @@ -28,7 +28,7 @@ *****************************************************************************/ #include "BatchingExample.h" -#include "SpineboyExample.h" +#include "IKExample.h" USING_NS_CC; using namespace spine; @@ -96,7 +96,7 @@ bool BatchingExample::init () { EventListenerTouchOneByOne* listener = EventListenerTouchOneByOne::create(); listener->onTouchBegan = [this] (Touch* touch, cocos2d::Event* event) -> bool { - Director::getInstance()->replaceScene(SpineboyExample::scene()); + Director::getInstance()->replaceScene(IKExample::scene()); return true; }; _eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this); diff --git a/spine-cocos2dx/example/Classes/IKExample.cpp b/spine-cocos2dx/example/Classes/IKExample.cpp new file mode 100644 index 000000000..4c2b6bc91 --- /dev/null +++ b/spine-cocos2dx/example/Classes/IKExample.cpp @@ -0,0 +1,116 @@ +/****************************************************************************** + * 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. + *****************************************************************************/ + +#include "IKExample.h" +#include "SpineboyExample.h" + +USING_NS_CC; +using namespace spine; + +Scene* IKExample::scene () { + Scene *scene = Scene::create(); + scene->addChild(IKExample::create()); + return scene; +} + +bool IKExample::init () { + if (!LayerColor::initWithColor(Color4B(128, 128, 128, 255))) return false; + + // Load the Spineboy skeleton and create a SkeletonAnimation node from it + // centered on the screen. + skeletonNode = SkeletonAnimation::createWithJsonFile("spineboy-pro.json", "spineboy.atlas", 0.6f); + skeletonNode->setPosition(Vec2(_contentSize.width / 2, 20)); + addChild(skeletonNode); + + // Queue the "walk" animation on the first track. + skeletonNode->setAnimation(0, "walk", true); + + // 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->setAnimation(1, "aim", true); + + // Next we setup a listener that receives and stores + // the current mouse location. The location is converted + // to the skeleton's coordinate system. + EventListenerMouse* mouseListener = EventListenerMouse::create(); + mouseListener->onMouseMove = [this] (cocos2d::Event* event) -> void { + // convert the mosue location to the skeleton's coordinate space + // and store it. + EventMouse* mouseEvent = dynamic_cast(event); + position = skeletonNode->convertToNodeSpace(mouseEvent->getLocationInView()); + }; + _eventDispatcher->addEventListenerWithSceneGraphPriority(mouseListener, this); + + // 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. + skeletonNode->setPostUpdateWorldTransformsListener([this] (SkeletonAnimation* node) -> void { + Bone* crosshair = node->findBone("crosshair"); + float localX = 0, localY = 0; + crosshair->getParent()->worldToLocal(position.x, position.y, localX, localY); + crosshair->setX(localX); + crosshair->setY(localY); + crosshair->setAppliedValid(false); + + node->getSkeleton()->updateWorldTransform(); + }); + + EventListenerTouchOneByOne* listener = EventListenerTouchOneByOne::create(); + listener->onTouchBegan = [this] (Touch* touch, cocos2d::Event* event) -> bool { + Director::getInstance()->replaceScene(SpineboyExample::scene()); + return true; + }; + + _eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this); + + scheduleUpdate(); + + return true; +} + +void IKExample::update (float deltaTime) { + +} diff --git a/spine-cocos2dx/example/Classes/IKExample.h b/spine-cocos2dx/example/Classes/IKExample.h new file mode 100644 index 000000000..3314350f4 --- /dev/null +++ b/spine-cocos2dx/example/Classes/IKExample.h @@ -0,0 +1,29 @@ +// +// IKExample.hpp +// spine-cocos2d-x +// +// Created by Mario Zechner on 28.10.19. +// + +#ifndef _IKEXAMPLE_H_ +#define _IKEXAMPLE_H_ + +#include "cocos2d.h" +#include + +class IKExample : public cocos2d::LayerColor { +public: + static cocos2d::Scene* scene (); + + CREATE_FUNC (IKExample); + + virtual bool init (); + + virtual void update (float deltaTime); + +private: + spine::SkeletonAnimation* skeletonNode; + cocos2d::Vec2 position; +}; + +#endif // _IKEXAMPLE_H_ diff --git a/spine-cocos2dx/example/proj.ios_mac/spine-cocos2d-x.xcodeproj/project.pbxproj b/spine-cocos2dx/example/proj.ios_mac/spine-cocos2d-x.xcodeproj/project.pbxproj index f0e9032a3..30c49fda1 100644 --- a/spine-cocos2dx/example/proj.ios_mac/spine-cocos2d-x.xcodeproj/project.pbxproj +++ b/spine-cocos2dx/example/proj.ios_mac/spine-cocos2d-x.xcodeproj/project.pbxproj @@ -205,6 +205,8 @@ 76AAA4471D1811B000C54FCB /* SpineboyExample.h in Sources */ = {isa = PBXBuildFile; fileRef = 76AAA3BF1D180F7C00C54FCB /* SpineboyExample.h */; }; 76AAA4571D18132D00C54FCB /* common in Resources */ = {isa = PBXBuildFile; fileRef = 76AAA4521D18132D00C54FCB /* common */; }; 76AAA4581D18132D00C54FCB /* common in Resources */ = {isa = PBXBuildFile; fileRef = 76AAA4521D18132D00C54FCB /* common */; }; + 76C893B0236715B8009D8DC8 /* IKExample.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 76C893AE236715B8009D8DC8 /* IKExample.cpp */; }; + 76C893B1236715B8009D8DC8 /* IKExample.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 76C893AE236715B8009D8DC8 /* IKExample.cpp */; }; 76D1BFE02029E35200A0272D /* SkeletonRendererSeparatorExample.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 76D1BFDF2029E35200A0272D /* SkeletonRendererSeparatorExample.cpp */; }; 76D1BFE12029E37700A0272D /* SkeletonRendererSeparatorExample.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 76D1BFDF2029E35200A0272D /* SkeletonRendererSeparatorExample.cpp */; }; 76D520E61EB362DD00572471 /* CoinExample.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 76D520E41EB362DD00572471 /* CoinExample.cpp */; }; @@ -392,6 +394,8 @@ 76AAA40A1D18106000C54FCB /* spine-cocos2dx.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = "spine-cocos2dx.cpp"; path = "../../src/spine/spine-cocos2dx.cpp"; sourceTree = ""; }; 76AAA40B1D18106000C54FCB /* spine-cocos2dx.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "spine-cocos2dx.h"; path = "../../src/spine/spine-cocos2dx.h"; sourceTree = ""; }; 76AAA4521D18132D00C54FCB /* common */ = {isa = PBXFileReference; lastKnownFileType = folder; path = common; sourceTree = ""; }; + 76C893AE236715B8009D8DC8 /* IKExample.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = IKExample.cpp; sourceTree = ""; }; + 76C893AF236715B8009D8DC8 /* IKExample.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IKExample.h; sourceTree = ""; }; 76D1BFDE2029E35100A0272D /* SkeletonRendererSeparatorExample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SkeletonRendererSeparatorExample.h; sourceTree = ""; }; 76D1BFDF2029E35200A0272D /* SkeletonRendererSeparatorExample.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SkeletonRendererSeparatorExample.cpp; sourceTree = ""; }; 76D520E41EB362DD00572471 /* CoinExample.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CoinExample.cpp; sourceTree = ""; }; @@ -562,6 +566,8 @@ 76AAA3BB1D180F7C00C54FCB /* RaptorExample.h */, 76AAA3BE1D180F7C00C54FCB /* SpineboyExample.cpp */, 76AAA3BF1D180F7C00C54FCB /* SpineboyExample.h */, + 76C893AE236715B8009D8DC8 /* IKExample.cpp */, + 76C893AF236715B8009D8DC8 /* IKExample.h */, ); name = Classes; path = ../Classes; @@ -887,6 +893,7 @@ 763104DD20BC1B5E00927A1E /* Triangulator.cpp in Sources */, 763104F320BC1B5E00927A1E /* Animation.cpp in Sources */, 76798D1D22A95AB400F77964 /* ConstraintData.cpp in Sources */, + 76C893B0236715B8009D8DC8 /* IKExample.cpp in Sources */, 76AAA40E1D18106000C54FCB /* SkeletonAnimation.cpp in Sources */, 76AAA4111D18106000C54FCB /* spine-cocos2dx.cpp in Sources */, 763104FC20BC1B5E00927A1E /* PathConstraintData.cpp in Sources */, @@ -953,6 +960,7 @@ 763105B420BC1B9700927A1E /* IkConstraintData.cpp in Sources */, 763105B520BC1B9700927A1E /* IkConstraintTimeline.cpp in Sources */, 763105B620BC1B9700927A1E /* Json.cpp in Sources */, + 76C893B1236715B8009D8DC8 /* IKExample.cpp in Sources */, 763105B720BC1B9700927A1E /* LinkedMesh.cpp in Sources */, 763105B820BC1B9700927A1E /* MathUtil.cpp in Sources */, 763105B920BC1B9700927A1E /* MeshAttachment.cpp in Sources */, diff --git a/spine-cocos2dx/src/spine/SkeletonAnimation.cpp b/spine-cocos2dx/src/spine/SkeletonAnimation.cpp index fae26293f..e323f1b0f 100644 --- a/spine-cocos2dx/src/spine/SkeletonAnimation.cpp +++ b/spine-cocos2dx/src/spine/SkeletonAnimation.cpp @@ -135,9 +135,11 @@ void SkeletonAnimation::update (float deltaTime) { super::update(deltaTime); deltaTime *= _timeScale; + if (_preUpdateListener) _preUpdateListener(this); _state->update(deltaTime); _state->apply(*_skeleton); _skeleton->updateWorldTransform(); + if (_postUpdateListener) _postUpdateListener(this); } void SkeletonAnimation::draw(cocos2d::Renderer *renderer, const cocos2d::Mat4 &transform, uint32_t transformFlags) { @@ -282,6 +284,14 @@ void SkeletonAnimation::setEventListener (const EventListener& listener) { _eventListener = listener; } +void SkeletonAnimation::setPreUpdateWorldTransformsListener(const UpdateWorldTransformsListener &listener) { + _preUpdateListener = listener; +} + +void SkeletonAnimation::setPostUpdateWorldTransformsListener(const UpdateWorldTransformsListener &listener) { + _postUpdateListener = listener; +} + void SkeletonAnimation::setTrackStartListener (TrackEntry* entry, const StartListener& listener) { getListeners(entry)->startListener = listener; } diff --git a/spine-cocos2dx/src/spine/SkeletonAnimation.h b/spine-cocos2dx/src/spine/SkeletonAnimation.h index 6e43feaed..2309f03d7 100644 --- a/spine-cocos2dx/src/spine/SkeletonAnimation.h +++ b/spine-cocos2dx/src/spine/SkeletonAnimation.h @@ -36,12 +36,15 @@ namespace spine { +class SkeletonAnimation; + typedef std::function StartListener; typedef std::function InterruptListener; typedef std::function EndListener; typedef std::function DisposeListener; typedef std::function CompleteListener; typedef std::function EventListener; +typedef std::function UpdateWorldTransformsListener; /** Draws an animated skeleton, providing an AnimationState for applying one or more animations and queuing animations to be * played later. */ @@ -87,6 +90,8 @@ public: void setDisposeListener (const DisposeListener& listener); void setCompleteListener (const CompleteListener& listener); void setEventListener (const EventListener& listener); + void setPreUpdateWorldTransformsListener(const UpdateWorldTransformsListener& listener); + void setPostUpdateWorldTransformsListener(const UpdateWorldTransformsListener& listener); void setTrackStartListener (TrackEntry* entry, const StartListener& listener); void setTrackInterruptListener (TrackEntry* entry, const InterruptListener& listener); @@ -119,6 +124,8 @@ protected: DisposeListener _disposeListener; CompleteListener _completeListener; EventListener _eventListener; + UpdateWorldTransformsListener _preUpdateListener; + UpdateWorldTransformsListener _postUpdateListener; private: typedef SkeletonRenderer super; diff --git a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/IKTest.java b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/IKTest.java index 6f3dbedaa..53e120cb6 100644 --- a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/IKTest.java +++ b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/IKTest.java @@ -61,10 +61,6 @@ public class IKTest extends ApplicationAdapter { // mix the two. The mouse position following // is performed in the render() method below. state.setAnimation(1, "aim", true); - - // apply the state once, so we have world - // bone positions that we can use. - skeleton.setToSetupPose(); } public void render () {