注:转载请注明转载,并附原链接 http://www.cnblogs.com/liaoguipeng/p/5867510.html 燕双飞情侣

一、动态换装原理

  1. 换装,无非就是对模型的网格,或者贴图进行针对性置换;
  2. 对于3D局部换装,我们可能需要单独换模型和贴图,也可能只需要单独置换贴图即可
  3. 对与Spine2D角色换装,我们基本上只需要针对性置换贴图,也就是Slot插槽上对应的附着物Attachment即可

二、换装理论分析

  1. Spine目前提供的换装是整体换装,也就是动画那边做好几套Skin,需要哪套直接调用SKeletonAnimation中的InitialSkin进行置换就行了,这个看起来很简单嘛。
  2. 但是,如果我们需要局部换装,难道让动画把每个局部都单独列出来,比如我们一个角色10套
  3. 皮肤,每套皮肤有对于10个位置可以进行任意更换,那么动画岂不是要做10! = 3 628 800 套皮肤?计算机装得下?程序调用逻辑不会出错?这么看来,这个方案不科学。
  4. 那么,我们有没有一种方法,可以实现到局部换装?于是,开始针对Spine导出文件进行分析;Spine可到处二进制和Json数据文件,为了方便分析,我们这次使用Json文件进行分析:


    Spine导出文件有Png,Json和Altas,Png只是静态贴图,我们暂且可以忽略;那么我们观察Altas,截取部分数据:

    Lead.png
    size: ,
    format: RGBA8888
    filter: Linear,Linear
    repeat: none
    L_hand000
    rotate: true
    xy: ,
    size: ,
    orig: ,
    offset: ,
    index: -
    L_leg000
    rotate: true
    xy: ,
    size: ,
    orig: ,
    offset: ,
    index: -

    并没有多大作用…  那么我们在看看另一个Json文件,截取部分信息:

    套装1: clothing001部分插槽和贴图数据

    "clothing001": {
    // 此处省略一大堆动画数据和插槽数据,我们目前看套装1的武器 weapon_C部分
    "hair_C": {
    "hair_C": { "name": "clothing001/hair001", "x": -14.38, "y": -11.92, "rotation": -93.18, "width": 100, "height": 78 }
    },
    "shield_C": {
    "shield_C": { "name": "clothing001/shield001", "x": 20.78, "y": -0.75, "rotation": -11.65, "width": 62, "height": 77 }
    },
    "weapon_C": {
    "weapon_C": { "name": "clothing001/weapon001", "x": 50.69, "y": -1.75, "rotation": -153.42, "width": 142, "height": 87 }
    }
    },

    套装2:clothing002部分插槽和贴图数据

    "clothing002": {
    "hair_C": {
    "hair_C": { "name": "cloting002/hair002", "x": 15.26, "y": -9.01, "rotation": -93.18, "width": 84, "height": 63 }
    },
    "shield_C": {
    "shield_C": { "name": "cloting002/shield002", "x": 20.78, "y": -0.75, "rotation": 92.7, "width": 80, "height": 84 }
    },
    "weapon_C": {
    "weapon_C": { "name": "cloting002/weapon002", "x": 74.02, "y": 0.77, "rotation": -153.42, "width": 190, "height": 108 }
    }
    }
    },

    通过数据对比,我们可以发现套装数据结构一致,只是内部存储的数据不同,那么我们尝试将clothing001套装和clothing002套装内的其中一部分数据进行对调,比如我们对调“weapon_C”的所有数据,把文件导入Unity会发生什么呢?

    对调前: 
                                
                              “cloth001”                                              “cloth002”

    对调后:
                                 
                              “cloth001”                                              “cloth002”

    预料之中,那么局部换装的实现,就应该不成问题了,那么我们回到代码层开始分析.

