spine-cpp 运行时文档

Licensing

将官方的Spine运行时整合到你的应用程序之前, 请仔细阅读 Spine运行时许可页面.

开始使用

spine-cpp 是一个通用的 C++ 运行时, 可将 Spine 动画集成到游戏引擎和使用 C++ 接口的框架中.

spine-cpp 提供了如下功能:

Spine-cpp 运行时是一个通用、独立于引擎的运行时, 用户只需通过 TextureLoader 实现加载你所需 texture, 并将渲染指令传入引擎的渲染系统即可.

spine-cpp 运行时遵循 C++11 标准, 与使用纯 C 的 spine-c 公开了完全相同的API.

其他的官方Spine运行时是基于spine-cpp编写的, 因此亦可作为引擎集成的示例以供研究:

注意: 本指南已假定你了解基本的运行时架构和Spine术语. 也请查阅API 参考文档以探索运行时的更多高级功能.

集成spine-cpp到项目中

CMake集成 (推荐方式)

将 spine-cpp 集成到你的项目中最简单的方法是通过 CMake FetchContent:

cmake
include(FetchContent)
FetchContent_Declare(
   spine-cpp
   GIT_REPOSITORY https://github.com/esotericsoftware/spine-runtimes.git
   GIT_TAG 4.3
   SOURCE_SUBDIR spine-cpp
)
FetchContent_MakeAvailable(spine-cpp)

# Link against spine-cpp
target_link_libraries(your_target spine-cpp)

这会自动获取并构建 spine-cpp 及其依赖.

最后在代码中引入 spine-cpp 头文件即可:

C++
#include <spine/spine.h>
using namespace spine;

手动集成

