• Unity
  • Animation Which Hides Sprite Slots Mid Animation Hides Early

@Harald,

Thanks I did try this, but now the gear stays on even after they should be hidden.

Do I need to check if the slot is disabled (if so how?) and then not attach? In my head it seems like if the slot is hidden, the gear attachment should be ignored since the parent is not active. But I guess the way it works is that when a slot is hidden during an animation its just a reference that still needs to be read and only if its active attach the gear?

Here was my initial attempt:

In my UnitAnimator script I set the callback:

 public Action skeletonAnimationUpdateCompleteCallback;
 public void Init(S_Unit unitState)
{

 _animator.UpdateComplete += delegate (ISkeletonAnimation animated)
 {
     skeletonAnimationUpdateCompleteCallback?.Invoke();
 };

 }

Then in my main unit class I call update gear on this:

       // Update gear on skeleton for animations which might hide it during the animation
       unitAnimator.skeletonAnimationUpdateCompleteCallback = UpdateGear;

public void UpdateGear()
   {
       if (_spriteAttacher == null)
       {
           X_Logger.LogError("Missing sprite attacher on " + gameObject);
       }

   // Check if has gear
   if (_unitState.trinketCardGO != null)
   {
       _spriteAttacher.Attach(_unitState.trinketState.gearSkinModel.spineSprite, T_EquipSlots.TRINKET);
   }

   if (_unitState.weaponCardGO != null)
   {
       _spriteAttacher.Attach(_unitState.weaponState.gearSkinModel.spineSprite, T_EquipSlots.WEAPON);
   }

   if (_unitState.armorCardGO != null)
   {
       M_GearSkin armor = _unitState.armorState.gearSkinModel;
       _spriteAttacher.Attach(armor.spineSprite, T_EquipSlots.ARMOR);
       _spriteAttacher.Attach(armor.frontLowerArmSprite, T_EquipSlots.FRONT_LOWER_ARM);
       _spriteAttacher.Attach(armor.frontUpperArmSprite, T_EquipSlots.FRONT_UPPER_ARM);
       _spriteAttacher.Attach(armor.backLowerArmSprite, T_EquipSlots.BACK_LOWER_ARM);
       _spriteAttacher.Attach(armor.backUpperArmSprite, T_EquipSlots.BACK_UPPER_ARM);
       _spriteAttacher.Attach(armor.frontArmorSprite, T_EquipSlots.BACK_UPPER_ARM);
   }

   if (_unitState.helmetCardGO != null)
   {
       if (_unitState.helmetState == null)
       {
           X_Logger.LogError("Null helmet state but non null helmet card go");
       }

       _spriteAttacher.Attach(_unitState.helmetState.gearSkinModel.spineSprite, T_EquipSlots.HELMET);

       if (_unitState.helmetState.gearSkinModel.helmetBackSprite)
       {
           _spriteAttacher.Attach(_unitState.helmetState.gearSkinModel.helmetBackSprite, T_EquipSlots.HELMET_BACK);
       }
       else
       {
           var helmetBackSlot = _skeletonAnimation.skeleton.FindSlot("helmetBack");
           helmetBackSlot.Attachment = null;
       }

       if (_unitState.helmetState.gearSkinModel.hideEars)
       {
           var frontEarSlot = _skeletonAnimation.skeleton.FindSlot("frontEar");
           frontEarSlot.Attachment = null;

           var backEarSlot = _skeletonAnimation.skeleton.FindSlot("backEar");
           backEarSlot.Attachment = null;
       }
   }
   }

In my UpdateGear code, do I need to check if the slot is active and if so attach null?

Also performing this is extremely expensive its dropping my fps from like 300 to 30 so I dont think this approach I took is correct.

Related Discussions
...
Aggressor a écrit

Do I need to check if the slot is disabled (if so how?) and then not attach?

At runtime the Slot itself does not hold any visibility state, it is just all Attachments which are disabled. So you can check for bool allHidden = slot.Attachment == null;.

