spine-cpp Runtime Documentation

Licensing

Please see the Spine Runtimes License before integrating the Spine Runtimes into your applications.

Introduction

spine-cpp is a generic runtime for integrating Spine animations in game engines and frameworks written in languages that can natively interface with C++.

spine-cpp provides functionality to:

As the spine-cpp runtime is a generic, engine independent runtime, users are required to implement a set of functions to provide engine specific file i/o and image loading to the spine-cpp runtime, render the data generated by the spine-cpp runtime via the engine's rendering system, and optionally integrate the data with the engine's physics system if more advanced use cases such as rag dolls are required.

The spine-cpp runtime is written using C++11 to guarantee compatibility with a wide range of platforms and compilers.

Other official Spine runtimes are based on spine-cpp and can serve as examples for integration with your engine of choice:

The following sections give a brief, engine independent overview of the spine-cpp runtime and how to use it. Most of the official Spine runtimes based on spine-cpp will encapsulate (parts of) the spine-cpp API in their own, easier to use API. It is still beneficial to understand the basics of the underlying spine-cpp runtime.

Note: This guide assumes you are familiar with the basic runtime architecture and terminology used by Spine. Please also consult the API reference to explore more advanced functions of the runtime.

Exporting Spine assets for spine-cpp

Please follow the instructions in the Spine User Guide on how to:

  1. Export skeleton & animation data to JSON or binary format
  2. Export texture atlases containing the images of your skeleton

An export of the skeleton data and texture atlas of your skeleton will yield the following files:

  1. skeleton-name.json or skeleton-name.skel, containing your skeleton and animation data.
  2. skeleton-name.atlas, containing information about the texture atlas.
  3. One or more .png files, each representing one page of your texture atlas containing the packed images your skeleton uses.

Note: Prefer the .skel binary format for smaller assets and faster loading times.

Note: Instead of creating one texture atlas per skeleton, you may also pack the images of multiple skeletons into a single texture atlas. Please refer to the texture packing guide.

Loading Spine assets

spine-cpp provides APIs to load texture atlases, Spine skeleton data (bones, slots, attachments, skins, animations) and define mix times between animations through animation state data. These three types of data, also known as setup pose data, are generally loaded once and then shared by every game object. The sharing mechanism is achieved by giving each game object its own skeleton and animation state, also known as instance data.

Note: For a more detailed description of the overall loading architecture consult the generic Spine Runtime Documentation.

Loading texture atlases

Texture atlas data is stored in a custom atlas format that describes the location of individual images within atlas pages. The atlas pages themselves are stored as plain .png files next to the atlas.

The Atlas class provides a constructor to load an atlas file from disk and a constructor to load an atlas file from raw in-memory data for this task. If loading the atlas failed, an assert will trigger in debug mode.

#include <spine/spine.h>

using namespace spine;

// Load the atlas from a file. The last argument is engine specific and is responsible
// for loading the image of each texture atlas page and converting it to an engine
// specific texture or material which can be used for rendering.
Atlas* atlas = new Atlas("myatlas.atlas", textureLoader);

// Load the atlas from memory, giving the memory location, the number
// of bytes and the directory relative to which atlas page textures
// should be loaded.
Atlas* atlas = new Atlas(atlasInMemory, atlasDataLengthInBytes, dir, textureLoader);

Loading skeleton data

Skeleton data (bones, slots, constraints, attachments, skins, animations) can be exported to human readable JSON or a binary format. spine-cpp stores skeleton data in SkeletonData class instances.

For loading the skeleton data from a JSON export, we create a SkeletonJson instance, which takes the previously loaded Atlas, set the scale of the skeleton and finally read the skeleton data from the file:

#include <spine/spine.h>

using namespace spine;

// Create a SkeletonJson used for loading and set the scale
// to make the loaded data two times as big as the original data
SkeletonJson json(atlas);
json.setScale(2);

// Load the skeleton .json file into a SkeletonData
SkeletonData* skeletonData = json.readSkeletonDataFile(filename);

// If loading failed, print the error and exit the app
if (!skeletonData) {
   printf("%s\n", json.getError().buffer());
   exit(0);
}

Loading skeleton data from a binary export works the same, except we use a SkeletonBinary instead:

// Create a SkeletonBinary used for loading and set the scale
// to make the loaded data two times as big as the original data
SkeletonBinary binary(atlas);
binary.setScale(2);

