实现2D纸娃娃系统和行走图切换
这篇笔记简单实现一个能够更换衣服、发型的2D纸娃娃系统,素材来自于《rimworld》。虽然这里并没有使用动画,但是即使加上动画原理也是一样的,只不过多了一堆序列帧贴图,还要做动画状态机,稍微麻烦一点。(其实《rimworld》里角色就是没有动画的,但是在那种特别的画风下,没有动画飘来飘去的小人一点都不违和,我非常喜欢那种风格)
效果图:
关于素材的加载
我们的player对象时这样安排的,其中的body、clothes、hair、head分别分到了四个sorting layer,以实现正确的遮挡显示。玩家给角色更换装备时,对应层的图就会改变,这是通过更改Sprite Renderer的Sprite属性实现的。
我们的资源是如图所示安排的,代码中绘制到小人身上的图是通过Resources.Load()
加载的。其中注意defs
文件夹,我们在这个文件夹里定义了一些XML信息,指示脚本该加载哪些图片。这样做的好处是提高了程序的可扩展性。
<def>
<clothes>
<label>1</label>
<name>LT</name>
<texture>hair/LT</texture>
</clothes>
<clothes>
<label>2</label>
<name>MA</name>
<texture>hair/MA</texture>
</clothes>
</def>
<def>
<clothes>
<label>1</label>
<name>shirt</name>
<texture>clothes/Parka_Female</texture>
</clothes>
<clothes>
<label>2</label>
<name>power armor</name>
<texture>clothes/PowerArmor_Female</texture>
</clothes>
</def>
脚本代码
注:代码里有换头发颜色的部分,是后加上去的,GIF里没有截到图
TextureLoader.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.IO;
using System.Xml;
public class TextureLoader : MonoBehaviour
{
public Text loading; //用于在加载时显示"Loading字样"
public bool loadOver = false; //加载完成改为true,PaperDoll会开始下一阶段初始化
public Sprite bodyFront; //这里body和head只使用一种,因此就没放进dict里
public Sprite bodyBack;
public Sprite bodySide;
public Sprite headFront;
public Sprite headBack;
public Sprite headSide;
public Dictionary<string, Hair> hairDict = new Dictionary<string, Hair>();
public Dictionary<string, Clothes> clothesDict = new Dictionary<string, Clothes>();
private void Awake()
{
LoadHairSprites();
LoadClothesSprites();
LoadHeadSprites();
LoadBodySprites();
loadOver = true;
loading.gameObject.SetActive(false);
}
private void LoadHairSprites()
{
//xml的定义文件就放在了defs文件夹里
string hairDefs = File.ReadAllText(Application.dataPath + "/defs/hair.xml");
//通过xml定义文件初始化所有Hair对象,并存放进dict
XmlDocument doc = new XmlDocument();
doc.LoadXml(hairDefs);
XmlElement root = (XmlElement) doc.SelectSingleNode("def");
XmlNodeList defs = root.ChildNodes;
foreach(XmlNode node in defs)
{
XmlNode label = node.SelectSingleNode("label");
XmlNode name = node.SelectSingleNode("name");
XmlNode texture = node.SelectSingleNode("texture");
Hair hair = new Hair(label.InnerText, name.InnerText, texture.InnerText);
hairDict.Add(hair.Label, hair);
}
}
private void LoadClothesSprites()
{
string clothesDefs = File.ReadAllText(Application.dataPath + "/defs/clothes.xml");
XmlDocument doc = new XmlDocument();
doc.LoadXml(clothesDefs);
XmlElement root = (XmlElement)doc.SelectSingleNode("def");
XmlNodeList defs = root.ChildNodes;
foreach (XmlNode node in defs)
{
XmlNode label = node.SelectSingleNode("label");
XmlNode name = node.SelectSingleNode("name");
XmlNode texture = node.SelectSingleNode("texture");
Clothes clothes = new Clothes(label.InnerText, name.InnerText, texture.InnerText);
clothesDict.Add(clothes.Label, clothes);
}
}
private void LoadHeadSprites()
{
//注意:Resources.Load()的参数,例如文件路径是Assets/Resources/face/a.png,那么参数写法就是"face/a"
//Load()函数能加载的对象必须放在Assets/Resources下,否则会返回null
headFront = Resources.Load<Sprite>("face/Female_Average_Normal_front");
headBack = Resources.Load<Sprite>("face/Female_Average_Normal_back");
headSide = Resources.Load<Sprite>("face/Female_Average_Normal_side");
}
private void LoadBodySprites()
{
bodyFront = Resources.Load<Sprite>("body/Naked_female_front");
bodyBack = Resources.Load<Sprite>("body/Naked_female_back");
bodySide = Resources.Load<Sprite>("body/Naked_female_side");
}
}
public class Clothes
{
public Clothes(string label, string name, string spritePath)
{
this.Label = label;
this.Name = name;
//例如spritePath="hair/LT",那么加载front对应的其实就是Resources.Load<Sprite>("hair/LT_front")
string spritePathFront = spritePath + "_front";
this.SpriteFront = Resources.Load<Sprite>(spritePathFront);
string spritePathBack = spritePath + "_back";
this.SpriteBack = Resources.Load<Sprite>(spritePathBack);
string spritePathSide = spritePath + "_side";
this.SpriteSide = Resources.Load<Sprite>(spritePathSide);
}
public string Label { get; set; }
public string Name { get; set; }
public Sprite SpriteFront{ get; set; }
public Sprite SpriteBack { get; set; }
public Sprite SpriteSide { get; set; }
}
public class Hair
{
public Hair(string label, string name, string spritePath)
{
this.Label = label;
this.Name = name;
string spritePathFront = spritePath + "_front";
this.SpriteFront = Resources.Load<Sprite>(spritePathFront);
string spritePathBack = spritePath + "_back";
this.SpriteBack = Resources.Load<Sprite>(spritePathBack);
string spritePathSide = spritePath + "_side";
this.SpriteSide = Resources.Load<Sprite>(spritePathSide);
}
public string Label { get; set; }
public string Name { get; set; }
public Sprite SpriteFront { get; set; }
public Sprite SpriteBack { get; set; }
public Sprite SpriteSide { get; set; }
}
PaperDoll.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class PaperDoll : MonoBehaviour
{
//当前使用的发型编号和衣服编号
private string currentHairIndexLabel;
private string currentclothesIndexLabel;
//两个下拉选择框
public Dropdown hairSelect;
public Dropdown clothesSelect;
public Dropdown hairColorSelect;
private Transform m_transform;
private TextureLoader m_textureLoader;
private SpriteRenderer bodySpriteRenderer;
private SpriteRenderer headSpriteRenderer;
private SpriteRenderer clothesSpriteRenderer;
private SpriteRenderer hairSpriteRenderer;
//当前渲染的player各层的sprite
private Sprite currentBody;
private Sprite currentHead;
private Sprite currentClothes;
private Sprite currentHair;
private Color currentHairColor;
//player有上下左右四个朝向,但图片里不需要把左右各画一遍,因为是镜像的,直接把sprite水平镜像反过来就可以了
private int isFlipRender = 1;
void Start()
{
m_transform = GetComponent<Transform>();
m_textureLoader = GetComponent<TextureLoader>();
bodySpriteRenderer = m_transform.Find("body").gameObject.GetComponent<SpriteRenderer>();
headSpriteRenderer = m_transform.Find("head").gameObject.GetComponent<SpriteRenderer>();
clothesSpriteRenderer = m_transform.Find("clothes").gameObject.GetComponent<SpriteRenderer>();
hairSpriteRenderer = m_transform.Find("hair").gameObject.GetComponent<SpriteRenderer>();
//等待直到TextureLoader加载完成
StartCoroutine(InitUIUntileLoadOver());
//开始时暂时将所有层初始化为正面
currentBody = m_textureLoader.bodyFront;
currentHead = m_textureLoader.headFront;
currentclothesIndexLabel = new List<string>(m_textureLoader.clothesDict.Keys)[0];
currentClothes = m_textureLoader.clothesDict[currentclothesIndexLabel].SpriteFront;
currentHairIndexLabel = new List<string>(m_textureLoader.hairDict.Keys)[0];
currentHair = m_textureLoader.hairDict[currentHairIndexLabel].SpriteFront;
currentHairColor = Color.white;
}
IEnumerator InitUIUntileLoadOver()
{
//等待直到TextureLoader加载完成
while (!m_textureLoader.loadOver)
{
yield return null;
}
//动态初始化dropdown控件的选项,以及选择事件
List<string> hairDictKeyList = new List<string>(m_textureLoader.hairDict.Keys);
hairSelect.AddOptions(hairDictKeyList);
hairSelect.onValueChanged.AddListener((int i) =>
{
currentHairIndexLabel = hairDictKeyList[i];
currentHair = m_textureLoader.hairDict[currentHairIndexLabel].SpriteFront;
});
List<string> clothesDictKeyList = new List<string>(m_textureLoader.clothesDict.Keys);
clothesSelect.AddOptions(new List<string>(m_textureLoader.clothesDict.Keys));
clothesSelect.onValueChanged.AddListener((int i) =>
{
currentclothesIndexLabel = clothesDictKeyList[i];
currentClothes = m_textureLoader.clothesDict[currentclothesIndexLabel].SpriteFront;
});
hairColorSelect.onValueChanged.AddListener((int i) =>
{
switch (i)
{
case 0:
currentHairColor = Color.white;
break;
case 1:
currentHairColor = Color.green;
break;
case 2:
currentHairColor = Color.black;
break;
}
});
}
void Update()
{
if(m_textureLoader.loadOver)
{
if (Input.GetKey(KeyCode.W))//根据行走方向改变贴图
{
currentBody = m_textureLoader.bodyBack;
currentHead = m_textureLoader.headBack;
currentHair = m_textureLoader.hairDict[currentHairIndexLabel].SpriteBack;
currentClothes = m_textureLoader.clothesDict[currentclothesIndexLabel].SpriteBack;
isFlipRender = 1;
}
else if (Input.GetKey(KeyCode.A))
{
currentBody = m_textureLoader.bodySide;
currentHead = m_textureLoader.headSide;
currentHair = m_textureLoader.hairDict[currentHairIndexLabel].SpriteSide;
currentClothes = m_textureLoader.clothesDict[currentclothesIndexLabel].SpriteSide;
isFlipRender = -1;//贴图里只有向右的,向左右就水平翻转
}
else if (Input.GetKey(KeyCode.S))
{
currentBody = m_textureLoader.bodyFront;
currentHead = m_textureLoader.headFront;
currentHair = m_textureLoader.hairDict[currentHairIndexLabel].SpriteFront;
currentClothes = m_textureLoader.clothesDict[currentclothesIndexLabel].SpriteFront;
isFlipRender = 1;
}
else if (Input.GetKey(KeyCode.D))
{
currentBody = m_textureLoader.bodySide;
currentHead = m_textureLoader.headSide;
currentHair = m_textureLoader.hairDict[currentHairIndexLabel].SpriteSide;
currentClothes = m_textureLoader.clothesDict[currentclothesIndexLabel].SpriteSide;
isFlipRender = 1;
}
//重新指定renderer渲染当前的sprite
bodySpriteRenderer.sprite = currentBody;
headSpriteRenderer.sprite = currentHead;
hairSpriteRenderer.sprite = currentHair;
clothesSpriteRenderer.sprite = currentClothes;
hairSpriteRenderer.color = currentHairColor;
//水平镜像翻转
transform.localScale = new Vector3(isFlipRender, 1, 1);
}
}
}
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。