Aggressor a écrit

Also performing this is extremely expensive its dropping my fps from like 300 to 30 so I dont think this approach I took is correct.

While I don't see what the _spriteAttacher.Attach() method is doing, it is not good to call skeleton.FindSlot() every frame, instead you should be caching the Slot by e.g. calling FindSlot() once it in your Init() method and saving it for later re-use.

Nevertheless, this does not explain the massive performance impact. Could you please post the code of your SpriteAttacher.Attach() method? Are you perhaps converting the Sprite to a RegionAttachment via the following code in every SpriteAttacher.Attach() call:

attachment = applyPMA ? sprite.ToRegionAttachmentPMAClone(attachmentShader) : sprite.ToRegionAttachment(SpriteAttacher.GetPageFor(sprite.texture, attachmentShader));

This would be very costly and would explains the performance impact.

Ok sure here is my attach method:

public void Attach(Sprite sprite, T_EquipSlots equipSlot)
        {
            var skeletonComponent = GetComponent<ISkeletonComponent>();
            Shader attachmentShader = Shader.Find(DefaultStraightAlphaShader);
            var slot = skeletonComponent.Skeleton.FindSlot(_SLOT_TYPE_TO_NAME[equipSlot]);
            if (sprite == null)
            {
                slot.Attachment = null;
                return;
            }
            X_Logger.Log("Attaching sprite " + sprite.name);
            // This is required to prevent the arms getting distorted with the wrong rotation
            RegionAttachment attachment;
            if (slot.Attachment != null)
            {
                float rotation = ((RegionAttachment)slot.Attachment).Rotation;
                attachment = sprite.ToRegionAttachmentPMAClone(attachmentShader, rotation: rotation);
            } else
            {
                attachment = sprite.ToRegionAttachmentPMAClone(attachmentShader);
            }
            slot.Attachment = attachment;
        }

I guess what confuses me is if the slot is null, how am I supposed to know if its null because the animation called setup pose and set it to null, or its null because the animation has hidden the slot?

My confusion stems from I don't understand how I my code is able to know when a slot is hidden during the animation. Checking for null won't work because the original issue is the slots get set to null when the animation starts. So if there is not a "hidden" property, how am I supposed to know that I should stop attaching the gear to the slot during an animation that hides them half way through?

Aggressor a écrit

I guess what confuses me is if the slot is null, how am I supposed to know if its null because the animation called setup pose and set it to null, or its null because the animation has hidden the slot?

So in the setup pose you have nothing attached, and then it is keyed to nothing in the die animation, did I get that right?
I'm afraid this is the main problem then, since there is actually no change happening (you change from null to null). If you then override the no-attachment state with the custom equipment from outside, you will always override them.

Two solutions come to my mind:
a) Setup State shall mirror the initial state, so you equip something instead of nothing.
b) Just create an event that triggers a "hide all equipment" method in code.

Option (b) would save you the necessary changes to both your assets and the code, so this would perhaps be the easier solution.

If you want to go for option (a): In order to improve performance of the Attach() method, you should move anything costly like: Shader.Find(), Skeleton.FindSlot() and most importantly all calls to sprite.ToRegionAttachmentPMAClone() to Start() (or your Init() method) to be called only once instead of every frame.

Ok we will setup an animation event can you point me to the docs which have a unity example for this.

I do have a question tho:

How do I get a callback when "SetupPose" is called within the skeleton? I need a hook so I can set the gear back.

I would use this SetupPose callback to re-assign the gear, and then I would use this "Hide" animation event to set it all to null


To clarify 1 thing though,

I do not call setup pose. If you look at the sample project a sprite is attached to a slot. And when the death animation is called, its immediately set to null (not from client code).

From a user perspective this seems like a bug. If I attach a sprite to a slot, and then hide it halfway through an animation, it should hide at that frame. I do not understand the point of allowing hiding of slots in the animator if when you use it in game, it instead hides the slot (sets it to null) at the start of the animation (without the client calling setup pose)

