• Runtimes
  • spine-ts - change region on an attachment during runtime

Related Discussions
...

I have a Spine character that I want to make customizable during runtime. For many of the customizations, I do not need to change the attachment in a slot, I only need to change the region (image from Atlas) to customize the character, for example hair, beard, armor tye, etc.

In the Spine editor I just change the region path under the slot's skin placeholder to create a bunch of different characters.

Is there a way to change the region for an attachment during runtime using spine-ts, I see in some of the Unity threads mentions of cloning attachments, I am wondering if that is the way or if there is something easier, if only the region/image needs to be changed.


Here's a first attempt, it kind of works, but feels pretty hacky (requires a skin reload to take effect and the hair is offset from the original location.)

I think the hardwiring of the uvs array to u,v,u2,v2 settings will also fail when the texture in the atlas is rotated.

I imagine there is a better 'clean' method to do this.

It's ok for the original skin to change, there is only one instantiation of the skeleton used (which might be something to revisit in the C3 plugin at some point.)

SetAttachment(slot, regionName){
    const skeleton = this.skeletonInfo.skeleton;
    const atlas = this.skeletonInfo.atlas;
    let newRegion  = atlas.findRegion(regionName)
    console.log(newRegion)
    let skin = skeleton.data.findSkin(this.skinName)
    let slotIndex = skeleton.findSlot(slot).data.index
    let regionAttachment = skin.getAttachment(slotIndex, 'hairs').copy();
    console.log(regionAttachment)
    regionAttachment.path = regionName
    regionAttachment.name = regionName
    regionAttachment.region = newRegion
    regionAttachment.uvs[0] = regionAttachment.region.u2
    regionAttachment.uvs[1] = regionAttachment.region.v2
    regionAttachment.uvs[2] = regionAttachment.region.u
    regionAttachment.uvs[3] = regionAttachment.region.v2
    regionAttachment.uvs[4] = regionAttachment.region.u
    regionAttachment.uvs[5] = regionAttachment.region.v
    regionAttachment.uvs[6] = regionAttachment.region.u2
    regionAttachment.uvs[7] = regionAttachment.region.v
    console.log(regionAttachment)
    console.log(skin)
    skin.setAttachment(slotIndex, 'hairs', regionAttachment)
},

Note by default attachments are shared across multiple skeleton instances, so if you change one it will change for all skeleton instances. That is one reason to clone the attachment instead of changing the existing one. Another reason is if you want to still use the original.

It's not as simple as just changing the region name, since the image needs to be loaded and set on the attachment. When skeleton data is loaded, the AttachmentLoader is responsible for doing that. The attachment loader can be customized, but often an AtlasAttachmentLoader is used. You can see what that does:
spine-runtimes/AtlasAttachmentLoader.ts at 3.8
You can do the same to change the region later (set RegionAttachment rendererObject and call RegionAttachment setRegion).

Thanks for the tips and the comments, I will try out some of the other techniques like AtlasAttachmentLoader.newRegionAttachment().

Right now, there is no issue for multiple skelton instances in my current implementation (though in a future optimization that may change.)

Currently, after I change the region, I do a skeleton.setSkinByName(this.skinName) and skeleton.setSlotsToSetupPose(), which seems to load the texture (and is probably heavier lifting this is required.)

When the atlas is created the textures are loaded. I don't think you need setSkinByName. Changing the region (which you do) and the renderObject (which you do not) should be sufficient.

Some more background and progress, I have single texture image and single atlas, single json, I have about 10 character skins included with different hair, weapons, etc. so I'm basically mixing and matching between characters attachments and the 'original' image size per slot (e.g. hair) are all the same.

So, all I am really looking for to just change the image / texture. I got something working, but it sure feels like a hack. I had to change the atlas export options to not delete white space (in x and y), so the image size stays the same as original (otherwise they will not scale properly.) Here's my hacky code, I'm still exploring trying to find a better way (and find out how to delete white space and scale properly.) I still have to do setSkindByName() to have it be updated.

Update code:

let newRegionAttachment  = atlasLoader.newRegionAttachment(this.skin, regionName, regionName)

let skin = skeleton.data.findSkin(this.skinName)
let slotIndex = skeleton.findSlot(slot).data.index

let regionAttachment = skin.getAttachment(slotIndex, 'hairs').copy();

newRegionAttachment.path = newRegionAttachment.name
newRegionAttachment.color = regionAttachment.color
newRegionAttachment.height = regionAttachment.height
newRegionAttachment.offset = regionAttachment.offset
newRegionAttachment.rotation = regionAttachment.rotation
newRegionAttachment.scaleX = regionAttachment.scaleX
newRegionAttachment.scaleY = regionAttachment.scaleY
newRegionAttachment.tempColor = regionAttachment.tempColor
newRegionAttachment.width = regionAttachment.width
newRegionAttachment.x = regionAttachment.x
newRegionAttachment.y = regionAttachment.y

skin.setAttachment(slotIndex, 'hairs', newRegionAttachment)

Example working in Construct 3 (using C3 Spine plugin - based on spine-ts lib ):

The RegionAttachment copy method creates a copy of the attachment, so you don't need to also set a bunch of fields afterward. Sorry I didn't think to mention the copy method earlier, it is a relatively new addition. If you just want to change the region on the existing attachment, you don't need to make a copy at all.

You should be able to rewrite your code like this (untested):

// First get the new region from the atlas.
let atlas = atlasLoader.atlas // If you don't already have a reference to the atlas, the loader does.
let region = atlas.findRegion(regionName);
if (region == null) throw new Error("Region not found in atlas: " + regionName);

// Get the existing attachment.
let skin = skeleton.data.findSkin(skinName)
let slotIndex = skeleton.data.findSlot(slotName).index
let existing = skin.getAttachment(slotIndex, 'hairs');

// Alternatively if the skin is set on the skeleton, you can get it from the skeleton.
let existing = skeleton.getAttachment(slotName, 'hairs');

// Now do what AtlasAttachmentLoader does:
// https://github.com/EsotericSoftware/spine-runtimes/blob/3.8/spine-ts/core/src/AtlasAttachmentLoader.ts#L42
region.renderObject = region;
existing.setRegion(region);

// Need to do one last thing, which SkeletonJson/SkeletonBinary do last:
// https://github.com/EsotericSoftware/spine-runtimes/blob/3.8/spine-ts/core/src/SkeletonJson.ts#L326-L340
existing.updateOffset();

Thanks Nate! This worked great. The only change I had to do was a slight tweak of slotName -> slotIndex in the below (I know the code sample was untested, so I understand.)

let existing = skeleton.getAttachment(slotIndex, 'hairs');

I'll also keep the copy method in mind, when/if I change to do creating multiple instances from one SkeletonData (which I understand would be more efficient.)

I'm going to add it to the Construct3 plugin and release soon.

Great! Glad that worked out. :beer:

Oops, I forgot it's getAttachmentByName for spine-ts. Either way works fine.

We'll see about improving things so region.renderObject = region; isn't needed in the future. The renderObject field allows for customization, but it's a bit weird to see. It could just default to that value instead.