• Unity
  • [suggestion] make default skin support combination.

Its hard to preview the whole shape for adjusting position (or related operation) in editor when the complete visual is suppose to be a combination of multiple skins (mix & match case). It also make custom script necessary for some use cases that only need to set the combination once.

Related Discussions
...
Nick a écrit

Its hard to preview the whole shape for adjusting position (or related operation) in editor when the complete visual is suppose to be a combination of multiple skins (mix & match case). It also make custom script necessary for some use cases that only need to set the combination once.

Hey Nick - not sure if you had already made a custom script, but I posted some code for a script that can apply a skin configuration and show it in editor mode in this thread a while back: Editor Multiple Skins Question

Could check it out and see if it helps :yes:

Oh quick note, I use Odin Inspector, so if you don't have that then you'd need to remove some attributes like the [ShowIf(...)], etc

Oh, I forgot about that posting, thanks very much for bringing that up @Jamez0r! :cooldoge:

@[supprimé]

Hi, thank you for the code. It is very helpful! 😃

I updated it to support both SkeletonAnimation and SkeletonGraphic because I am using SkeletonGraphic. I also removed the odin dependency and add a little UI enhancement to the inspector.

Note that I also updated the following code for my current version of runtime. I am not sure which is the new API.

buildSkin.AddSkin(skinCur);
//buildSkin.AddAttachments(skinCur); // note: old version API?

@[supprimé]
I found that SkeletonGraphic don't have EditorSkipSkinSync like SkeletonAnimation. Is it not needed for SkeletonGraphic?