// Load the skeleton .skel file into a SkeletonData
SkeletonData* skeletonData = binary.readSkeletonDataFile(filename);

// If loading failed, print the error and exit the app
if (!skeletonData) {
   printf("%s\n", binary.getError().buffer());
   exit(0);
}

Preparing animation state data

Spine supports smooth transitions (crossfades) when switching from one animation to another. The crossfades are achieved by mixing one animation with another for a specific mix time. The spine-cpp runtime provides the AnimationStateData struct to define these mix times:

#include <spine/spine.h>

using namespace spine;

// Create the spAnimationStateData
AnimatonStateData* animationStateData = new AnimationStateData(skeletonData);

// Set the default mix time between any pair of animations in seconds.
animationStateData->setDefaultMix(0.1f);

// Set the mix time between from the "jump" to the "walk" animation to 0.2 seconds,
// overwriting the default mix time for this from/to pair.
animationStateData->setMix("jump", "walk", 0.2f);

The mix times defined in AnimationStateData can also be overwritten explicitly when applying animations (see below).

Skeletons

Setup pose data (skeleton data, texture atlases) are supposed to be shared between game objects. spine-cpp provides the Skeleton class to facilitate this sharing. Every game object receives its own instance of Skeleton which in turn references a SkeletonData and Atlas instance as data sources.

The Skeleton can be freely modified, e.g. by procedurally modifying bones, applying animations or setting attachments and skins specific to a game object, while the underlying skeleton data and texture atlas stay in tact. This way, SkeletonData and Atlas instances can be shared by any amount of game objects.

Creating skeletons

To create a Skeleton instance:

Skeleton* skeleton = new Skeleton(skeletonData);

Every game object will need its own Skeleton. The bulk of the data remains in SkeletonData and Atlas and will be shared by all Skeleton instances to vastly reduce memory consumption and texture switches. The life-time of a Skeleton is thus coupled with the life-time of its corresponding game object.

Bones

A skeleton is a hierarchy of bones, with slots attached to bones, and attachments attached to slots.

Finding bones

All bones in a skeleton have a unique name by which they can be fetched from the skeleton:

// returns 0 if no bone of that name could be found
Bone* bone = skeleton->findBone("mybone");

Local transform

A bone is affected by its parent bone, all the way back to the root bone. E.g. when rotating a bone, all its child bones and all their children are also rotated. To accomplish these hierarchical transformations, each bone has a local transformation relative to its parent bone consisting of:

  • x and y coordinates relative to the parent.
  • rotation in degrees.
  • scaleX and scaleY.
  • shearX and shearY in degrees.

The local transform of a bone can be manipulated procedurally or via applying an animation. The former allows to implement dynamic behavior like having a bone point at a mouse cursor, let feet bones follow the terrain etc. Both procedural modification of the local transform as well as applying animations can be done simultaniously. The end result will be a single combined local transform.

World transform

Once all the local transforms are setup, either through procedurally modifying the local transforms of bones or by applying animations, we need the world transform of each bone for rendering and physics.

The calculation starts at the root bone, and then recursively calculates all child bone world transforms. The calculation also applies IK, transform and path constraints defined by the artist in the Spine editor.

To calculate the world transforms, we need to first update the skeleton's frame time for physics, followed by calculating the actual transforms:

skeleton->update(deltaTime);
skeleton->updateWorldTransform(spine::Physics_Update);

deltaTime specifies the time passed between the current and last frame, given in seconds. The second parameter to spSkeleton_updateWorldTransform specifies if and how physics should be applied. spine::Physics_Update is a good default value.

The result is stored on each bone, and consists of:

  • a, b, c, and d: a 2x2 column major matrix encoding rotation, scale and shear of the bone.
  • worldX, worldY: the world position of the bone.

Note that worldX and worldY are offset by skeleton->getX() and skeleton->getY(). These two properties can be used to position the skeleton in your game engine's world coordinate system.

In general, the bones' world transforms should never be modified directly. Instead, they should always be derived from the local transforms of the bones in the skeleton by calling Skeleton::updateWorldTransform(). The local transforms can be set either procedurally, e.g. setting the rotation of a bone so it points to the mouse cursor, or by applying animations (see below), or both. Once the (procedural) animation is applied, Skeleton::updateWorldTransform() is called and the resulting world transforms are recalculated based on the local transform as well as any constraints that are applied to bones.

Coordinate system conversion

