• Unity
  • Sprite attaching to skel sometimes shows black

Hello!

I have some simple code set up to attach sprites to skels at runtime, when some of our creatures pick up items to carry. It's based off some of your sample code and works perfectly most of the time. But every now and then, the attached sprite shows up as black, like so:

Here's the core code in question:

void attachSpriteToSkeleton(SkeletonAnimation _skelAnim, MeshRenderer _mesh, Slot _slot, Sprite _sprite)
   {
      //Get handle to a few things that we'll be using it a lot
      Skeleton skel = _skelAnim.skeleton;
      string slotName = _slot.Data.Name;
      string newAttachmentName = _sprite.name;
      //All attachment changes will be applied to the skin. We use a clone so other instances will not be affected.
      Skin newSkin = skel.UnshareSkin(true, false);
      //Create an attachment from a Unity Sprite
      RegionAttachment newAttachment = _sprite.ToRegionAttachmentPMAClone(_mesh.material);
      //Add the attachement to the slot
      int slotIndex = _skelAnim.skeleton.FindSlotIndex(slotName);
      newSkin.AddAttachment(slotIndex, newAttachmentName, newAttachment);
      //Set the skin to the one we made, and then make our attachments visible. 
      //Note that any keying to the slot in the anim will still be recognized, so make sure that's all good to go! 
      skel.SetSkin(newSkin);
      skel.SetToSetupPose();
      skel.SetAttachment(slotName, newAttachmentName);
   }

Like I said pretty straightforward, and will probably look familiar to you 🙂 Any insight would be appreciated! Not really sure why it would only be happening rarely and not consistently... I'll also post some images of various related settings below.

The spine files are brought in as json data and individual assets, the latter of which are packed and referenced via tk2d. The tk2d atlas settings are:

The sprite that gets attached is loaded from Resources at runtime, and has the following settings:

Related Discussions
...
  • تم التحرير

First and foremost: Your game looks amazing! So cute!!! 🙂

I can see nothing obviously wrong with your code. The import settings will also consistently yield either correct or incorrect results, but not fail sometimes - so they are out of question for now.

What I would rather asume is that it's the conditions under which the method is getting called (side-effects / order of other methods being called), or that there is a bug within the Spine runtime.

From where do you call the method attachSpriteToSkeleton()? Is it called in Update/LateUpdate/FixedUpdate or in some other context? Some stack-context would be helpful here.
You could also add some null-checks with debug output to trap when some calls return null, e.g. here:

//Create an attachment from a Unity Sprite
RegionAttachment newAttachment = _sprite.ToRegionAttachmentPMAClone(_mesh.material);
if (newAttachment == null)
    Debug.LogError("newAttachment was null!");
//Add the attachement to the slot

In any case, if you could create a minimal version of this project that still shows the error and email it to contact@esotericsoftware.com as a zip package, we could have a more elaborate view on the issue.

Oh thanks for the compliments! I'll pass that along to our artist 🙂

Good to know it's not import settings, I suspected as much but nice to get that affirmed. Yea let me give a little more stack context and see if that helps at all. If not we can then try sending a minimal project over!

The call stack basically goes like this:

  • The agent's PickUp() is invoked (from a coroutine that's actively checking and updating the agent's state) when it comes in contact with a pickup. That function looks like this...
public override bool PickUp(BCarryable _carryable)
   {
      if (_carryable != null && _carryable.OnCarry(this)) {
         CurrentlyCarrying = _carryable;
         if (_carryable.HasRequiredHolders && !_carryable.IsHeavy) {
            AgentAnimator.SetAnimation("Idle", "", CurrentlyCarrying);
            _carryable.transform.SetParent(transform);
            updateCarryablePosAndFace(_carryable);
            //Wait one frame before attempting so the anim has a chance to set
            BCoroutine.WaitAndPerform(() => attemptAttachToSpineSkeleton(_carryable), -1);
            //Play some sounds
            if(agentPlayerComponent != null) {
               BFMODInterfacer.Instance.PlayPCPickupSpriteling();
            } else if(agentSpritelingComponent != null) {
               if(!_carryable.IsHeavy) {
                  BFMODInterfacer.Instance.PlaySpritelingPickupItem(transform.position, _carryable.WorldObject.ObjectType);
               }
            }
         }
         return true;
      }
      return false;
   }

...

  • In it, the agent's animation is updated to Carry (if it has one) in AgentAnimator.SetAnimation. (I'll share the entire AgentAnimator class below.)
  • Then some non-animation-related behavior happens.
  • At the end of that function, attemptAttachToSpineSkelton() is queued up, but it waits 1 frame before getting invoked. We had to do this to make sure the spine animation had actually updated first before attempting the attaching.
  • That looks like this...