如果需要手工集成:

  1. 使用 git (git clone https://github.com/esotericsoftware/spine-runtimes) 或者 zip 压缩包获取 Spine Runtimes 源码
  2. 将以下源文件加入项目:
    • spine-cpp/src 目录下的源文件

  3. 将以下目录引入项目: spine-cpp/include

最后在代码中引入 spine-cpp 头文件即可:

C++
#include <spine/spine.h>
using namespace spine;

导出适用spine-cpp的Spine资产

请按照Spine用户指南中的操作步骤, 了解如何:

  1. skeleton & 动画数据导出为JSON或二进制格式
  2. 导出包含skeleton图像的texture atlases

导出的skeleton数据和texture atlas将产生以下文件:

  1. skeleton-name.jsonskeleton-name.skel 文件, 包含了skeleton和动画数据.
  2. skeleton-name.atlas, 包含了texture atlas的相关信息.
  3. 一张或多张 .png 文件, 每一个文件表示texture atlas中的一页, 每个页则指打包成页的skeleton所引用的图像.

注意: 可以将多个 skeleton 的图像打包成一个 texture atlas. 请见 texture 打包指南.

加载Spine资产

spine-cpp提供了加载 texture atlases, Spine skeleton 数据(骨骼、槽位、附件、皮肤、动画)和通过动画状态数据定义动画间 mix 时间的 API. 这三种类型的数据, 也被称为 setup pose 数据, 通常加载一次后就能被每个游戏对象(game object)共享. 共享机制是通过赋予每个游戏对象其不同的 Skelton 和动画状态来实现的, 亦称其为实例数据.

注意: 关于全局加载架构的更详细描述, 请参考更通用的 Spine 运行时文档.

加载texture atlases

Texture atlas数据以自有的 atlas 格式存储, 它描述了 atlas 页中各图像的位置. Atlas 页本身则以普通的 .png 文件的形式存在 atlas 文件旁边.

spine-cpp 使用 TextureLoader 接口来加载 textures. 你得在你的引擎中自行实现该接口:

实现你的TextureLoader

C++
class MyTextureLoader : public TextureLoader {
public:
   virtual void load(AtlasPage& page, const String& path) {
      // Load texture from path
      void* texture = engine_load_texture(path.buffer());

      // Store the texture in the page
      page.texture = texture;

      // Set texture dimensions (required)
      page.width = texture_width;
      page.height = texture_height;
   }

   virtual void unload(void* texture) {
      // Unload the texture
      engine_unload_texture(texture);
   }
};

加载atlas

实现了 TextureLoader 后便能用它加载 atlas:

C++
// Create your texture loader
MyTextureLoader* textureLoader = new MyTextureLoader();

// Load atlas from file, the textureLoader is retained by the atlas until the atlas is disposed
// Atlas will use the DefaultExtension to load the file from the given path. This assumes
// stdio.h is available on the system.
Atlas* atlas = new Atlas("path/to/skeleton.atlas", textureLoader);

// Or load atlas from memory
const char* atlasData = read_file_to_string("path/to/skeleton.atlas");
Atlas* atlas = new Atlas(atlasData, strlen(atlasData), "path/to/atlas/dir", textureLoader);

Atlas 构造函数会:

  1. 解析 atlas 数据
  2. 在每个 atlas 页上调用你的 TextureLoader
  3. 根据 texture 引用来设置区域(regions)

加载 skeleton 数据

Skeleton数据(骨骼、槽位、附件、皮肤、动画)可导出为人类可读的 JSON 文件或二进制格式. spine-cpp 将 Skelton 数据存储在 SkeletonData 对象中.

从JSON文件中加载

C++
// Create a JSON loader using the atlas
SkeletonJson json(*atlas);

// Optionally set the scale
json.setScale(0.5f); // Scale skeleton to 50%

// Load skeleton data from file
SkeletonData* skeletonData = json.readSkeletonDataFile("path/to/skeleton.json");

// Or load from memory
const char* jsonString = read_file_to_string("path/to/skeleton.json");
SkeletonData* skeletonData = json.readSkeletonData(jsonString);

// Check for errors
if (!skeletonData) {
   printf("Error loading skeleton: %s\n", json.getError().buffer());
   exit(1);
}

从二进制文件中加载

C++
// Create a binary loader using the atlas
SkeletonBinary binary(*atlas);

// Optionally set the scale
binary.setScale(0.5f); // Scale skeleton to 50%

// Load skeleton data from file
SkeletonData* skeletonData = binary.readSkeletonDataFile("path/to/skeleton.skel");

// Or load from memory
unsigned char* binaryData = read_file_to_bytes("path/to/skeleton.skel", &dataLength);
SkeletonData* skeletonData = binary.readSkeletonData(binaryData, dataLength);

// Check for errors
if (!skeletonData) {
   printf("Error loading skeleton: %s\n", binary.getError().buffer());
   exit(1);
}

注意: 二进制格式更适合生产环境, 它比JSON格式加载更快尺寸更小.

准备动画状态数据

当从一个动画切换到另一个动画时, Spine 可进行平滑过渡(淡入淡出). 淡入淡出(crossfades)是通过在指定mix时间内将一个动画与另一个动画混合(mix)来实现的. spine-cpp 运行时提供了 AnimationStateData 类来保存这些 mix 时间:

C++
// Create the animation state data
AnimationStateData* animStateData = new AnimationStateData(skeletonData);

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

// Set the mix time between specific animations, overwriting the default
animStateData->setMix("jump", "walk", 0.2f);

AnimationStateData 中定义的 mix 时间会被应用动画这一操作显式覆盖(见下文).

Skeletons

游戏对象间会共享 Setup pose 数据(skeleton 数据, texture atlases 等). 每个游戏对象都有自己的 Skeleton 实例, Skeleton 实例引用了 SkeletonDataAtlas 实例作为数据源.

Skeleton 可被自由修改, 例如程序化修改 skeleton、应用动画或设置游戏对象的特定附件和皮肤, 而底层skeleton 数据和 texture atlas 不受影响, 如此便能让任意数量的游戏对象高效地实现实例共享.

创建skeletons

C++
Skeleton* skeleton = new Skeleton(skeletonData);

每个游戏对象都需要自己的 skeleton 实例. 大部分数据由所有 Skeleton 实例共享, 以最大程度减少内存消耗和 texture 切换开销.

注意: 当不再使用后, 应通过 delete skeleton 显式删除 Skeletons.

骨骼(Bones)

Skeleton包含了骨骼的层次结构, 槽位附着于骨骼, 而附件附加于槽位.

查找骨骼

Skeleton中的所有骨骼均以其唯一名称命名:

C++
// Returns NULL if no bone of that name exists
Bone* bone = skeleton->findBone("mybone");

本地变换(local transform)

一根骨骼受其父骨骼的影响, 该限制可一直追溯自根骨骼. 骨骼继承变换的方式由其 变换继承 设置控制. 每根骨骼都存储了相对于其父骨骼的本地变换, 包括:

  • 相对于父骨骼的xy 位置.
  • rotation 角度.
  • scaleXscaleY.
  • shearXshearY 角度.

通过骨骼的 pose (BoneLocal)访问局部变换:

C++
Bone* bone = skeleton->findBone("mybone");
BoneLocal& pose = bone->getPose();

// Get local transform properties
float x = pose.getX();
float y = pose.getY();
float rotation = pose.getRotation();
float scaleX = pose.getScaleX();
float scaleY = pose.getScaleY();
float shearX = pose.getShearX();
float shearY = pose.getShearY();

// Modify local transform
pose.setPosition(100, 50);
pose.setRotation(45);
pose.setScale(2, 2);

骨骼的本地变换可随程序代码或应用动画而改变. 两者都可以同时进行, 结果将被合并存储在一个姿态(pose)中.

世界变换(World transform)

一旦设置了所有的本地变换, 无论是通过程序还是通过应用动画来修改骨骼的本地变换, 最终都需要每块骨骼的世界变换来进行渲染和物理计算.

计算从根骨骼开始, 然后递归地计算所有子骨骼的世界变换. 该计算同时计入了 IK变换(transform)路径(path)约束.

计算世界变换:

C++
skeleton->update(deltaTime);
skeleton->updateWorldTransform(Physics_Update);

deltaTime 指定当前帧和上一帧间经过的时间, 单位为秒. 第二个参数用于指定物理行为, 其中 Physics_Update 已经是一个较好的默认值.

世界变换可通过骨骼已应用 pose (BonePose) 访问:

C++
BonePose& applied = bone->getAppliedPose();

// Get world transform matrix components
float a = applied.getA(); // 2x2 matrix encoding
float b = applied.getB(); // rotation, scale
float c = applied.getC(); // and shear
float d = applied.getD();

// Get world position
float worldX = applied.getWorldX();
float worldY = applied.getWorldY();

请注意, worldXworldY 是 skeleton 的 x 和 y 位置偏移量.

不应直接修改骨骼的世界变换. 且它们应仅通过调用 skeleton->updateWorldTransform() 从 skeleton 的本地变换中获取.

坐标系转换

spine-cpp 提供了在不同坐标系之间转换的函数. 这些函数假设经计算已得出了世界变换:

C++
Bone* bone = skeleton->findBone("mybone");
BonePose& applied = bone->getAppliedPose();

// Get world rotation and scale
float rotationX = applied.getWorldRotationX();
float rotationY = applied.getWorldRotationY();
float scaleX = applied.getWorldScaleX();
float scaleY = applied.getWorldScaleY();

// Transform between world and local space
float localX, localY, worldX, worldY;
applied.worldToLocal(worldX, worldY, localX, localY);
applied.localToWorld(localX, localY, worldX, worldY);

// Transform rotations
float localRotation = applied.worldToLocalRotation(worldRotation);
float worldRotation = applied.localToWorldRotation(localRotation);

注意: 在调用 skeleton->updateWorldTransform() 后, 对骨骼(及其所有子骨骼)的本地变换的修改才会反映在骨骼的世界变换上.

定位(Positioning)

默认情况下, 一个skeleton默认位于游戏中世界坐标系的原点. 如需在游戏的世界坐标系中定位 skeleton:

C++
// Make a skeleton follow a game object
skeleton->setX(myGameObject->worldX);
skeleton->setY(myGameObject->worldY);

// Or set both at once
skeleton->setPosition(myGameObject->worldX, myGameObject->worldY);

注意: 在调用 skeleton->updateWorldTransform() 后, 对 skeleton 的位置修改才会反映在 skeleton 的世界变换上.

翻转(Flipping)

可以对 skeleton 进行垂直或水平翻转. 这样就可以把为某个方向制作的动画用于相反的方向:

C++
skeleton->setScaleX(-1); // Flip horizontally
skeleton->setScaleY(-1); // Flip vertically

// Or both at once
skeleton->setScale(-1, 1); // Flip horizontally
skeleton->setScale(1, -1); // Flip vertically

对于 Y 轴朝下的坐标系(Spine 默认假设 Y 轴朝上), 请使用此全局设置:

C++
Bone::setYDown(true); // Affects all skeletons

注意: 在下一次调用 skeleton->updateWorldTransform() 后, 对 skeleton 的缩放修改才会反映在 skeleton 的世界变换上.

设置皮肤

美术同事可能为 skeleton 添加了多个皮肤, 以丰富同某个 skeleton 的视觉变化, 例如一个包含不同装备的 skeleton. 运行时中的一个皮肤本质是一个映射, 它定义了哪个附件 位于 skeleton 的哪个 槽位.

每个 skeleton 至少有一套 setup pose 中的皮肤. 额外皮肤则以唯一名称来互相区分:

C++
// Set a skin by name
skeleton->setSkin("my_skin_name");

// Set the default setup pose skin
skeleton->setSkin(nullptr);

创建自定义皮肤

可以在运行时混搭组合已有的皮肤来创建自定义皮肤:

C++
// Create a new custom skin
Skin* customSkin = new Skin("custom-character");

// Add multiple skins to create a mix-and-match combination
customSkin->addSkin(skeletonData->findSkin("skin-base"));
customSkin->addSkin(skeletonData->findSkin("armor/heavy"));
customSkin->addSkin(skeletonData->findSkin("weapon/sword"));
customSkin->addSkin(skeletonData->findSkin("hair/long"));

// Apply the custom skin to the skeleton
skeleton->setSkin(customSkin);

注意: 不再需要自定义皮肤时, 必须手动使用 delete customSkin 删除它.

Note: 设置皮肤时会影响已激活的附件. 详情请参阅 更换皮肤 一节.

设置附件

可以在 skeleton 的槽位中直接分配一张附件, 比如可以用来切换武器:

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

// Clear the attachment on the "hand" slot
skeleton->setAttachment("hand", nullptr);

运行时将先在活动皮肤中搜索该附件, 如果搜索失败才会在默认皮肤中搜索.

着色(Tinting)

你可以给 skeleton 中的所有附件着色:

C++
// Tint all attachments red with half transparency
skeleton->getColor().set(1.0f, 0.0f, 0.0f, 0.5f);

// Or using individual components
skeleton->getColor().r = 1.0f;
skeleton->getColor().g = 0.0f;
skeleton->getColor().b = 0.0f;
skeleton->getColor().a = 0.5f;

注意: spine-cpp 中的颜色是以 RGBA 的形式给出的, 各通道的取值范围为 [0-1].

每个槽位也有自己的颜色属性可供运行时操作:

C++
Slot* slot = skeleton->findSlot("mySlot");
SlotPose& pose = slot->getPose();
Color& slotColor = pose.getColor();
// The slot color is multiplied with the skeleton color when rendering

动画中也能 key 入槽位颜色. 若你手动更改了一个槽位的颜色, 那么一段 key 了该槽位颜色的动画将会覆盖你手动设置的颜色.

应用动画

美术同事可通过Spine编辑器创建多个命名唯一的 动画. 一段动画其实是一组时间轴. 每条时间轴指定了属性值如何随时间变化, 例如骨骼变换、附件可见性、槽位颜色等.

时间轴 API

Spine-cpp 提供了 时间轴API. 这一底层功能允许完全自由定制应用动画的方式.

动画状态 API

在几乎所有情况下, 都建议使用动画状态API而非时间轴API. 它能够:

  • 按时间推进来应用动画
  • 队列动画
  • 在动画间实现 mix
  • 同时应用多个动画

动画状态 API 内部使用的也是时间轴 API.

spine-cpp用 AnimationState 类表示动画状态. 每个游戏对象有其自己的 skeleton 状态实例和动画状态实例. 它们与全部其他实例共享着底层的 SkeletonDataAnimationStateData 来减少内存开销.

创建动画状态

C++
AnimationState* animationState = new AnimationState(animationStateData);

构造函数接收在加载期间创建的 AnimationStateData, 该实例定义了默认 mix 时间以及特定动画间淡入淡出的 mix 时间.

注意: 当不再需要某个动画状态对象时, 必须显式调用 delete animationState 将其删除.

轨道 & 队列

一个动画状态实例管理着一条或多条轨道. 每条轨道本质是一个动画的列表, 这些动画按它们被添加到轨道中的顺序来进行回放. 这一行为被称为队列. 轨道的索引始于0.

可以像这样在某条轨道上队列一段动画:

C++
// Add "walk" animation to track 0, looping, without delay
int track = 0;
bool loop = true;
float delay = 0;
animationState->addAnimation(track, "walk", loop, delay);

可以一次队列多个动画来创建动画序列:

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

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

// Idle indefinitely after jumping
animationState->addAnimation(0, "idle", true, 0);

也可以清空一条轨道中的所有动画:

C++
// Clear track 0
animationState->clearTrack(0);

// Clear all tracks
animationState->clearTracks();

清空轨道, 添加新动画到轨道上, 并从前一段动画过渡到该动画:

C++
// Clear track 0 and crossfade to "shot" (not looped)
animationState->setAnimation(0, "shot", false);

// Queue "idle" to play after "shot"
animationState->addAnimation(0, "idle", true, 0);

过渡到 skeleton 的 setup pose:

C++
// Clear track 0 and crossfade to setup pose over 0.5 seconds
animationState->setEmptyAnimation(0, 0.5f);

// Or queue a crossfade to setup pose as part of a sequence
animationState->addEmptyAnimation(0, 0.5f, 0);

更复杂的游戏可能希望在不同的轨道上队列动画:

C++
// Walk on track 0
animationState->setAnimation(0, "walk", true);

// Simultaneously shoot on track 1
animationState->setAnimation(1, "shoot", false);

注意: 高轨道上的动画将覆盖低轨道上的动画, 因此应当注意确保要同时播放的动画别 key 进相同属性.

轨道条目(Track Entries)

每当你在一个动画状态的轨道上入队一个动画, 函数均将返回一个轨道条目实例:

C++
TrackEntry& entry = animationState->setAnimation(0, "walk", true);

轨道条目就可以进一步定制动画回放实例:

C++
// Override the mix duration when transitioning to this animation
entry.setMixDuration(0.5f);

轨道条目在它所代表的动画播放完成前都是有效的. 只要动画还在播放就可存储并复用轨道条目.也能调用 getCurrent 获取当前正在播放的动画的轨道条目:

C++
TrackEntry* current = animationState->getCurrent(0);

事件

一个动画状态实例在播放队列动画时将产生事件, 以通知监听器如下状态更改:

  • 动画播放 开始(started).
  • 动画播放 中断(interrupted), 例如清空了一条轨道.
  • 动画播放 完成(completed), 如果循环播放动画则该事件会多次触发.
  • 动画播放 结束(ended), 既可能缘于动画播放中断亦可能是非循环动画播放完成.
  • 动画及其对应TrackEntry已被 释放(disposed) 且不再可用.
  • 触发了 用户自定义的 事件(event).

你可以注册一个函数来监听这些事件, 这个函数可以注册到动画状态,也可以注册到某个轨道条目实例上. C++11 的 lambdas 表达式写法如下:

C++
// Lambda with captured context
MyGameContext* context = getMyGameContext();

auto listener = [context](AnimationState* state, EventType type, TrackEntry* entry, Event* event) {
   switch (type) {
      case EventType_Start:
         printf("Animation %s started\n", entry->getAnimation()->getName().buffer());
         break;
      case EventType_Interrupt:
         printf("Animation interrupted\n");
         break;
      case EventType_End:
         printf("Animation ended\n");
         break;
      case EventType_Complete:
         printf("Animation completed (loops fire this each loop)\n");
         context->onAnimationComplete(); // Access captured context
         break;
      case EventType_Dispose:
         printf("Track entry disposed\n");
         break;
      case EventType_Event:
         // User-defined event from animation
         if (event) {
            const String& name = event->getData().getName();
            printf("Event: %s\n", name.buffer());

            // Access event data
            int intValue = event->getIntValue();
            float floatValue = event->getFloatValue();
            const String& stringValue = event->getStringValue();

            // Handle specific events
            if (name == "footstep") {
               context->playFootstepSound(intValue); // Use int as foot ID
            }
         }
         break;
   }
};

// Register listener for all tracks
animationState->setListener(listener);

// Or register listener for a specific track entry
TrackEntry& entry = animationState->setAnimation(0, "walk", true);
entry.setListener(listener);

// Alternative: inline lambda for simple cases
animationState->setListener([](AnimationState* state, EventType type, TrackEntry* entry, Event* event) {
   if (type == EventType_Complete) {
      printf("Animation loop completed: %s\n", entry->getAnimation()->getName().buffer());
   }
});

// Clear listeners by setting to nullptr
animationState->setListener(nullptr);
entry.setListener(nullptr);

如需处理复杂事件, 可以使用 AnimationStateListenerObject:

C++
class MyAnimationListener : public AnimationStateListenerObject {
   MyGameContext* context;

public:
   MyAnimationListener(MyGameContext* ctx) : context(ctx) {}

   virtual void callback(AnimationState* state, EventType type, TrackEntry* entry, Event* event) override {
      switch (type) {
         case EventType_Start:
            context->onAnimationStart(entry->getAnimation()->getName());
            break;
         case EventType_Event:
            if (event && event->getData().getName() == "attack") {
               context->dealDamage(event->getFloatValue());
            }
            break;
         // Handle other events...
      }
   }
};

// Use the listener object
MyAnimationListener* listener = new MyAnimationListener(context);
animationState->setListener(listener);

// Remember to delete when done
delete listener;

轨道条目在它所代表的动画播放完成前均为有效状态. 在该条目被销毁前, 已注册的监听器都会因事件触发而被调用.

应用动画状态

动画状态本身是基于时间的. 你需要帧间间隔(delta time)来推进动画状态, 并将其应用到 skeleton 上:

C++
// In your game loop
void update(float deltaTime) {
   // Advance the animation state by deltaTime seconds
   animationState->update(deltaTime);

   // Apply the animation state to the skeleton's local transforms
   animationState->apply(*skeleton);

   // Calculate world transforms for rendering
   skeleton->update(deltaTime);
   skeleton->updateWorldTransform(Physics_Update);
}

animationState->update() 会根据帧间间隔推进所有轨道并触发 事件.

animationState->update() 会根据所有轨道的当前状态来设置 skeleton 的局部变换, 包括:

  • 应用单个动画
  • 在动画之间进行淡入淡出
  • 叠加多个轨道的动画

在应用动画之后, 需调用 skeleton->updateWorldTransform() 来计算用于渲染的世界变换

渲染

spine-cpp 提供了渲染器无关的接口来绘制 skeleton. 渲染过程会生成 RenderCommand 对象, 每个对象代表一批带有 blend 模式和 texture 信息的三角形, 可提交给任意图形 API.

渲染命令

在更新 skeleton 的世界变换后, 便应生成渲染命令:

C++
// Using skeleton renderer (reusable for multiple skeletons, not thread-safe)
SkeletonRenderer renderer;
RenderCommand* command = renderer.render(*skeleton);

渲染器会自动处理以下事项:

  • 将拥有相同 texture 和 blend 模式的连续区域(region)及网格附件中的三角形合批
  • 为剪裁(clipping)附件应用剪裁
  • 生成优化后的绘制调用(draw calls)

每个渲染命令包含以下内容:

  • 顶点数据(位置、UV 坐标、颜色)
  • 三角形索引数据
  • 采样来的 texture
  • Blend 模式(normal、additive、multiply、screen)

处理渲染命令

将所有渲染命令逐一提交给图形 API:

C++
// Simplified graphics API for illustration
void render_skeleton(RenderCommand* firstCommand) {
   RenderCommand* command = firstCommand;

   while (command) {
      // Get command data
      float* positions = command->positions;
      float* uvs = command->uvs;
      uint32_t* colors = command->colors;
      uint16_t* indices = command->indices;
      int numVertices = command->numVertices;
      int numIndices = command->numIndices;

      // Get texture and blend mode
      void* texture = command->texture;
      BlendMode blendMode = command->blendMode;

      // Set graphics state
      graphics_bind_texture(texture);
      graphics_set_blend_mode(blendMode);

      // Submit vertices and indices to GPU
      graphics_set_vertices(positions, uvs, colors, numVertices);
      graphics_draw_indexed(indices, numIndices);

      // Move to next command
      command = command->next;
   }
}

Blend模式

根据 Blend 模式配置图形 API 的 blend 函数:

C++
void graphics_set_blend_mode(BlendMode mode, bool premultipliedAlpha) {
   switch (mode) {
      case BlendMode_Normal:
         // Premultiplied: src="/?originalUrl=https%3A%2F%2Fzh.esotericsoftware.com%2FGL_ONE%2C%2520dst%3DGL_ONE_MINUS_SRC_ALPHA%253Cbr">         // Straight: src="/?originalUrl=https%3A%2F%2Fzh.esotericsoftware.com%2FGL_SRC_ALPHA%2C%2520dst%3DGL_ONE_MINUS_SRC_ALPHA%253Cbr">         break;
      case BlendMode_Additive:
         // Premultiplied: src="/?originalUrl=https%3A%2F%2Fzh.esotericsoftware.com%2FGL_ONE%2C%2520dst%3DGL_ONE%253Cbr">         // Straight: src="/?originalUrl=https%3A%2F%2Fzh.esotericsoftware.com%2FGL_SRC_ALPHA%2C%2520dst%3DGL_ONE%253Cbr">         break;
      case BlendMode_Multiply:
         // Both: src="/?originalUrl=https%3A%2F%2Fzh.esotericsoftware.com%2FGL_DST_COLOR%2C%2520dst%3DGL_ONE_MINUS_SRC_ALPHA%253Cbr">         break;
      case BlendMode_Screen:
         // Both: src="/?originalUrl=https%3A%2F%2Fzh.esotericsoftware.com%2FGL_ONE%2C%2520dst%3DGL_ONE_MINUS_SRC_COLOR%253Cbr">         break;
   }
}

实现示例

完整的渲染实现, 可见于:

这些示例项目展示了如何将 spine-cpp 渲染功能集成到不同的图形API和框架中.

内存管理

Spine-cpp 使用标准 C++ 内存管理范式. 任何通过 new 关键字创建的对象都需使用 delete 关键字来删除.

对象的生命周期管理应遵循如下原则:

  • 在游戏或关卡启动时创建由实例数据(Atlas, SkeletonData, AnimationStateData)共享的 setup pose 数据, 在游戏或关卡结束时销毁.
  • 在创建相应的游戏对象时创建实例数据(Skeleton, AnimationState), 并在销毁游戏对象时销毁它.

轨道条目(TrackEntry)由 AnimationState 管理, 不应手动删除. 其有效期从动画入队开始, 直到触发销毁(dispose)事件为止.

在创建对象时, 需要传入其他对象的引用. 引用方对象永远不会删除被引用对象:

  • 删除 Skeleton 不会删除 SkeletonDataAtlas. 因为 skeleton 数据很可能被其他 skeleton 实例共享.
  • 删除 SkeletonData 不会删除 Atlas. 同样是因为 atlas 可能被多个 skeleton 数据实例共享.

自定义内存分配和文件I/O

spine-cpp 使用扩展系统(Extension)来处理内存分配和文件 I/O. 你可以通过创建自己的 Extension 类来定制这些行为:

C++
class MyExtension : public spine::SpineExtension {
public:
   virtual void* _alloc(size_t size, const char* file, int line) override {
      // Your custom allocator
      return my_custom_malloc(size);
   }

   virtual void* _calloc(size_t size, const char* file, int line) override {
      void* ptr = my_custom_malloc(size);
      if (ptr) memset(ptr, 0, size);
      return ptr;
   }

   virtual void* _realloc(void* ptr, size_t size, const char* file, int line) override {
      return my_custom_realloc(ptr, size);
   }

   virtual void _free(void* mem, const char* file, int line) override {
      my_custom_free(mem);
   }

   virtual char* _readFile(const String& path, int* length) override {
      // Your custom file reader
      return my_custom_file_reader(path.buffer(), length);
   }
};

// Set your extension before using spine-cpp
MyExtension* extension = new MyExtension();
spine::SpineExtension::setInstance(extension);

内存泄漏检测

spine-cpp 提供了一个 DebugExtension 类, 它可以包装另一个扩展来跟踪内存分配并检测泄漏:

C++
// Create your base extension (or use the default)
spine::DefaultSpineExtension* baseExtension = new spine::DefaultSpineExtension();

// Wrap it with DebugExtension
spine::DebugExtension* debugExtension = new spine::DebugExtension(baseExtension);
spine::SpineExtension::setInstance(debugExtension);

// ... use spine-cpp normally ...

// Check for leaks
debugExtension->reportLeaks(); // Prints all unfreed allocations
size_t usedMemory = debugExtension->getUsedMemory(); // Get current memory usage

// Clear tracking (useful for resetting between tests)
debugExtension->clearAllocations();

DebugExtension 会跟踪以下内容:

  • 所有分配了内存的文件名和行号
  • 内存使用统计
  • 重复释放(Double-free)检测
  • 未跟踪的内存警告

如需在代码中跟踪 Spine 对象的分配并包含文件和行信息, 请使用 placement new 运算符:

C++
// Instead of:
Skeleton* skeleton = new Skeleton(skeletonData);

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

// This allows DebugExtension to report the exact location of allocations

这在开发过程中查找内存泄漏极具价值.