It is often easier to manipulate bones in the world coordinate system, as this is where coordinates from other entities or input events are usually given. However, since the world transform should not be directly manipulated, we need to apply any changes to a bone based on world coordinate system calculations to the local transform of that bone.

The spine-cpp runtimes provides functions to extract rotation and scale information from the 2x2 world transform matrix of a bone, and transform locations and rotations from local space to world space and vice versa. All these functions assume that the bones' world transforms have been calculated before by calling Skeleton::updateWorldTransform():

Bone* bone = skeleton->findBone("mybone");

// Get the rotation of a bone in world space relative to the world space x-axis in degrees
float rotationX = bone->getWorldRotationX();

// Get the rotation of a bone in world space relative to the world space y-axis in degrees
float rotationY = bone->getWorldRotationY();

// Get the scale of a bone in world space relative to the world space x-axis
float scaleX = bone->getWorldScaleX();

// Get the scale of a bone in world space relative to the world space y-axis
float scaleY = bone->getWorldScaleY();

// Transform a position given in world space to a bone's local space
float localX = 0, localY = 0;
bone->worldToLocal(worldX, worldY, localX, localY);

// Transform a position given in a bone's local space to world space
float worldX = 0, worldY = 0;
bone->localToWorld(localX, localY, worldX, worldY);

// Transform a rotation given in the world space to the bone's local space
float localRotationX = bone->worldToLocalRotation(bone)

Note: Your modifications to the local transform of a bone (and thereby all its children) will be reflected in the bone's world transform after the next call to Skeleton::updateWorldTransform().

Positioning

By default, a skeleton is assumed to be at the origin of the game's world coordinate system. To position a skeleton in a game's world coordinate system, you can use the x and y properties:

// make a skeleton follow a game object in world space
skeleton->setX(myGameObject->worldX);
skeleton->setY(myGameObject->worldY);

Note: Your modifications to the skeleton x and y properties will be reflected in the bone world transforms after the next call to Skeleton::updateWorldTransform().

Flipping

A skeleton can be flipped vertically or horizontally. This allows reusing animations made for one direction for the opposing direction, or for working in coordinate systems with the y-axis pointing downwards (Spine assumes y-axis up by default):

// flip vertically around the x-axis
skeleton->setScaleX(-1);

// flip horizontally around the y-axis
skeleton->setScaleY(-1);

Note: Your modifications to the skeleton scaleX and scaleY properties will be reflected in the bone world transforms after the next call to Skeleton::updateWorldTransform().

Setting skins

The artist creating the Spine skeleton may have added multiple skins to the skeleton to add visual variations of the same skeleton, e.g. a female and male version. The spine-cpp runtime stores skins in instances of Skin.

A skin at runtime is a map defining which attachment goes into which slot of the skeleton. Every skeleton has at least one skin which defines which attachment is on what slot in the skeleton's setup pose. Additionally skins have a name to identify them.

Setting a skin on a skeleton via spine-cpp:

// set a skin by name
skeleton->setSkin("my_skin_name");

// set the default setup pose skin by passing NULL
skeleton->setSkin(NULL);

Note: Setting a skin takes into account what skin and hence which attachments have previously been set. Please refer to the generic runtime guide for more information on setting skins.

Setting attachments

spine-cpp allows setting a single attachment on a skeleton's slot directly, e.g. to switch out weapons. The attachment is first searched in the active skin, and if this fails, in the default setup pose skin:

// Set the attachment called "sword" on the "hand" slot
skeleton->setAttachment("hand", "sword");

// Clear the attachment on the slot "hand" so nothing is shown
skeleton->setAttachment(skeleton, "hand", "");

Tinting

You can tint all attachments in a skeleton by setting the skeleton's color:

// tint all attachments with red and make the skeleton half translucent.
skeleton->getColor().set(1, 0, 0, 0.5f);

Note: Colors in spine-cpp are given as RGBA, with values for each channel in the range [0-1].

When rendering a skeleton, the renderer walks though the draw order of slots on the skeleton and renders the currently active attachment on each slot. In addition to the skeleton's color, every slot also has a color which can be manipulated at runtime:

Slot* slot = skeleton->findSlotByName("mySlot");
slot->getColor().set(1, 0, 1, 1);

Note that slot colors can also be animated. If you manually change a slot's color and then apply an animation that keys that slot's color, your manual change will be overwritten.

Applying animations