void attemptAttachToSpineSkeleton(BCarryable _carryable)
   {
      //If the agent is a spriteling, attempt attaching the sprite to the skel
      if (GetComponent<BSpriteling>() && AgentAnimator.AttachCarryableToSkel(_carryable.Type)) {
         //If it succeeds in attaching, disable the renderers on the in-game object since the skel version is active
         _carryable.DisableRenderingForSpineAttachment(true);
      }
   }

...

  • So it's in there that the actual AttachCarryableToSkel function is called. That kicks off a new stack in the BAgentAnimator class, which I've posted in it's entirely here: https://gist.github.com/csumsky3/9916618e5095b2b6bf176c3aefa2d97d
  • Basically AttachCarryableToSkel retrieves the right sprites based on the pickup, then, only if the sprite isn't null, kicks off attachSpriteToSkeleton, which is the original function I posted to you.
  • One pretty important note about our animation setup: each agent has 3 separate skel files: Front, Back, and Side (which gets duplicated, flipped, and then assigned to the left and right vars respectively in BAgentAnimator). We enable/disable them appropriately as agents move and rotate around.

Hopefully that's a decent amount of information for now! If that still doesn't get us anywhere, I'll send you a simple project to check out 🙂 Thanks for the help!

Other random notes:

  • All of these animations were originally made in spine 3.6 and imported via the spine-unity runtime 3.6. The problem was originally happening there (rarely).
  • We recently updated the runtime to 3.7 (in preparation for some new things), but didn't update the animations. The problem was still seen, as rarely and inconsistently as before. Didn't seem to change anything.
  • Just today we also updated the animations to be exported out of spine 3.7, and also changed the pipeline to use atlases created from spine, instead of tk2d (because tk2d is pretty badly broken as of unity 2018.3.... but that's another story lol). I haven't seen the issue yet, but as I've been saying it's pretty rare so that doesn't say much yet. I'll keep trying.

Thanks for the detailed writeup!

Unfortuantely I still don't see anything obviously wrong, and now that I see the additional layers of complexity with the coroutines, it all gets much harder to diagnose without a debugger.

One thing I would look at is the BCoroutine.WaitAndPerform method - I assume it does nothing more than starting a coroutine with the method, but any assumption may be a wrong one. I also don't know what _carryable.DisableRenderingForSpineAttachment(true); does, maybe this could lead to problems when called multiple times or similar..

In short: I guess we will need the repro project here, unless you discover something new with debug print statements that gives us the required info what exactly went wrong.

Ok cool, yea I figured... but it was easier to do a little write-up than pack up a sample project haha. I'll work on getting you something though, thanks for the attention so far!

A little more insight into those thoughts:

  • BCoroutine.WaitAndPerform just kicks off a coroutine yea. Normally if that second param is >= 0, it'll do a WaitForSeconds, then do the action. If you pass in -1 like I'm doing though, it just does a yield return null (waits 1 frame), then does the action.
  • _carryable.DisableRenderingForSpineAttachment(true); is just disabling the sprite on the in-game pickup object, since we're resource-loading a new sprite to attach to the skel, and we don't want the pickup's image showing on screen twice while the agent is holding it. They're separate sprites, and separate objects - I'm pretty comfortable saying that couldn't be interfering.

EDIT: Hmm just had a quick thought - would it be problematic if the skel was being disabled or enabled in the same frame that the sprite was getting attached? That would be possible in rare cases if the agent was changing face direction in the same frame that they were picking something up, since we use separate skels for their different face directions.

To add little more detail there, for face directions the agent isn't in we disable both the MeshRenderer and the SkeletonAnimation components.

An enabled face direction:

A disabled face direction:

(p.s. I don't know how to make my reply show as a new message! it just keeps showing as an edit to my last message... hopefully this notifies you as if it's a reply haha)

Regarding the BCoroutine.WaitAndPerform - this seems ok then.

Hmm just had a quick thought - would it be problematic if the skel was being disabled or enabled in the same frame that the sprite was getting attached?

Hm, maybe this together with the call to _carryable.DisableRenderingForSpineAttachment(true) could do some harm - I don't know, but I still would guess that it has something to do with one thing being disabled (or set to null) in the wrong order and therefore too early.

If breakpoints can't be set reasonably, I would augment the whole code with a lot of print statements like "In Method X part 1" if there are no null-checks failing at all. Then pause as soon as the problem has occurred and then check if you can find some missing/unexpected/out-of-order calls logged, etc.

Unfortunately I still can't help too much without the repro package I fear..

PS: You should perhaps refill the red and blue pixels in your Unity version 😉.

Cool cool, just wanted to throw that random thought out while I had it, in case it triggered something you knew haha.

PS: You should perhaps refill the red and blue pixels in your Unity version 😉.

Oh haha, I just have it set to change green when in play mode, so I don't do anything stupid like make a bunch of adjustments in the inspector, forgetting the game is running haha.

Hehe, yes - the different play mode color scheme is a very good idea in general, I don't want to know how many changes have been lost by Unity users in the past that way! 🙂

Major breakthrough! I was putting together that sample project for you guys and was doing some testing in it to make sure I could actually repro the issue. It took awhile... the issue is really rare (maybe 1 in 200-300 attachments). But once it did happen I decided to take a peek at the runtime materials on the skeletons, and I think I found the issue! It looks like the defective materials are using the "Standard" shader, instead of the "Spine/Skeleton" shader. I have no idea why it would be getting the wrong one yet, but definitely progress.

Here's a vid of me poking around at runtime (apologies again for my green play mode tint!). The vid capture doesn't see any popout / dropdown menus, so there are some dead moments in there where you can't see what I'm doing (e.g. 0:45-1:00 can pretty much be skipped), but you'll see around 1:30 I manually change the defective material from the "Standard" shader to the "Spine/Skeleton" shader, and all is well again.

https://www.dropbox.com/s/s6adhrz43nimq8h/issue%20is%20wrong%20shader.mp4?dl=0

While I continue to get this sample project built for you, any ideas off the top of your head why it would randomly assign the wrong shader? Or maybe any ways to force it?

Oh, this is interesting! Thanks very much for figuring this out - I don't yet have any idea why that happens, but now I know what to search for!

Having seen everything in motion now: OMG - your game looks so cute! Awesome work! 🙂

And no need to apologize for the green editor tint at all, I was just joking! 🙂

Haha well thank you so much, very kind of you to say 🙂 We'll be showing it at GDC if you'll be there and want to check it out!

Do you have a secure/private way for me to send you the project?

Very cool! Unfortunately I will not be at GDC (since I'm living in Europe) - will be attending Reboot Develop on the other hand, but I guess it's unlikely you will exhibit there.. Anyway - we wish you all the best for the expo!

Regarding sending the project securely:
Please send it to contact@esotericsoftware.com, I can assure you that it will only be used for bugfixing purposes.

Sent!

4 أيام لاحقا

Some things that I found out:

First I could not reproduce the problem in Unity 2018.3.0f1, then upgraded to f2 and instantly could! Maybe related to the unity version and either some implicit behaviour change, or a bug in a Unity version - maybe you could test that with different Unity versions?

I can confirm that the shader or material is not null. After some time I got this error message, but no wrong Sprite (at least I did not see any).

Assertion failed on expression: '!m_CoroutineEnumeratorGCHandle.HasTarget()'
UnityEngine.Coroutine:Finalize()

One more very interesting thing: I just closed unity and reopened it - et voila, the problem occurred instantly in the first 10 attachments (or the very first? don't know), but only if I don't attach the debugger (so it's definitely a timing issue..)!


I found the cause!

The problem is with your 4 movement directions having SpineAnimations for each direction, but all start disabled and having no Material assigned (maybe that is cleared by unity upon save? I don't know) except for the FrontAnimation version - then you have to pick up one of your spritelings before they turned in all directions and throw them at a target resource, then when the method attachSpriteToSkeleton() below is called, the _mesh for the disabled directions has no materials assigned in the inspector and will get the default material assigned implicitly upon calling the method:

You can add the following code to trap the error case:

in BAgentAnimator.cs:

void attachSpriteToSkeleton(SkeletonAnimation _skelAnim, MeshRenderer _mesh, Slot _slot, Sprite _sprite)
   {
      // Add this on top of the method to catch the problem case:
      if (!_mesh.material.shader.name.Contains("Spine"))
         Debug.LogError("shader with different name found!" + _mesh.material.shader.name);

So long story short: You have to make sure that the material gets either saved or is assigned properly before attaching the Sprite.


I noticed that the material is cleared by the SkeletonAnimation when the MeshRenderer is deactivated. Since this is undesired behaviour, I have quickly fixed it. So you should be able to update the Spine unitypackage and enable and then again disable your MeshRenderers, then all material references should be restored and kept intact, then apply you prefabs again.

You can download the latest unitypackage here as usual:
Spine Unity Download

Please let me know if this solved your problem.

Awesome thank you so much! Implemented these changes and so far so good! Since it's rare I'm still gonna keep an eye out, but we have what we need now to track it if it happens to pop back up 🙂 You guys are the best!

Glad we could make it work 🙂!

Actually it was not rare when I hurried up to grab the spriteling quickly before he turns. After I knew what I had to do, I had about 70% chance of reproducing it instantly.

Ah yea, good point! With that in mind I think we're all set!