axmol/tests/cpp-tests/Classes/SpineTest/SpineTest.cpp

515 lines
18 KiB
C++
Raw Normal View History

2019-11-23 20:27:39 +08:00
/*******************************************************************************
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 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 "SpineTest.h"
#include <iostream>
#include <fstream>
#include <string.h>
#include "spine/spine.h"
#include "renderer/CCColorizer.h"
2019-11-23 20:27:39 +08:00
using namespace cocos2d;
using namespace std;
using namespace spine;
2020-01-05 03:09:32 +08:00
#define NUM_SKELETONS 50
#define SPINE_NODE_SCALE_FACTOR 0.4
static Cocos2dTextureLoader textureLoader;
PowInterpolation pow2(2);
PowOutInterpolation powOut2(2);
SwirlVertexEffect effect(400, powOut2);
#define SCALE_SKELETON_NODE(node) do { if(node) node->setScale(SPINE_NODE_SCALE_FACTOR); } while(false)
2019-11-23 20:27:39 +08:00
//------------------------------------------------------------------
//
// SpineTestScene
//
//------------------------------------------------------------------
2020-01-05 03:09:32 +08:00
2019-11-23 20:27:39 +08:00
SpineTests::SpineTests()
{
2020-01-05 03:09:32 +08:00
auto fu = FileUtils::getInstance();
_searchPaths = fu->getSearchPaths();
fu->addSearchPath("spine", true);
2019-11-23 20:27:39 +08:00
ADD_TEST_CASE(BatchingExample);
ADD_TEST_CASE(CoinExample);
ADD_TEST_CASE(GoblinsExample);
2020-01-05 03:09:32 +08:00
ADD_TEST_CASE(IKExample);
ADD_TEST_CASE(MixAndMatchExample);
2019-11-23 20:27:39 +08:00
ADD_TEST_CASE(RaptorExample);
2020-01-05 03:09:32 +08:00
ADD_TEST_CASE(SkeletonRendererSeparatorExample);
2019-11-23 20:27:39 +08:00
ADD_TEST_CASE(SpineboyExample);
ADD_TEST_CASE(TankExample);
2020-01-05 03:09:32 +08:00
#ifdef COCOS2D_DEBUG
debugExtension = new DebugExtension(SpineExtension::getInstance());
#endif
}
SpineTests::~SpineTests()
{
FileUtils::getInstance()->setSearchPaths(_searchPaths);
SkeletonBatch::destroyInstance();
SkeletonTwoColorBatch::destroyInstance();
#ifdef COCOS2D_DEBUG
debugExtension->reportLeaks();
delete debugExtension;
#endif
2019-11-23 20:27:39 +08:00
}
SpineTestLayer::SpineTestLayer()
: _title("")
{}
2020-01-05 03:09:32 +08:00
SpineTestLayer::~SpineTestLayer()
{
}
2019-11-23 20:27:39 +08:00
std::string SpineTestLayer::title() const
{
return _title;
}
2020-01-05 03:09:32 +08:00
bool SpineTestLayer::init()
{
if (!TestCase::init()) return false;
EventListenerTouchOneByOne* listener = EventListenerTouchOneByOne::create();
listener->onTouchBegan = [this](Touch* touch, cocos2d::Event* event) -> bool {
if (!skeletonNode) return true;
_touchIndex = (_touchIndex + 1) % 3;
if (_touchIndex == 0)
{
skeletonNode->setDebugBonesEnabled(false);
skeletonNode->setTimeScale(1.0f);
}
else if (_touchIndex == 1)
{
skeletonNode->setDebugBonesEnabled(true);
skeletonNode->setTimeScale(1.0f);
}
else if (_touchIndex == 2)
{
skeletonNode->setDebugBonesEnabled(true);
skeletonNode->setTimeScale(0.3f);
}
return true;
};
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
SCALE_SKELETON_NODE(skeletonNode);
return true;
}
2019-11-23 20:27:39 +08:00
// BatchingExample
bool BatchingExample::init () {
if (!SpineTestLayer::init()) return false;
2020-01-05 03:09:32 +08:00
_title = "Batching";
_atlas = new (__FILE__, __LINE__) Atlas("spineboy.atlas", &textureLoader, true);
2019-11-23 20:27:39 +08:00
CCASSERT(_atlas, "Error reading atlas file.");
2020-01-05 03:09:32 +08:00
2019-11-23 20:27:39 +08:00
// This attachment loader configures attachments with data needed for cocos2d-x rendering.
// Do not dispose the attachment loader until the skeleton data is disposed!
2020-01-05 03:09:32 +08:00
_attachmentLoader = new (__FILE__, __LINE__) Cocos2dAtlasAttachmentLoader(_atlas);
2019-11-23 20:27:39 +08:00
// Load the skeleton data.
2020-01-05 03:09:32 +08:00
SkeletonJson* json = new (__FILE__, __LINE__) SkeletonJson(_attachmentLoader);
json->setScale(0.6f); // Resizes skeleton data to 60% of the size it was in Spine.
_skeletonData = json->readSkeletonDataFile("spineboy-pro.json");
CCASSERT(_skeletonData, json->getError().isEmpty() ? json->getError().buffer() : "Error reading skeleton data file.");
delete json;
2019-11-23 20:27:39 +08:00
// Setup mix times.
2020-01-05 03:09:32 +08:00
_stateData = new (__FILE__, __LINE__) AnimationStateData(_skeletonData);
_stateData->setMix("walk", "jump", 0.2f);
_stateData->setMix("jump", "run", 0.2f);
2019-11-23 20:27:39 +08:00
int xMin = _contentSize.width * 0.10f, xMax = _contentSize.width * 0.90f;
int yMin = 0, yMax = _contentSize.height * 0.7f;
2020-01-05 03:09:32 +08:00
for (int i = 0; i < NUM_SKELETONS; i++) {
2019-11-23 20:27:39 +08:00
// Each skeleton node shares the same atlas, skeleton data, and mix times.
SkeletonAnimation* skeletonNode = SkeletonAnimation::createWithData(_skeletonData, false);
skeletonNode->setAnimationStateData(_stateData);
2020-01-05 03:09:32 +08:00
2019-11-23 20:27:39 +08:00
skeletonNode->setAnimation(0, "walk", true);
2020-01-05 03:09:32 +08:00
skeletonNode->addAnimation(0, "jump", true, RandomHelper::random_int(0, 300) / 100.0f);
2019-11-23 20:27:39 +08:00
skeletonNode->addAnimation(0, "run", true);
2020-01-05 03:09:32 +08:00
// alternative setting two color tint for groups of 10 skeletons
// should end up with #skeletons / 10 batches
// if (j++ < 10)
// skeletonNode->setTwoColorTint(true);
// if (j == 20) j = 0;
// skeletonNode->setTwoColorTint(true);
2019-11-23 20:27:39 +08:00
skeletonNode->setPosition(Vec2(
2020-01-05 03:09:32 +08:00
RandomHelper::random_int(xMin, xMax),
RandomHelper::random_int(yMin, yMax)
));
2019-11-23 20:27:39 +08:00
addChild(skeletonNode);
}
2020-01-05 03:09:32 +08:00
SCALE_SKELETON_NODE(skeletonNode);
2019-11-23 20:27:39 +08:00
return true;
}
2020-01-05 03:09:32 +08:00
BatchingExample::~BatchingExample() {
2019-11-23 20:27:39 +08:00
// SkeletonAnimation instances are cocos2d-x nodes and are disposed of automatically as normal, but the data created
// manually to be shared across multiple SkeletonAnimations needs to be disposed of manually.
2020-01-05 03:09:32 +08:00
delete _skeletonData;
delete _stateData;
delete _attachmentLoader;
delete _atlas;
2019-11-23 20:27:39 +08:00
}
2020-01-05 03:09:32 +08:00
bool CoinExample::init() {
2019-11-23 20:27:39 +08:00
2020-01-05 03:09:32 +08:00
if (!SpineTestLayer::init()) return false;
_title = "Coin";
skeletonNode = SkeletonAnimation::createWithBinaryFile("coin-pro.skel", "coin.atlas", 1);
skeletonNode->setAnimation(0, "animation", true);
skeletonNode->setPosition(Vec2(_contentSize.width / 2, _contentSize.height / 2));
2019-11-23 20:27:39 +08:00
addChild(skeletonNode);
2020-01-05 03:09:32 +08:00
scheduleUpdate();
SCALE_SKELETON_NODE(skeletonNode);
2019-11-23 20:27:39 +08:00
return true;
}
2020-01-05 03:09:32 +08:00
bool GoblinsExample::init() {
2019-11-23 20:27:39 +08:00
if (!SpineTestLayer::init()) return false;
2020-01-05 03:09:32 +08:00
_title = "Goblins";
skeletonNode = SkeletonAnimation::createWithJsonFile("goblins-pro.json", "goblins.atlas", 1.5f);
2019-11-23 20:27:39 +08:00
skeletonNode->setAnimation(0, "walk", true);
skeletonNode->setSkin("goblin");
2020-01-05 03:09:32 +08:00
2019-11-23 20:27:39 +08:00
skeletonNode->setPosition(Vec2(_contentSize.width / 2, 20));
addChild(skeletonNode);
2020-01-05 03:09:32 +08:00
SCALE_SKELETON_NODE(skeletonNode);
2019-11-23 20:27:39 +08:00
return true;
}
2020-01-05 03:09:32 +08:00
bool IKExample::init() {
2019-11-23 20:27:39 +08:00
if (!SpineTestLayer::init()) return false;
2020-01-05 03:09:32 +08:00
_title = "IKExample";
// 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.
2019-11-23 20:27:39 +08:00
skeletonNode->setAnimation(0, "walk", true);
2020-01-05 03:09:32 +08:00
// 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<EventMouse*>(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"); // The bone should be cached
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();
});
SCALE_SKELETON_NODE(skeletonNode);
return true;
}
MixAndMatchExample::~MixAndMatchExample() {
delete skin;
2020-10-23 11:37:52 +08:00
delete skin2;
2020-01-05 03:09:32 +08:00
}
bool MixAndMatchExample::init() {
if (!SpineTestLayer::init()) return false;
2019-11-23 20:27:39 +08:00
2020-01-05 03:09:32 +08:00
_title = "Mix and Match";
skeletonNode = SkeletonAnimation::createWithBinaryFile("mix-and-match-pro.skel", "mix-and-match.atlas", 0.5);
skeletonNode->setAnimation(0, "dance", true);
// Create a new skin, by mixing and matching other skins
// that fit together. Items making up the girl are individual
// skins. Using the skin API, a new skin is created which is
// a combination of all these individual item skins.
SkeletonData* skeletonData = skeletonNode->getSkeleton()->getData();
skin = new (__FILE__, __LINE__) Skin("mix-and-match");
skin->addSkin(skeletonData->findSkin("skin-base"));
skin->addSkin(skeletonData->findSkin("nose/short"));
skin->addSkin(skeletonData->findSkin("eyelids/girly"));
skin->addSkin(skeletonData->findSkin("eyes/violet"));
skin->addSkin(skeletonData->findSkin("hair/brown"));
skin->addSkin(skeletonData->findSkin("clothes/hoodie-orange"));
skin->addSkin(skeletonData->findSkin("legs/pants-jeans"));
skin->addSkin(skeletonData->findSkin("accessories/bag"));
skin->addSkin(skeletonData->findSkin("accessories/hat-red-yellow"));
skeletonNode->getSkeleton()->setSkin(skin);
2020-10-23 11:37:52 +08:00
skeletonNode->setPosition(Vec2(_contentSize.width / 3, _contentSize.height / 2 - 100 ));
2020-01-05 03:09:32 +08:00
addChild(skeletonNode);
SCALE_SKELETON_NODE(skeletonNode);
2020-10-16 17:23:29 +08:00
Colorizer::enableNodeIntelliShading(skeletonNode, Vec3(92.0f, 1.0f, 1.2f), Vec3::ZERO);
2020-10-23 11:37:52 +08:00
/* -------- skeletonNode2 with same spine animation file ------------ */
auto skeletonNode2 = SkeletonAnimation::createWithBinaryFile("mix-and-match-pro.skel", "mix-and-match.atlas", 0.5);
skeletonNode2->setAnimation(0, "dance", true);
// Create a new skin, by mixing and matching other skins
// that fit together. Items making up the girl are individual
// skins. Using the skin API, a new skin is created which is
// a combination of all these individual item skins.
SkeletonData* skeletonData2 = skeletonNode->getSkeleton()->getData();
skin2 = new (__FILE__, __LINE__) Skin("mix-and-match");
skin2->addSkin(skeletonData->findSkin("skin-base"));
skin2->addSkin(skeletonData->findSkin("nose/short"));
skin2->addSkin(skeletonData->findSkin("eyelids/girly"));
skin2->addSkin(skeletonData->findSkin("eyes/violet"));
skin2->addSkin(skeletonData->findSkin("hair/brown"));
skin2->addSkin(skeletonData->findSkin("clothes/hoodie-orange"));
skin2->addSkin(skeletonData->findSkin("legs/pants-jeans"));
skin2->addSkin(skeletonData->findSkin("accessories/bag"));
skin2->addSkin(skeletonData->findSkin("accessories/hat-red-yellow"));
skeletonNode2->getSkeleton()->setSkin(skin2);
skeletonNode2->setPosition(Vec2(_contentSize.width / 1.5, _contentSize.height / 2 - 100));
addChild(skeletonNode2);
SCALE_SKELETON_NODE(skeletonNode2);
Colorizer::enableNodeIntelliShading(skeletonNode2, Vec3(45.0f, 1.0f, 1.2f), Vec3::ZERO);
2020-01-05 03:09:32 +08:00
return true;
}
bool RaptorExample::init() {
if (!SpineTestLayer::init()) return false;
_title = "Raptor";
skeletonNode = SkeletonAnimation::createWithJsonFile("raptor-pro.json", "raptor.atlas", 0.5f);
skeletonNode->setAnimation(0, "walk", true);
skeletonNode->addAnimation(1, "gun-grab", false, 2);
skeletonNode->setTwoColorTint(true);
effect.setCenterY(200);
swirlTime = 0;
skeletonNode->setVertexEffect(&effect);
2019-11-23 20:27:39 +08:00
skeletonNode->setPosition(Vec2(_contentSize.width / 2, 20));
addChild(skeletonNode);
2020-01-05 03:09:32 +08:00
scheduleUpdate();
SCALE_SKELETON_NODE(skeletonNode);
2019-11-23 20:27:39 +08:00
return true;
}
2020-01-05 03:09:32 +08:00
void RaptorExample::update(float fDelta) {
swirlTime += fDelta;
float percent = spine::MathUtil::fmod(swirlTime, 2);
if (percent > 1) percent = 1 - (percent - 1);
effect.setAngle(pow2.interpolate(-60.0f, 60.0f, percent));
}
2019-11-23 20:27:39 +08:00
2020-01-05 03:09:32 +08:00
bool SkeletonRendererSeparatorExample::init() {
2019-11-23 20:27:39 +08:00
if (!SpineTestLayer::init()) return false;
2020-01-05 03:09:32 +08:00
_title = "Seperator";
// Spineboy's back, which will manage the animation and GPU resources
// will render only the front slots of Spineboy
skeletonNode = SkeletonAnimation::createWithJsonFile("spineboy-pro.json", "spineboy.atlas", 0.6f);
skeletonNode->setMix("walk", "jump", 0.4);
skeletonNode->setAnimation(0, "walk", true);
skeletonNode->setSlotsRange(skeletonNode->findSlot("rear-upper-arm")->getData().getIndex(), skeletonNode->findSlot("rear-shin")->getData().getIndex());
skeletonNode->setPosition(Vec2(_contentSize.width / 2, 20));
// A simple rectangle to go between the front and back slots of Spineboy
betweenNode = DrawNode::create();
Vec2 rect[4];
rect[0] = Vec2(0, 0);
rect[1] = Vec2(40, 0);
rect[2] = Vec2(40, 200);
rect[3] = Vec2(0, 200);
betweenNode->drawPolygon(rect, 4, Color4F(1, 0, 0, 1), 1, Color4F(1, 0, 0, 1));
betweenNode->setPosition(Vec2(_contentSize.width / 2 + 30, 20));
// Spineboy's front, doesn't manage any skeleton, animation or GPU resources, but simply
// renders the back slots of Spineboy. The skeleton, animatio state and GPU resources
// are shared with the front node!
frontNode = SkeletonRenderer::createWithSkeleton(skeletonNode->getSkeleton());
frontNode->setSlotsRange(frontNode->findSlot("neck")->getData().getIndex(), -1);
frontNode->setPosition(Vec2(_contentSize.width / 2, 20));
// Add the front, between and back node in the correct order to this scene
addChild(skeletonNode);
addChild(betweenNode);
addChild(frontNode);
scheduleUpdate();
SCALE_SKELETON_NODE(skeletonNode);
SCALE_SKELETON_NODE(frontNode);
return true;
}
void SkeletonRendererSeparatorExample::update(float deltaTime) {
// Test releasing memory.
// Director::getInstance()->replaceScene(SpineboyExample::scene());
}
bool SpineboyExample::init() {
if (!SpineTestLayer::init()) return false;
_title = "Spineboy";
skeletonNode = SkeletonAnimation::createWithJsonFile("spineboy-pro.json", "spineboy.atlas", 0.6f);
skeletonNode->setStartListener([](TrackEntry* entry) {
log("%d start: %s", entry->getTrackIndex(), entry->getAnimation()->getName().buffer());
});
skeletonNode->setInterruptListener([](TrackEntry* entry) {
log("%d interrupt", entry->getTrackIndex());
});
skeletonNode->setEndListener([](TrackEntry* entry) {
log("%d end", entry->getTrackIndex());
});
skeletonNode->setCompleteListener([](TrackEntry* entry) {
log("%d complete", entry->getTrackIndex());
});
skeletonNode->setDisposeListener([](TrackEntry* entry) {
log("%d dispose", entry->getTrackIndex());
});
skeletonNode->setEventListener([](TrackEntry* entry, spine::Event* event) {
log("%d event: %s, %d, %f, %s", entry->getTrackIndex(), event->getData().getName().buffer(), event->getIntValue(), event->getFloatValue(), event->getStringValue().buffer());
});
2019-11-23 20:27:39 +08:00
skeletonNode->setMix("walk", "jump", 0.4);
skeletonNode->setMix("jump", "run", 0.4);
skeletonNode->setAnimation(0, "walk", true);
2020-01-05 03:09:32 +08:00
TrackEntry* jumpEntry = skeletonNode->addAnimation(0, "jump", false, 1);
2019-11-23 20:27:39 +08:00
skeletonNode->addAnimation(0, "run", true);
2020-01-05 03:09:32 +08:00
skeletonNode->setTrackStartListener(jumpEntry, [](TrackEntry* entry) {
2019-11-23 20:27:39 +08:00
log("jumped!");
2020-01-05 03:09:32 +08:00
});
2019-11-23 20:27:39 +08:00
// skeletonNode->addAnimation(1, "test", true);
// skeletonNode->runAction(RepeatForever::create(Sequence::create(FadeOut::create(1), FadeIn::create(1), DelayTime::create(5), NULL)));
2020-01-05 03:09:32 +08:00
2019-11-23 20:27:39 +08:00
skeletonNode->setPosition(Vec2(_contentSize.width / 2, 20));
addChild(skeletonNode);
2020-01-05 03:09:32 +08:00
2019-11-23 20:27:39 +08:00
scheduleUpdate();
2020-01-05 03:09:32 +08:00
SCALE_SKELETON_NODE(skeletonNode);
2019-11-23 20:27:39 +08:00
return true;
}
2020-01-05 03:09:32 +08:00
void SpineboyExample::update(float deltaTime) {
2019-11-23 20:27:39 +08:00
// Test releasing memory.
// Director::getInstance()->replaceScene(SpineboyExample::scene());
}
2020-01-05 03:09:32 +08:00
bool TankExample::init() {
2019-11-23 20:27:39 +08:00
if (!SpineTestLayer::init()) return false;
2020-01-05 03:09:32 +08:00
_title = "Tank";
skeletonNode = SkeletonAnimation::createWithBinaryFile("tank-pro.skel", "tank.atlas", 0.5f);
skeletonNode->setAnimation(0, "shoot", true);
2019-11-23 20:27:39 +08:00
skeletonNode->setPosition(Vec2(_contentSize.width / 2 + 400, 20));
addChild(skeletonNode);
2020-01-05 03:09:32 +08:00
SCALE_SKELETON_NODE(skeletonNode);
2019-11-23 20:27:39 +08:00
return true;
}