The Spine editor lets artists create multiple, uniquely named animations. An animation is a set of timelines. Each timeline specifies at what frame what property of a bone or the skeleton should change to what value. There are many different types of timelines, from timelines defining the transform of a bone over time, to timelines that change the drawing order. Timelines are part of the skeleton data and stored in Animation instances within SkeletonData in spine-cpp.

Timeline API

spine-cpp provides a timeline API should the need arise to directly work with timelines. This low-level functionality allows you to fully customize the way animations defined by your artist are applied to a skeleton.

Animation state API

In almost all cases, you should use the animation state API instead of the timeline API. The animation state API makes task such as applying animations over time, queueing animations, mixing between animations, and applying multiple animations at the same time considerably easier than the low-level timeline API. The animation state API uses the timeline API internally and can thus be seen as a wrapper.

spine-cpp represents an animation state via the AnimationState class. Just like skeletons, an AnimationState is instantiated per game object. In general, you will have one Skeleton and one AnimationState instance per game object in your game. And just like Skeleton, the AnimationState will share SkeletonData (wherein animations and their timelines are stored) and AnimationStateData (wherein mix times are stored) with all other AnimationState instances, sourcing the same skeleton data.

Creating animation states

To create an AnimationStateinstance:

AnimationState* animationState = new AnimationState(animationStateData);

The function takes an AnimationStateData which is usually created when the skeleton data is loaded, and which defines the default mix time as well as mix times between specific animations for crossfades.

Tracks & Queueing

An animation state manages one or more tracks. Each track is a list of animations that should be played back in the order they were added to the track. This is known as queuing. Tracks are indexed starting from 0.

You can queue an animation on a track like this:

// Add the animation "walk" to track 0, without delay, and let it loop indefinitely
int track = 0;
bool loop = true;
float delay = 0;
animationState->addAnimation(track, "walk", loop, delay);

You can queue multiple animations at once as a fire and forget way to create animation sequences:

// Start walking (note the looping)
animationState->addAnimation(0, "walk", true, 0);

// Jump after 3 seconds
animationState->addAnimation(0, "jump", false, 3);

// Once we are done jumping, idle indefinitely
animationState->addAnimation(0, "idle", true, 0);

You can also clear all animations queued in a track:

// Clear all animations queued on track 0
animationState->clearTrack(0);

// Clear all animations queued on all tracks
animationState->clearTracks(animationState);

Instead of clearing and adding a new animation to a track, you can call AnimationState::setAnimation(). This will clear all tracks, but remember what the last played back animation was before clearing and crossfade to the newly set animation. This way you can smoothly transition from one animation sequence to the next. You can add more animations to the track after calling AnimationState::setAnimation() by calling AnimationState::addAnimation():

// Whatever is currently playing on track 0, clear the track and crossfade
// to the "shot" animation, which should not be looped (last parameter).
animationState->addAnimation(0, "shot", false, 0);

// After shooting, we want to idle again
animationState->addAnimation(0, "idle", true, 0);

To crossfade to the setup pose of the skeleton from an animation, you can use AnimationState::setEmptyAnimation(), AnimationState::addEmptyAnimation(), where the former clears the current track and crossfades to the skeleton, and the later enqueues a crossfade to the setup pose as part of the animation sequence on the track:

// Whatever is currently playing on track 0, clear the track and crossfade
// to the setup pose for 0.5 seconds (mix time).
animationState->setEmptyAnimation(0, 0.5f);

// Add a crossfade to the setup pose for 0.5 seconds as part of the animation
// sequence in track 0, with a delay of 1 second.
animationState->addEmptyAnimation(0, 0.5f, 1)

For simple games, using a single track is usually enough to achieve your goals. More complex games may want to queue animations on separate tracks, e.g. to simultaniously play back a walk animation while shooting. This is where the real power of Spine comes into play:

// Apply the "walk" animation on track 0 indefinitely.
animationState->setAnimation(0, "walk", true);

// Simultaniously apply a "shot" animation on track 1 once.
animationState->setAnimation(1, "shot", false);

Note that if you apply animations simultaniously like that, animations on the higher track will overwrite animations on the lower track for every value both animations have keyed. For authoring animations, this means to make sure two animations to be played back simultaniously don't key the same values in the skeleton, e.g. the same bone, attachment, color etc. Additive animation blending lets you add the results of timelines on different tracks that effect the same skeleton property.

You can control the mixing of animations on different tracks via track entries

Track Entries