Below is the modified code in case anyone need it. (Please don't mind that I changed the coding style a little bit for my own viewing preference)


using Spine;
using Spine.Unity;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif


[ExecuteInEditMode]
public class SpineSkinApplicator : MonoBehaviour
{
    public SkeletonAnimation targetSkeletonAnimation;

public SkeletonGraphic targetSkeletonGraphic;

public bool ignoreSkinNotFoundError = false;

public Skeleton Skeleton => targetSkeletonGraphic != null 
                    ? targetSkeletonGraphic.Skeleton 
                    : targetSkeletonAnimation?.Skeleton;


#if UNITY_EDITOR
    public bool EditorSkipSkinSync
    {
        get => targetSkeletonAnimation != null ? targetSkeletonAnimation.EditorSkipSkinSync : false;
        set
        {
            if (targetSkeletonAnimation != null)
                targetSkeletonAnimation.EditorSkipSkinSync = value;

        // note: targetSkeletonGraphic Don't have EditorSkipSkinSync property.
    }
}
#else
    public bool EditorSkipSkinSync { get; set; } = false;
#endif

public enum AutomaticApplicationOption { None, OnStart }
public AutomaticApplicationOption automaticApplyOption = AutomaticApplicationOption.OnStart;

public enum SkinApplicationType { ReplaceSkin, AddToSkin }

public SkinApplicationType applicationType;

public bool replaceSkinApplyInEditMode = true;

// note: attritbute disabled to support both SkeletonAnimation and SkeletonGraphic
//[SpineSkin(dataField = "targetSkeletonAnimation")]
public List<string> skinEntries = new List<string>(1);

void Start()
{
    var activate = Application.isPlaying 
        ? automaticApplyOption == AutomaticApplicationOption.OnStart
        : applicationType == SkinApplicationType.ReplaceSkin && replaceSkinApplyInEditMode;
    
    if (activate)
    {
        Activate();
    }
}


private void OnValidate()
{

    if (Application.isPlaying) { return; }

    BindSpineComponent();

    if (IsTargetReady)
    {
        if (applicationType == SkinApplicationType.ReplaceSkin && replaceSkinApplyInEditMode)
        {
            EditorSkipSkinSync = true;
            Activate();
        }
        else
        {
            EditorSkipSkinSync = false;
        }
    }
}

public void BindSpineComponent()
{
    if (!IsTargetReady)
    {
        targetSkeletonAnimation = GetComponent<SkeletonAnimation>();

        if (!IsTargetReady)
            targetSkeletonGraphic = GetComponent<SkeletonGraphic>();
    }
}

private void OnDestroy()
{
    EditorSkipSkinSync = false;
}


public void Activate()
{
    if (IsTargetAndSkeletonValid)
    {
        switch (applicationType)
        {
            case SkinApplicationType.ReplaceSkin:
                ApplyAsReplace();
                break;
            case SkinApplicationType.AddToSkin:
                ApplyAsAddToSkin();
                break;
        }
    }
}


public void ApplyAsReplace()
{
    Skin buildSkin = new Skin("buildSkin");

    foreach (var skinName in skinEntries)
    {
        var skinCur = Skeleton.Data.FindSkin(skinName);
        if (skinCur != null)
        {
            buildSkin.AddSkin(skinCur);
            //buildSkin.AddAttachments(skinCur); // note: old version API?
        }
        else
        {
            if (ignoreSkinNotFoundError == false)
            {
                Debug.LogError("Error: skin not found, skinName: " + skinName, gameObject);
            }
        }
    }

    Skeleton.SetSkin(buildSkin);
    Skeleton.SetSlotsToSetupPose();
}


public void ApplyAsAddToSkin()
{

    foreach (string skinName in skinEntries)
    {

        Skin newSkin = Skeleton.Data.FindSkin(skinName);
        if (newSkin != null)
        {
            Skin currentSkin = Skeleton.Skin;
            Skin combinedSkin = new Skin("combinedSkin");
            combinedSkin.AddSkin(currentSkin);
            combinedSkin.AddSkin(newSkin);
            Skeleton.SetSkin(combinedSkin);
            Skeleton.SetSlotsToSetupPose();
        }
        else
        {
            if (ignoreSkinNotFoundError == false)
            {
                Debug.LogError("Error: skin not found, skinName: " + skinName, gameObject);
            }
        }
    }

}

public bool IsTargetReady => targetSkeletonAnimation != null || targetSkeletonGraphic != null;
public bool IsTargetAndSkeletonValid => Skeleton != null;

}


#region [ ========== Inspector ========== ]
#if UNITY_EDITOR

[CustomEditor(typeof(SpineSkinApplicator))]
public class SpineSkinApplicator_Inspector : Editor
{
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

    var inst = (SpineSkinApplicator)target;

    GUILayout.BeginVertical();
    {
        GUILayout.BeginHorizontal("box");
        {
            GUILayout.Label("Utility");
            
            GUILayout.FlexibleSpace();

            if (GUILayout.Button("Bind Spine Skeleton Component"))
            {
                inst.BindSpineComponent();
            }
        }
        GUILayout.EndHorizontal();

        DrawSkinList(inst);
    }
    GUILayout.EndVertical();
}


enum SkinOp { None, Add, Remove}

Vector2 scrollOffset = Vector2.zero;

string skinOptionSearchPattern = string.Empty;
int visibleSkinOptionsCount = 0;

void DrawSkinList(SpineSkinApplicator inst)
{
    if (!inst.IsTargetReady)
        return;

    GUILayout.BeginVertical("box");
    {
        GUILayout.BeginHorizontal("box");
        {
            var total = inst.Skeleton.Data.Skins.Count - 1;// -1 for "default" skin.
            GUILayout.Label($"Skins {visibleSkinOptionsCount} / {total}");
            GUILayout.FlexibleSpace();
            GUILayout.Label("Search");
            skinOptionSearchPattern = GUILayout.TextField(skinOptionSearchPattern, GUILayout.MinWidth(120));
        }
        GUILayout.EndHorizontal();

        GUILayout.Space(4);

        scrollOffset = GUILayout.BeginScrollView(scrollOffset);
        {
            var skinOp = SkinOp.None;

            string targetSkin = string.Empty;
        
            visibleSkinOptionsCount = 0;

            foreach (var skin in inst.Skeleton.Data.Skins)
            {
                if (skin.Name == "default")
                    continue;

                if (skin.Name.IndexOf(skinOptionSearchPattern, System.StringComparison.OrdinalIgnoreCase) <= -1 && skinOptionSearchPattern.Length > 0)
                    continue;

                visibleSkinOptionsCount++;

                GUILayout.BeginHorizontal();
                {
                    var selected = inst.skinEntries.Contains(skin.Name);
                    
                    var newSelected = GUILayout.Toggle(selected, skin.Name);
                    if (newSelected != selected)
                    {
                        skinOp = newSelected? SkinOp.Add : SkinOp.Remove;
                        targetSkin = skin.Name;
                    }
                }
                GUILayout.EndHorizontal();
            }

            switch(skinOp)
            {
                case SkinOp.Add: inst.skinEntries.Add(targetSkin); break;
                case SkinOp.Remove: inst.skinEntries.Remove(targetSkin); break;
            }

            EditorUtility.SetDirty(inst.gameObject);
            Undo.RecordObject(inst.gameObject, "Spine Skin Combination Updated");
        }
        GUILayout.EndScrollView();
    }
    GUILayout.EndVertical();
}
}
#endif
#endregion [ ========== Inspector (End) ========== ]

Thanks for sharing your code adaptations @Nick!

Nick a écrit

I found that SkeletonGraphic don't have EditorSkipSkinSync like SkeletonAnimation. Is it not needed for SkeletonGraphic?

It should not be needed at SkeletonGraphic, since it's update is triggered a bit differently.