• 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 wrote

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:

@[삭제됨]

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?

@[삭제됨]
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 wrote

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.