Every time you enqueue an animation on a track of an animation state, the corresponding functions will return an TrackEntry instance. This track entry allows you to further customize both the queued animation, as well as its mixing behaviour with regards to animations on the same or other tracks. See the TrackEntry documentation for the full API.

As an example, lets assume the mix time between a "walk" and "run" animation defined in an AnimationStateData is to high for this specific game object in its current situation. You can modify the mix time between "walk" and "run" ad-hoc, just for this specific queued animation:

// Walk indefinitely
sanimationState->setAnimation(0, "walk", true);

// At some point, queue the run animation. We want to speed up the mixing
// between "walk" and "run" defined in the `AnimationStateData` (let's say 0.5 seconds)
// for this one specific call to be faster (0.1 seconds).
TrackEntry* entry = animationState->addAnimation(0, "run", true, 0);
entry->setMixDuration(0.1f);

You can hold on to the TrackEntry to modify it over time. The TrackEntry will be valid for as long as the animation is queued on that track. Once the animation is completed, the TrackEntry will be deallocated. Any subsequent access will be invalid and likely result in a segfault. You can register a listener to get notified when the animation and hence the track entry are no longer valid.

Events

An animation state generates events while playing back queued animations to notify a listener about changes:

  • An animation started.
  • An animation was interrupted, e.g. by clearing a track.
  • An animation was completed, which may occur multiple times if looped.
  • An animation has ended, either due to interruption or it has completed and is not looped.
  • An animation and its corresponding TrackEntry have been disposed and are no longer valid.
  • A user defined event was fired.

You can listen for these events by registering a function either with the animation state, or with individual TrackEntry instances returned by the animation state.

// Define the function that will be called when an event happens
void callback (AnimationState* state, EventType type, TrackEntry* entry, Event* event) {
   const String& animationName = (entry && entry->getAnimation()) ? entry->getAnimation()->getName() : String("");

   switch (type) {
   case EventType_Start:
      printf("%d start: %s\n", entry->getTrackIndex(), animationName.buffer());
      break;
   case EventType_Interrupt:
      printf("%d interrupt: %s\n", entry->getTrackIndex(), animationName.buffer());
      break;
   case EventType_End:
      printf("%d end: %s\n", entry->getTrackIndex(), animationName.buffer());
      break;
   case EventType_Complete:
      printf("%d complete: %s\n", entry->getTrackIndex(), animationName.buffer());
      break;
   case EventType_Dispose:
      printf("%d dispose: %s\n", entry->getTrackIndex(), animationName.buffer());
      break;
   case EventType_Event:
      printf("%d event: %s, %s: %d, %f, %s\n", entry->getTrackIndex(), animationName.buffer(), event->getData().getName().buffer(), event->getIntValue(), event->getFloatValue(),
            event->getStringValue().buffer());
      break;
   }
   fflush(stdout);
}

// Register the function as a listener on the animation state. It will be called for all
// animations queued on the animation state.
animationState->setListener(myListener);

// Or you can register the function as a listener for events for a specific animation you enqueued
TrackEntry* trackEntry = animationState->setAnimation(0, "walk", true);
trackEntry->setListener(myListener);

User defined events are perfect to mark times in an animation at which sounds should be played back, e.g. foot steps.

Changes made to the animation state within a listener, such as setting a new animation, are not applied to skeletons until the next time AnimationState::apply is called. You can immediately apply the changes within the listener:

void myListener(AnimationState* state, EventType type, TrackEntry* entry, Event* event) {
   if (somecondition) {
      state->setAnimation(0, "run", false);
      state->update(0);
      state->apply(skeleton);
   }
}

Applying animation states

Animation states are inherently time-based. To advance their state, you will need to update them every tick, supplying the amount of time that has passed since the last update in seconds:

state->update(deltaTimeInSeconds);

This will advance the playback of animations on each track, coordinate crossfades and call any listeners you might have registered.

After updating the animation state, you want to apply it to a skeleton to update its bones local transforms, attachments, slot colors, draw order and anything else that can be animated:

state->apply(*skeleton);

With the skeleton posed and animated, you finally update its bones world transforms to prepare it for rendering or physics:

skeleton->update(deltaTime);
skeleton->updateWorldTransform();

Putting it all together

Here's a simplified example of how to put all the above together, from loading and instantiating to applying animations (scroll to see all the code):

// Setup pose data, shared by all skeletons
Atlas* atlas;
SkeletonData* skeletonData;
AnimationStateData* animationStateData;