Aggressor a écrit

Ok we will setup an animation event can you point me to the docs which have a unity example for this.

It's the second example scene in the spine-unity package, Spine Examples/Getting Started/2 Controlling Animation

On the documentation page it's listed here:
spine-unity Runtime Documentation: Processing AnimationState Events

Aggressor a écrit

How do I get a callback when "SetupPose" is called within the skeleton? I need a hook so I can set the gear back.

There is no single call to set things to Setup Pose, every single animation timeline will set attachments when at the first frame or when keys are reached. So you would receive one callback per attachment timeline if it existed, which would not be very desirable.

From this question I just noticed that in option (b) I was missing to mention that you need to add a "re-equip all items" event at the first frame as well. Sorry about that.

As long as that doesnt cause a flicker we will try that

Thanks.

Will report back when implemented.


@Harald, this documentation seems to require a SkeletonMecanim component to add events.

Can manual events at time stamps be added using a SkeletonAnimation?

Aggressor a écrit

@[supprimé], this documentation seems to require a SkeletonMecanim component to add events.

I'm afraid I do not quite understand what you mean by this sentence.

Both the referenced example scene and the documentation section are demonstrating event receiving at SkeletonAnimation. You add the event in Spine, then use the shown code to react to the event in Unity using spine-unity.

Aggressor a écrit

Can manual events at time stamps be added using a SkeletonAnimation?

You add the events in the Spine Editor.

Ok I thought from the documentation you had to have the controller asset and would set events using the Unity animator animations. I will try as you suggest thank you.


This worked:

_skeletonAnimation.AnimationState.Event += (TrackEntry, Event) =>
{
    if (Event.Data.Name == "requipAllGear")
    {
        UpdateGear();
    }

if (Event.Data.Name == "removeAllGear")
{
    RemoveAllGear();
}
};

Thanks for the help


The only problem now, is there is a split second freeze in the animation when the gear is removed. Im wondering if theres any performance optimizations you can think of?

public void RemoveAllGear()
{
    _spriteAttacher.Attach(null, T_EquipSlots.TRINKET);
    _spriteAttacher.Attach(null, T_EquipSlots.WEAPON);
    _spriteAttacher.Attach(null, T_EquipSlots.ARMOR);
    _spriteAttacher.Attach(null, T_EquipSlots.FRONT_LOWER_ARM);
    _spriteAttacher.Attach(null, T_EquipSlots.FRONT_UPPER_ARM);
    _spriteAttacher.Attach(null, T_EquipSlots.BACK_LOWER_ARM);
    _spriteAttacher.Attach(null, T_EquipSlots.BACK_UPPER_ARM);
    _spriteAttacher.Attach(null, T_EquipSlots.BACK_UPPER_ARM);
    _spriteAttacher.Attach(null, T_EquipSlots.HELMET);
    _spriteAttacher.Attach(null, T_EquipSlots.HELMET_BACK);
}

Yes, as mentioned above, your calls to Skeleton.FindSlot(), Shader.Find(DefaultStraightAlphaShader); (which could be moved below the if (sprite == null) branch anyway) and also GetComponent() should not be called every time. You can cache the results as suggested.

I needed the slot originally in order to know what to set to null but your suggestion is correct so I instead make a custom call just to clear all slots once!

public void SetAllSlotsToNull()
{
    var skeletonComponent = GetComponent<ISkeletonComponent>();
    foreach (Slot slot in skeletonComponent.Skeleton.Slots)
    {
        slot.Attachment = null;
    }
}

Phew.

Further still as your suggestion, I should cache these slots and find them in the init! I am surprised it took me so long to do this optimization!

Thank you, I know this was a long thread. I will be sure to send along some game keys when this is finished!

Aggressor a écrit

Thank you, I know this was a long thread. I will be sure to send along some game keys when this is finished!

No problem, very glad that you've got it working!