• Unity
  • Runtime Skin Update EditorMode

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

I'm dynamically creating skins via sprites attachments and applying them within Unity during runtime. (exactly like the Mix and Match example)

One issue I found was that I can only see these changes when I play the game. So I attempted to update the character during onValidate() (Similar to this example http://esotericsoftware.com/forum/How-can-I-save-code-changed-spine-skeleton-prefab-8589)

I also run the script in [ExecuteAlways].

The problem is that the changes apply when I change out out a attachment, but then instantly go back to the previous none skin state.

What's weird is that because of me running in ExecuteAlways the Start method fires off after I save a change to my script, and updates the skin correctly.

Tho as soon as I click it goes back to default skin.

I'm not entirely sure what's going on here. When I Debug.Log the Apply() function it shows that it only fires once on the onValidate()

So I'm curious if I'm doing something else wrong or if there may be an issue with spine.

Modified Mix and Match for example purposes.

/******************************************************************************
 * Spine Runtimes Software License v2.5
 *
 * Copyright (c) 2013-2016, Esoteric Software
 * All rights reserved.
 *
 * You are granted a perpetual, non-exclusive, non-sublicensable, and
 * non-transferable license to use, install, execute, and perform the Spine
 * Runtimes software and derivative works solely for personal or internal
 * use. Without the written permission of Esoteric Software (see Section 2 of
 * the Spine Software License Agreement), you may not (a) modify, translate,
 * adapt, or develop new applications using the Spine Runtimes or otherwise
 * create derivative works or improvements of the Spine Runtimes or (b) remove,
 * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
 * or other intellectual property or proprietary rights notices on or in the
 * Software, including any copy thereof. Redistributions in binary or source
 * form must include this license and terms.
 *
 * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL ESOTERIC SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
 * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
 * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *****************************************************************************/

using UnityEngine;
using Spine.Unity.Modules.AttachmentTools;
using System.Collections;
using Spine;
using Spine.Unity;

namespace Anisoft
{
    // This is an example script that shows you how to change images on your skeleton using UnityEngine.Sprites.
    [ExecuteAlways]
    public class MixAndMatchEdit : MonoBehaviour
    {

    #region Inspector
    [SpineSkin]
    public string templateAttachmentsSkin = "base";
    public Material sourceMaterial; // This will be used as the basis for shader and material property settings.

    [Header("Visor")]
    public Sprite visorSprite;
    [SpineSlot] public string visorSlot;
    [SpineAttachment(slotField: "visorSlot", skinField: "baseSkinName")] public string visorKey = "goggles";

    [Header("Gun")]
    public Sprite gunSprite;
    [SpineSlot] public string gunSlot;
    [SpineAttachment(slotField: "gunSlot", skinField: "baseSkinName")] public string gunKey = "gun";

    [Header("Runtime Repack")]
    public bool repack = true;
    public BoundingBoxFollower bbFollower;

    [Header("Do not assign")]
    public Texture2D runtimeAtlas;
    public Material runtimeMaterial;
    #endregion

    Skin customSkin;

    void OnValidate()
    {
        if (sourceMaterial == null)
        {
            var skeletonAnimation = GetComponent<SkeletonAnimation>();
            if (skeletonAnimation != null)
                sourceMaterial = skeletonAnimation.SkeletonDataAsset.atlasAssets[0].PrimaryMaterial;
        }

        Apply();
    }

    private void Start()
    {
        Apply();
    }

    void Apply()
    {
        var skeletonAnimation = GetComponent<SkeletonAnimation>();
        var skeleton = skeletonAnimation.Skeleton;

        // STEP 0: PREPARE SKINS
        // Let's prepare a new skin to be our custom skin with equips/customizations. We get a clone so our original skins are unaffected.
        customSkin = customSkin ?? new Skin("custom skin"); // This requires that all customizations are done with skin placeholders defined in Spine.
                                                            //customSkin = customSkin ?? skeleton.UnshareSkin(true, false, skeletonAnimation.AnimationState); // use this if you are not customizing on the default skin.
        var templateSkin = skeleton.Data.FindSkin(templateAttachmentsSkin);

        customSkin.AddAttachments(templateSkin);

        // STEP 1: "EQUIP" ITEMS USING SPRITES
        // STEP 1.1 Find the original/template attachment.
        // Step 1.2 Get a clone of the original/template attachment.
        // Step 1.3 Apply the Sprite image to the clone.
        // Step 1.4 Add the remapped clone to the new custom skin.

        // Let's do this for the visor.
        if (visorSprite != null)
        {
            int visorSlotIndex = skeleton.FindSlotIndex(visorSlot); // You can access GetAttachment and SetAttachment via string, but caching the slotIndex is faster.
            Attachment templateAttachment = templateSkin.GetAttachment(visorSlotIndex, visorKey);  // STEP 1.1
            Attachment newAttachment = templateAttachment.GetRemappedClone(visorSprite, sourceMaterial); // STEP 1.2 - 1.3
            customSkin.SetAttachment(visorSlotIndex, visorKey, newAttachment); // STEP 1.4
        }

        if (gunSprite != null)
        {
            // And now for the gun.
            int gunSlotIndex = skeleton.FindSlotIndex(gunSlot);
            Attachment templateGun = templateSkin.GetAttachment(gunSlotIndex, gunKey); // STEP 1.1
            Attachment newGun = templateGun.GetRemappedClone(gunSprite, sourceMaterial); // STEP 1.2 - 1.3
            if (newGun != null) customSkin.SetAttachment(gunSlotIndex, gunKey, newGun); // STEP 1.4
        }
        // customSkin.RemoveAttachment(gunSlotIndex, gunKey); // To remove an item.
        // customSkin.Clear()
        // Use skin.Clear() To remove all customizations.
        // Customizations will fall back to the value in the default skin if it was defined there.
        // To prevent fallback from happening, make sure the key is not defined in the default skin.

        // STEP 3: APPLY AND CLEAN UP.
        // Recommended, preferably at level-load-time: REPACK THE CUSTOM SKIN TO MINIMIZE DRAW CALLS
        //             IMPORTANT NOTE: the GetRepackedSkin() operation is expensive - if multiple characters
        //             need to call it every few seconds the overhead will outweigh the draw call benefits.
        //
        //             Repacking requires that you set all source textures/sprites/atlases to be Read/Write enabled in the inspector.
        //             Combine all the attachment sources into one skin. Usually this means the default skin and the custom skin.
        //             call Skin.GetRepackedSkin to get a cloned skin with cloned attachments that all use one texture.
        if (repack)
        {
            var repackedSkin = new Skin("repacked skin");
            repackedSkin.AddAttachments(skeleton.Data.DefaultSkin); // Include the "default" skin. (everything outside of skin placeholders)
            repackedSkin.AddAttachments(customSkin); // Include your new custom skin.
            repackedSkin = repackedSkin.GetRepackedSkin("repacked skin", sourceMaterial, out runtimeMaterial, out runtimeAtlas); // Pack all the items in the skin.
            skeleton.SetSkin(repackedSkin); // Assign the repacked skin to your Skeleton.
            if (bbFollower != null) bbFollower.Initialize(true);
        }
        else
        {
            skeleton.SetSkin(customSkin); // Just use the custom skin directly.
        }

        skeleton.SetSlotsToSetupPose(); // Use the pose from setup pose.
        skeletonAnimation.Update(0); // Use the pose in the currently active animation.

        Resources.UnloadUnusedAssets();
    }
}
}

Thank you for taking the time to look at my issue.

As a quick guess, I asume that other Spine scripts are being executed right after your script is called. You could search for other calls to OnValidate in the Spine runtime code and change script execution order of your script to after this (or any) Spine scripts.

Oh okay, you can change the order of OnValidate? Interesting okay, Ill have to find out how to do that.

Edit*

Okay I checked through all the OnValidate functions

I sent a Debug.Log for each of them to see when they executed but none of these executed meaning these OnValidate aren't running.

There were other OnValidate but they were only for example scripts.

I'm curious what else could be running. Ill do some checks for OnAfterSerialization

Edit* No the api doesn't carry any of that either, I'm curious what else could be updating after my stuff. All I'm doing is taking the MixAndMatch example adding OnValidate

Edit* I think I found where the problem is

override public void OnInspectorGUI () {
      bool multi = serializedObject.isEditingMultipleObjects;
      DrawInspectorGUI(multi);
      if (serializedObject.ApplyModifiedProperties() || SpineInspectorUtility.UndoRedoPerformed(Event.current) ||
         AreAnyMaskMaterialsMissing()) {
         if (!Application.isPlaying) {
            if (multi) {
               foreach (var o in targets) EditorForceInitializeComponent((SkeletonRenderer)o);
            } else {
               EditorForceInitializeComponent((SkeletonRenderer)target);
            }
            SceneView.RepaintAll();
         }
      }

I took one last look and can verify that its how the OnInspectorGUI for the skeletonRenderer refreshes the sprite.

The question is how I make sure my own skin stuff is applied after all this OnInpsectorGUI stuff the skeletonRenderer is calling.


I guess I could add a custom editor to my script with my own OnInspectorGUI that overwrites what the SkeletonRenderer does.
Alternatively I could try allowing the SkeletonRenderer Initialize check for a customskin, and if that exists apply that instead.

Any other suggestions to tackle this more smoothly?

I have just discovered the source of your problem - there are unnecessary updates happening in the editor, I will release a bugfix for this very soon.

I will let you know once this is done.


The updated unitypackages containing the bugfix are now available.

You can download them here as usual:
Spine Unity Download

Thanks for the quick response! It sorta fixed part of the issue however I'm still having issues of my skin settings not staying. I'll look further into OnInspectorGUI and try and see what else may be causing this issue.

Any suggestions to try and fix this?

Toggling repack makes it sorta stay. I checked the order of SetSkin and my SetSkin is calling correctly I think so my guess is that somewhere else is still overwriting it?


Okay I think I found it. This check here forces the skin to reset if it doesn't match. So this conflicts with what I'm doing if I'm changing the skin.

If I write a custom skin name and its not the initial components name, this will force the skin to be the component skin I believe.

static bool UpdateIfSkinMismatch (SkeletonRenderer skeletonRenderer) {
         if (!skeletonRenderer.valid) return false;

        Debug.Log("update if skin is mismatched owo?");


        var skin = skeletonRenderer.Skeleton.Skin;
     string skeletonSkinName = skin != null ? skin.Name : null;
     string componentSkinName = skeletonRenderer.initialSkinName;
     bool defaultCase = skin == null && string.IsNullOrEmpty(componentSkinName);
     bool fieldMatchesSkin = defaultCase || string.Equals(componentSkinName, skeletonSkinName, System.StringComparison.Ordinal);

     if (!fieldMatchesSkin) {
        Skin skinToSet = string.IsNullOrEmpty(componentSkinName) ? null : skeletonRenderer.Skeleton.Data.FindSkin(componentSkinName);
        skeletonRenderer.Skeleton.Skin = skinToSet;
        skeletonRenderer.Skeleton.SetSlotsToSetupPose();
        skeletonRenderer.LateUpdate();
        return true;
     }
     return false;
  }

What do you think we should do to fix this. This can be an issue for those who'd like to visualize dynamic skin changes in the editor.

Then I see two solutions:
a) to not only update the skin but also update the skeletonRenderer.initialSkinName accordingly.
b) disabling the call to UpdateIfSkinMismatch, however this would need to be done in the Spine code.

a) worked perfectly! Can I help out adding this suggestion to the docs for people interesting dynamically adding skins and seeing the change in the editor? This would help a bunch of people out!

Thanks very much for the offer, right at the moment we are doing a great re-write of the Spine-Unity docs pages, so currently that's a bit of a bad timing. Or did you mean the documentation in code?

7 дней спустя

Anything I can do to help you or others out.
If I can somehow help contribute to those who may be looking for the same thing I'd love to share my findings.

Also I found that adding a customSkin string to the skeletonRenderer and then having the initialize skin check for that value helped remove a lot of these issues initialize skin stuff.