// 5 skeleton instances and their animation states
Skeleton* skeleton[5];
AnimationState* animationState[5];
char* animationNames[] = { "walk", "run", "shot" };

void setup() {
   // setup your engine so textures can be loaded for atlases, create a window, etc.
   engine_setup();

   // Load the texture atlas
   atlas = new Atlas("spineboy.atlas", MyEngineTextureLoader());
   if (atlas.getPages().size() == 0) {
      printf("Failed to load atlas");
      delete atlas;
      exit(0);
   }

   // Load the skeleton data
   SkeletonJson json(atlas);
   skeletonData = json.readSkeletonDataFile("spineboy.json");
   if (!skeletonData) {
      printf("Failed to load skeleton data");
      delete atlas;
      exit(0);
   }

   // Setup mix times
   animationStateData = new AnimationStateData(skeletonData);
   animationStateData->setDefaultMix(0.5f);
   animationStateDAta->setMix("walk", "run", 0.2f);
   animationStateData->setMix("walk", "shot", 0.1f);
}

void mainLoop() {
   // Create 5 skeleton instances and animation states
   // representing 5 game objects
   for (int i = 0; i < 5; i++) {
      // Create the skeleton and put it at a random position
      Skeleton* skeleton = new Skeleton(skeletonData);
      skeleton->setX(random(0, 200));
      skeleton->setY(random(0, 200));

      // Create the animation state and enqueue a random animation, looping
      AnimationState *animationState = new AnimationState(animationStateData);
      animationState->setAnimation(0, animationNames[random(0, 3)], true);
   }

   while (engine_gameIsRunning()) {
      engine_clearScreen();

      // update the game objects
      for (int i = 0; i < 5; i++) {
         Skeleton* skeleton = skeletons[i];
         AnimationState* animationState = animationStates[i];

         // First update the animation state by the delta time
         animationState->update(engine_getDeltaTime());

         // Next, apply the state to the skeleton
         animationState->apply(skeleton);

       // Update the skeleton's frame time for physics
       skeleton->update(engine_getDeltaTime());

         // Calculate world transforms for rendering
         skeleton->updateWorldTransform();

         // Hand off rendering the skeleton to the engine
         engine_drawSkeleton(skeleton);
      }
   }

   // Dispose of the instance data. Normally you'd do this when
   // a game object is disposed.
   for (int i = 0; i < 5) {
      delete skeletons[i];
      delete animationStates[i];
   }
}

void dispose() {
   // dispose all the shared resources
   delete atlas;
   delete skeletonData;
   delete animationStateData;
}

int main(int argc, char* argv) {
   setup();
   mainLoop();
   dispose();
}

Note the distinction of setup pose data (Atlas, SkeletonData, AnimationStateData) and instance data (Skeleton, AnimationState) and their different life-times.

Memory management

We have tried to make spine-cpp memory management as straight forward as possible. Any class or struct that is allocted via new needs to be deallocated with the corresponding delete. The life-time of class instances depends on what type of class instance it is. General rules of thumb:

  • Create setup pose data shared by instance data (Atlas, SkeletonData, AnimationStateData) at game or level startup, dispose it at game or level end.
  • Create instance data (Skeleton, AnimationState) when the corresponding game object is created, dispose it when the corresponding game object is destroyed.

Track entries (TrackEntry) are valid from a call to one of the enqueuing animation state functions (AnimationState::setAnimation(), AnimationState::addAnimation(), AnimationState::setEmptyAnimation(), AnimationState::addEmptyAnimation()) until the EventType_dispose event is send to your listener. Accessing the track entry after this event will likely result in a segmentation fault.

When creating structs, you often pass in other structs as references. The referencing struct will never dispose the referenced struct. E.g. an Skeleton references a SkeletonData which in turn references an Atlas.

  • Disposing the Skeleton will not dispose the SkeletonData nor the Atlas. This makes sense, as the SkeletonData is likely shared by other Skeletoninstances.
  • Disposing the SkeletonData will not dispose the Atlas. This also makes sense, as the Atlas may be shared by other SkeletonData instances, e.g. if the atlas contains the images of multiple skeletons.

If you use a custom allocator, you can overwrite Spine's allocation strategy by implementing your own SpineExtension. Your custom SpineExtension may derrive from DefaultSpineExtension and should overwrite the _alloc, _calloc, _realloc and _free (inheriting the implementation of _readFile). You can then set the extension with a call to spine::SpineExtension::setInstance() on program startup. Alternatively, if you do not use Spine runtime engine integration, you have to implement the spine::getDefaultExtension() method to provide Spine with an extension that's compatible with the memory management and file management of your engine.

