• Runtimes
  • Is Attachment API only exposed in Unity Runtimes?

I'd like to move away from the Unity ecosystem to another engine, but all other alternatives I've looked at only support animation playback and skins defined in the Spine editor. The attachment API for runtime only skin purposes, where new skin images come from outside sources, is not exposed.

Think of a decently complex character customization/equipment system with hundreds of items and the intent to update the amount of available customization options while the game is live.

Both Godot and Defold would fit my use cases if not for a lack of Spine Attachment API. Am I correct in my assessment?

Related Discussions
...
T.Fly() изменил(а) название на "Is Attachment API only exposed in Unity Runtimes? ".

I tried an experiment recently in Gdscript where I successfully set the attachment of SpineSprite A's slot using SpineSprite B's attachment. Both SpineSprite uses a different atlas. This is some extraneous information but AtlasAttachmentLoader in Godot would definitely be nice. The Spine-C++ has the AtlasAttachmentLoader class but isn't exposed in Godot. I had been tinkering with it but without success.

The reason you didn't have success is because it's not straight forward to support this and also support Godot's assets pipeline. There's an issue open for this, but I'm honestly not sure how to expose this the best at the moment. I was hoping the SlotNode's would help with this, but appearently it doesn't cover all use cases.

If you can describe a specific use case in detail, I can see if I can come up with something that works and doesn't break existing things.

    Mario How do Spine runtimes in Godot handle skeletons with many skins? I assume it's the same as Unity - once the SpineSprite is instantiated/loaded all related skins are also loaded in memory even if not displayed.

    I've had the game literally stall in Unity when instantiating a complex skeleton with dozens of skins due to atlas count/size. This is one issue attachment API solves since I can control when a particular skin is loaded into memory.

    SpineSlot node also sorta allows for that manual control, but it doesn't really work for complex setups since you're inserting an unrelated skeleton that's difficult to fully sync.

    Oh, that's a different problem then. Skins are part of the skeleton data, there's really no way to separate that. However, they are super lightweight, basically just a mapping from slot/attachment -> image in an atlas or other image source.

    What is not part of the skeleton data is the atlas. And loading that is what likely introduces the stalls you saw. If you stuff all your images for all skins into a single atlas, then yes, loading may take a while if your atlas has a dozen 2048x2048 pages of images.

    A way to fix this is to generate an atlas of only the images you need for a specific skin on the fly at runtime. If you had full access to the spine-cpp API, you could then create an AtlasAttachmentLoader that creates an Atlas from your on-the-fly packed atlas data. With that attachment loader, you'd then load the SkeletonData, which resolves references to images via the attachment loader your provided, and thus from your on-the-fly packed atlas. And with that SkeletonData in hand, you can then create one or more Skeleton instances.

    In spine-unity, all of that is exposed to you, as you have full access to spine-csharp. This is also the case for
    spine-monogame and spine-ts based runtimes. The core Spine APIs are implemented in the "native" language of the respective framework/engine/platform.

    spine-godot uses spine-cpp under the hood, as implementing the core Spine APIs in GDScript would be prohibtively slow. We can not expose the full spine-cpp API surface to GDScript (or C#) as it is humongous, but more importantly because it is manually memory managed. A manually memory managed C++ API does not map well to automatically memory managed languages like GDScript or C#. spine-godot thus has to limit itself in what it exposes, and expose it in a Godot idiomatic way (has to be useable from GDScript and other scripting languages).

    And that is the reason why you generally can not create new Spine "objects" like bones, constraints, or attachments in spine-godot. You'd create those in GDScript (or C#, or any other memory managed language that's supported in Godot). That would have to create a corresponding C++ instance under the hood. Now the question becomes who is managing the "life-time" of that object. On the managed side, it's the GDScript ref counting mechanism or .NET's garbage collector. We could tie the release of the native memory object to the ref count reaching 0 in GDScript (or a garbage collector finalizer in .NET), but those are not deterministic. There are a gazillion edge cases that will result in native memory not being released, or native memory being released to early. Here's an example.

    You create an attachment on the GDScript side and set it on a skin. That creates a native memory equivalent, as spine-cpp needs a native memory representation to work with. Your GDScript only holds on to the GDScript side object until the end of the function/method. The reference counter goes to 0 and the native memory attachment is freed. Meanwhile, the skin has no idea that the attachment you just set on it is no longer valid in native memory. The SpineSprite is rendered, which uses the now freed attachment, and crashes with a seg fault.

    The opposite can happen as well. Let's say you constructed a custom attachment and set it on different skins. No skin could take on the life time management of the attachment, as they don't know about each other. So who will "free" the attachment? The user? Then the API becomes super cumbersome, because all of a sudden, the user has to care about managing the life-time of attachments manually. Forgot that you released an attachement but keep using it in a skin in another part of your game? You get a segfault. Forget to release an attachement? You get a memory leak. We do not want to expose our spine-godot users to that kind of API.

    So, we have not exposed all of the spine-cpp API yet, as it is unclear how to best expose these kind of things. I did make an exception for skins. You can construct a new skin in GDScript via SpineSprite::new_skin. That will create a SpineSkin on the GDScript (or C#) side, which is reference counted (or GCed). The assumption is that you will not get rid of a reference to the skin that is still in use. If you set such a skin on a SpineSprite it will increase the reference count, so even if you get rid of references to it in your game code, the SpineSprite will still hold on to it, until you set another skin. Have a look at the corresponding code of SpineSkin and all source code that referes to SpineSkin. It's not pretty. E.g. when you set a skin you call SpineSkeleton::set_skin. That internally actually has to hold on to the skin you pass in, just in case your game side code doesn't keep a reference to it. It also has to unref the previously set skin if any.

    void SpineSkeleton::set_skin(Ref<SpineSkin> new_skin) {
    	SPINE_CHECK(skeleton, )
    	if (last_skin.is_valid()) last_skin.unref();
    	last_skin = new_skin;
    	skeleton->setSkin(new_skin.is_valid() && new_skin->get_spine_object() ? new_skin->get_spine_object() : nullptr);
    }

    But it's the only way we can make this work at all, due to the conflict between manually managed and automatically managed languages we have to bridge.

    For skins, this is comparatively trivial. For attachments, this becomes much, much harder. As attachments can be held in many more places. Ref counting can help, but it's not a silver bullet. Which means we'd potentially expose an API to users that can either leak memory like crazy or explode with segfaults.

    And that's why I haven't exposed attachemnts yet. And we haven't even talked about the internals of attachments, which need to reference engine specific things like textures. Not the case for skins. Entirely new can of worms.

      Mario Thank you for going in depth on the topic. I now much better understand why it's such a hard problem to tackle in Godot's context. Gonna look into some of those spine-ts options. Perhaps I can make something work with that tech stack.

      Might as well share my struggle with Spine-Godot source.

      Altering Skeleton draworder size would crash the scene. Clearing the draworder and populating it in any order is okay.

      I got the AtlasAttachmentLoader to return a Spine Attachment and set it to a Skeleton Slot but it doesn't show up.

      I don't have use case in mind just like tinkering what might be useful later. Wish it was easy 😂.

      🫂