• Runtimes
  • Change the texture of an attachment at runtime

Some background info - I'm updating a game that was originally written in Phaser 3.18.1 using Spine runtime 3.7.94. The only real update that I'm doing to it is to bring it up to Phaser 3.55.2 and Spine runtime 3.8.99. Within this game there is a customizable character that is also a Spine animation. The customization is just changing the body color of the character and also a pattern can be applied if desired (there are 12 different colors that can be used for the body color and for the pattern color). There are approximately 672 different combinations. I've attached two customizations to give an idea of what is possible.

In the original code, this is how this customization was done:

this.character = this.add.spine(0, 0, "fuzzbug", "Fall", true); // this creates the spine object; this is a Phaser.Scene

// the color canvas creates the color that the character will be (plus the pattern and color)
let colorCanvas = new ACanvas(this, "colorCanvas", 258, 259); // this creates a Canvas (ACanvas is a wrapper class that we use to encompass some functionality)
if(this.stencil != "")
{
    colorCanvas.addSquare(this.stencilColor); // makes the entire canvas the color provided
    colorCanvas.setComposition(ACanvas.compositions.DESTINATION_OUT); // changes the composition of the canvas
    colorCanvas.addImage(this.stencilData[this.stencil].x, this.stencilData[this.stencil].y, this.stencil); // draws the stencil/pattern to the canvas
    colorCanvas.setComposition(ACanvas.compositions.DESTINATION_OVER);
}
colorCanvas.addSquare(this.color); // makes the entire canvas the color provided

// the spine canvas is the canvas that contains the body of the character. the color canvas is then applied to it to paint the character body the color
let spineCanvas = new ACanvas(this, "spineCanvas", this.atlasSize.width, this.atlasSize.height); // atlas size is 1024x1024
spineCanvas.addImage(0, 0, this.textureKey); // texture key is character body image key
spineCanvas.setComposition(ACanvas.compositions.MULTIPLY);
spineCanvas.context.drawImage(colorCanvas.context.canvas, this.bodyData.x, this.bodyData.y); // 
spineCanvas.setComposition(ACanvas.compositions.DESTINATION_IN);
spineCanvas.addImage(0, 0, this.textureKey);

// create a new WebGLTexture
let gl = this.game.context; // this is a Phaser.Scene
let texture = gl.createTexture();

gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, spineCanvas.context.getImageData(0, 0, this.atlasSize.width, this.atlasSize.height));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

// set the texture into the skin of the spin/skeleton
this.character.skeleton.skin.attachments[7]["images/Base"].region.texture.texture = texture;

And this worked. It was probably a bit hacky but it did the job (note that I'm not the original developer of this game, I'm simply porting it).

The issue I'm running into now (after solving a few other issues), is that when I set the texture, it now applies the texture to every instance of the Spine object that was created:

this.characterA = this.add.spine(0, 0, "fuzzbug", "Fall", true);
// do the customization from above for characterA
this.characterB = this.add.spine(0, 0, "fuzzbug", "Fall", true);
// do a different customization for characterB
// both characterA and characterB have the same body instead of being different

I've tried the following:

this.character = this.add.spine(0, 0, "fuzzbug", "Fall", true);
let baseAttachment = this.character.skeleton.getAttachment(7, "images/Base");
let skin = new spine.Skin(myUniqueSkinName); // create a new skin for this spin
skin.copySkin(this.character.skeletonData.defaultSkin);
skin.setAttachment(7, "images/Base", a.copy()); // I've tried including this and also not including this...
this.character.setSkin(skin); // set the new skin
// do the customization

But this doesn't seem to work. After a bit of testing and reading through documentation, the Skin gets completely copied, so it is unique between the two instances, however the Regions (which are TextureAtlasRegions) are still the same instance, so when I do this.character.spine.skeleton.getAttachment(7, "images/Base").region.texture.texture = texture; it still changes every Skin that is using it, because even though the Skins are different instances, the regions they have are references to the same region, so both get changed. I tried hacking my way down and creating a new TextureAtlasRegion but I haven't been able to get it to work and I feel like there has to be a better way to do this.

I do realize that "Skins" I think are meant to be a solution for this, but considering there are 672 or so combinations of colors/patterns, it doesn't seem like a great way to do it, because setting all of those up in Spine will be a huge pain I would think. If there were only a dozen options maybe. But there has to be some way to say "take this body part and change what texture it is using to render" or something similar? And then make sure that each instance of the same Spine animation can use a different one.

Any help would be great appreciated. Thanks!

Related Discussions
...
  • Изменено

This seems to work in Phaser 3.6 beta6. Just clone the region reference, modify its texture and set it back on the attachment, that will affect only that spine instance.

Psuedo code:

spineRegion = yourSpine.skeleton.getAttachmentByName('slot name','attachment name').region
newRegion = objectClone(spineRegion)
newRegion.texture.texture = yourTexture
yourSpine.findSlot('slot name').attachment.setRegion(newRegion)
stupot написал

This seems to work in Phaser 3.6 beta6. Just clone the region reference, modify its texture and set it back on the attachment, that will affect only that spine instance.

Psuedo code:

spineRegion = yourSpine.skeleton.getAttachmentByName('slot name','attachment name').region
newRegion = objectClone(spineRegion)
newRegion.texture.texture = yourTexture
yourSpine.findSlot('slot name').attachment.setRegion(newRegion)