Spine also comes with a simple memory leak detector in the form of a SpineExtension wrapper class called DebugExtension in spine/Debug.h. Wrapping another extension with DebugExtension will enable the debug extension to track your allocations, along with the file locations and line numbers, provided you allocated your Spine objects like this:

Skeleton* skeleton = new (__FILE__, __LINE__) Skeleton(skeletonData);

The __FILE__ and __LINE__ arguments are used by the debug extension to keep track where allocations have happened. When your program, exits, you can let the debug extension print a report to stdout.

#include <spine/Extension.h>
#include <spine/Debug.h>

static DebugExtension *debugExtension = NULL;

// This will be used by Spine to get the initial extension instance.
SpineExtension* spine::getDefaultExtension() {
   // return a default spine extension that uses standard malloc for memory
   // management, and wrap it in a debug extension.
   debugExtension = new DebugExtension(new DefaultSpineExtension());
   return debugExtension;
}

int main (int argc, char** argv) {
   ... your app code allocating Spine objects via `new (__FILE__, __LINE__) SpineClassName()` and deallocating via `delete instance` ...

   debugExtension->reportLeaks
();
}

Integrating spine-cpp in your engine

Integrating the sources

spine-cpp is a set a of C++ header and implementation files located in the spine-cpp/spine-cpp folder of the runtime Git repository. You can either copy the sources into your project or use CMake's FetchContent.

Copy sources

  1. Clone the Spine runtime repository. Use the version branch corresponding with your Spine Editor branch.
  2. Include the files in the spine-cpp/spine-cpp/src/spine folder in your build.
  3. Add the spine-cpp/spine-cpp/include folder to your header search paths.

CMake FetchContent

Starting with Spine version 4.2, you can also use CMake's FetchContent feature to easily integrate the spine-cpp runtime in your project, as shown in the example CMakeLists.txt file below:

cmake_minimum_required(VERSION 3.14)
project(MyProject C)

set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(FETCHCONTENT_QUIET NO)

