Spine 骨骼动画入门:skeleton 加载 4.2 版本

其实本篇刚开始是讲的 4.2 版本的,但是软件的原因和部分金币的原因,我决定换成 3.8 版本的,上一篇我们只需要下载 3.8 版本的即可,剩下的库形成是一样的!(我算是白看了,但是不要缺乏从头来过的勇气!)但是这一篇我会继续写完的!
但是说句抱歉的:后面可能就不会写 4.2 版本的了,但是这一篇已经很多知识了,大家要会自己去查阅对应的头文件,这样对于任何库,其实都是可以学会并应用的!
其实到这里我才发现的,有兴趣的可以往下看!

在游戏开发和可视化项目中,Spine 骨骼动画凭借轻量化、高灵活性成为 2D 角色动画的首选方案。但刚接触时,SkeletonData、Texture Atlas、AttachmentLoader等核心类的关系和加载流程极易混淆。本文将以乐高 / 高达拼装为核心比喻,通俗解析 Spine 核心类的作用,并给出QT 框架 环境下的完整加载代码,最终实现 Spine 骨骼Setup Pose(初始绑定姿势) 的加载 —— 这是所有骨骼动画操作的基础,后续的动画播放、姿势修改均基于此展开。
一、核心类通俗解析(乐高拼装对应版)
在开始代码实操前,先把 Spine 加载流程中的核心类按高达拼装的逻辑讲透,每个类的职责、存储的信息都清晰对应,拒绝专业术语绕晕:
| Spine 核心类 / 组件 | 乐高 / 高达拼装对应 | 核心职责 & 存储信息 |
|---|---|---|
TextureLoader | 取件快递员 | 仅负责加载 / 卸载纹理图集,按开发平台(QT/VS/libGDX)的规则实现图片读写,无其他功能;必须实现load(取图)、unload(卸图)方法,相当于不同平台取件的 “身份码”。 |
Texture Atlas(纹理图集) | 高达零件收纳盒 + 贴纸集 | 存储骨骼的所有外观素材,包含两部分:1 张拼接好的大图(所有贴纸 / 素材拼合,减少加载开销)+1 个.atlas文本文件(记录每张小素材的名字、在大图中的位置、尺寸、旋转等切片信息)。 |
SkeletonData | 高达拼装说明书(蓝图) | 存储骨骼的静态只读核心规则(加载后不可修改),包含:①骨骼结构(骨头数量、父子关系、可旋转 / 移动范围);②Slot(贴纸挂钩)与 Atlas 素材的对应关系;③骨骼初始姿势(Setup Pose)、预设动画列表等。无 Atlas 的 SkeletonData 只是 “透明骨骼架子”。 |
AttachmentLoader | 贴贴纸的拼装工具 | 骨骼素材与结构的绑定桥梁,先攥住 Texture Atlas 掌握所有素材信息,再对照 SkeletonData 的规则,完成 “搭透明骨骼架子 + 给 Slot 贴对应素材” 的核心操作。 |
Skeleton | 拼好的可动高达成品 | 基于 SkeletonData 创建的动态可操作实例,是最终开发中唯一需要直接操作的对象;Setup Pose 状态下的 Skeleton 拥有完整外观、正确骨骼结构,支持手动调姿势、后续播放动画,所有动态操作均在该实例上完成。 |
Slot | 骨骼上的贴纸挂钩 | 隶属于骨骼,是素材的 “挂载点”,一个 Slot 同一时间仅能挂载一张素材,多个 Slot 可叠在同一骨骼位置实现素材叠加(如左手戴手套 + 拿枪)。 |
核心关系:SkeletonData定义 “骨骼长什么样、怎么动”,Texture Atlas提供 “骨骼的外观素材”,二者通过AttachmentLoader绑定,最终生成Skeleton成品 —— 缺任一组件,都无法得到完整的可显示骨骼。
二、各个类的详细解析
实际上,我们需要知道:在 TextureLoader 之前有一个注意事项:
SpineExtension 封装的是所有跨平台 / 跨引擎的「底层基础操作」:内存管理 + 文件读取,这两个操作是Spine 运行的根基——任何 Spine 功能(包括 TextureLoader 纹理加载),底层都会依赖这两个操作。
Spine 的默认实现(DefaultSpineExtension)是基于标准 C 库的:
- 内存:malloc/calloc/free;
- 文件读取:fopen/fread/fclose。
但Qt 平台对这两个操作有「兼容性壁垒」,标准 C 库的逻辑在 Qt 里会直接失效!
所以在 Qt 平台下集成 Spine 时,SpineExtension 的自定义实现是必须的,而我们要做的 TextureLoader 继承实现,和这个 SpineExtension 的实现是 Qt 平台适配 Spine 的两个核心步骤,二者缺一不可,且前者是后者的基础(内存 / 文件读取是纹理加载的前置依赖)。
Spine 加载纹理的流程是:
解析.atlas文件(获取纹理图片路径/尺寸)→ 调用TextureLoader.load加载对应图片。如果不自定义 SpineExtension 重写文件读取,第一步解析.atlas 就会失败,TextureLoader 连被调用的机会都没有 —— 这就是「SpineExtension 是 TextureLoader 基础」的核心原因。
我们来看看源码:
// SP_API:Spine 跨平台导出宏,保证类/函数可在动态库/静态库中被外部项目引用
class SP_API SpineExtension {
public:
/**
* @brief 模板静态方法:类型安全的内存分配(封装 _alloc 纯虚函数)
* @tparam T 要分配的对象类型(支持任意自定义类型/内置类型)
* @param num 要分配的对象个数(而非字节数,模板自动计算总字节)
* @param file 调用该函数的源码文件路径(由编译器宏 __FILE__ 传入,调试用)
* @param line 调用该函数的源码行号(由编译器宏 __LINE__ 传入,调试用)
* @return 分配成功返回 T 类型的指针,失败返回 NULL
* @details Spine 内部推荐使用的内存分配接口,自动计算 `sizeof(T) * num` 总字节数,
* 避免手动计算字节导致的类型错误,最终调用当前扩展实例的 _alloc 纯虚函数
*/
template<typename T>
static T *alloc(size_t num, const char *file, int line) {
return (T *) getInstance()->_alloc(sizeof(T) * num, file, line);
}
/**
* @brief 模板静态方法:类型安全的内存分配并初始化(封装 _calloc 纯虚函数)
* @tparam T 要分配的对象类型
* @param num 要分配的对象个数
* @param file 调用文件路径(调试用)
* @param line 调用行号(调试用)
* @return 分配成功返回 T 类型指针(内存字节全置0),失败返回 NULL
* @details 功能等价于 C 标准库 calloc,分配后自动将所有字节初始化为 0,
* 适合初始化数组/结构体,最终调用当前扩展实例的 _calloc 纯虚函数
*/
template<typename T>
static T *calloc(size_t num, const char *file, int line) {
return (T *) getInstance()->_calloc(sizeof(T) * num, file, line);
}
/**
* @brief 模板静态方法:类型安全的内存重分配(封装 _realloc 纯虚函数)
* @tparam T 原内存的对象类型
* @param ptr 已分配的旧内存指针(由 alloc/calloc 分配)
* @param num 重新分配的对象个数
* @param file 调用文件路径(调试用)
* @param line 调用行号(调试用)
* @return 成功返回新内存地址,失败返回 NULL(旧内存地址仍有效,不会释放)
* @details 用于动态调整内存大小(如 Spine 动态数组扩容),自动计算新的总字节数,
* 最终调用当前扩展实例的 _realloc 纯虚函数
*/
template<typename T>
static T *realloc(T *ptr, size_t num, const char *file, int line) {
return (T *) getInstance()->_realloc(ptr, sizeof(T) * num, file, line);
}
/**
* @brief 模板静态方法:类型安全的内存释放(封装 _free 纯虚函数)
* @tparam T 要释放的内存对象类型
* @param ptr 要释放的内存指针(由 alloc/calloc/realloc 分配,不可为野指针)
* @param file 调用文件路径(调试用)
* @param line 调用行号(调试用)
* @details Spine 内部推荐使用的内存释放接口,和 alloc/calloc/realloc 一一对应,
* 最终调用当前扩展实例的 _free 纯虚函数,释放失败会导致内存泄漏
*/
template<typename T>
static void free(T *ptr, const char *file, int line) {
getInstance()->_free((void *) ptr, file, line);
}
/**
* @brief 模板静态方法:内存释放前的预处理(封装 _beforeFree 虚函数)
* @tparam T 要释放的内存对象类型
* @param ptr 即将释放的内存指针
* @details 可选的预处理接口,默认空实现,开发者可重写 _beforeFree 实现自定义逻辑,
* 如:释放对象关联的资源、置空指针、打印释放日志等,在 _free 执行前调用
*/
template<typename T>
static void beforeFree(T *ptr) {
getInstance()->_beforeFree((void *) ptr);
}
/**
* @brief 静态方法:Spine 核心文件读取接口(封装 _readFile 纯虚函数)
* @param path 要读取的文件路径(Spine 内部为 atlas/json/skel 骨骼/图集文件)
* @param length 输出参数:读取到的文件内容字节长度(成功赋值,失败置0)
* @return 成功返回文件内容的字符数组指针(堆内存,需后续调用 free 释放),失败返回 NULL
* @details Spine 加载所有配置/数据文件的统一入口,最终调用当前扩展实例的 _readFile 纯虚函数,
* QT 开发中需重写 _readFile 用 QFile 实现,解决跨平台/中文路径问题
*/
static char *readFile(const String &path, int *length) {
return getInstance()->_readFile(path, length);
}
/**
* @brief 静态方法:手动设置 Spine 扩展实例(单例模式)
* @param inSpineExtension 自定义的 SpineExtension 子类实例指针
* @details 1. 用于手动指定 Spine 使用的扩展实例,替代默认的 getDefaultExtension() 实现;
* 2. 调用时机:需在 Spine 初始化(加载图集/骨骼)之前,否则不生效;
* 3. 生命周期:手动设置的实例需开发者自行管理,Spine 不会自动销毁。
*/
static void setInstance(SpineExtension *inSpineExtension);
/**
* @brief 静态方法:获取当前 Spine 扩展实例(单例模式核心)
* @return 返回 SpineExtension 子类的实例指针(自定义扩展/默认扩展)
* @details 1. 单例逻辑:Spine 全局唯一的扩展实例,所有静态工具方法都通过该实例调用纯虚函数;
* 2. 初始化时机:第一次调用时,若未手动 setInstance(),则自动调用 getDefaultExtension() 获取默认实例;
* 3. 核心作用:屏蔽底层平台实现,Spine 核心库仅通过该实例调用平台相关接口(内存/文件)。
*/
static SpineExtension *getInstance();
/**
* @brief 虚析构函数
* @details 保证子类(如 QtSpineExtension)析构时,按多态规则正确调用子类析构函数,
* 避免基类指针指向子类对象时,析构不彻底导致的内存泄漏
*/
virtual ~SpineExtension();
/// Implement this function to use your own memory allocator
/**
* @brief 纯虚函数:底层内存分配接口(对应 C 标准库 malloc)
* @param size 要分配的**字节数**(非对象个数,上层模板会自动计算)
* @param file 调用文件路径(调试用)
* @param line 调用行号(调试用)
* @return 成功返回内存地址,失败返回 NULL
* @details Spine 所有内存分配的最终底层实现,**必须由开发者重写**,
* 可实现为 C 标准库 malloc/Qt 内存接口/自定义内存池
*/
virtual void *_alloc(size_t size, const char *file, int line) = 0;
/**
* @brief 纯虚函数:底层内存分配并初始化接口(对应 C 标准库 calloc)
* @param size 要分配的**字节数**
* @param file 调用文件路径(调试用)
* @param line 调用行号(调试用)
* @return 成功返回内存地址(字节全置0),失败返回 NULL
* @details **必须由开发者重写**,分配后需保证内存所有字节初始化为 0
*/
virtual void *_calloc(size_t size, const char *file, int line) = 0;
/**
* @brief 纯虚函数:底层内存重分配接口(对应 C 标准库 realloc)
* @param ptr 旧内存地址(由 _alloc/_calloc 分配)
* @param size 重新分配的**字节数**
* @param file 调用文件路径(调试用)
* @param line 调用行号(调试用)
* @return 成功返回新内存地址,失败返回 NULL(旧内存不释放)
* @details **必须由开发者重写**,需保证旧内存数据的正确拷贝(若扩容)
*/
virtual void *_realloc(void *ptr, size_t size, const char *file, int line) = 0;
/// If you provide a spineAllocFunc, you should also provide a spineFreeFunc
/**
* @brief 纯虚函数:底层内存释放接口(对应 C 标准库 free)
* @param mem 要释放的内存地址(由 _alloc/_calloc/_realloc 分配)
* @param file 调用文件路径(调试用)
* @param line 调用行号(调试用)
* @details **必须由开发者重写**,和底层分配接口一一对应(如 malloc 分配对应 free 释放),
* 若分配和释放接口不匹配,会导致内存泄漏/程序崩溃
*/
virtual void _free(void *mem, const char *file, int line) = 0;
/**
* @brief 纯虚函数:底层文件读取接口
* @param path 要读取的文件路径(Spine 封装的 String 类型)
* @param length 输出参数:读取的字节长度(成功赋值,失败置0)
* @return 成功返回堆内存的字符数组指针,失败返回 NULL
* @details Spine 所有文件读取的最终底层实现,**必须由开发者重写**,
* QT 开发中推荐用 QFile 实现,需注意:返回的内存需由 _free 释放
*/
virtual char *_readFile(const String &path, int *length) = 0;
/**
* @brief 虚函数:内存释放前的预处理接口(默认空实现)
* @param ptr 即将释放的内存地址
* @details 可选重写的接口,无纯虚要求,默认空实现(SP_UNUSED(ptr) 屏蔽未使用参数警告),
* 开发者可在此实现自定义预处理逻辑,如释放关联资源、打印日志等
*/
virtual void _beforeFree(void *ptr) { SP_UNUSED(ptr); }
protected:
/**
* @brief 受保护的构造函数
* @details 1. 禁止外部直接实例化基类(SpineExtension 是抽象基类,本就无法实例化);
* 2. 允许子类(如 QtSpineExtension)调用基类构造函数完成初始化;
* 3. 配合单例模式,保证扩展实例仅由 getInstance()/setInstance() 管理。
*/
SpineExtension();
private:
/**
* @brief 私有静态成员:Spine 扩展的全局单例实例
* @details 1. 初始值为 NULL,第一次调用 getInstance() 时初始化;
* 2. 私有属性,禁止外部直接修改,仅能通过 setInstance() 手动设置;
* 3. 所有静态工具方法的最终调用目标,是跨平台扩展的核心载体。
*/
static SpineExtension *_instance;
};
小提示:我们在认识一个库的时候,头文件的查阅是很重要的,特别是 spine 4.2 ,对于 AI 来说,AI 就是笨蛋了,莫向外求,相信自己!当然了,也可以相信我!
开发实操示例:
头文件:qt_spine_extension.h
#ifndef QT_SPINE_EXTENSION_H
#define QT_SPINE_EXTENSION_H
// 引入Spine核心头文件
#include "spine/Extension.h"
#include "spine/SpineString.h"
// QT核心头文件(文件读取/日志)
#include <QFile>
#include <QDebug>
#include <QByteArray>
// SP_API:沿用Spine的跨平台导出宏,保证类能被外部引用
class SP_API QtSpineExtension : public spine::SpineExtension {
public:
// 构造/析构函数
QtSpineExtension();
virtual ~QtSpineExtension() override;
protected:
// 重写SpineExtension的纯虚函数:内存管理四件套
virtual void* _alloc(size_t size, const char* file, int line) override;
virtual void* _calloc(size_t size, const char* file, int line) override;
virtual void* _realloc(void* ptr, size_t size, const char* file, int line) override;
virtual void _free(void* mem, const char* file, int line) override;
// 重写SpineExtension的纯虚函数:QT实现文件读取(核心,加载atlas/json骨骼文件)
virtual char* _readFile(const spine::String& path, int* length) override;
};
// 声明Spine强制要求实现的外部函数(解决链接错误的核心)
extern "C" SP_API spine::SpineExtension* getDefaultExtension();
#endif // QT_SPINE_EXTENSION_H
源文件:qt_spine_extension.cpp
#include "qt_spine_extension.h"
#include <cstdlib>
#include <cstring>
// 构造函数:空实现,仅做基类初始化
QtSpineExtension::QtSpineExtension() : spine::SpineExtension() {}
// 析构函数:虚析构,保证子类析构时多态调用
QtSpineExtension::~QtSpineExtension() {}
// 实现内存分配(对应malloc):用C标准库,兼容Spine内存管理
void* QtSpineExtension::_alloc(size_t size, const char* file, int line) {
void* mem = std::malloc(size);
// 调试可选:打印内存分配信息(定位内存泄漏)
qDebug() << "[Spine内存分配] " << size << "字节 | " << file << "第" << line << "行";
return mem;
}
// 实现内存分配并初始化(对应calloc):分配后字节置0
void* QtSpineExtension::_calloc(size_t size, const char* file, int line) {
void* mem = std::calloc(1, size);
// 调试可选
qDebug() << "[Spine内存分配并初始化] " << size << "字节 | " << file << "第" << line << "行";
return mem;
}
// 实现内存重分配(对应realloc):动态调整内存大小
void* QtSpineExtension::_realloc(void* ptr, size_t size, const char* file, int line) {
void* mem = std::realloc(ptr, size);
// 调试可选
qDebug() << "[Spine内存重分配] " << size << "字节 | " << file << "第" << line << "行";
return mem;
}
// 实现内存释放(对应free):和分配接口一一对应,避免内存泄漏
void QtSpineExtension::_free(void* mem, const char* file, int line) {
if (mem) {
std::free(mem);
// 调试可选
qDebug() << "[Spine内存释放] | " << file << "第" << line << "行";
}
}
// 核心实现:QT原生文件读取(替代C标准库fread,跨平台性更好)
// 用于Spine加载.atlas纹理图集、.json/.skel骨骼数据文件
char* QtSpineExtension::_readFile(const spine::String& path, int* length) {
// 初始化输出参数:读取失败时长度置0
*length = 0;
// 将Spine的String转换为QT的QString路径,解决中文路径兼容问题
QString qPath = QString::fromStdString(path.buffer());
QFile file(qPath);
// 以"只读+二进制"模式打开文件(Spine文件均为二进制/纯文本,二进制模式避免换行符问题)
if (!file.open(QIODevice::ReadOnly | QIODevice::Unbuffered | QIODevice::Text)) {
qWarning() << "[Spine文件读取失败] 路径:" << qPath << " | 原因:" << file.errorString();
return nullptr;
}
// 读取文件所有内容到QT字节数组
QByteArray byteData = file.readAll();
file.close();
// 读取失败判断
if (byteData.isEmpty()) {
qWarning() << "[Spine文件读取失败] 内容为空 | 路径:" << qPath;
return nullptr;
}
// 给Spine返回数据:需在堆上分配内存(Spine会在使用后调用_free释放)
*length = byteData.size();
char* data = static_cast<char*>(_alloc(*length + 1, __FILE__, __LINE__));
std::memcpy(data, byteData.constData(), *length);
data[*length] = '\0'; // 末尾加结束符,兼容C字符串解析
qDebug() << "[Spine文件读取成功] " << *length << "字节 | 路径:" << qPath;
return data;
}
// 全局静态扩展实例:保证单例,生命周期和程序一致(Spine内部管理,无需手动销毁)
static QtSpineExtension g_QtSpineExtension;
// 实现Spine强制要求的getDefaultExtension()函数!!
// 解决undefined reference to spine::getDefaultExtension()的核心代码
namespace spine{
SpineExtension* getDefaultExtension() {
return &g_QtSpineExtension;
}
}
2.1 TextureLoader(取件快递员)
取高达零件的快递员,仅负责按平台规则加载 / 卸载纹理图集的大图,无其他功能;不同平台(QT/VS/libGDX)需实现该类的抽象方法,相当于不同平台取件的 “身份码”。
这个类的本质就是 Spine 中的纯虚基类(virtual class),必须继承后重写抽象方法才能使用,无自定义成员变量,核心是规范图片加载 / 卸载的接口规则。
核心成员方法(均为纯虚方法,必须重写)
| 方法签名(C++) | 方法作用 | 乐高比喻 | 开发注意 |
|---|---|---|---|
virtual void load(AtlasPage &page, const String &path) = 0; | 【取件】按传入的图片路径,加载纹理图集的大图,并将图片资源存入AtlasPage对象 | 快递员按地址去仓库取高达零件收纳盒,将盒子放到指定工作台(AtlasPage) | 需按平台实现图片加载(QT 用QPixmap、VS 用 STB),加载后必须给page设置宽高并存入图片资源 |
virtual void unload(void *texture) = 0; | 【还件】释放load方法加载的图片资源,入参为load中存入AtlasPage的图片资源指针 | 快递员将不用的零件收纳盒送回仓库,释放工作台空间 | 需与load的图片加载方式匹配释放(QT 用delete、STB 用stbi_image_free),避免内存泄漏 |
load方法从文件路径加载一张图片, 并在AtlasPage上设置rendererObject字段.unload方法用于销毁已加载的图片.
例如, 这是加载某个运行时中Texture的示例:
void load (AtlasPage page, String path) {
Texture texture = GameToolkit.loadTexture(path);
page.rendererObject = texture;
}
void unload (Object rendererObject) {
Texture texture = (Texture)rendererObject;
texture.dispose();
}
开发实操示例(QT 版重写)
// 实现QT版TextureLoader(快递员,加载Atlas大图)
class QTTextureLoader : public spine::TextureLoader {
public:
// 重写load:QT平台加载图片为QPixmap
virtual void load(spine::AtlasPage &page, const spine::String &path) override
{
// std::string 对象接收路径
std::string pathStr(path.buffer());
// 转化成 QT 可以识别的字符串
QString qImgPath = QString::fromStdString(pathStr);
// 加载图片为 QPixmap
QPixmap* texturePixmap = new QPixmap(qImgPath);
if (texturePixmap->isNull()) {
qDebug() << "[QTTextureLoader] 图片加载失败:" << qImgPath;
return;
}
// 核心赋值1:将QT原生的纹理对象指针存入AtlasPage的万能指针texture
// page.texture是void*类型,支持跨平台存储任意平台的纹理对象(QT=QPixmap*,VS=STBImage*)
page.texture = texturePixmap;
// 核心赋值2:设置大图的实际像素宽高(Spine必传!)
// Spine依赖这两个值计算AtlasRegion(小素材)在大图中的裁剪/显示位置,缺失会导致渲染错位/空白
page.width = texturePixmap->width(); // 获取QPixmap的实际宽度(像素)
page.height = texturePixmap->height(); // 获取QPixmap的实际高度(像素)
// 7. 调试日志:输出加载成功信息,方便开发排查问题(发布版本可注释)
qDebug() << "[QTTextureLoader-Success] 图片加载成功"
<< "| 路径:" << qImgPath
<< "| 像素尺寸:" << page.width << "x" << page.height;
}
// 重写unload:QT平台释放QPixmap
virtual void unload(void *texture) override
{
// 1. 万能指针强转为QT的QPixmap*类型:和load中存入的类型完全匹配,安全强转
QPixmap* texturePixmap = static_cast<QPixmap*>(texture);
// 2. 安全释放:判空后再删除,避免野指针/重复释放导致的程序崩溃
if (texturePixmap) {
delete texturePixmap; // 释放堆创建的QPixmap对象,回收内存
texturePixmap = nullptr; // 指针置空,防止成为"悬空指针"(指向已释放的内存)
qDebug() << "[QTTextureLoader-Success] 图片资源已释放"; // 调试日志,发布版本可注释
}
// 若texture为null,直接跳过,无操作
}
};
2.2 Texture Atlas(纹理图集)- 含 Atlas/AtlasPage/AtlasRegion
高达零件收纳盒 + 贴纸集,存储骨骼的所有外观素材,是 Spine 优化素材加载的核心组件。
Texture Atlas 并非单一类,而是由3 个关联类组成的素材体系,实际开发中主要操作顶层的Atlas类,底层AtlasPage/AtlasRegion由 Spine 自动管理,核心是 “一张大图 + 一个.atlas 配置文件”。
Atlas:纹理图集主类,对应 “整个零件收纳盒”,管理所有图片页和素材区域;AtlasPage:纹理图集的图片页,对应 “收纳盒的单个隔板”,存储单张大图的资源、宽高、格式;AtlasRegion:纹理图集的素材区域,对应 “隔板上的单个零件 / 贴纸”,存储单张小素材的名字、在大图中的位置、尺寸等切片信息。
层级上:Region 是最下层的小贴纸,Page 是贴贴纸的大隔板,Atlas 是装隔板的收纳盒;
开发上:只需要操作收纳盒(Atlas)拿贴纸,隔板(Page)和贴纸(Region)的摆放 / 整理,全由 Spine 自动搞定。
1. 核心类:Atlas(纹理图集主类)
// SP_API:Spine跨平台导出宏,保证该类在静态库/动态库中能被外部项目正常引用
// 继承SpineObject:Spine所有核心类的基类,提供基础的内存管理/对象标识能力,无实际业务逻辑
class SP_API Atlas : public SpineObject {
public:
/**
* @brief 构造函数1:通过.atlas配置文件路径加载纹理图集
* @param path .atlas配置文件的**完整路径**(相对/绝对),如 "D:/res/hongyan001_a.atlas"
* @param textureLoader 平台纹理加载器指针(如你实现的QTTextureLoader*),Spine通过它加载图集大图
* @param createTexture 是否自动创建平台纹理,默认true(开发中无需修改,让Spine自动加载图片)
* @details Spine会自动读取.atlas文件内容,解析出所有AtlasPage/AtlasRegion,
* 并调用textureLoader->load()加载每张大图的平台原生纹理(如QT的QPixmap)
* @note QT开发中最常用的构造函数,直接传入.atlas文件路径和自定义TextureLoader即可
*/
Atlas(const String &path, TextureLoader *textureLoader, bool createTexture = true);
/**
* @brief 构造函数2:通过内存中的.atlas数据加载纹理图集(进阶用法)
* @param data 内存中存储的.atlas配置文件原始数据(字符数组)
* @param length data数组的有效字节长度
* @param dir .atlas文件所在的目录路径(用于拼接图片的相对路径)
* @param textureLoader 平台纹理加载器指针(如QTTextureLoader*)
* @param createTexture 是否自动创建平台纹理,默认true
* @details 适用于**内存加载/资源打包**场景(如将.atlas文件嵌入程序内存、打包为资源包),
* 无需读取本地.atlas文件,直接从内存解析
* @note QT开发中一般不用,仅做资源加密/打包时使用
*/
Atlas(const char *data, int length, const char *dir, TextureLoader *textureLoader, bool createTexture = true);
/**
* @brief 析构函数:释放Atlas的所有资源
* @details 自动完成3件核心事:
* 1. 遍历所有AtlasPage,调用_textureLoader->unload()释放平台纹理资源(如QT的QPixmap);
* 2. 销毁所有AtlasPage和AtlasRegion对象,回收堆内存;
* 3. 清空_pages和_regions容器,置空_textureLoader指针;
* @note 开发者只需调用delete atlas即可,无需手动释放内部的Page/Region,避免内存泄漏
*/
~Atlas();
/**
* @brief 垂直翻转所有图集纹理的V轴(Y轴)坐标
* @details 适配不同图形API的纹理坐标规范(如OpenGL的纹理V轴和QT/QPainter的V轴方向相反)
* @note 2D Spine骨骼+QT开发中**无需调用**,Spine默认适配QT的坐标体系,调用后会导致素材上下颠倒
*/
void flipV();
/// Returns the first region found with the specified name. This method uses String comparison to find the region, so the result
/// should be cached rather than calling this method multiple times.
/// @return The region, or NULL.
/**
* @brief 根据素材名称查找对应的AtlasRegion(小素材单元)
* @param name 要查找的素材名称(对应.atlas文件中Region的name,如"hongyan_arm")
* @return 找到则返回AtlasRegion*,未找到则返回NULL
* @details 遍历_regions容器,通过**字符串完全匹配**查找首个符合名称的Region
* @warning 性能注意点:字符串遍历匹配效率较低,**多次使用的Region必须缓存结果**,
* 不要反复调用findRegion(如循环中调用),否则会降低程序性能
* @example 正确用法:AtlasRegion* arm = atlas->findRegion("hongyan_arm"); 后续直接用arm指针
*/
AtlasRegion *findRegion(const String &name);
/**
* @brief 获取图集所有AtlasPage(大图页)的容器引用
* @return 引用返回Vector<AtlasPage*>,可直接遍历/读取所有大图页
* @details Vector是Spine自定义的动态数组容器(功能等价于C++的std::vector)
* @note 开发中一般仅**只读遍历**(如自定义渲染时获取所有大图纹理),无需修改容器内容,
* Spine内部会自动管理Page的创建/销毁
*/
Vector<AtlasPage *> &getPages();
/**
* @brief 获取图集所有AtlasRegion(小素材)的容器引用
* @return 引用返回Vector<AtlasRegion*>,可直接遍历/读取所有小素材
* @details 包含所有AtlasPage中的所有小素材,按.atlas文件中的解析顺序存储
* @note 开发中仅**只读使用**,无需手动添加/删除Region,由Spine自动维护
*/
Vector<AtlasRegion *> &getRegions();
private:
/**
* @brief 存储图集所有大图页的动态数组(Spine自定义Vector,等价于std::vector<AtlasPage*>)
* @details 每个元素对应一张拼接大图的AtlasPage对象,由Spine解析.atlas文件时自动创建并添加
* @note 私有成员,开发者通过public方法getPages()访问,无需直接操作
*/
Vector<AtlasPage *> _pages;
/**
* @brief 存储图集所有小素材的动态数组(Spine自定义Vector,等价于std::vector<AtlasRegion*>)
* @details 包含所有大图页中的所有小素材,是Spine纹理图集的**最小素材单元集合**
* @note 私有成员,开发者通过public方法getRegions()/findRegion()访问,无需直接操作
*/
Vector<AtlasRegion *> _regions;
/**
* @brief 平台纹理加载器的指针(如QTTextureLoader*)
* @details 由构造函数传入,Atlas内部通过它调用load()加载纹理、unload()释放纹理
* @note 私有成员,Atlas析构时不会销毁该指针(TextureLoader由开发者创建和管理)
*/
TextureLoader *_textureLoader;
/**
* @brief Atlas的核心私有加载方法:实际解析.atlas数据的逻辑实现
* @param begin .atlas配置数据的起始字符指针
* @param length .atlas数据的有效字节长度
* @param dir .atlas文件所在目录(用于拼接图片相对路径)
* @param createTexture 是否自动创建平台纹理
* @details 两个公有构造函数最终都会调用该方法,完成**统一的.atlas数据解析**:
* 1. 按Spine的.atlas语法解析数据,提取Page/Region的配置信息;
* 2. 创建AtlasPage/AtlasRegion对象,初始化其属性;
* 3. 若createTexture为true,调用_textureLoader->load()加载平台纹理;
* 4. 将创建的Page/Region添加到_pages/_regions容器中;
* @note 私有方法,开发者无法直接调用,由Spine内部自动执行
*/
void load(const char *begin, int length, const char *dir, bool createTexture);
};
2. 底层类:AtlasPage(图集图片页)
// SP_API:Spine 定义的跨平台导出宏,保证该类在动态库/静态库中能被外部正常引用
// 继承 SpineObject:Spine 所有核心类的基类,一般提供基础的内存管理/对象标识能力,无实际业务逻辑
class SP_API AtlasPage : public SpineObject {
public:
/**
* @brief 当前图集大图页的唯一名称
* @details 对应 .atlas 配置文件中每个图片页的名称(如 hongyan001_a.png)
* @note Spine 内部用于区分多个 AtlasPage,开发者调试时可通过该名称定位具体大图
*/
String name;
/**
* @brief 当前大图的文件路径(相对/绝对)
* @details 由 Spine 从 .atlas 文件中自动解析而来
* @note TextureLoader 加载图片时,底层就是使用该路径获取图片地址
*/
String texturePath;
/**
* @brief 图片的像素存储格式(Format 是 Spine 自定义枚举)
* @details 决定图片在内存中红、绿、蓝、透明通道的存储方式和位宽
* @note 构造函数默认 Format_RGBA8888(红/绿/蓝/透明各8位),QT 加载的 PNG/JPG 均为此格式,无需修改
*/
Format format;
/**
* @brief 纹理缩小过滤模式(TEXTURE_FILTER_ENUM 是 Spine 自定义过滤枚举)
* @details 图片显示尺寸 < 实际尺寸时,GPU 的像素采样方式
* @note 默认 TextureFilter_Nearest(邻近过滤),2D 骨骼用此模式边缘更清晰,无模糊感
*/
TEXTURE_FILTER_ENUM minFilter;
/**
* @brief 纹理放大过滤模式(TEXTURE_FILTER_ENUM 是 Spine 自定义过滤枚举)
* @details 图片显示尺寸 > 实际尺寸时,GPU 的像素采样方式
* @note 默认 TextureFilter_Nearest(邻近过滤),若需平滑模糊效果可改为 Linear 线性过滤
*/
TEXTURE_FILTER_ENUM magFilter;
/**
* @brief 纹理U轴(X轴)环绕模式(TextureWrap 是 Spine 自定义环绕枚举)
* @details 纹理坐标超出 [0,1] 范围时,U轴的像素填充规则
* @note 默认 TextureWrap_ClampToEdge(边缘夹紧),超出部分显示图片边缘像素,Spine 图集无越界场景,无需修改
*/
TextureWrap uWrap;
/**
* @brief 纹理V轴(Y轴)环绕模式(TextureWrap 是 Spine 自定义环绕枚举)
* @details 纹理坐标超出 [0,1] 范围时,V轴的像素填充规则
* @note 默认 TextureWrap_ClampToEdge(边缘夹紧),与 uWrap 配合,保证图集素材无重复/扭曲
*/
TextureWrap vWrap;
/**
* @brief 大图的实际像素宽度/高度
* @details 加载图片后必须手动赋值(如 QT 中通过 QPixmap::width()/height() 获取)
* @note Spine 核心依赖属性!用于计算 AtlasRegion(小素材)在大图中的裁剪/显示位置,缺失会导致渲染错位/空白
*/
int width, height;
/**
* @brief 是否启用预乘alpha通道(Premultiplied Alpha)
* @details 标记 RGB 颜色通道是否已与 Alpha 透明通道提前相乘
* @note 默认 false(未预乘),QT 原生 QPixmap/普通图片均为未预乘格式,修改为 true 会导致透明区域显示异常
*/
bool pma;
/**
* @brief 当前 AtlasPage 在 Atlas 所有页面列表中的索引序号
* @details 从 0 开始计数,Spine 内部用于遍历、管理多个大图页
* @note 由 Spine 自动赋值,开发者无需关注和修改
*/
int index;
/**
* @brief 跨平台万能纹理指针,存储平台原生的纹理资源对象
* @details void* 类型支持适配所有开发平台,是 Spine 跨平台的核心设计
* @note QT 场景下:赋值为堆创建的 QPixmap* 指针;释放时强转为 QPixmap* 后 delete
* 其他平台:VS=STBImage* / libGDX=Texture* / Unity=Texture2D*
*/
void *texture;
/**
* @brief AtlasPage 构造函数,显式创建(禁止隐式类型转换)
* @param inName 当前大图页的名称,作为构造唯一入参
* @details 初始化所有成员变量的默认值,避免野值,由 Spine 解析 .atlas 文件时自动调用
* @note 宽高 width/height、纹理指针 texture 会在 TextureLoader::load() 中被重新赋值
*/
explicit AtlasPage(const String &inName)
: name(inName) // 初始化大图页名称为传入的 inName
, format(Format_RGBA8888) // 像素格式默认:RGBA8888(色彩最完整,兼容性最好)
, minFilter(TextureFilter_Nearest) // 缩小过滤默认:邻近过滤(2D骨骼边缘清晰)
, magFilter(TextureFilter_Nearest) // 放大过滤默认:邻近过滤(2D骨骼边缘清晰)
, uWrap(TextureWrap_ClampToEdge) // U轴环绕默认:边缘夹紧(无越界重复)
, vWrap(TextureWrap_ClampToEdge) // V轴环绕默认:边缘夹紧(无越界重复)
, width(0) // 宽度默认0,加载图片后由 TextureLoader 赋值
, height(0) // 高度默认0,加载图片后由 TextureLoader 赋值
, pma(false) // 预乘alpha默认关闭,适配QT原生图片
, index(0) // 索引默认0,由 Spine 自动管理赋值
, texture(NULL) // 纹理指针默认空,加载图片后由 TextureLoader 赋值
{
// 构造函数无额外逻辑,仅做成员初始化
}
};
3. 底层类:AtlasRegion(图集素材区域)
// ===================== 纹理区域基类:TextureRegion =====================
// SP_API:Spine 跨平台导出宏,保证类可在动态库/静态库中被外部项目引用
// 继承 SpineObject:Spine 基础对象基类,统一管理核心对象的内存与生命周期
class SP_API TextureRegion : public SpineObject {
public:
/**
* @brief 渲染器关联对象(平台相关,核心渲染桥接属性)
* @details 存储与当前平台渲染器绑定的纹理对象指针,不同平台类型不同:
* - QT/OpenGL 平台:指向 QOpenGLTexture* 或 OpenGL 纹理ID(GLuint);
* - 其他平台:可指向 DirectX 纹理、Cocos 纹理等;
* 作用:Spine 渲染时通过该指针绑定底层实际纹理,是跨平台渲染的核心桥接属性。
*/
void *rendererObject;
/**
* @brief 纹理UV坐标 - 左上角U值(纹理归一化坐标,范围 0.0 ~ 1.0)
* @details UV坐标是纹理映射的核心,将纹理的像素坐标转换为归一化坐标(与纹理实际尺寸无关),
* u 对应纹理的水平方向,v 对应垂直方向,(u, v) 确定纹理左上角的采样位置。
*/
float u, v;
/**
* @brief 纹理UV坐标 - 右下角U2/V2值(纹理归一化坐标,范围 0.0 ~ 1.0)
* @details (u2, v2) 确定纹理右下角的采样位置,与 (u, v) 配合,定义完整的纹理采样区域,
* 渲染时通过这四个值完成「纹理坐标→屏幕坐标」的映射。
*/
float u2, v2;
/**
* @brief 纹理旋转角度(单位:度)
* @details 标识该纹理区域是否被旋转(仅支持 90/180/270 等90度整数倍旋转),
* Spine 打包图集时为了节省空间,会自动旋转部分子纹理,该属性用于渲染时反向旋转还原。
*/
int degrees;
/**
* @brief 纹理渲染偏移X(像素单位)
* @details 补偿纹理的绘制偏移,解决「纹理实际尺寸」与「骨骼附件期望尺寸」不一致的问题,
* 渲染时会将纹理在X方向偏移该值,保证骨骼动画的绘制位置准确。
*/
float offsetX, offsetY;
/**
* @brief 纹理区域的实际显示宽/高(像素单位)
* @details 该纹理区域最终在屏幕上绘制的尺寸,可能与原始纹理尺寸不同(如缩放、裁剪后),
* 是 Spine 渲染时的实际绘制尺寸。
*/
int width, height;
/**
* @brief 纹理的原始宽/高(像素单位)
* @details 纹理未经过任何缩放、裁剪、旋转的原始尺寸,用于骨骼动画的尺寸计算、锚点定位,
* 与 width/height 配合,解决纹理变形后的原始尺寸还原问题。
*/
int originalWidth, originalHeight;
/**
* @brief 构造函数:默认无参构造
* @details 所有属性均初始化为0/NULL,保证对象创建时的属性一致性,
* Spine 加载纹理/图集时会自动为各属性赋值,开发者无需手动调用。
*/
TextureRegion(): rendererObject(NULL), u(0), v(0), u2(0), v2(0), degrees(0), offsetX(0), offsetY(0), width(0), height(0), originalWidth(0), originalHeight(0) {};
/**
* @brief 析构函数:空实现
* @details 纹理区域本身不管理 rendererObject 的内存(由平台渲染器/图集管理),
* 仅做对象析构的占位,无实际释放逻辑。
*/
~TextureRegion() {};
};
// ===================== 图集子纹理区域类:AtlasRegion =====================
// SP_API:Spine 跨平台导出宏,保证类可在动态库/静态库中被外部项目引用
// 继承 TextureRegion:复用基类所有纹理坐标、尺寸、渲染偏移等核心属性,仅扩展图集相关专属属性
class SP_API AtlasRegion : public TextureRegion {
public:
/**
* @brief 所属的图集页面指针
* @details 一个 Spine 纹理图集(.atlas 文件)会被拆分为多个「图集页面(AtlasPage)」,
* 每个页面对应一张实际的纹理图片(PNG/JPG);该属性关联当前子纹理所属的页面,
* 渲染时通过 page 找到底层实际的 rendererObject(纹理对象)完成绑定。
*/
AtlasPage *page;
/**
* @brief 子纹理的唯一名称
* @details 与 .atlas 配置文件中定义的子纹理名称完全一致,是 Spine 骨骼附件匹配纹理的核心标识,
* 骨骼动画中,每个附件会通过该名称找到对应的 AtlasRegion 进行渲染。
*/
String name;
/**
* @brief 子纹理在图集页面中的索引(序号)
* @details 同一图集页面下的所有子纹理,按加载顺序分配唯一整数值索引,
* 用于 Spine 内部的快速查找与数组管理,开发者无需手动修改。
*/
int index;
/**
* @brief 子纹理在图集页面中的左上角像素坐标(X/Y)
* @details 基于图集页面原始纹理的像素坐标,标识子纹理在大图集中的左上角位置,
* Spine 加载图集时会根据 .atlas 文件自动计算并赋值,同时转换为基类的 UV 坐标(u/v/u2/v2)。
*/
int x, y;
/**
* @brief 9宫格分割参数(Spine 专用,动态int数组)
* @details 存储格式:[上, 下, 左, 右],单位为像素;用于实现纹理的9宫格自适应拉伸(如按钮、边框),
* 表示纹理四个方向的「保留区域」,拉伸时仅拉伸中间区域,边缘保持不变;无需9宫格时数组为空。
*/
Vector<int> splits;
/**
* @brief 9宫格填充参数(Spine 专用,动态int数组)
* @details 存储格式:[上, 下, 左, 右],单位为像素;与 splits 配合使用,
* 补偿9宫格分割后的纹理偏移、空白问题,保证拉伸后的纹理显示完整;无需9宫格时数组为空。
*/
Vector<int> pads;
/**
* @brief 子纹理附加名称集合(动态字符串数组)
* @details 存储该子纹理的别名、关联名称,支持「一个纹理对应多个骨骼附件名称」的多匹配场景,
* 由 .atlas 文件自动加载,开发者极少手动操作。
*/
Vector <String> names;
/**
* @brief 子纹理附加浮点参数集合(动态浮点数组)
* @details Spine 为扩展预留的自定义参数位,可存储透明度、缩放比例、旋转系数等浮点值,
* 若 .atlas 文件中未定义,该数组保持为空。
*/
Vector<float> values;
};
2.3 SkeletonData(高达拼装说明书 / 蓝图)
高达拼装说明书,存储骨骼的静态只读核心规则(加载后不可修改),无纹理图集的SkeletonData只是 “透明的骨骼架子”。
该类是 Spine 骨骼动画的核心根数据类,用于存储骨骼的设置姿势(Setup Pose) 和所有无状态基础数据(骨骼、插槽、皮肤、动画、约束等),是从 Spine 编辑器导出的 .json/.skel 文件解析后的核心数据载体,全程只读无状态(仅存储数据,不处理动画逻辑),注释贴合 Spine 数据加载、骨骼实例化的实际使用逻辑:
/// Stores the setup pose and all of the stateless data for a skeleton.
/// 存储骨骼的【设置姿势(默认初始姿势)】和所有【无状态核心数据】,全程只读,不参与动画运行时计算
class SP_API SkeletonData : public SpineObject {
// 友元类:仅允许骨骼二进制解析器、JSON解析器、骨骼实例类访问私有成员
// 保证数据仅由Spine内部解析赋值,开发者无需手动修改私有属性
friend class SkeletonBinary;
friend class SkeletonJson;
friend class Skeleton;
public:
/// @brief 构造函数:初始化所有属性为默认值,创建空的骨骼数据容器
SkeletonData();
/// @brief 析构函数:释放所有内部管理的动态资源(骨骼、插槽、动画等),避免内存泄漏
~SkeletonData();
/// Finds a bone by comparing each bone's name.
/// It is more efficient to cache the results of this method than to call it multiple times.
/// @return May be NULL.
/**
* @brief 根据骨骼名称查找骨骼数据
* @param boneName 要查找的骨骼名称(与Spine编辑器中定义一致)
* @return 找到返回BoneData*,未找到返回NULL
* @details 线性遍历骨骼数组匹配名称,性能一般;建议多次使用时缓存结果,避免重复调用
*/
BoneData *findBone(const String &boneName);
/// @return May be NULL.
/**
* @brief 根据插槽名称查找插槽数据
* @param slotName 要查找的插槽名称
* @return 找到返回SlotData*,未找到返回NULL
*/
SlotData *findSlot(const String &slotName);
/// @return May be NULL.
/**
* @brief 根据皮肤名称查找皮肤数据
* @param skinName 要查找的皮肤名称(默认皮肤名称为"default")
* @return 找到返回Skin*,未找到返回NULL
*/
Skin *findSkin(const String &skinName);
/// @return May be NULL.
/**
* @brief 根据事件名称查找事件数据
* @param eventDataName 要查找的事件名称(Spine编辑器中定义的动画事件)
* @return 找到返回EventData*,未找到返回NULL
*/
spine::EventData *findEvent(const String &eventDataName);
/// @return May be NULL.
/**
* @brief 根据动画名称查找动画数据
* @param animationName 要查找的动画名称(如"idle"、"walk")
* @return 找到返回Animation*,未找到返回NULL
* @details 骨骼动画运行时,通过该方法获取动画数据并赋值给Skeleton实例
*/
Animation *findAnimation(const String &animationName);
/// @return May be NULL.
/**
* @brief 根据名称查找IK约束数据
* @param constraintName 要查找的IK约束名称
* @return 找到返回IkConstraintData*,未找到返回NULL
*/
IkConstraintData *findIkConstraint(const String &constraintName);
/// @return May be NULL.
/**
* @brief 根据名称查找变换约束数据
* @param constraintName 要查找的变换约束名称
* @return 找到返回TransformConstraintData*,未找到返回NULL
*/
TransformConstraintData *findTransformConstraint(const String &constraintName);
/// @return May be NULL.
/**
* @brief 根据名称查找路径约束数据
* @param constraintName 要查找的路径约束名称
* @return 找到返回PathConstraintData*,未找到返回NULL
*/
PathConstraintData *findPathConstraint(const String &constraintName);
/// @return May be NULL.
/**
* @brief 根据名称查找物理约束数据(Spine高版本新增)
* @param constraintName 要查找的物理约束名称
* @return 找到返回PhysicsConstraintData*,未找到返回NULL
*/
PhysicsConstraintData *findPhysicsConstraint(const String &constraintName);
/**
* @brief 获取骨骼数据的名称
* @return 骨骼名称(与Spine编辑器中骨骼文件名称一致)
*/
const String &getName();
/**
* @brief 设置骨骼数据的名称
* @param inValue 要设置的骨骼名称
*/
void setName(const String &inValue);
/// The skeleton's bones, sorted parent first. The root bone is always the first bone.
/**
* @brief 获取骨骼数据数组(按父骨骼优先排序,根骨骼始终是第一个元素)
* @return 骨骼数据动态数组引用Vector<BoneData *>
* @details 数组顺序严格遵循「父骨骼在前,子骨骼在后」,保证骨骼实例化时能正确建立父子关系
*/
Vector<BoneData *> &getBones();
/**
* @brief 获取插槽数据数组(按设置姿势的绘制顺序排序)
* @return 插槽数据动态数组引用Vector<SlotData *>
* @details 插槽顺序决定了骨骼附件的绘制层级,靠前的插槽先绘制,靠后的后绘制(覆盖上层)
*/
Vector<SlotData *> &getSlots();
/// All skins, including the default skin.
/**
* @brief 获取所有皮肤数据数组(包含默认皮肤)
* @return 皮肤数据动态数组引用Vector<Skin *>
*/
Vector<Skin *> &getSkins();
/// The skeleton's default skin.
/// By default this skin contains all attachments that were not in a skin in Spine.
/// @return May be NULL.
/**
* @brief 获取骨骼的默认皮肤
* @return 默认皮肤指针Skin*,无默认皮肤返回NULL
* @details 默认皮肤包含所有在Spine编辑器中「未分配到自定义皮肤」的附件,
* 骨骼实例化后,若未手动设置皮肤,会自动使用默认皮肤
*/
Skin *getDefaultSkin();
/**
* @brief 设置骨骼的默认皮肤
* @param inValue 要设置的默认皮肤指针
*/
void setDefaultSkin(Skin *inValue);
/**
* @brief 获取所有动画事件数据数组
* @return 事件数据动态数组引用Vector<EventData *>
*/
Vector<spine::EventData *> &getEvents();
/**
* @brief 获取所有动画数据数组
* @return 动画数据动态数组引用Vector<Animation *>
* @details 包含骨骼的所有动画(如待机、行走、攻击),由Spine编辑器导出时定义
*/
Vector<Animation *> &getAnimations();
/**
* @brief 获取所有IK约束数据数组
* @return IK约束数据动态数组引用Vector<IkConstraintData *>
*/
Vector<IkConstraintData *> &getIkConstraints();
/**
* @brief 获取所有变换约束数据数组
* @return 变换约束数据动态数组引用Vector<TransformConstraintData *>
*/
Vector<TransformConstraintData *> &getTransformConstraints();
/**
* @brief 获取所有路径约束数据数组
* @return 路径约束数据动态数组引用Vector<PathConstraintData *>
*/
Vector<PathConstraintData *> &getPathConstraints();
/**
* @brief 获取所有物理约束数据数组(Spine高版本新增)
* @return 物理约束数据动态数组引用Vector<PhysicsConstraintData *>
*/
Vector<PhysicsConstraintData *> &getPhysicsConstraints();
/**
* @brief 获取骨骼的默认X坐标(设置姿势下的根骨骼X偏移)
* @return X坐标值(像素/世界单位,由Spine编辑器导出)
*/
float getX();
/**
* @brief 设置骨骼的默认X坐标
* @param inValue 要设置的X坐标值
*/
void setX(float inValue);
/**
* @brief 获取骨骼的默认Y坐标(设置姿势下的根骨骼Y偏移)
* @return Y坐标值(像素/世界单位,由Spine编辑器导出)
*/
float getY();
/**
* @brief 设置骨骼的默认Y坐标
* @param inValue 要设置的Y坐标值
*/
void setY(float inValue);
/**
* @brief 获取骨骼的宽度(Spine编辑器中定义的骨骼边界宽度)
* @return 宽度值(像素/世界单位)
*/
float getWidth();
/**
* @brief 设置骨骼的宽度
* @param inValue 要设置的宽度值
*/
void setWidth(float inValue);
/**
* @brief 获取骨骼的高度(Spine编辑器中定义的骨骼边界高度)
* @return 高度值(像素/世界单位)
*/
float getHeight();
/**
* @brief 设置骨骼的高度
* @param inValue 要设置的高度值
*/
void setHeight(float inValue);
/**
* @brief 获取骨骼的参考缩放比例(Spine高版本新增,用于适配不同分辨率)
* @return 参考缩放比例值
*/
float getReferenceScale();
/**
* @brief 设置骨骼的参考缩放比例
* @param inValue 要设置的参考缩放比例值
*/
void setReferenceScale(float inValue);
/// The Spine version used to export this data, or NULL.
/**
* @brief 获取导出该骨骼数据的Spine编辑器版本
* @return 版本字符串(如"4.1.0"),无版本信息返回空字符串
*/
const String &getVersion();
/**
* @brief 设置导出该骨骼数据的Spine编辑器版本
* @param inValue 要设置的版本字符串
*/
void setVersion(const String &inValue);
/**
* @brief 获取骨骼数据的哈希值(Spine编辑器导出时自动生成,用于数据校验)
* @return 哈希值字符串
*/
const String &getHash();
/**
* @brief 设置骨骼数据的哈希值
* @param inValue 要设置的哈希值字符串
*/
void setHash(const String &inValue);
/**
* @brief 获取图片资源的路径(Spine编辑器中设置的图集图片根路径)
* @return 图片路径字符串,无设置返回空字符串
*/
const String &getImagesPath();
/**
* @brief 设置图片资源的路径
* @param inValue 要设置的图片根路径
*/
void setImagesPath(const String &inValue);
/**
* @brief 获取音频资源的路径(Spine编辑器中设置的音频文件根路径)
* @return 音频路径字符串,无设置返回空字符串
*/
const String &getAudioPath();
/**
* @brief 设置音频资源的路径
* @param inValue 要设置的音频根路径
*/
void setAudioPath(const String &inValue);
/// The dopesheet FPS in Spine. Available only when nonessential data was exported.
/**
* @brief 获取Spine编辑器中时间轴的FPS(帧率)
* @return 帧率值(如30、60),仅导出「非核心数据」时可用
*/
float getFps();
/**
* @brief 设置Spine编辑器中时间轴的FPS
* @param inValue 要设置的帧率值
*/
void setFps(float inValue);
private:
String _name; // 骨骼数据名称
Vector<BoneData *> _bones; // 骨骼数据数组(父骨骼优先排序,根骨骼第一个)
Vector<SlotData *> _slots; // 插槽数据数组(按设置姿势的绘制顺序排序)
Vector<Skin *> _skins; // 所有皮肤数据数组(包含默认皮肤)
Skin *_defaultSkin; // 默认皮肤指针
Vector<EventData *> _events; // 动画事件数据数组
Vector<Animation *> _animations; // 所有动画数据数组
Vector<IkConstraintData *> _ikConstraints; // IK约束数据数组
Vector<TransformConstraintData *> _transformConstraints; // 变换约束数据数组
Vector<PathConstraintData *> _pathConstraints; // 路径约束数据数组
Vector<PhysicsConstraintData *> _physicsConstraints; // 物理约束数据数组(高版本新增)
float _x, _y; // 骨骼默认X/Y坐标(设置姿势下的根骨骼偏移)
float _width, _height; // 骨骼边界宽/高(Spine编辑器中定义)
float _referenceScale; // 参考缩放比例(高版本新增,适配不同分辨率)
String _version; // 导出该数据的Spine编辑器版本
String _hash; // 数据哈希值(用于数据校验)
Vector<char *> _strings; // 内部字符串缓存(Spine内部用,管理解析的字符串资源)
// Nonessential. 非核心数据(导出时可选择是否包含,不影响骨骼核心运行)
float _fps; // Spine编辑器时间轴FPS
String _imagesPath; // 图片资源根路径
String _audioPath; // 音频资源根路径
};
2.4 AttachmentLoader(贴贴纸的拼装工具)
贴贴纸的拼装工具,是骨骼素材与结构的绑定桥梁,先攥住纹理图集掌握所有素材信息,再对照SkeletonData的规则完成 “搭架子 + 贴贴纸”。
Spine 提供2 个常用实现类,开发中直接使用即可,无需自定义(仅需在创建时绑定Atlas):
AtlasAttachmentLoader:开发首选,与纹理图集绑定,从Atlas中查找素材并创建附件,适配绝大多数 2D 开发场景;AttachmentLoader:基类,纯虚类,需自定义实现附件创建逻辑,适合特殊素材加载场景(如动态生成素材)。
核心类:AtlasAttachmentLoader(图集附件加载器)
普通类,继承自AttachmentLoader,构造时必须传入Atlas对象,核心是将Atlas素材转换为 Spine 的Attachment(附件,即 “贴纸”)。
// SP_API:Spine 跨平台导出宏,保证类可在动态库/静态库中被外部项目引用
// 继承 AttachmentLoader:实现抽象附件加载器的所有纯虚函数,提供基于图集的附件创建逻辑
class SP_API AtlasAttachmentLoader : public AttachmentLoader {
public:
RTTI_DECL // Spine 运行时类型识别宏,用于动态判断类类型(如向下转型、类型校验)
/**
* @brief 构造函数:初始化图集附件加载器,绑定所属的纹理图集
* @param atlas 已加载完成的 Atlas 纹理图集指针(包含所有 AtlasRegion 子纹理)
* @details 必须传入有效且已加载的 Atlas 实例,后续创建附件时会通过该图集匹配子纹理
*/
explicit AtlasAttachmentLoader(Atlas *atlas);
/**
* @brief 创建区域附件(最常用的基础附件,对应单个 AtlasRegion 子纹理)
* @param skin 该附件所属的骨骼皮肤
* @param name 附件名称(与 Spine 编辑器中定义的附件名一致)
* @param path 附件关联的图集路径/子纹理名称(用于匹配 Atlas 中的 AtlasRegion)
* @param sequence 序列帧配置(NULL 表示非序列帧附件,Spine 高版本序列帧功能)
* @return 创建成功的 RegionAttachment 指针,由 Spine 内部管理内存
* @details 区域附件是 Spine 最基础的附件类型,对应单个矩形子纹理(如骨骼的普通贴图),
* 创建后会自动通过 path 匹配 Atlas 中的 AtlasRegion,并完成纹理绑定
*/
virtual RegionAttachment *newRegionAttachment(Skin &skin, const String &name, const String &path, Sequence *sequence);
/**
* @brief 创建网格附件(带顶点权重的复杂附件,适配不规则形状纹理)
* @param skin 该附件所属的骨骼皮肤
* @param name 附件名称
* @param path 附件关联的图集路径/子纹理名称
* @param sequence 序列帧配置(NULL 表示非序列帧)
* @return 创建成功的 MeshAttachment 指针
* @details 网格附件包含自定义顶点数据和纹理坐标,用于渲染不规则形状的纹理(如人物的衣服、头发),
* 同样通过 path 匹配 Atlas 中的 AtlasRegion 绑定纹理
*/
virtual MeshAttachment *newMeshAttachment(Skin &skin, const String &name, const String &path, Sequence *sequence);
/**
* @brief 创建边界框附件(碰撞检测专用,无纹理)
* @param skin 该附件所属的骨骼皮肤
* @param name 附件名称
* @return 创建成功的 BoundingBoxAttachment 指针
* @details 边界框附件无纹理,仅存储矩形/多边形边界数据,用于骨骼动画的碰撞检测、范围判断,
* 不与 Atlas 纹理产生关联,创建时无需匹配子纹理
*/
virtual BoundingBoxAttachment *newBoundingBoxAttachment(Skin &skin, const String &name);
/**
* @brief 创建路径附件(用于路径约束,定义骨骼跟随的路径,无纹理)
* @param skin 该附件所属的骨骼皮肤
* @param name 附件名称
* @return 创建成功的 PathAttachment 指针
* @details 路径附件用于定义骨骼的跟随路径(如人物沿曲线移动),仅存储路径顶点数据,无纹理关联
*/
virtual PathAttachment *newPathAttachment(Skin &skin, const String &name);
/**
* @brief 创建点附件(标记骨骼上的特定点位,无纹理)
* @param skin 该附件所属的骨骼皮肤
* @param name 附件名称
* @return 创建成功的 PointAttachment 指针
* @details 点附件用于标记骨骼上的关键点位(如技能释放点、武器挂载点),仅存储坐标数据,无纹理关联
*/
virtual PointAttachment *newPointAttachment(Skin &skin, const String &name);
/**
* @brief 创建裁剪附件(用于裁剪其他附件的渲染区域,无纹理)
* @param skin 该附件所属的骨骼皮肤
* @param name 附件名称
* @return 创建成功的 ClippingAttachment 指针
* @details 裁剪附件用于定义裁剪区域,渲染时会裁剪该区域外的其他附件(如人物被遮挡的部分),无纹理关联
*/
virtual ClippingAttachment *newClippingAttachment(Skin &skin, const String &name);
/**
* @brief 配置附件的通用属性(Spine 内部回调,补充附件初始化逻辑)
* @param attachment 已创建的任意类型附件指针
* @details 由 Spine 内部在附件创建后自动调用,可用于补充设置附件的通用参数(如默认缩放、偏移),
* 基础实现为空,开发者可重写该方法实现自定义附件配置
*/
virtual void configureAttachment(Attachment *attachment);
/**
* @brief 根据子纹理名称查找图集里的 AtlasRegion
* @param name 要查找的子纹理名称(与 Atlas 中定义的 AtlasRegion::name 一致)
* @return 找到返回 AtlasRegion 指针,未找到返回 NULL
* @details 内部封装了 Atlas::findRegion() 方法,是创建纹理类附件时(Region/Mesh)的核心调用,
* 用于匹配附件与对应的子纹理
*/
AtlasRegion *findRegion(const String &name);
private:
Atlas *_atlas; // 绑定的纹理图集指针,所有附件的纹理匹配都基于该图集完成
};
开发实操注意
AtlasAttachmentLoader的所有方法均由 Spine 在加载SkeletonData和创建Skeleton时自动调用,开发中仅需完成构造时绑定Atlas,无需手动调用任何方法!
2.5 Slot(骨骼上的贴纸挂钩)
骨骼上的贴纸挂钩,隶属于骨骼,是素材(Attachment)的唯一挂载点,一个Slot同一时间仅能挂载一张素材,多个Slot可叠在同一骨骼位置实现素材叠加(如左手戴手套 + 拿枪)。
普通类,由Skeleton根据SkeletonData中的SlotData自动创建,每个Slot对应一个SlotData,属于Skeleton的动态成员。
该类是 Spine 骨骼动画的核心渲染单元类,继承自 SpineObject,作为「骨骼(Bone)」和「附件(Attachment)」的中间桥梁,一个骨骼可挂载多个插槽,每个插槽承载一个可视化附件(纹理、网格等),同时管理附件的颜色、透明度、形变等运行时渲染状态,是骨骼绘制层级和外观控制的关键。
// SP_API:Spine 跨平台导出宏,保证类可在动态库/静态库中被外部项目引用
// 继承 SpineObject:遵循 Spine 核心对象内存管理规范,由自定义扩展接口(QtSpineExtension)管理内存
class SP_API Slot : public SpineObject {
// 友元类:Spine 内部各类时间轴、约束、骨骼核心类,仅允许这些类访问私有成员,保证运行时状态修改的安全性
friend class VertexAttachment;
friend class Skeleton;
friend class SkeletonBounds;
friend class SkeletonClipping;
friend class AttachmentTimeline;
friend class RGBATimeline;
friend class RGBTimeline;
friend class AlphaTimeline;
friend class RGBA2Timeline;
friend class RGB2Timeline;
friend class DeformTimeline;
friend class DrawOrderTimeline;
friend class EventTimeline;
friend class IkConstraintTimeline;
friend class PathConstraintMixTimeline;
friend class PathConstraintPositionTimeline;
friend class PathConstraintSpacingTimeline;
friend class ScaleTimeline;
friend class ShearTimeline;
friend class TransformConstraintTimeline;
friend class TranslateTimeline;
friend class TwoColorTimeline;
public:
/**
* @brief 构造函数:创建插槽实例,绑定插槽数据和所属骨骼
* @param data 插槽的基础配置数据(SlotData,由Spine编辑器导出、SkeletonData管理)
* @param bone 插槽所属的骨骼(Bone),插槽的位置、旋转等状态跟随该骨骼同步变化
* @details 插槽实例由Spine内部创建(解析SkeletonData时),开发者无需手动实例化,
* 构造时会初始化所有运行时状态为SlotData中定义的设置姿势(Setup Pose)
*/
Slot(SlotData &data, Bone &bone);
/**
* @brief 将插槽恢复到「设置姿势(Setup Pose)」
* @details 重置插槽的所有运行时状态为初始值:恢复颜色、透明度、默认附件、清除形变数据等,
* 调用Skeleton::setToSetupPose()时,会遍历所有插槽执行该方法
*/
void setToSetupPose();
/**
* @brief 获取插槽的基础配置数据
* @return 插槽数据引用(SlotData),包含插槽的名称、默认颜色、绘制顺序等静态配置
*/
SlotData &getData();
/**
* @brief 获取插槽所属的骨骼
* @return 骨骼引用(Bone),插槽的所有渲染状态都基于该骨骼的变换(位置、旋转、缩放)计算
*/
Bone &getBone();
/**
* @brief 获取插槽所属的骨骼实例
* @return 骨骼实例引用(Skeleton),通过所属骨骼向上获取,方便快速关联骨骼根实例
*/
Skeleton &getSkeleton();
/**
* @brief 获取插槽的主颜色(RGBA)
* @return 颜色引用(Color),用于叠加到插槽承载的附件纹理上,实现整体颜色/透明度修改
* @details 运行时可动态修改,如设置为红色(1,0,0,1)、半透明(1,1,1,0.5),不影响原始纹理
*/
Color &getColor();
/**
* @brief 获取插槽的暗色(深色叠加层,Spine双色调功能)
* @return 暗色引用(Color),用于实现双色调渲染效果(如人物受伤时的暗红色叠加)
*/
Color &getDarkColor();
/**
* @brief 判断插槽是否启用了双色调功能(是否设置了有效暗色)
* @return 启用返回true,未启用返回false;未启用时,渲染不会处理暗色叠加
*/
bool hasDarkColor();
/// May be NULL.
/**
* @brief 获取插槽当前承载的附件
* @return 附件指针(Attachment*),当前无附件时返回NULL
* @details 一个插槽同一时间只能承载一个附件,切换附件时会替换原有附件(无需手动释放旧附件)
*/
Attachment *getAttachment();
/**
* @brief 为插槽设置/切换附件
* @param inValue 要设置的附件指针,传NULL则移除插槽当前所有附件
* @details 骨骼换装/换动作的核心方法,如将“待机武器”附件切换为“攻击武器”附件,
* 设置后附件会继承插槽的颜色、透明度等状态,跟随所属骨骼渲染
*/
void setAttachment(Attachment *inValue);
/**
* @brief 获取当前附件的状态标识
* @return 附件状态整数值,由Spine内部管理,用于标记附件的加载、激活等状态
*/
int getAttachmentState();
/**
* @brief 设置当前附件的状态标识
* @param state 要设置的状态值,仅Spine内部动画时间轴、约束类调用,开发者无需手动修改
*/
void setAttachmentState(int state);
/**
* @brief 获取插槽的形变数据数组
* @return 浮点型动态数组引用(Vector<float>),存储附件的顶点形变、权重偏移等数据
* @details 用于网格附件(MeshAttachment)的动态形变、皮肤混合、动画关键帧形变,
* 无变形时数组为空,由Spine动画时间轴自动赋值
*/
Vector<float> &getDeform();
/**
* @brief 获取序列帧附件的当前帧索引
* @return 帧索引整数值,从0开始;非序列帧附件返回0
*/
int getSequenceIndex();
/**
* @brief 设置序列帧附件的当前帧索引
* @param index 要切换的帧索引,超出序列帧范围时会按循环规则处理
* @details 仅对序列帧类型附件有效,用于手动控制序列帧播放(如逐帧切换特效)
*/
void setSequenceIndex(int index);
private:
SlotData &_data; // 插槽的基础配置数据,只读,存储静态初始配置
Bone &_bone; // 插槽所属的骨骼,插槽变换跟随骨骼,生命周期与骨骼绑定
Skeleton &_skeleton; // 插槽所属的骨骼根实例,快速关联骨骼全局数据
Color _color; // 插槽主颜色,运行时可动态修改,实现附件颜色/透明度叠加
Color _darkColor; // 插槽暗色,双色调渲染专用,未启用时为无效颜色
bool _hasDarkColor; // 双色调功能启用标识,标记是否需要处理暗色叠加
Attachment *_attachment; // 插槽当前承载的附件,NULL表示无附件,运行时可动态切换
int _attachmentState; // 附件内部状态标识,Spine内部使用,开发者无需操作
int _sequenceIndex; // 序列帧附件的当前帧索引,非序列帧附件始终为0
Vector<float> _deform; // 附件形变数据数组,存储顶点偏移、权重变化等,无变形时为空
};
2.6 Skeleton(拼好的可动高达成品)
拼好的可动高达成品,基于SkeletonData创建的动态可操作实例,是开发中唯一需要直接操作的核心对象;Setup Pose 状态下的Skeleton拥有完整外观、正确骨骼结构,所有动态操作(调姿势、播动画、换素材)均在该实例上完成。
普通类,构造时必须传入SkeletonData对象,自动从SkeletonData中复制骨骼、插槽等基础数据,生成可操作的动态实例。
该类是 Spine 骨骼动画的运行时核心实例类,继承自SpineObject,基于SkeletonData静态数据模板创建,封装了骨骼运行时的所有动态状态(骨骼世界变换、皮肤、插槽绘制顺序、全局颜色、物理约束等),是动画更新、骨骼控制、渲染驱动的唯一入口,所有骨骼的动态操作与动画播放都围绕该类展开。
// SP_API:Spine 跨平台导出宏,支持动态库/静态库跨项目引用
// 继承 SpineObject:遵循 Spine 核心对象内存管理规范,由自定义扩展接口(QtSpineExtension)管理内存分配与释放
class SP_API Skeleton : public SpineObject {
// 友元类:Spine 内部动画状态、时间轴、约束、裁剪等核心类,仅允许这些类访问私有成员,保证运行时状态修改的安全性
friend class AnimationState;
friend class SkeletonBounds;
friend class SkeletonClipping;
friend class AttachmentTimeline;
friend class RGBATimeline;
friend class RGBTimeline;
friend class AlphaTimeline;
friend class RGBA2Timeline;
friend class RGB2Timeline;
friend class DeformTimeline;
friend class DrawOrderTimeline;
friend class EventTimeline;
friend class IkConstraintTimeline;
friend class PathConstraintMixTimeline;
friend class PathConstraintPositionTimeline;
friend class PathConstraintSpacingTimeline;
friend class ScaleTimeline;
friend class ScaleXTimeline;
friend class ScaleYTimeline;
friend class ShearTimeline;
friend class ShearXTimeline;
friend class ShearYTimeline;
friend class TransformConstraintTimeline;
friend class RotateTimeline;
friend class TranslateTimeline;
friend class TranslateXTimeline;
friend class TranslateYTimeline;
friend class TwoColorTimeline;
public:
/**
* @brief 构造函数:基于骨骼静态数据模板创建运行时实例
* @param skeletonData 骨骼静态数据(SkeletonData),由.json/.skel文件解析生成,可被多个Skeleton实例共享
* @details 构造时会自动根据SkeletonData创建骨骼、插槽、约束等运行时对象,初始化所有状态为设置姿势(Setup Pose),
* 开发者需传入已解析完成的SkeletonData实例,无需手动创建骨骼/插槽等子对象
*/
explicit Skeleton(SkeletonData *skeletonData);
/**
* @brief 析构函数:释放骨骼实例的所有运行时资源
* @details 自动销毁骨骼、插槽、约束等所有子对象,释放占用的内存,开发者只需在骨骼不再使用时释放Skeleton实例即可
*/
~Skeleton();
/// Caches information about bones and constraints. Must be called if bones, constraints or weighted path attachments are added or removed.
/**
* @brief 更新骨骼与约束的缓存信息
* @details 当手动添加/移除骨骼、约束,或修改带权重的路径附件时,必须调用该方法刷新缓存,
* 否则约束计算、世界变换更新会出现异常,日常动画播放无需手动调用
*/
void updateCache();
/**
* @brief 打印更新缓存的调试信息
* @details 仅用于开发调试,输出骨骼、约束的缓存排序、依赖关系等信息,生产环境可忽略
*/
void printUpdateCache();
/// Updates the world transform for each bone and applies all constraints.
/// See [World transforms](http://esotericsoftware.com/spine-runtime-skeletons#World-transforms) in the Spine Runtimes Guide.
/**
* @brief 更新所有骨骼的世界变换矩阵,并应用所有约束(IK/路径/变换/物理)
* @param physics 物理更新模式(枚举),控制物理约束是否生效、力的计算方式
* @details 骨骼动画核心方法,**每次动画更新后、渲染前必须调用**,否则骨骼位置/旋转不会同步到屏幕,
* 内部会按约束依赖关系排序,依次计算骨骼世界变换、应用各类约束
*/
void updateWorldTransform(Physics physics);
/**
* @brief 递归更新指定骨骼及其子骨骼的世界变换,并应用约束
* @param physics 物理更新模式
* @param parent 父骨骼,仅更新该父骨骼下的所有子骨骼
* @details 局部更新方法,适用于仅修改部分骨骼时的性能优化,无需更新整个骨骼树
*/
void updateWorldTransform(Physics physics, Bone *parent);
/// Sets the bones, constraints, and slots to their setup pose values.
/**
* @brief 将骨骼、约束、插槽的所有状态重置为设置姿势(Setup Pose)
* @details 恢复骨骼初始位置、旋转、缩放,重置插槽颜色/附件,清除约束的动态修改,
* 是骨骼回到初始状态的快捷方法
*/
void setToSetupPose();
/// Sets the bones and constraints to their setup pose values.
/**
* @brief 仅将骨骼和约束重置为设置姿势
* @details 保留插槽的当前状态(颜色、附件等),仅恢复骨骼的变换和约束配置
*/
void setBonesToSetupPose();
/**
* @brief 仅将插槽重置为设置姿势
* @details 恢复插槽的默认颜色、附件,清除形变数据,保留骨骼的变换和约束状态,适用于换装后重置插槽外观
*/
void setSlotsToSetupPose();
/// @return May be NULL.
/**
* @brief 根据骨骼名称查找运行时骨骼对象
* @param boneName 骨骼名称(与Spine编辑器中定义一致)
* @return 找到返回Bone*,未找到返回NULL
* @details 常用方法,用于获取指定骨骼进行动态控制(如手动修改位置、旋转)
*/
Bone *findBone(const String &boneName);
/// @return May be NULL.
/**
* @brief 根据插槽名称查找运行时插槽对象
* @param slotName 插槽名称(与Spine编辑器中定义一致)
* @return 找到返回Slot*,未找到返回NULL
* @details 核心方法,用于获取插槽修改外观(颜色、透明度)、切换附件(换装)
*/
Slot *findSlot(const String &slotName);
/// Sets a skin by name (see setSkin).
/**
* @brief 根据皮肤名称设置当前骨骼皮肤
* @param skinName 皮肤名称(与SkeletonData中定义一致,默认皮肤名为"default")
* @details 封装了findSkin+setSkin逻辑,找不到对应皮肤时无操作,适用于直接通过名称换装
*/
void setSkin(const String &skinName);
/// Attachments from the new skin are attached if the corresponding attachment from the old skin was attached.
/// If there was no old skin, each slot's setup mode attachment is attached from the new skin.
/// After changing the skin, the visible attachments can be reset to those attached in the setup pose by calling
/// See Skeleton::setSlotsToSetupPose()
/// Also, often AnimationState::apply(Skeleton&) is called before the next time the
/// skeleton is rendered to allow any attachment keys in the current animation(s) to hide or show attachments from the new skin.
/// @param newSkin May be NULL.
/**
* @brief 设置骨骼当前使用的皮肤(核心换装方法)
* @param newSkin 要切换的皮肤实例(Skin*),传NULL则移除当前皮肤(仅保留设置姿势的默认附件)
* @details 1. 切换皮肤时,若旧皮肤有附件挂载,新皮肤会自动挂载同名附件;
* 2. 无旧皮肤时,会从新皮肤挂载插槽设置姿势的默认附件;
* 3. 切换后建议调用setSlotsToSetupPose()重置插槽,或调用AnimationState::apply()让动画适配新皮肤;
* 是骨骼换装的核心方法,支持动态切换外观(如人物换衣服、武器)
*/
void setSkin(Skin *newSkin);
/// @return May be NULL.
/**
* @brief 根据插槽名称和附件名称获取附件实例
* @param slotName 插槽名称
* @param attachmentName 附件名称
* @return 找到返回Attachment*,未找到返回NULL
* @details 用于获取指定插槽的指定附件,为后续插槽切换附件做准备
*/
Attachment *getAttachment(const String &slotName, const String &attachmentName);
/// @return May be NULL.
/**
* @brief 根据插槽索引和附件名称获取附件实例
* @param slotIndex 插槽索引(SkeletonData中Slots数组的索引,比名称查找更高效)
* @param attachmentName 附件名称
* @return 找到返回Attachment*,未找到返回NULL
* @details 性能优于按名称查找插槽,适合高频次的附件获取操作
*/
Attachment *getAttachment(int slotIndex, const String &attachmentName);
/// @param attachmentName May be empty.
/**
* @brief 根据插槽名称和附件名称为插槽设置附件
* @param slotName 插槽名称
* @param attachmentName 附件名称(传空字符串则移除插槽当前附件)
* @details 快捷换装方法,直接通过名称为插槽绑定附件,无需手动查找插槽和附件,找不到时无操作
*/
void setAttachment(const String &slotName, const String &attachmentName);
/// @return May be NULL.
/**
* @brief 根据名称查找IK约束实例
* @param constraintName IK约束名称
* @return 找到返回IkConstraint*,未找到返回NULL
*/
IkConstraint *findIkConstraint(const String &constraintName);
/// @return May be NULL.
/**
* @brief 根据名称查找变换约束实例
* @param constraintName 变换约束名称
* @return 找到返回TransformConstraint*,未找到返回NULL
*/
TransformConstraint *findTransformConstraint(const String &constraintName);
/// @return May be NULL.
/**
* @brief 根据名称查找路径约束实例
* @param constraintName 路径约束名称
* @return 找到返回PathConstraint*,未找到返回NULL
*/
PathConstraint *findPathConstraint(const String &constraintName);
/// @return May be NULL.
/**
* @brief 根据名称查找物理约束实例(Spine高版本新增)
* @param constraintName 物理约束名称
* @return 找到返回PhysicsConstraint*,未找到返回NULL
*/
PhysicsConstraint *findPhysicsConstraint(const String &constraintName);
/// Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose.
/// @param outX The horizontal distance between the skeleton origin and the left side of the AABB.
/// @param outY The vertical distance between the skeleton origin and the bottom side of the AABB.
/// @param outWidth The width of the AABB
/// @param outHeight The height of the AABB.
/// @param outVertexBuffer Reference to hold a Vector of floats. This method will assign it with new floats as needed.
// @param clipping Pointer to a SkeletonClipping instance or NULL. If a clipper is given, clipping attachments will be taken into account.
/**
* @brief 获取骨骼当前姿势下的轴对齐包围盒(AABB),包含所有区域/网格附件
* @param outX 输出:包围盒左边界与骨骼原点的水平距离
* @param outY 输出:包围盒下边界与骨骼原点的垂直距离
* @param outWidth 输出:包围盒宽度
* @param outHeight 输出:包围盒高度
* @param outVertexBuffer 输出:存储计算包围盒所用的顶点数据,由方法自动分配
* @details 用于碰撞检测、相机跟随、视口裁剪等场景,仅计算可见的纹理类附件,不包含无纹理附件(边界框/点等)
*/
void getBounds(float &outX, float &outY, float &outWidth, float &outHeight, Vector<float> &outVertexBuffer);
/**
* @brief 重载:获取骨骼带裁剪附件的轴对齐包围盒
* @param clipper 骨骼裁剪实例(SkeletonClipping*),传NULL则不考虑裁剪
* @details 当骨骼使用裁剪附件时,调用该方法可获取裁剪后的实际可见包围盒,更精准的碰撞/裁剪判断
*/
void getBounds(float &outX, float &outY, float &outWidth, float &outHeight, Vector<float> &outVertexBuffer, SkeletonClipping *clipper);
/**
* @brief 获取骨骼的根骨骼
* @return 根骨骼指针(Bone*),永远非NULL,是骨骼树的最顶层骨骼,所有骨骼的最终父节点
* @details 根骨骼的变换控制整个骨骼的全局位置、旋转、缩放
*/
Bone *getRootBone();
/**
* @brief 获取骨骼绑定的静态数据模板
* @return 骨骼静态数据(SkeletonData*),可用于查找动画、皮肤、原始配置等
*/
SkeletonData *getData();
/**
* @brief 获取所有骨骼对象数组(按父骨骼优先排序,根骨骼为第一个元素)
* @return 骨骼数组引用(Vector<Bone *>),包含骨骼树的所有运行时骨骼对象
*/
Vector<Bone *> &getBones();
/**
* @brief 获取骨骼更新缓存列表
* @return 可更新对象数组引用(Vector<Updatable *>),存储骨骼、约束等需要按顺序更新的对象
* @details Spine 内部使用,用于按依赖关系排序更新,开发者无需手动操作
*/
Vector<Updatable *> &getUpdateCacheList();
/**
* @brief 获取所有插槽对象数组(按设置姿势的原始顺序)
* @return 插槽数组引用(Vector<Slot *>),顺序与SkeletonData中定义一致,不随绘制顺序修改而变化
*/
Vector<Slot *> &getSlots();
/**
* @brief 获取骨骼的实际绘制顺序数组
* @return 插槽数组引用(Vector<Slot *>),顺序决定附件的绘制层级(靠前先绘,靠后覆盖)
* @details 可动态修改该数组的顺序,实现骨骼附件的层级调整(如人物走到障碍物后,修改绘制顺序实现遮挡)
*/
Vector<Slot *> &getDrawOrder();
/**
* @brief 获取所有IK约束实例数组
* @return IK约束数组引用(Vector<IkConstraint *>)
*/
Vector<IkConstraint *> &getIkConstraints();
/**
* @brief 获取所有路径约束实例数组
* @return 路径约束数组引用(Vector<PathConstraint *>)
*/
Vector<PathConstraint *> &getPathConstraints();
/**
* @brief 获取所有变换约束实例数组
* @return 变换约束数组引用(Vector<TransformConstraint *>)
*/
Vector<TransformConstraint *> &getTransformConstraints();
/**
* @brief 获取所有物理约束实例数组(Spine高版本新增)
* @return 物理约束数组引用(Vector<PhysicsConstraint *>)
*/
Vector<PhysicsConstraint *> &getPhysicsConstraints();
/**
* @brief 获取骨骼当前使用的皮肤
* @return 皮肤实例(Skin*),未设置皮肤时返回NULL
*/
Skin *getSkin();
/**
* @brief 获取骨骼的全局颜色
* @return 颜色引用(Color),会叠加到所有插槽的颜色上,实现骨骼整体颜色/透明度修改
* @details 全局颜色优先级低于插槽颜色,最终渲染颜色 = 全局颜色 * 插槽颜色 * 纹理颜色
*/
Color &getColor();
/**
* @brief 设置骨骼的全局位置(根骨骼位置)
* @param x 全局X坐标
* @param y 全局Y坐标
* @details 快捷方法,等价于修改根骨骼的位置,控制整个骨骼在世界空间的位置
*/
void setPosition(float x, float y);
/**
* @brief 获取骨骼全局X坐标
* @return X坐标值
*/
float getX();
/**
* @brief 设置骨骼全局X坐标
* @param inValue 要设置的X坐标值
*/
void setX(float inValue);
/**
* @brief 获取骨骼全局Y坐标
* @return Y坐标值
*/
float getY();
/**
* @brief 设置骨骼全局Y坐标
* @param inValue 要设置的Y坐标值
*/
void setY(float inValue);
/**
* @brief 获取骨骼全局X轴缩放比例
* @return X轴缩放值,1为原始大小,大于1放大,小于1缩小
*/
float getScaleX();
/**
* @brief 设置骨骼全局X轴缩放比例
* @param inValue 要设置的X轴缩放值
* @details 作用于整个骨骼树,所有骨骼都会继承该缩放比例
*/
void setScaleX(float inValue);
/**
* @brief 获取骨骼全局Y轴缩放比例
* @return Y轴缩放值
*/
float getScaleY();
/**
* @brief 设置骨骼全局Y轴缩放比例
* @param inValue 要设置的Y轴缩放值
*/
void setScaleY(float inValue);
/**
* @brief 获取骨骼的运行时间(物理约束专用)
* @return 运行时间(秒),由update(delta)方法累计
*/
float getTime();
/**
* @brief 设置骨骼的运行时间(物理约束专用)
* @param time 要设置的运行时间(秒)
*/
void setTime(float time);
/**
* @brief 更新骨骼的物理状态(仅对物理约束生效)
* @param delta 时间增量(秒),通常为当前帧与上一帧的时间差(如1/60)
* @details 累计骨骼运行时间,更新物理约束的力、速度等状态,需在每帧动画更新时调用
*/
void update(float delta);
/// Rotates the physics constraint so next {@link #update(Physics)} forces are applied as if the bone rotated around the specified point in world space.
/**
* @brief 物理约束平移(高版本物理功能)
* @param x 世界空间X平移量
* @param y 世界空间Y平移量
* @details 旋转物理约束,使下一次updateWorldTransform时,力的作用效果等同于骨骼绕世界空间指定点平移
*/
void physicsTranslate(float x, float y);
/// Calls {@link PhysicsConstraint#rotate(float, float, float)} for each physics constraint.
/**
* @brief 物理约束旋转(高版本物理功能)
* @param x 世界空间旋转中心点X坐标
* @param y 世界空间旋转中心点Y坐标
* @param degrees 旋转角度(单位:度)
* @details 对所有物理约束调用旋转方法,使骨骼绕世界空间指定点旋转,并更新物理约束力的计算
*/
void physicsRotate(float x, float y, float degrees);
private:
SkeletonData *_data; // 绑定的骨骼静态数据模板,可共享
Vector<Bone *> _bones; // 所有运行时骨骼对象数组(父骨骼优先排序)
Vector<Slot *> _slots; // 所有运行时插槽对象数组(原始顺序,与SkeletonData一致)
Vector<Slot *> _drawOrder; // 插槽绘制顺序数组,决定附件渲染层级
Vector<IkConstraint *> _ikConstraints;// 所有IK约束实例数组
Vector<TransformConstraint *> _transformConstraints; // 所有变换约束实例数组
Vector<PathConstraint *> _pathConstraints; // 所有路径约束实例数组
Vector<PhysicsConstraint *> _physicsConstraints; // 所有物理约束实例数组(高版本新增)
Vector<Updatable *> _updateCache; // 更新缓存列表,存储需按顺序更新的骨骼/约束
Skin *_skin; // 当前使用的皮肤,NULL表示未设置
Color _color; // 骨骼全局颜色,叠加到所有插槽
float _scaleX, _scaleY; // 骨骼全局缩放比例(X/Y轴)
float _x, _y; // 骨骼全局位置(根骨骼位置,世界空间)
float _time; // 骨骼运行时间,物理约束专用,由update(delta)累计
// 以下为内部排序方法,按约束依赖关系、骨骼父子关系排序,保证更新/计算顺序正确
void sortIkConstraint(IkConstraint *constraint);
void sortPathConstraint(PathConstraint *constraint);
void sortPhysicsConstraint(PhysicsConstraint *constraint);
void sortTransformConstraint(TransformConstraint *constraint);
void sortPathConstraintAttachment(Skin *skin, size_t slotIndex, Bone &slotBone);
void sortPathConstraintAttachment(Attachment *attachment, Bone &slotBone);
void sortBone(Bone *bone);
static void sortReset(Vector<Bone *> &bones);
};
Spine 骨骼动画核心类关系与核心加载流程(Setup Pose)
结合乐高 / 高达拼装的比喻,把核心类的关联逻辑和Setup Pose(初始绑定姿势)的加载步骤梳理得清晰易懂,所有流程均围绕「静态数据定义规则 - 平台适配加载资源 - 桥梁绑定融合 - 生成动态可操作实例」展开,是 Spine 所有动画操作的基础。
Spine 核心类分为平台适配层、静态数据层、桥梁绑定层、动态实例层四层,层与层之间单向依赖、职责单一,缺一不可;类与类的关系可概括为「数据定义者 - 资源提供者 - 桥梁绑定者 - 最终操作对象」,以下是分层关系 + 核心关联逻辑:
1. 四层架构与类的归属
| 层级 | 核心类 / 组件 | 乐高 / 高达比喻 | 核心职责 | 特性 |
|---|---|---|---|---|
| 平台适配层 | SpineExtension、TextureLoader | 快递员 + 取件规则 | 解决跨平台(QT/VS)的底层操作:内存管理、文件读取、纹理加载 / 释放 | 必须按平台自定义实现 |
| 静态数据层 | Texture Atlas、SkeletonData | 零件收纳盒 + 拼装说明书 | 提供骨骼外观素材和静态规则(骨骼结构 / 插槽 / 初始姿势 / 动画) | 加载后只读不可修改 |
| 桥梁绑定层 | AttachmentLoader(AtlasAttachmentLoader) | 贴贴纸的拼装工具 | 关联纹理图集和骨骼数据,按规则将素材绑定到骨骼插槽,完成 “架子 + 素材” 的融合 | 开发中直接使用现成实现(AtlasAttachmentLoader),无需自定义 |
| 动态实例层 | Slot、Skeleton | 贴纸挂钩 + 拼好的可动成品 | Slot 是素材挂载点,Skeleton 是最终可操作实例,承载所有动态状态(姿势 / 颜色 / 附件) | 开发中唯一直接操作的对象 |
2. 核心类强关联逻辑(关键)
SpineExtension 是所有类的基础:TextureLoader、Texture Atlas、SkeletonData 的底层均依赖 SpineExtension 的内存管理和文件读取,QT 平台必须自定义实现,否则标准 C 库的操作会失效;
TextureLoader 与 Texture Atlas 是强绑定:Texture Atlas(素材收纳盒)必须通过 TextureLoader(快递员)加载图集大图,TextureLoader 的load/unload方法由 Texture Atlas 内部自动调用;
SkeletonData 与 Texture Atlas 相互独立:SkeletonData 是 “无外观的透明骨骼架子规则”,Texture Atlas 是 “无规则的纯素材”,二者本身无关联,必须通过桥梁才能融合;
AttachmentLoader 是核心桥梁:
- 构造时必须传入已加载的 Texture Atlas,掌握所有素材信息;
- 解析SkeletonData时自动被调用,将图集素材与骨骼插槽绑定,生成带外观的附件;
- 开发中首选
AtlasAttachmentLoader(Spine 现成实现),直接绑定 Atlas 即可;
Slot 是 SkeletonData 与 Skeleton 的中间载体:SkeletonData 中存储SlotData(插槽静态配置),Skeleton 实例化时会根据SlotData创建Slot(动态插槽对象),Slot 是 ** 骨骼(Bone)和附件(Attachment)** 的唯一挂载点,一个 Slot 同一时间仅能挂载一个素材;
Skeleton 依赖 SkeletonData 生成:Skeleton 是基于 SkeletonData 的动态可操作实例,继承 SkeletonData 的所有静态规则,结合 AttachmentLoader 绑定的素材,最终生成带完整外观、可动的 Setup Pose 成品;
所有静态层对象可被多个动态实例共享:一个 Texture Atlas + 一个 SkeletonData,可生成多个 Skeleton 实例(如多个相同角色),节省内存。
3. 类的核心关联公式
// 基础条件:平台适配层实现完成
平台适配层(SpineExtension+TextureLoader)→ 加载 静态数据层(Texture Atlas + SkeletonData)
// 桥梁绑定:融合素材与规则
AttachmentLoader(绑定Texture Atlas)+ SkeletonData → 生成 带素材的骨骼附件(Attachment)
// 最终生成:动态可操作实例
Skeleton(基于SkeletonData)+ 带素材的附件 → Setup Pose状态的Skeleton(可动成品)
Setup Pose 核心加载流程(4 步拼装法)
Setup Pose 的加载是严格的顺序执行流程,反向 / 跳过步骤会直接报 “素材找不到”“空指针” 等错误,核心目标是生成带完整外观、正确骨骼结构的可操作 Skeleton 实例,QT 框架和 VS 原生 C++ 环境下流程完全一致,仅平台适配层的实现不同,以下是通用 4 步流程,每步包含核心目标、操作要点、调用逻辑:
前置准备
- 集成 Spine C++ SDK 到项目中(QT/VS 通用);
- 按平台实现自定义 SpineExtension(QT 必须,VS 可使用默认
DefaultSpineExtension); - 按平台实现自定义 TextureLoader(QT 用 QPixmap,VS 用 STBImage);
- 准备好 Spine 导出的资源文件:
.atlas(图集配置)、图集大图(PNG/JPG)、.json/.skel(骨骼数据)。
步骤 1:加载纹理图集(Texture Atlas)—— 喊快递员取零件收纳盒
加载骨骼的所有外观素材,生成只读的 Texture Atlas 实例,包含所有图集大图和子素材(AtlasRegion)的切片信息。
操作要点
- 先创建自定义 TextureLoader 实例(平台专属);
- 传入
.atlas文件路径和 TextureLoader 实例,创建 Texture Atlas 对象; - Spine 内部会自动调用 TextureLoader 的
load方法,加载图集大图并赋值到 AtlasPage 的rendererObject(平台原生纹理指针,如 QT 的 QPixmap*)。
核心调用逻辑
// 伪代码(通用)
自定义TextureLoader *loader = new 平台专属TextureLoader(); // QT=QTTextureLoader,VS=STBTextureLoader
spine::Atlas *atlas = new spine::Atlas("xxx.atlas", loader); // 自动调用loader->load()加载大图
这个代码示例注意看上面的示例代码!
步骤 2:加载骨骼数据(SkeletonData)—— 拿拼装说明书
核心目标
解析.json/.skel文件,生成只读的 SkeletonData 实例,包含骨骼结构、插槽配置、初始姿势(Setup Pose)、动画列表等静态规则,此时的 SkeletonData 是 “透明骨骼架子”,无任何外观素材。
操作要点
-
选择解析器:JSON 用
SkeletonJson,二进制.skel用SkeletonBinary(二进制解析更快,推荐); -
解析器构造时必须传入AttachmentLoader 实例(开发中直接用
AtlasAttachmentLoader,并绑定步骤 1 的 Atlas); -
调用解析器的
readSkeletonData方法,传入.json/.skel路径,生成 SkeletonData; -
解析过程中,AttachmentLoader 会自动匹配图集素材,为 SkeletonData 的插槽创建对应的附件(Attachment)。
核心调用逻辑
// 伪代码(通用)
// 1. 创建桥梁:AtlasAttachmentLoader,绑定已加载的图集
spine::AtlasAttachmentLoader *attachmentLoader = new spine::AtlasAttachmentLoader(atlas);
// 2. 创建解析器(二选一:SkeletonJson/SkeletonBinary)
spine::SkeletonBinary *binary = new spine::SkeletonBinary(attachmentLoader); // 解析.skelfile
// spine::SkeletonJson *json = new spine::SkeletonJson(attachmentLoader); // 解析.json文件
// 3. 解析骨骼文件,生成SkeletonData(透明架子+素材绑定)
spine::SkeletonData *skeletonData = binary->readSkeletonData("xxx.skel");
// 注:解析完成后,可释放解析器(binary/json),不影响SkeletonData
步骤 3:初始化附件与骨骼规则绑定 —— 用拼装工具搭架子 + 贴素材
核心目标
完成素材与骨骼的最终绑定,将 AttachmentLoader 匹配好的附件关联到 SkeletonData 的对应插槽,确保每个插槽都有初始姿势的默认素材,这一步由 Spine 内部自动完成,开发者无需手动操作。
关键细节
- 绑定触发时机:解析器(SkeletonJson/SkeletonBinary)调用
readSkeletonData时,会自动调用 AttachmentLoader 的newRegionAttachment/newMeshAttachment等方法; - 绑定规则:按 SkeletonData 中 SlotData 的名称,匹配 Atlas 中同名的 AtlasRegion,为插槽创建初始附件;
- 异常处理:若 Atlas 中无 SkeletonData 所需的素材名称,会返回 NULL,最终 Skeleton 对应插槽无外观(空白)。
步骤 4:生成 Setup Pose 状态的 Skeleton 实例 —— 拼好可动成品
核心目标
基于已绑定素材的 SkeletonData,创建动态可操作的 Skeleton 实例,并自动初始化到 Setup Pose(初始绑定姿势),这是开发中唯一需要直接操作的对象,后续的动画播放、姿势修改、换装均基于该实例。
操作要点
传入 SkeletonData 实例,构造 Skeleton 对象;
Spine 构造时会自动完成 3 件事:
- 根据 SkeletonData 的 BoneData 创建所有骨骼(Bone),建立父子关系;
- 根据 SlotData 创建所有插槽(Slot),并挂载步骤 3 绑定的默认附件;
- 将骨骼、插槽、约束全部重置为 Setup Pose(初始位置 / 旋转 / 缩放,默认素材 / 颜色);
可选操作:设置 Skeleton 的全局位置、缩放(如setPosition(x, y)设置骨骼在屏幕的坐标)。
核心调用逻辑
// 伪代码(通用)
// 1. 创建Skeleton实例,自动初始化到Setup Pose
spine::Skeleton *skeleton = new spine::Skeleton(skeletonData);
// 2. 可选:设置骨骼全局位置、缩放(开发中常用)
skeleton->setPosition(300, 400); // 屏幕坐标(x,y)
skeleton->setScaleX(1.0f);
skeleton->setScaleY(1.0f);
// 3. 必做:更新骨骼世界变换(让姿势生效,渲染前必须调用)
skeleton->updateWorldTransform(spine::Physics::None);
三、加载流程关键注意事项(避坑核心)
顺序不可颠倒:必须先加载Texture Atlas,再加载SkeletonData,否则 AttachmentLoader 无法找到素材,会出现 “素材缺失 / 空白”;
平台适配是前提:QT 平台必须先自定义实现SpineExtension(解决内存 / 文件读取),再实现TextureLoader(解决纹理加载),二者缺一不可;VS 原生 C++ 可直接用默认DefaultSpineExtension,仅需实现TextureLoader;
资源释放要匹配:
Texture Atlas析构时,会自动调用TextureLoader的unload方法释放纹理;Skeleton析构时会释放自身的动态资源,SkeletonData和Texture Atlas可复用,无需随 Skeleton 释放;- 最终释放顺序:
Skeleton→SkeletonData→Texture Atlas→TextureLoader/AttachmentLoader;
Setup Pose 是基础:Skeleton构造后默认就是 Setup Pose,若后续修改了姿势 / 附件,可调用skeleton->setToSetupPose()恢复初始状态;
更新世界变换是必做:每次修改 Skeleton 的姿势、缩放、位置后,必须调用updateWorldTransform,否则修改不会生效,渲染出的骨骼姿势错误。
四、核心流程极简梳理(一句话版)
先通过平台适配层(SpineExtension+TextureLoader)加载纹理图集(素材)和骨骼数据(规则),再用 AtlasAttachmentLoader 将素材与规则绑定,最终基于绑定后的 SkeletonData 创建 Skeleton 实例,初始化到 Setup Pose 并更新世界变换,得到可操作的完整骨骼对象。
所有后续操作(动画播放、手动调姿势、换装、颜色修改),均在Setup Pose 状态的 Skeleton 实例上完成。
#include <QApplication>
#include <QPixmap>
#include <QString>
#include <QDebug>
#include <memory>
#include "mainwindow.h"
// 引入Spine核心头文件(如果pro中INCLUDEPATH配置正确,直接这样引即可)
#include "spine/spine.h"
#include "spine/Atlas.h"
#include "spine/AtlasAttachmentLoader.h"
#include "spine/SkeletonJson.h"
#include "spine/SpineString.h"
#include "qt_spine_extension.h"
// 1. 实现QT版TextureLoader(快递员,加载Atlas大图)
class QTTextureLoader : public spine::TextureLoader {
public:
// 重写load:QT平台加载图片为QPixmap
virtual void load(spine::AtlasPage &page, const spine::String &path) override
{
// std::string 对象接收路径
std::string pathStr(path.buffer());
// 转化成 QT 可以识别的字符串
QString qImgPath = QString::fromStdString(pathStr);
// 加载图片为 QPixmap
QPixmap* texturePixmap = new QPixmap(qImgPath);
if (texturePixmap->isNull()) {
qDebug() << "[QTTextureLoader] 图片加载失败:" << qImgPath;
return;
}
// 核心赋值1:将QT原生的纹理对象指针存入AtlasPage的万能指针texture
// page.texture是void*类型,支持跨平台存储任意平台的纹理对象(QT=QPixmap*,VS=STBImage*)
page.texture = texturePixmap;
// 核心赋值2:设置大图的实际像素宽高(Spine必传!)
// Spine依赖这两个值计算AtlasRegion(小素材)在大图中的裁剪/显示位置,缺失会导致渲染错位/空白
page.width = texturePixmap->width(); // 获取QPixmap的实际宽度(像素)
page.height = texturePixmap->height(); // 获取QPixmap的实际高度(像素)
// 7. 调试日志:输出加载成功信息,方便开发排查问题(发布版本可注释)
qDebug() << "[QTTextureLoader-Success] 图片加载成功"
<< "| 路径:" << qImgPath
<< "| 像素尺寸:" << page.width << "x" << page.height;
}
// 重写unload:QT平台释放QPixmap
virtual void unload(void *texture) override
{
// 1. 万能指针强转为QT的QPixmap*类型:和load中存入的类型完全匹配,安全强转
QPixmap* texturePixmap = static_cast<QPixmap*>(texture);
// 2. 安全释放:判空后再删除,避免野指针/重复释放导致的程序崩溃
if (texturePixmap) {
delete texturePixmap; // 释放堆创建的QPixmap对象,回收内存
texturePixmap = nullptr; // 指针置空,防止成为"悬空指针"(指向已释放的内存)
qDebug() << "[QTTextureLoader-Success] 图片资源已释放"; // 调试日志,发布版本可注释
}
// 若texture为null,直接跳过,无操作
}
};
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
// 1. 创建并显示Qt主窗口(原逻辑保留)
QMainWindow w;
w.setWindowTitle("Spine Qt 演示窗口");
w.resize(800, 600);
w.show();
// 2. 初始化Spine核心对象(原逻辑保留)
QTTextureLoader qtLoader;
std::unique_ptr<spine::Atlas> atlas;
std::unique_ptr<spine::SkeletonData> skeletonData;
const QString atlasPath = "D:/Spine/spine/res/hongyan-spine/hongyan-spine/hongyan002_a/hongyan002_a.atlas";
const QString skeletonJsonPath = "D:/Spine/spine/res/hongyan-spine/hongyan-spine/hongyan002_a/hongyan002_a.json";
qDebug() << "===== 开始加载Spine资源 =====";
qDebug() << "[准备加载] Atlas路径:" << atlasPath;
qDebug() << "[准备加载] 骨骼Json路径:" << skeletonJsonPath;
// 3. 加载Atlas(核心修改1:加try-catch,捕获Spine解析异常)
try {
atlas = std::make_unique<spine::Atlas>(atlasPath.toStdString().c_str(), &qtLoader);
// 强制触发Atlas实际加载(解决懒加载问题)
// 访问getPages()会让Spine解析atlas文件、调用QTTextureLoader加载图片
size_t pageCount = atlas->getPages().size();
size_t regionCount = atlas->getRegions().size();
// 校验Atlas是否真的加载成功(非空+有页面/区域)
if (atlas && pageCount > 0 && regionCount > 0) {
qDebug() << "[Atlas-SUCCESS] 纹理图集加载成功!";
qDebug() << "[Atlas-INFO] 包含大图数:" << pageCount << " | 小素材数:" << regionCount;
// 1. 创建AtlasAttachmentLoader → 核心桥梁:连接Atlas和骨骼 (就是拿出工具,准备使用)
// 原理:SkeletonJson解析骨骼文件时,通过它调用atlas->findRegion()根据名称匹配素材
spine::AtlasAttachmentLoader attachmentLoader(atlas.get());
// 2. 创建SkeletonJson解析器 → 专门解析Spine导出的.json骨骼配置文件
spine::SkeletonJson skeletonJson(&attachmentLoader);
// 3. 解析.json文件,生成SkeletonData(骨骼核心数据:包含所有骨骼/附件/插槽/皮肤等信息)
spine::SkeletonData* tempData = skeletonJson.readSkeletonDataFile(skeletonJsonPath.toStdString().c_str());
// 安全的话,就交给智能指针管理
if (tempData) {
skeletonData.reset(tempData); // 智能指针接管内存,自动析构
qDebug() << "[SkeletonData-SUCCESS] 骨骼数据加载成功!【无动画设置】";
qDebug() << "[SkeletonData-INFO] 骨骼总数:" << skeletonData->getBones().size();
qDebug() << "[SkeletonData-INFO] 插槽总数:" << skeletonData->getSlots().size();
qDebug() << "[SkeletonData-INFO] 皮肤总数:" << skeletonData->getSkins().size();
} else {
qDebug() << "[SkeletonData-ERROR] 骨骼数据加载失败!原因:" << skeletonJson.getError().buffer();
atlas.reset(); // 素材加载成功但骨骼解析失败,释放Atlas避免内存泄漏
skeletonData.reset(); // 兜底置空
}
} else {
qDebug() << "[Atlas-ERROR] 纹理图集加载空壳!路径错误/文件损坏/无素材";
// 兜底:空壳直接释放,避免内存泄漏
atlas.reset();
}
} catch (const std::exception& e) {
// 捕获Spine解析atlas文件的异常(比如文件格式错、路径不存在)
qDebug() << "[Atlas-EXCEPTION] 加载失败,原因:" << e.what();
atlas.reset();
}
return a.exec();
}

更多推荐
所有评论(0)