We do something similar, but I've found a way to not even need a default atlas file at all.
You still need to create a custom attachment loader, inheriting from Cocos2dAtlasAttachmentLoader, but the trick is the implementation of the following two overrides:
static void deleteAttachmentVertices(void* vertices) {
delete (spine::AttachmentVertices*)vertices;
}
static unsigned short quadTriangles[6] = { 0, 1, 2, 2, 3, 0 };
static void setAttachmentVertices(spine::RegionAttachment* attachment) {
spine::AtlasRegion* region = (spine::AtlasRegion*)attachment->getRendererObject();
auto renderObject = (region == nullptr || region->page == nullptr) ? nullptr : (Texture2D*)region->page->getRendererObject();
spine::AttachmentVertices* attachmentVertices = new spine::AttachmentVertices(renderObject, 4, quadTriangles, 6);
V3F_C4B_T2F* vertices = attachmentVertices->_triangles->verts;
for (int i = 0, ii = 0; i < 4; ++i, ii += 2) {
vertices[i].texCoords.u = attachment->getUVs()[ii];
vertices[i].texCoords.v = attachment->getUVs()[ii + 1];
}
attachment->setRendererObject(attachmentVertices, deleteAttachmentVertices);
}
static void setAttachmentVertices(spine::MeshAttachment* attachment) {
spine::AtlasRegion* region = (spine::AtlasRegion*)attachment->getRendererObject();
auto renderObject = (region == nullptr || region->page == nullptr) ? nullptr : (Texture2D*)region->page->getRendererObject();
spine::AttachmentVertices* attachmentVertices = new spine::AttachmentVertices(renderObject,
attachment->getWorldVerticesLength() >> 1, attachment->getTriangles().buffer(), attachment->getTriangles().size());
V3F_C4B_T2F* vertices = attachmentVertices->_triangles->verts;
for (int i = 0, ii = 0, nn = attachment->getWorldVerticesLength(); ii < nn; ++i, ii += 2) {
vertices[i].texCoords.u = attachment->getUVs()[ii];
vertices[i].texCoords.v = attachment->getUVs()[ii + 1];
}
attachment->setRendererObject(attachmentVertices, deleteAttachmentVertices);
}
void CustomAttachmentLoader::configureAttachment(spine::Attachment* attachment) {
if (attachment->getRTTI().isExactly(spine::RegionAttachment::rtti)) {
setAttachmentVertices((spine::RegionAttachment*)attachment);
}
else if (attachment->getRTTI().isExactly(spine::MeshAttachment::rtti)) {
setAttachmentVertices((spine::MeshAttachment*)attachment);
}
}
spine::MeshAttachment* CustomAttachmentLoader::newMeshAttachment(spine::Skin& skin, const spine::String& name, const spine::String& path)
{
SP_UNUSED(skin);
auto regionP = findRegion(path);
if (!regionP)
{
auto attachmentP = new(__FILE__, __LINE__) spine::MeshAttachment(name);
return attachmentP;
}
return Cocos2dAtlasAttachmentLoader::newMeshAttachment(skin, name, path);
}
spine::RegionAttachment* CustomAttachmentLoader::newRegionAttachment(spine::Skin& skin, const spine::String& name, const spine::String& path)
{
SP_UNUSED(skin);
auto regionP = findRegion(path);
if (!regionP)
{
auto attachmentP = new(__FILE__, __LINE__) spine::RegionAttachment(name);
return attachmentP;
}
return Cocos2dAtlasAttachmentLoader::newRegionAttachment(skin, name, path);
}
What happens here is that it doesn't find a region, since no atlas file is loaded, and since the region is null, it will still create a sort of placeholder attachment that you can later replace yourself.
When you load the skeleton data, just make sure you cache it, and you should be able to re-use it to create new spine::SkeletonAnimation
instances.
Now, I assume you have a list of image IDs and what slot/placeholder they map to on the model, and that you have created sprite sheets from those items. You then simply do something similar to this:
auto textureCache = Director::getInstance()->getTextureCache();
auto texture = textureCache->getTextureForKey(pngFilename);
if (texture == nullptr)
{
auto image = new Image();
Image::setPNGPremultipliedAlphaEnabled(false); // to avoid applying PMA on load since textures saved from Cocos2d RenderTexture are already PMA
image->initWithImageFile(pngFilename);
Image::setPNGPremultipliedAlphaEnabled(true);
texture = textureCache->addImage(image, pngFilename);
CC_SAFE_RELEASE(image);
}
Assuming your dynamically generated sprite sheet is in PLIST format, then do this:
auto spriteCache = SpriteFrameCache::getInstance();
spriteCache->addSpriteFramesWithFile(plistFilename);
auto page = spine::AtlasUtilities::ToSpineAtlasPage(texture);
If it's not in PLIST format, then you'll need to create a loader for that format yourself.
Once this is loaded, you then loop through all the slots and placeholders that belong to the items the player needs to equip, and replace those attachments.
This is a snippet of code from our app:
auto templateSlots = _skeleton->getSlots();
auto frameCache = SpriteFrameCache::getInstance();
const auto imageFilesPrefix = std::string("cache/player/");
spine::Vector<spine::String> slotPlaceholders{};
for (auto&& slotPair : item.Slots)
{
auto slotName = slotPair.first;
const auto itemSlotIndex = _skeleton->findSlotIndex(slotName.c_str());
if (itemSlotIndex == -1)
continue;
slotPlaceholders.clear();
_templateSkin->findNamesForSlot(itemSlotIndex, slotPlaceholders);
for (size_t i = 0; i < slotPlaceholders.size(); i++)
{
auto& placeholderId = slotPlaceholders[i];
// Get the attachment associated with this placeholder, because we need the attachment name
auto templateAtt = _templateSkin->getAttachment(itemSlotIndex, placeholderId);
if (!templateAtt)
{
continue;
}
auto templateAttachmentName = templateAtt->getName();
auto frame = frameCache->getSpriteFrameByName(imageFilesPrefix + std::string(templateAttachmentName.buffer()));
if (!frame)
continue;
auto spr = Sprite::createWithSpriteFrame(frame);
auto atlasRegion = spine::AtlasUtilities::ToAtlasRegion(spr, page);
auto newSkinAtt = spine::AttachmentCloneExtensions::GetRemappedClone(templateAtt, atlasRegion, true, true, true);
if (newSkinAtt != nullptr)
{
// These 2 commented lines are only required for Spine Runtime < v3.8
//auto oldAtt = mySkin->getAttachment(itemSlotIndex, placeholderId);
//delete oldAtt;
mySkin->removeAttachment(itemSlotIndex, placeholderId);
mySkin->setAttachment(itemSlotIndex, placeholderId, newSkinAtt);
}
}
}
spine::AtlasUtilities::ToAtlasRegion
and spine::AttachmentCloneExtensions::GetRemappedClone
are somewhat based on the Unity/C# version of the code, located here: https://github.com/EsotericSoftware/spine-runtimes/tree/3.8/spine-unity/Assets/Spine/Runtime/spine-unity/Utility
We store the attachments in the dynamically generated sprite sheet with the same name as the attachments from the default skin. For example, say the template skin has a slot named HatLarge, with placeholder that has an attachment named skin/HatLarge
. Now if the user selects an item with a name FancyHat.png to equip, then when we generate the sprite sheet, it will store the name as skin/HatLarge
in the sprite atlas. In our case, we also add an extra prefix, like cache/player/
, to avoid name collisions in the cocos2d-x SpriteFrameCache, and you can see in the snippet of code above how we deal with it. All of this makes it so much easier to work with.
This works for our application, so I'm quite certain it will work for what you're trying to do.