加载Skeleton数据 - Spine运行时指南

其实本篇刚开始是讲的 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):

  1. AtlasAttachmentLoader:开发首选,与纹理图集绑定,从Atlas中查找素材并创建附件,适配绝大多数 2D 开发场景;
  2. 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 步流程,每步包含核心目标、操作要点、调用逻辑:

前置准备

  1. 集成 Spine C++ SDK 到项目中(QT/VS 通用);
  2. 按平台实现自定义 SpineExtension(QT 必须,VS 可使用默认DefaultSpineExtension);
  3. 按平台实现自定义 TextureLoader(QT 用 QPixmap,VS 用 STBImage);
  4. 准备好 Spine 导出的资源文件:.atlas(图集配置)、图集大图(PNG/JPG)、.json/.skel(骨骼数据)。

步骤 1:加载纹理图集(Texture Atlas)—— 喊快递员取零件收纳盒

加载骨骼的所有外观素材,生成只读的 Texture Atlas 实例,包含所有图集大图和子素材(AtlasRegion)的切片信息。

操作要点

  1. 先创建自定义 TextureLoader 实例(平台专属);
  2. 传入.atlas文件路径和 TextureLoader 实例,创建 Texture Atlas 对象;
  3. 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 是 “透明骨骼架子”,无任何外观素材。

操作要点

  1. 选择解析器:JSON 用SkeletonJson,二进制.skel用SkeletonBinary(二进制解析更快,推荐);

  2. 解析器构造时必须传入AttachmentLoader 实例(开发中直接用AtlasAttachmentLoader,并绑定步骤 1 的 Atlas);

  3. 调用解析器的readSkeletonData方法,传入.json/.skel路径,生成 SkeletonData;

  4. 解析过程中,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 内部自动完成,开发者无需手动操作。

关键细节
  1. 绑定触发时机:解析器(SkeletonJson/SkeletonBinary)调用readSkeletonData时,会自动调用 AttachmentLoader 的newRegionAttachment/newMeshAttachment等方法;
  2. 绑定规则:按 SkeletonData 中 SlotData 的名称,匹配 Atlas 中同名的 AtlasRegion,为插槽创建初始附件;
  3. 异常处理:若 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();
}

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