I haven't been able to get this to work. I'm running into two main issues, the first is what the objectClone function is. Shallow clones don't seem to work (the Spines still share the textures), while deep clones fail because the region object (a TextureAtlasRegion object) has a reference to another TextureAtlasRegion and you eventually get a Maximum call stack size exceeded error.

The second issue is there is no setRegion function on the attachment object. I've tried just doing yourSpine.findSlot('slot name').attachment.region = newRegion; but this doesn't work

If you have some more definitive code that shows how you were able to do it, that would be helpful.

But I'm relatively sure this isn't going to work anyway, because the attachment objects are still shared between the two spine objects, so when you change the region property of one, they are all going to see that change. You would need to clone the whole attachment object. I've tried creating a new skin and then doing the above but still not able to get it to work. I'm also using Phaser 3.55.2 so it's possible there's a difference between 3.55.2 and 3.6 beta.

I did find a sort of workaround though - when I load the spine, instead of just loading it once and then creating it multiple times, I instead load it multiple times and use the different loaded objects to create the spines:

// config to load the spine
{ key: 'fuzzbug', jsonPath: "assets/spine/fuzzbug.json", atlasPath: "assets/spine/fuzzbug.atlas" },
{ key: 'fuzzbug_alt0', jsonPath: "assets/spine/fuzzbug.json", atlasPath: "assets/spine/fuzzbug.atlas" }
{ key: 'fuzzbug_alt1', jsonPath: "assets/spine/fuzzbug.json", atlasPath: "assets/spine/fuzzbug.atlas" }

// when creating the spine objects, do
this.add.spine(0, 0, "fuzzbug", "Fall", true);
this.add.spine(0, 0, "fuzzbug_alt0", "Fall", true);
this.add.spine(0, 0, "fuzzbug_alt1", "Fall", true);

The end result is they don't share the same rendering texture and it can be freely modified without issue, and they still have all the same animations and everything, since it's still the same spine files being loaded. The downside is the data has to be loaded multiple times (and stored in memory multiple times) and you have to load it as many times as you need unique versions on screen... luckily I only ever need at most 3 so I only need to load it 3 times.

I do realize that "Skins" I think are meant to be a solution for this, but considering there are 672 or so combinations of colors/patterns, it doesn't seem like a great way to do it, because setting all of those up in Spine will be a huge pain I would think.

Skins are used for this, but as you said, you'd not want to set them all up in the Spine editor if you many combinations. Instead you'd want to configure the skin(s) at runtime.
Runtime Skins - Spine Runtimes Guide: Creating attachments

The SkeletonData holds animations, attachments, skins, and other objects that are shared across the skeletons. This means you only need to load that data once and many Skeleton instances can use it.
Runtime Architecture - Spine Runtimes Guide: Class diagram

To do what you want, you need to copy the region attachment. This gives you an instance that is not in the SkeletonData and not used by any skeleton instances (yet).

Next you need to put it in a skin. If the skin is from the SkeletonData, it could be in use by other skeleton instances, so you probably want to create a new skin, possibly by copying an existing one so other attachments are already assigned.

Once your skin is ready, you need to set it on the skeleton, see Skeleton skin. Afterward you may want to adjust which attachments are visible, eg using Skeleton setAttachment or Skeleton setSlotsToSetupPose.

Note that the skin is what allows the animations to call Skeleton setAttachment to show an attachment by skin placeholder name, without knowing what actual attachment will be set. All your animations that show or hide attachments will still work, hiding or showing your copied attachment if that has been keyed.

To help understand the above, I highly suggest reading through the Spine Runtimes code. There is not a huge amount and it will help a lot.

Lastly, you now have your skeleton configured with a new attachment that is not shared by other skeleton instances, and all your animations still work. Now you can modify the attachment for that specific skeleton instance, without affecting any other skeletons.

To change the texture region an attachment will use to render, you usually modify a renderer field on the attachment, but it varies by game toolkit. To see that, you could read through the skeleton rendering code for your game toolkit and see how they use the attachment to render.

To help understand how an attachment is configured with a texture region, I suggest reading the code for the AttachmentLoader that the runtime uses. This will show you what fields that game toolkit sets on the attachment to configure it for later rendering.

I don't have much knowledge of Phaser or how they integrated the Spine Runtimes, but hopefully the above helps!

sreaddev написал

The second issue is there is no setRegion function on the attachment object. I've tried just doing yourSpine.findSlot('slot name').attachment.region = newRegion; but this doesn't work

This shows where the setRegion() is, you will have to change the slot name to whatever you are using otherwise you won't get a valid attachment

For the object clone, I used a very useful mix() from here: https://zellwk.com/blog/copy-properties-of-one-object-to-another-object/

My test case was that I had several instances of the same spine, If I modified the texture on the region, all instances changed. If I cloned the region, changed that texture and set that region to just one spine, then just that spine changed. I've no idea how robust this is, could easily fall apart later with attachment changes and anims. As Nate says, also using skins would by less hacky.

If you have patterns and colours and a lot of combinations of both, why don't you just have a slot for the colour and a slot for patterns, then you independently choose both components.

OK, no problem. You may still find it helpful to see the 4.0+ copy method so you can write your own for 3.8.