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.