三、Spine底层库源代码分析

  1. 我们先观察Spine提供的整套换装源代码:

    namespace Spine.Unity {
    [ExecuteInEditMode, RequireComponent(typeof(CanvasRenderer), typeof(RectTransform)), DisallowMultipleComponent]
    [AddComponentMenu("Spine/SkeletonGraphic (Unity UI Canvas)")]
    public class SkeletonGraphic : MaskableGraphic, ISkeletonComponent, IAnimationStateComponent, ISkeletonAnimation { #region Inspector
    public SkeletonDataAsset skeletonDataAsset;
    public SkeletonDataAsset SkeletonDataAsset { get { return skeletonDataAsset; } } [SpineSkin(dataField:"skeletonDataAsset")]
    public string initialSkinName = "default"; [SpineAnimation(dataField:"skeletonDataAsset")]
    public string startingAnimation;
    public bool startingLoop;
    public float timeScale = 1f;
    public bool freeze;

    根据initialSkinName我们继续往下查找

    // Set the initial Skin and Animation
    if (!string.IsNullOrEmpty(initialSkinName))
    skeleton.SetSkin(initialSkinName);

    /// <summary>Sets a skin by name (see SetSkin).</summary>
    public void SetSkin (String skinName) {
    Skin skin = data.FindSkin(skinName);
    if (skin == null) throw new ArgumentException("Skin not found: " + skinName, "skinName");
    SetSkin(skin);
    }

    /// <summary>Sets the skin used to look up attachments before looking in the {@link SkeletonData#getDefaultSkin() default
    /// skin}. Attachmentsfrom the new skin are attached if the corresponding attachment from the old skin was attached. If
    /// there was no old skin, each slot's setup mode attachment is attached from the new skin.</summary>
    /// <param name="newSkin">May be null.</param>
    public void SetSkin (Skin newSkin) {
    if (newSkin != null) {
    if (skin != null)
    newSkin.AttachAll(this, skin);
    else {
    ExposedList<Slot> slots = this.slots;
    for (int i = , n = slots.Count; i < n; i++) {
    Slot slot = slots.Items[i];
    String name = slot.data.attachmentName;
    if (name != null) {
    Attachment attachment = newSkin.GetAttachment(i, name);
    if (attachment != null) slot.Attachment = attachment;
    }
    }
    }
    }
    skin = newSkin;
    }

    就是这个函数,SetSkin(Skin newSkin),通过传入Skin套装数据,遍历所有对应的骨骼,更新骨骼上对应的贴图数据。

  2. 那么,思路就来了,我们Spine进行局部换装,不也就是更新某个SKin中某个Slot上对应的Attachment数据么?
    因此,想想现实世界中的拼图游戏,假设我们有N块整体拼图(数据库Skin),整体拼图中对于的每一块小拼图碎片(Attachment),都能在其他N-1块整体配图(数据库Skin)找到唯一一块相同规格(相同的Slot)但是画面不同的拼图碎片(Attachment),我们需要一个动态的拼图底板(动态Skin来容纳我们的拼图碎片(Attachment),而要想拼一块完整的拼图,我们只需要从N块整体拼图(数据库Skin)中,任意取对应规格(Slot)的配图碎片(Attachment)组装到动态的拼图底板(动态Skin)中,全部Slot组装完成以后,我们就可以得到一幅完整又与其他表现不同的拼图。

        
  3. 分析得出,我们要求动画提供一套动态贴图DynamicSkin(作为新手初始套装),并且保证同一个角色的所有套装贴图卡槽保持一致,然后需要换装的贴图,做成多套完整的整套Skin贴图当作数据使用。

四、修改源代码,实现Spine局部换装目的

(1) 数据处理

1.1 全局枚举Slot数据

    首先,我们得有套装数据,我们才可以进行数据获取,那么你可以使用数据库或者配置文件进行获取,此处暂时以临时文件替代:
    全局枚举数据方便调用和传值,待会我们可以手动做数据映射:
public enum ESkin
{
[Description("不存在此套装,错误反馈专用")]
Null = ,
[Description("动态组装套装基础")]
Dynamic = ,
[Description("可选套装1")]
Clothing001 = ,
[Description("可选套装2")]
Clothing002 = ,
}; public enum ESlot
{
Null = ,
Blet = , // 腰带 //
Weapon = , // 武器 //
BodyArmour = , // 盔甲 //
Hair = , // 头发 //
LeftHand = , // 左手 //
LeftPauldron = , // 左护肩 //
Leftleg = , // 左腿 //
LeftShoes = , // 左鞋 //
Shield = , // 护盾 //
RightHand = , // 右手 //
RightPauldron = , // 右护肩 //
Rightleg = , // 右腿 //
RightShoes = , // 右鞋子 //
}

1.2  Skin_Attactment 字典数据

然后我们针对角色有一个套装更改类 SetSkin ,如果后期拓展多个角色,你完全可以手动抽象出父类和共性的操作方法,此处以一个进行展示:

/// <summary>
/// 初始化贴图套装数据
/// </summary>
private void InitSkinData( )
{
_skinContainAttachments.Add(ESkin.Clothing001 , new List<string>()
{
"clothing001/belt001",
"clothing001/body001",
"clothing001/hair001",
"clothing001/L_hand001",
"clothing001/shield001",
"clothing001/L_pauldron001",
"clothing001/R_hand001",
"clothing001/weapon001",
"clothing001/R_pauldron001",
"clothing001/R_leg001",
"clothing001/L_leg001",
"clothing001/L_shoes001",
"clothing001/R_shoes001",
});
_skinContainAttachments.Add(ESkin.Clothing002 , new List<string>()
{
"cloting002/body002",
"cloting002/hair002",
"cloting002/L_hand002",
"cloting002/shield002",
"cloting002/L_pauldron002",
"cloting002/R_hand002",
"cloting002/weapon002",
"cloting002/R_pauldron002",
"cloting002/L_leg002",
"cloting002/L_shoes002",
"cloting002/R_leg002",
"cloting002/R_shoes002",
});
}

1.3 角色数据

也就是需要更换的部位:通常我们可能从背包中取出某部分装备,然后让玩家点击就进行更换
针对数据方便处理,我们需要给对应的某个装备进行标示,其中包括表现层中的数据:该装备属于哪套贴图Skin,属于哪个卡槽Slot;还有数据层的数据:该装备对玩家某些属性产生什么影响,此处我们只预留接口,不进行讨论;
代码如下:

using UnityEngine;
using UnityEngine.UI; namespace Assets.Game.Animation
{
/// <summary>
/// 带按钮功能的皮肤部位映射
/// </summary>
public class SkinInfo : MonoBehaviour
{
[SerializeField, Header("皮肤套装")]
public ESkin ESkin;
[SerializeField, Header("插槽")]
public ESlot Slot; public SetSkin SkinTarget; #region 属性字段
// TODO 套装属性,进行哪些更改
#endregion
public SkinInfo( ESkin eSkin , ESlot slot )
{
ESkin = eSkin;
Slot = slot;
} public Button SkinButton; /// <summary>
/// 更新装备数据
/// </summary>
private void InvokeDataRefresh()
{
} public void Awake( )
{
SkinTarget = GameObject.Find("LeadSpineAnim").GetComponent<SetSkin>();
SkinButton = GetComponent<Button>(); SkinButton.onClick.AddListener(() =>
{
var clickSuccessed = SkinTarget.ReceiveClick(SkinButton, ESkin , Slot);
if (clickSuccessed) //回调函数处理,进行属性数据更新
{
InvokeDataRefresh();
}
});
} }
}

(2)数据映射

得到数据以后,我们需要把数据跟枚举进行相互映射,方便我们调用

2.1 插槽数据

/// <summary>
/// 根据插槽枚举映射对应插槽名称
/// </summary>
public string MappingESlot2Name( ESlot eSlot )
{
// TODO 映射,通过附着物名称找对应插槽
switch ( eSlot )
{
case ESlot.Blet:
return "blet_C";
case ESlot.Weapon:
return "weapon_C";
case ESlot.BodyArmour:
return "body_C";
case ESlot.Hair:
return "hair_C";
case ESlot.Shield:
return "shield_C";
case ESlot.LeftHand:
return "L_hand_C";
case ESlot.Leftleg:
return "L_leg_C";
case ESlot.LeftShoes:
return "L_shoes_C";
case ESlot.LeftPauldron:
return "L_pauldron_C";
case ESlot.RightHand:
return "R_hand_C";
case ESlot.RightPauldron:
return "R_pauldron_C";
case ESlot.Rightleg:
return "R_leg_C";
case ESlot.RightShoes:
return "R_shoes_C";
default:
throw new ArgumentOutOfRangeException("attachment" , eSlot , "换装目标不存在");
}
}

/// <summary>
/// 根据插槽名称映射插槽枚举,与MappingESlot2Name( ESlot eSlot )互逆
/// </summary>
/// <param name="slotName"></param>
/// <returns></returns>
public ESlot MappingName2ESlot( string slotName )
{
// TODO 映射,通过插槽找对应附着物类型
if ( slotName == "blet_C" ) return ESlot.Blet;
if ( slotName == "weapon_C" ) return ESlot.Weapon;
if ( slotName == "body_C" ) return ESlot.BodyArmour;
if ( slotName == "hair_C" ) return ESlot.Hair;
if ( slotName == "shield_C" ) return ESlot.Shield;
if ( slotName == "L_hand_C" ) return ESlot.LeftHand;
if ( slotName == "L_leg_C" ) return ESlot.Leftleg;
if ( slotName == "L_shoes_C" ) return ESlot.LeftShoes;
if ( slotName == "L_pauldron_C" ) return ESlot.LeftPauldron;
if ( slotName == "R_hand_C" ) return ESlot.RightHand;
if ( slotName == "R_pauldron_C" ) return ESlot.RightPauldron;
if ( slotName == "R_leg_C" ) return ESlot.Rightleg;
if ( slotName == "L_pauldron_C" ) return ESlot.LeftPauldron;
if ( slotName == "R_shoes_C" ) return ESlot.RightShoes;
return ESlot.Null;
}

2.2 Skin贴图数据映射

/// <summary>
/// 根据套装贴图枚举映射贴图名称
/// </summary>
private string MappingEskin2Name( ESkin eSkin )
{
switch ( eSkin )
{
case ESkin.Clothing001:
return "clothing001";
case ESkin.Clothing002:
return "clothing002";
default:
throw new ArgumentOutOfRangeException("eSkin" , eSkin , "The Skin Cannot Found in Character's Spine");
}
} /// <summary>
/// 通过附着物名称查找对应的套装,对_skinContainAttachments进行遍历取Key
/// </summary>
/// <param name="attachmentName">插槽对应的附着物名称</param>
private ESkin GetSkinByAttachment( string attachmentName )
{
if ( !_skinContainAttachments.Any(skins => skins.Value.Contains(attachmentName)) ) return ESkin.Null;
var eSkins = _skinContainAttachments.SingleOrDefault(skin => skin.Value.Contains(attachmentName));
return eSkins.Key;
}

(3) 换装代码

3.1 换装顺序:

我们需要进行的操作是:
①  等待Spine官方源码加载套装(此套装必须设置为新手动态套装,在此基础上我们再进行动态组合)
②  读取新手套装中所有插槽数据和附着物数据,缓存到字典
③  反序列化从数据库/系统存储/文本数据存储中得到的实际套装数据
④  缓存数据和新手套装数据进行差异化对比,对有差别部分,以缓存数据为准,进行局部换图
⑤  换图操作后,需要对缓存数据表进行及时更新
⑥  切记,一切操作需要在官方Spine加载完成后,在Start函数中进行更新,否则会异常报空

/// <summary>
/// 数据层初始化
/// </summary>
public void Awake( )
{
InitSkinData(); } /// <summary>
/// 行为表现层操作
/// </summary>
public void Start( )
{
_skeletonAnimation = GetComponent<SkeletonAnimation>();
_skeleton = _skeletonAnimation.skeleton; //TODO 测试数据
//PlayerPrefsDataHlpers.SetBool(_playerSkinInitFlag , false); // 注释部分用于清理套装缓存数据 //_isPlayerSkinInit = PlayerPrefsDataHlpers.GetBool(_playerSkinInitFlag);
//if ( !_isPlayerSkinInit )
InitSkinDataAtStart();
//PlayerPrefsDataHlpers.SetBool(_playerSkinInitFlag , true);
ReloadSkinByDataAtGameStart(); }

3.2  数据定义和数据表缓存校验

#region Spine Animation Script
private SkeletonAnimation _skeletonAnimation; private Skeleton _skeleton;
#endregion #region User Data
private readonly Dictionary<ESkin , List<string>> _skinContainAttachments = new Dictionary<ESkin , List<string>>(); // 通过附着物映射skin private readonly Dictionary<ESlot , string> _dynamicSlotToAttachments = new Dictionary<ESlot , string>(); // 实际使用中的数据 private readonly string _dynamicSkinName = "clothing000";
#endregion #region 数据表缓存校验 private void SetSlotToAttachment( ESlot eAttach , string attchmentName,bool saveDatabase )
{
bool isExitKey = _dynamicSlotToAttachments.ContainsKey(eAttach);
if ( !isExitKey )
{
_dynamicSlotToAttachments.Add(eAttach , attchmentName);
}
else
{
_dynamicSlotToAttachments[eAttach] = attchmentName; // Reference Type Don't need to Reassignment Value
} // 是否写入数据表 //
if (saveDatabase)
{
EncodingAttachment(eAttach , attchmentName);
}
} /// <summary>
/// 编码写入缓存(也可另改写为服务器存储)
/// </summary>
/// <param name="eAttach">对应插槽</param>
/// <param name="attchmentName">对于贴图名称</param>
private void EncodingAttachment( ESlot eAttach , string attchmentName )
{
int id = (int) eAttach;
string flag = string.Concat("slot" , id.ToString());
PlayerPrefsDataHlpers.SetString(flag , attchmentName);
} /// <summary>
/// 解码取出缓存套装数据
/// </summary>
/// <returns></returns>
private Dictionary<ESlot , string> DecodingAttachment( )
{
var slotToAttachments = new Dictionary<ESlot , string>(); var fristSlot = (ESlot) Enum.Parse(typeof(ESlot) , "Null" , true);
int start = (int) fristSlot;
int length = Enum.GetNames(typeof(ESlot)).Length;
int end = start + length;
for ( int i = start ; i < end ; i++ )
{
string flag = string.Concat("slot" , i.ToString());
string attchmentName = PlayerPrefsDataHlpers.GetString(flag);
if ( attchmentName != string.Empty )
{
ESlot eSlot = (ESlot) i;
slotToAttachments.Add(eSlot , attchmentName);
}
} return slotToAttachments;
} #endregion

3.3  读取默认套装数据,作为动态套装的基础对比数据表

private bool _isPlayerSkinInit; // 开关: 用于测试数据缓存 // /// <summary>
/// TODO : 完善局部换装逻辑
/// 1、初次游戏,记录基准 Slot -> Attachment Table
/// 2、任何时刻实时更改套装任何部分,立刻更新映射表数据层
/// 3、再次游戏,基于基准装,重新根据数据表缓存数据映射表现层
/// 4、双重数据表校验,只要基准表和实际表任何部分不一致,认定装备需要Reloading
/// </summary>
public void InitSkinDataAtStart( )
{
// 默认设置必须为基准装 Clothing000 //
_skeletonAnimation.initialSkinName = _dynamicSkinName; //var curSkin = _skeleton.Skin; ExposedList<Slot> slots = _skeleton.slots;
for ( int i = , n = slots.Count ; i < n ; i++ )
{
Slot slot = slots.Items[i];
String slotName = slot.data.attachmentName;
if ( slotName != null )
{
ESlot eSlot = MappingName2ESlot(slotName);
Attachment attachment = LGetAttachment(i , slotName , _dynamicSkinName); // Find Attachment By Slot With Base Skin
if ( attachment == null ) continue; string attahName = attachment.Name; // 是否写入数据表
SetSlotToAttachment(eSlot , attahName,false); }
}
}

3.4  读取基础数据表以后,读取缓存数据,对比数据进行局部换装


/// <summary>
/// 在基础套装自动加载完成以后,手动调用此函数
/// 为了局部换装数据不错乱,哪怕整套Skin换都需要更新数据表中的数据
/// </summary>
public void ReloadSkinByDataAtGameStart( )
{
var slotToAttachments = DecodingAttachment();
CompareAndSetAttachments(slotToAttachments);
}

数据对比函数:

/// <summary>
/// 对比数据表跟目前数据表(游戏初始加载后的Spine内置套装数据)差异,并更新数据和表现
/// </summary>
/// <param name="targetAttchments">缓存数据表(目标数据)</param>
private void CompareAndSetAttachments(Dictionary<ESlot, string> targetAttchments)
{
var curAttachments = _dynamicSlotToAttachments; var fristSlot = (ESlot) Enum.Parse(typeof(ESlot) , "Null" , true);
int start = (int) fristSlot; foreach (var eSlotKey in targetAttchments )
{
ESlot slotKey = eSlotKey.Key;
var curAttachment = curAttachments[slotKey];
var targetAttachment = targetAttchments[slotKey]; if ( curAttachment == null || curAttachment != targetAttachment )
{
ESkin eSkins = GetSkinByAttachment(targetAttachment);
if ( eSkins == ESkin.Null )
{
throw new Exception("Eskin 不存在与=数据表_skinContainAttachments中");
}
LChangeSkinBaseOnDynamicSkin(eSkins , slotKey);
}
}
}

3.4  核心换装代码

换装函数对外入口点:

/// <summary>
/// 基于动态套装,改变局部并重新组合动态套装
/// </summary>
/// <param name="eTargetSkin">取值套装</param>
/// <param name="eSlot">目标插槽</param>
public bool LChangeSkinBaseOnDynamicSkin( ESkin eTargetSkin , ESlot eSlot )
{
Skin dynamicSkin = _skeleton.data.FindSkin(_dynamicSkinName); var success = LSetSkin(dynamicSkin , eTargetSkin , eSlot);
return success;
}


批量换装操作:

/// <summary>
/// 批量换装,必须保证传入的数组一一对应
/// </summary>
/// <returns>批量换装只要有其中一处换不成功,整体算作失败,需要手动进行数据回滚</returns>
public bool LChangeBitchSkinBaseOnDynamicSkin(ESkin[] eTargetSkins, ESlot[] eSlots)
{
if (eTargetSkins.Length != eSlots.Length) return false;
for (int i = ; i < eSlots.Length; i++)
{
var success = LChangeSkinBaseOnDynamicSkin(eTargetSkins[i],eSlots[i]);
if (!success)
{
return false; // 任意一件换不成功,整体换装失败
}
}
return true;
}



内部换装处理函数:

/// <summary>
/// 内部处理:针对传入的需要更改的套装(实时套装),从目标皮肤中根据目标卡槽取出皮肤数据进行替换赋值操作,
/// 数据层和表现层同时处理变化
/// </summary>
/// <param name="dynamicSkin">赋值套装</param>
/// <param name="eSkin">取值套装枚举</param>
/// <param name="eSlot">目标插槽枚举</param>
private bool LSetSkin( Skin dynamicSkin , ESkin eSkin , ESlot eSlot )
{ if ( dynamicSkin != null )
{
ExposedList<Slot> slots = _skeleton.slots;
for ( int i = , n = slots.Count ; i < n ; i++ )
{
// Get //
Slot slot = slots.Items[i];
var targetSlotName = MappingESlot2Name(eSlot);
if ( slot.data.name != targetSlotName ) continue; string attachName = slot.data.attachmentName;
if ( attachName != null )
{
string targetSkinName = MappingEskin2Name(eSkin);
Attachment attachment;
if ( attachName == targetSlotName )
{
attachment = LGetAttachment(i , targetSlotName , targetSkinName); // 重写L Get
dynamicSkin.Attachments.Remove(new Skin.AttachmentKeyTuple(i , targetSlotName));
dynamicSkin.Attachments.Add(new Skin.AttachmentKeyTuple(i , targetSlotName) , attachment); }
else
{
attachment = dynamicSkin.GetAttachment(i , attachName); // 默认Skeleton Get
} // Set //
if ( attachment != null )
{
slot.Attachment = attachment;
var attahName = attachment.Name;
SetSlotToAttachment(eSlot , attahName,true);
break;
}
}
}
_skeleton.slots = slots;
}
_skeleton.skin = dynamicSkin;
return true;
}

针对性查找真实贴图中的附着点数据(是数据不是String哦,有效的完整的Attachment数据)

/// <summary>
/// 通过指定的Skin找到对应附着点的附着物Attachment
/// </summary>
public Attachment LGetAttachment( int slotIndex , string slotName , string skinName )
{
var targetSkin = _skeleton.data.FindSkin(skinName);
var attachments = targetSkin.Attachments; Attachment attachment;
attachments.TryGetValue(new Skin.AttachmentKeyTuple(slotIndex , slotName) , out attachment);
return attachment;
}

(4)  用户交互部分:

#region 接受用户点击处理
/// <summary>
/// 可交互对象内部绑定了对应的SkinInfo,根据SkinInfo赋值字段进行查找
/// </summary>
/// <param name="skinButton">换装按键</param>
/// <param name="eSkin">对应皮肤</param>
/// <param name="slot">对应插槽</param>
public bool ReceiveClick( Button skinButton , ESkin eSkin , ESlot slot )
{
return ResetActulSlotToAttachment(skinButton , eSkin , slot);
}

四、结果展示

最终测试结果,实现了动态换装,批量换装,局部换装,整体换装,动画运行时换装,换装数据缓存等,如图所示:

五、结束感言

如果你喜欢本篇博课,或者想和我探讨,请关注我的博客园(燕双飞情侣),感谢您的观看。

注:转载请注明转载,并附原链接 http://www.cnblogs.com/liaoguipeng/p/5867510.html 燕双飞情侣

Unity动态换装之Spine换装的更多相关文章

  1. DragonBones龙骨换装(局部换装+全局换装)

    参考: Egret官方换装动画 Egret换装三种方式 CSDN(全局换装) egret使用DragonBones实现简单的换装 换装,主要是替换任意插槽的图片,来达到局部换装的目的. 游戏中可以只制 ...

  2. [Unity][Heap sort]用Unity动态演示堆排序的过程(How Heap Sort Works)

    [Unity][Heap sort]用Unity动态演示堆排序的过程 How Heap Sort Works 最近做了一个用Unity3D动态演示堆排序过程的程序. I've made this ap ...

  3. 先装Net Framework 后 装 IIS的处理办法

    先装IIS话,后面装Net Framework时候会自动注册 处理aspx和ashx等的处理扩展程序 先装Net Framework 后 装 IIS.扩展程序注册在命令:aspnet_regiis - ...

  4. Unity动态字体在手机上出现字体丢失问题解决

    在我们游戏的开发过程中,在部分手机上运行游戏的时候,出现了字体丢失的问题,出问题的手机似乎用的都是高通芯片. 使用的unity是4.2.0版本,ngui是3.4.9版本. 在unity的论坛及unit ...

  5. 当互联网遇上家装,十大家装O2O混战

    2015年已过去大半,装修O2O就出现了新的局面:为数众多的家居网络平台在家装O2O领域还未站稳脚跟,新的入局者就打出超低价格登场.新老O2O家装大战迅速展开,除了拼价格还拼品牌和体验,家装O2O的好 ...

  6. 装了VS2005再装IIS,结果出了些小问题 访问IIS元数据库失败

    版本信息: Microsoft .NET Framework 版本:2.0.50727.42; ASP.NET 版本:2.0.50727.42 装了VS2005再装IIS,结果出了些小问题访问IIS元 ...

  7. 今天抠图,Python实现一键换底片!想换什么换什么(附源码)

    前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 生活中我们会拍很多的证件照,有的要求红底,有的是白底,有的是蓝底,今天不通 ...

  8. 装hadoop的第一步,装ubuntu并换源并装jdk

    如何装ubuntu,这个自己百度.具体安装网站:http://www.ubuntu.com 我安装的是ubuntu Server版本的,然后是全英文安装.所以它的源自动定位到美国 下面是如何换源的,第 ...

  9. 使用 css/less 动态更换主题色(换肤功能)

    前言 说起换肤功能,前端肯定不陌生,其实就是颜色值的更换,实现方式有很多,也各有优缺点 一.看需求是什么 一般来说换肤的需求分为两种: 1. 一种是几种可供选择的颜色/主题样式,进行选择切换,这种可供 ...

随机推荐

  1. 【luogu P2580 于是他错误的点名开始了】 题解

    题目链接:https://www.luogu.org/problemnew/show/P2580 我真的永远都爱stl #include <map> #include <cstdio ...

  2. Android学习笔记_74_Android回调函数触发的几种方式 广播 静态对象

    一.通过广播方式: 1.比如登录.假如下面这个方法是外界调用的,那么怎样在LoginActivity里面执行登录操作,成功之后在回调listener接口呢?如果是平常的类,可以通过构造函数将监听类对象 ...

  3. HTML5之表单新增类型介绍

    1.html5的input标签的type类型新增介绍: 2.表单新增属性介绍: 3.代码示例: <!doctype html> <html> <head></ ...

  4. AngularJS 使用 even 和 odd 表格

    <!DOCTYPE html><html><head><meta http-equiv="Content-Type" content=&q ...

  5. springboot jar 部署到linux之后 获取类资源文件问题-- 仅限linux 下 情况比较特殊 需要获取打到jar内的 讲台资源 只能通过流获取,根据路径获取不到指定文件 nullpointExption

    https://blog.csdn.net/qq_27000425/article/details/72897282 ClassPathResource类,如果没有指定相对的类名,该类将从类的根路径开 ...

  6. 解析 Nginx 负载均衡策略

    转载:https://www.cnblogs.com/wpjamer/articles/6443332.html 1 前言 随着网站负载的不断增加,负载均衡(load balance)已不是陌生话题. ...

  7. qt项目:员工信息管理系统

    开发一个员工信息管理系统 一.项目具体要求: 1.用qt开发界面,数据库用QSqlite 数据库文件名:demostudent.db 2.通过界面能够查看到数据库中员工信息表中内容,包括员工姓名.年龄 ...

  8. leetcode笔记(三)207. Course Schedule

    题目描述(原题目链接) There are a total of n courses you have to take, labeled from 0 to n-1. Some courses may ...

  9. poj 2553 The Bottom of a Graph : tarjan O(n) 存环中的点

    /** problem: http://poj.org/problem?id=2553 将所有出度为0环中的点排序输出即可. **/ #include<stdio.h> #include& ...

  10. Percona-Tookit工具包之pt-query-digest

      Preface       Performance issues are what DBA most concerned thing.There're always a lot of SQL qu ...