# Fetch the spine-runtimes repository and make the spine-cpp library available
include(FetchContent)
FetchContent_Declare(
spine-runtimes
GIT_REPOSITORY https://github.com/esotericsoftware/spine-runtimes.git
GIT_TAG 4.2
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(spine-runtimes)
FetchContent_GetProperties(spine-runtimes)
if(NOT spine-runtimes_POPULATED)
   FetchContent_Populate(spine-runtimes)
endif()
add_subdirectory(${spine-runtimes_SOURCE_DIR}/spine-c ${CMAKE_BINARY_DIR}/spine-runtimes)

# Create a simple C++ executable
file(GLOB SOURCES "src/*.cpp")
add_executable(MyExecutable ${SOURCES})
target_include_directories(MyExecutable PRIVATE src/)

# Link the spine-cpp library
target_link_libraries(MyExecutable spine-cpp)

Implementing memory and file I/O

When compiling your project, you may encounter linker errors for the functions that the spine-cpp runtime expects you to implement. For example, compiling with Clang might yield:

Undefined symbols for architecture x86_64:
"spine::getDefaultExtension()", referenced from:
    spine::SpineExtension::getInstance() in libspine-cpp.a(Extension.cpp.o)
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

The linker cannot find the function responsible for returning a SpineExtension. The spine-cpp runtime expects you to implement a class derived from `SpineExtension`` using APIs provided by your engine. The extension is defined in Extension.h.

If you are satisfied with using malloc, free and FILE based file I/O, you can use the DefaultSpineExtension and implement the missing function like this:

#include <spine/Extension.h>

spine::SpineExtension *spine::getDefaultExtension() {
   return new spine::DefaultSpineExtension();
}

Otherwise, derive from either SpineExtension or DefaultSpineExtension and override the _malloc, _calloc, _realloc, _free and _readFile methods.

Implementing a TextureLoader

spine-cpp's Atlas class expects an instance of TextureLoader to load and create an engine-specific texture representation for a single atlas page. The TextureLoader class has two methods: load() to load a method for an atlas page given a path, and unload to dispose of the texture.

The load function may store the texture in the atlas page via assigning it to AtlasPage::texture. This makes it easy later on to get the texture an attachment references via a region in an atlas page.

The load function's path parameter is the path to the page image file, relative to the .atlas file path passed to the Atlas constructor, or relative to the dir parameter of the second Atlas constructor that loads the atlas from meory.

Assume your engine provides the following API to work with textures:

struct Texture {
   // ... OpenGL handle, image data, whatever ...
   int width;
   int height;
};

Texture* engine_loadTexture(const char* file);
void engine_disposeTexture(Texture* texture);

Implementing TextureLoader is then as simple as:

#include <spine/TextureLoader.h>

class MyTextureLoader: public TextureLoader {
   public:
      TextureLoader() { }

      virtual ~TextureLoader() { }

      // Called when the atlas loads the texture of a page.
      virtual void load(AtlasPage& page, const String& path) {
         Texture* texture = engine_loadTexture(path);

         // if texture loading failed, we simply return.
         if (!texture) return;

         // store the Texture on the rendererObject so we can
         // retrieve it later for rendering.
         page.texture = texture;

      }

      // Called when the atlas is disposed and itself disposes its atlas pages.
      virtual void unload(void* texture) {
         // the texture parameter is the texture we stored in the page via page->setRendererObject()
         engine_disposeTexture(texture);
      }
}

The resulting texture loader can then be passed to either of the two Atlas constructors.

Implementing Rendering

Rendering a Spine skeleton involves rendering all currently active region and mesh attachments in the current draw order. The draw order is defined as an array of slots on the skeleton. Additionally, clipping attachments may clip region and mesh attachments.

spine-cpp provides the SkeletonRenderer class to easily generate textured, vertex-colored, blended triangle meshes from a skeleton's active attachments, which you can then render with your engine of choice.

Assume your engine has the following API:

// A single vertex with UV
struct Vertex {
   // Position in x/y plane
   float x, y;

   // UV coordinates
   float u, v;

   // Packed RGBA color
   uint32_t color;
};

enum BlendMode {
   // See http://esotericsoftware.com/git/spine-runtimes/blob/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/BlendMode.java#L37
   // for how these translate to OpenGL source/destination blend modes.
   BLEND_NORMAL,
   BLEND_ADDITIVE,
   BLEND_MULTIPLY,
   BLEND_SCREEN,
}

// Draw the given mesh.
// - vertices is a pointer to an array of Vertex structures
// - indices is a pointer to an array of indices. Consecutive indices of 3 form a triangle.
// - numIndices the number of indices, must be divisble by 3, as there are 3 vertices in a triangle.
// - texture the texture to use
// - blendMode the blend mode to use
void engine_drawMesh(Vertex* vertices, unsigned short* indices, size_t numIndices, Texture* texture, BlendMode blendmode);

The rendering process can then be implemented like this:

// Container to temporarily store vertices
spine::Vector<Vertex> vertices;
// A single SkeletonRenderer instance (assuming rendering is performed single-threaded)
SkeletonRenderer skeletonRenderer;

void drawSkeleton(Skeleton &skeleton) {
   RenderCommand *command = skeletonRenderer.render(skeleton);
   while (command) {
      Vertex vertex;
      float *positions = command->positions;
      float *uvs = command->uvs;
      uint32_t *colors = command->colors;
      uint16_t *indices = command->indices;
      Texture *texture = (Texture *)command->texture;      
      for (int i = 0, j = 0, n = command->numVertices * 2; i < n; ++i, j += 2) {
         vertex.x = positions[j];
         vertex.y = positions[j + 1];
         vertex.u = uvs[j];
         vertex.v = uvs[j + 1];
         vertex.color = colors[i];
         vertices->add(vertex);
      }
      BlendMode blendMode = command->blendMode; // Spine blend mode equals engine blend mode
      engine_drawMesh(vertices->buffer(), command->indices, command->numIndices, texture, blendMode)
      vertices.clear()
      command = command->next;
   }
}

SkeletonRenderer::render() returns a linked list of RenderCommand instances. Each RenderCommand represents a single mesh, consisting of vertices, indices, blend mode and texture. To render a RenderCommand with your engine, transform the vertices into the format you require, set the blend mode and texture, and draw the transformed vertices and indices. Repeat for the remaining render commands, iterating through them via RenderCommand::next.

SkeletonRenderer batches the meshes of multiple attachments into a single RenderCommand if their blend mode and texture is the same, minimizing the number of draw calls in your engine without requiring you to implement batching yourself.

The implementation above supports all Spine features except two-color tinting. Please refer to the spine-glfw runtime for an example implementation using OpenGL.