spine-cpp 运行时文档
Licensing
将官方的Spine运行时整合到你的应用程序之前, 请仔细阅读 Spine运行时许可页面.
开始使用
spine-cpp 是一个通用的 C++ 运行时, 可将 Spine 动画集成到游戏引擎和使用 C++ 接口的框架中.
spine-cpp 提供了如下功能:
- 加载并操作 Spine skeletons 和 texture atlases
- 应用并mix 动画
- 管理并应用skeleton的 皮肤
- 根据当前的 skeleton pose, 槽位&附件状态 处理和计算渲染和物理引擎所需的数据.
Spine-cpp 运行时是一个通用、独立于引擎的运行时, 用户只需通过 TextureLoader 实现加载你所需 texture, 并将渲染指令传入引擎的渲染系统即可.
spine-cpp 运行时遵循 C++11 标准, 与使用纯 C 的 spine-c 公开了完全相同的API.
其他的官方Spine运行时是基于spine-cpp编写的, 因此亦可作为引擎集成的示例以供研究:
- spine-ios - iOS 集成
- spine-flutter - Flutter 集成
- spine-sdl - SDL 集成
- spine-glfw - GLFW 集成
- spine-ue - 虚幻引擎集成
- spine-godot - Godot 集成
集成spine-cpp到项目中
CMake集成 (推荐方式)
将 spine-cpp 集成到你的项目中最简单的方法是通过 CMake 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 头文件即可:
using namespace spine;
手动集成
如果需要手工集成:
- 使用 git (
git clone https://github.com/esotericsoftware/spine-runtimes) 或者 zip 压缩包获取 Spine Runtimes 源码 - 将以下源文件加入项目:
spine-cpp/src目录下的源文件
- 将以下目录引入项目:
spine-cpp/include
最后在代码中引入 spine-cpp 头文件即可:
using namespace spine;
导出适用spine-cpp的Spine资产

请按照Spine用户指南中的操作步骤, 了解如何:
- 将skeleton & 动画数据导出为JSON或二进制格式
- 导出包含skeleton图像的texture atlases
导出的skeleton数据和texture atlas将产生以下文件:

skeleton-name.json或skeleton-name.skel文件, 包含了skeleton和动画数据.skeleton-name.atlas, 包含了texture atlas的相关信息.- 一张或多张
.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
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:
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 构造函数会:
- 解析 atlas 数据
- 在每个 atlas 页上调用你的 TextureLoader
- 根据 texture 引用来设置区域(regions)
加载 skeleton 数据
Skeleton数据(骨骼、槽位、附件、皮肤、动画)可导出为人类可读的 JSON 文件或二进制格式. spine-cpp 将 Skelton 数据存储在 SkeletonData 对象中.
从JSON文件中加载
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);
}
从二进制文件中加载
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 时间:
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 实例引用了 SkeletonData 和 Atlas 实例作为数据源.
Skeleton 可被自由修改, 例如程序化修改 skeleton、应用动画或设置游戏对象的特定附件和皮肤, 而底层skeleton 数据和 texture atlas 不受影响, 如此便能让任意数量的游戏对象高效地实现实例共享.
创建skeletons
每个游戏对象都需要自己的 skeleton 实例. 大部分数据由所有 Skeleton 实例共享, 以最大程度减少内存消耗和 texture 切换开销.
注意: 当不再使用后, 应通过
delete skeleton显式删除 Skeletons.
骨骼(Bones)
Skeleton包含了骨骼的层次结构, 槽位附着于骨骼, 而附件附加于槽位.
查找骨骼
Skeleton中的所有骨骼均以其唯一名称命名:
Bone* bone = skeleton->findBone("mybone");
本地变换(local transform)
一根骨骼受其父骨骼的影响, 该限制可一直追溯自根骨骼. 骨骼继承变换的方式由其 变换继承 设置控制. 每根骨骼都存储了相对于其父骨骼的本地变换, 包括:
- 相对于父骨骼的
x和y位置. rotation角度.scaleX和scaleY.shearX和shearY角度.
通过骨骼的 pose (BoneLocal)访问局部变换:
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)约束.
计算世界变换:
skeleton->updateWorldTransform(Physics_Update);
deltaTime 指定当前帧和上一帧间经过的时间, 单位为秒. 第二个参数用于指定物理行为, 其中 Physics_Update 已经是一个较好的默认值.
世界变换可通过骨骼已应用 pose (BonePose) 访问:
// 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();
请注意, worldX 和 worldY 是 skeleton 的 x 和 y 位置偏移量.
不应直接修改骨骼的世界变换. 且它们应仅通过调用 skeleton->updateWorldTransform() 从 skeleton 的本地变换中获取.
坐标系转换
spine-cpp 提供了在不同坐标系之间转换的函数. 这些函数假设经计算已得出了世界变换:
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:
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 进行垂直或水平翻转. 这样就可以把为某个方向制作的动画用于相反的方向:
skeleton->setScaleY(-1); // Flip vertically
// Or both at once
skeleton->setScale(-1, 1); // Flip horizontally
skeleton->setScale(1, -1); // Flip vertically
对于 Y 轴朝下的坐标系(Spine 默认假设 Y 轴朝上), 请使用此全局设置:
注意: 在下一次调用
skeleton->updateWorldTransform()后, 对 skeleton 的缩放修改才会反映在 skeleton 的世界变换上.
设置皮肤
美术同事可能为 skeleton 添加了多个皮肤, 以丰富同某个 skeleton 的视觉变化, 例如一个包含不同装备的 skeleton. 运行时中的一个皮肤本质是一个映射, 它定义了哪个附件 位于 skeleton 的哪个 槽位.
每个 skeleton 至少有一套 setup pose 中的皮肤. 额外皮肤则以唯一名称来互相区分:
skeleton->setSkin("my_skin_name");
// Set the default setup pose skin
skeleton->setSkin(nullptr);
创建自定义皮肤
可以在运行时混搭组合已有的皮肤来创建自定义皮肤:
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 的槽位中直接分配一张附件, 比如可以用来切换武器:
skeleton->setAttachment("hand", "sword");
// Clear the attachment on the "hand" slot
skeleton->setAttachment("hand", nullptr);
运行时将先在活动皮肤中搜索该附件, 如果搜索失败才会在默认皮肤中搜索.
着色(Tinting)
你可以给 skeleton 中的所有附件着色:
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].
每个槽位也有自己的颜色属性可供运行时操作:
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 状态实例和动画状态实例. 它们与全部其他实例共享着底层的 SkeletonData 和 AnimationStateData 来减少内存开销.
创建动画状态
构造函数接收在加载期间创建的 AnimationStateData, 该实例定义了默认 mix 时间以及特定动画间淡入淡出的 mix 时间.
注意: 当不再需要某个动画状态对象时, 必须显式调用
delete animationState将其删除.
轨道 & 队列
一个动画状态实例管理着一条或多条轨道. 每条轨道本质是一个动画的列表, 这些动画按它们被添加到轨道中的顺序来进行回放. 这一行为被称为队列. 轨道的索引始于0.
可以像这样在某条轨道上队列一段动画:
int track = 0;
bool loop = true;
float delay = 0;
animationState->addAnimation(track, "walk", loop, delay);
可以一次队列多个动画来创建动画序列:
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);
也可以清空一条轨道中的所有动画:
animationState->clearTrack(0);
// Clear all tracks
animationState->clearTracks();
清空轨道, 添加新动画到轨道上, 并从前一段动画过渡到该动画:
animationState->setAnimation(0, "shot", false);
// Queue "idle" to play after "shot"
animationState->addAnimation(0, "idle", true, 0);
过渡到 skeleton 的 setup pose:
animationState->setEmptyAnimation(0, 0.5f);
// Or queue a crossfade to setup pose as part of a sequence
animationState->addEmptyAnimation(0, 0.5f, 0);
更复杂的游戏可能希望在不同的轨道上队列动画:
animationState->setAnimation(0, "walk", true);
// Simultaneously shoot on track 1
animationState->setAnimation(1, "shoot", false);
注意: 高轨道上的动画将覆盖低轨道上的动画, 因此应当注意确保要同时播放的动画别 key 进相同属性.
轨道条目(Track Entries)
每当你在一个动画状态的轨道上入队一个动画, 函数均将返回一个轨道条目实例:
轨道条目就可以进一步定制动画回放实例:
entry.setMixDuration(0.5f);
轨道条目在它所代表的动画播放完成前都是有效的. 只要动画还在播放就可存储并复用轨道条目.也能调用 getCurrent 获取当前正在播放的动画的轨道条目:
事件
一个动画状态实例在播放队列动画时将产生事件, 以通知监听器如下状态更改:
- 动画播放 开始(started).
- 动画播放 中断(interrupted), 例如清空了一条轨道.
- 动画播放 完成(completed), 如果循环播放动画则该事件会多次触发.
- 动画播放 结束(ended), 既可能缘于动画播放中断亦可能是非循环动画播放完成.
- 动画及其对应
TrackEntry已被 释放(disposed) 且不再可用. - 触发了 用户自定义的 事件(event).
你可以注册一个函数来监听这些事件, 这个函数可以注册到动画状态,也可以注册到某个轨道条目实例上. C++11 的 lambdas 表达式写法如下:
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:
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 上:
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 的世界变换后, 便应生成渲染命令:
SkeletonRenderer renderer;
RenderCommand* command = renderer.render(*skeleton);
渲染器会自动处理以下事项:
- 将拥有相同 texture 和 blend 模式的连续区域(region)及网格附件中的三角形合批
- 为剪裁(clipping)附件应用剪裁
- 生成优化后的绘制调用(draw calls)
每个渲染命令包含以下内容:
- 顶点数据(位置、UV 坐标、颜色)
- 三角形索引数据
- 采样来的 texture
- Blend 模式(normal、additive、multiply、screen)
处理渲染命令
将所有渲染命令逐一提交给图形 API:
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 函数:
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-sfml: 基于 SFML 的渲染器
- spine-sdl: 基于 SDL 的渲染器
- spine-glfw: 带有 GLFW 的 OpenGL 渲染器
- spine-ue: 虚幻引擎渲染器
- spine-godot: Godot 渲染器
这些示例项目展示了如何将 spine-cpp 渲染功能集成到不同的图形API和框架中.
内存管理
Spine-cpp 使用标准 C++ 内存管理范式. 任何通过 new 关键字创建的对象都需使用 delete 关键字来删除.
对象的生命周期管理应遵循如下原则:
- 在游戏或关卡启动时创建由实例数据(
Atlas,SkeletonData,AnimationStateData)共享的 setup pose 数据, 在游戏或关卡结束时销毁. - 在创建相应的游戏对象时创建实例数据(
Skeleton,AnimationState), 并在销毁游戏对象时销毁它.
轨道条目(TrackEntry)由 AnimationState 管理, 不应手动删除. 其有效期从动画入队开始, 直到触发销毁(dispose)事件为止.
在创建对象时, 需要传入其他对象的引用. 引用方对象永远不会删除被引用对象:
- 删除
Skeleton不会删除SkeletonData或Atlas. 因为 skeleton 数据很可能被其他 skeleton 实例共享. - 删除
SkeletonData不会删除Atlas. 同样是因为 atlas 可能被多个 skeleton 数据实例共享.
自定义内存分配和文件I/O
spine-cpp 使用扩展系统(Extension)来处理内存分配和文件 I/O. 你可以通过创建自己的 Extension 类来定制这些行为:
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 类, 它可以包装另一个扩展来跟踪内存分配并检测泄漏:
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 运算符:
Skeleton* skeleton = new Skeleton(skeletonData);
// Use:
Skeleton* skeleton = new (__FILE__, __LINE__) Skeleton(skeletonData);
// This allows DebugExtension to report the exact location of allocations
这在开发过程中查找内存泄漏极具价值.