最近整理了一下项目的换装系统,有一些想法,在这里分享一下。
我们的角色拆分了四个部件,头发,身体,腿和脚,对应到gameobject是这样的,000是骨骼根节点。
装备导出是这样的,skinnedmeshrenderer在000同级的子节点上,装备导出也是带骨骼信息的。
第一种实现办法
把装备gameobject挂到角色根节点下,然后通过setactive设置原部件和装备的显隐。
新装备上的skinnedmeshrenderer.bones,需要按照名字在角色的骨骼上找到对应的替换,不然动画就不认了,其他的都很简单不细说了,直接上代码。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ObjSwaper : MonoBehaviour
{
public SkinnedMeshRenderer[] srcEquips = new SkinnedMeshRenderer[4]; // 原来的部件
public SkinnedMeshRenderer[] equips = new SkinnedMeshRenderer[4]; // 预备换的部件
Animation anim = null;
void Start()
{
anim = gameObject.GetComponent<Animation>();
anim.CrossFade("idle1");
}
void Update ()
{
if (Input.GetKeyDown(KeyCode.P))
{
Swap();
}
}
public void Swap()
{
Transform[] transforms = gameObject.GetComponentsInChildren<Transform>(false);
List<Transform> boneList = new List<Transform>();
for (int i = 0 ; i < equips.Length ; ++i)
{
SkinnedMeshRenderer smr = null;
if (equips[i] != null)
{
smr = equips[i];
equips[i].transform.parent.gameObject.SetActive(true);
srcEquips[i].gameObject.SetActive(false);
}
else
{
smr = srcEquips[i];
srcEquips[i].gameObject.SetActive(true);
}
boneList.Clear();
foreach (Transform bone in smr.bones)
{
foreach (Transform item in transforms)
{
if (item.name != bone.name)
continue;
boneList.Add(item);
break;
}
}
smr.bones = boneList.ToArray();
smr.rootBone = srcEquips[i].rootBone;
}
}
}
这种办法实现简单,这里说一下缺点:
1 看到找骨骼那两层for循环了吧,这个东西每次换装都要查一下,很费。而且最重要的是UnityEngine.Object.name这个访问是会产生GC的,频繁调用会出发系统GC造成顿卡。
2 角色分了4个部分,渲染就需要至少4个DrawCall,如果同屏角色多的话,渲染压力很大。
3 角色除了原有的资源,还引用了新的装备资源,如果很多穿不同装备角色在一起,内存压力也不小。
现在我们说第二种办法
http://www.cnblogs.com/shamoyuu/p/6505561.html
思路是从这里借鉴的,把需要的部件mesh和贴图合并,这样渲染的时候就只用1个DrawCall,合并完成后原资源可以直接释放。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Combiner : MonoBehaviour
{
public SkinnedMeshRenderer[] srcEquips = new SkinnedMeshRenderer[4]; // 原来的部件
public SkinnedMeshRenderer[] equips = new SkinnedMeshRenderer[4]; // 预备换的部件
Animation anim = null;
void Start()
{
anim = gameObject.GetComponent<Animation>();
anim.CrossFade("idle1");
}
void Update ()
{
if (Input.GetKeyDown(KeyCode.P))
{
Combine();
anim.cullingType = AnimationCullingType.BasedOnRenderers;
}
}
public void Combine()
{
SkinnedMeshRenderer mainRender = gameObject.GetComponent<SkinnedMeshRenderer>();
if (mainRender == null)
{
mainRender = gameObject.AddComponent<SkinnedMeshRenderer>();
mainRender.material = new Material(Shader.Find("Legacy Shaders/Diffuse"));
}
Transform[] transforms = gameObject.GetComponentsInChildren<Transform>(false);
List<CombineInstance> combineInstances = new List<CombineInstance>();
List<Transform> boneList = new List<Transform>();
List<Texture2D> textures = new List<Texture2D>();
int width = 0;
int height = 0;
int uvCount = 0;
List<Vector2[]> uvList = new List<Vector2[]>();
for (int i = 0; i < equips.Length; ++i)
{
srcEquips[i].gameObject.SetActive(false);
SkinnedMeshRenderer smr = null;
Transform[] bones = null;
if (equips[i] == null)
{
smr = srcEquips[i];
bones = smr.bones;
}
else
{
smr = equips[i];
bones = smr.bones;
}
for (int sub = 0; sub < smr.sharedMesh.subMeshCount; ++sub)
{
CombineInstance ci = new CombineInstance();
ci.mesh = smr.sharedMesh;
ci.subMeshIndex = sub;
combineInstances.Add(ci);
}
uvList.Add(smr.sharedMesh.uv);
uvCount += smr.sharedMesh.uv.Length;
if (smr.material.mainTexture != null)
{
textures.Add(smr.GetComponent<Renderer>().material.mainTexture as Texture2D);
width += smr.GetComponent<Renderer>().material.mainTexture.width;
height += smr.GetComponent<Renderer>().material.mainTexture.height;
}
foreach (Transform bone in bones)
{
foreach (Transform item in transforms)
{
if (item.name != bone.name)
continue;
boneList.Add(item);
break;
}
}
}
string combineName = "combine";
for (int i = 0; i < combineInstances.Count; ++i)
{
combineName = string.Format("{0}_{1}", combineInstances[i].mesh.name, combineName);
}
mainRender.sharedMesh = new Mesh();
mainRender.sharedMesh.name = combineName;
mainRender.sharedMesh.CombineMeshes(combineInstances.ToArray(), true, false);
mainRender.bones = boneList.ToArray();
int texW = Util.get2Pow(width);
int texH = Util.get2Pow(height);
Texture2D skinnedMeshAtlas = new Texture2D(texW, texH);
skinnedMeshAtlas.name = mainRender.sharedMesh.name;
Rect[] packingResult = skinnedMeshAtlas.PackTextures(textures.ToArray(), 0, 1024);
Vector2[] atlasUVs = new Vector2[uvCount];
// 因为将贴图都整合到了一张图片上,所以需要重新计算UV
int j = 0;
for (int i = 0; i < uvList.Count; i++)
{
foreach (Vector2 uv in uvList[i])
{
atlasUVs[j].x = Mathf.Lerp(packingResult[i].xMin, packingResult[i].xMax, uv.x);
atlasUVs[j].y = Mathf.Lerp(packingResult[i].yMin, packingResult[i].yMax, uv.y);
j++;
}
}
// 设置贴图和UV
mainRender.material.mainTexture = skinnedMeshAtlas;
mainRender.sharedMesh.uv = atlasUVs;
}
}
思路不复杂,只是一些API的调用,说一些问题:
1 合并贴图的时候要求贴图是read/write enable的,这个在游戏运行时内存里面会多一份拷贝,考虑到合并后的贴图,内存是比较吃紧的,mesh也有相似的问题。由于装备的特殊性,不同角色穿完全相同的一套装备比较少见,所以这里就算做了cache意义也不大。
2 合并之后动画就不认了,需要调一次 anim.cullingType = AnimationCullingType.BasedOnRenderers,或者干脆把animation干掉重新add就好了。想来是因为换了renderer,animation需要重新刷新一次的缘故吧。
3 这个方案限制比较多,如果身上的装备使用了不同的材质,合并之后特殊材质的效果就没了。而且还是使用了两层for循环刷骨骼的办法。
方案三,先上代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MeshSwaper : MonoBehaviour
{
public SkinnedMeshRenderer[] srcEquips = new SkinnedMeshRenderer[4]; // 原来的部件
public SkinnedMeshRenderer[] equips = new SkinnedMeshRenderer[4]; // 预备换的部件
BonesData[] bonesData = new BonesData[4];
public Transform bonesRoot = null;
Animation anim = null;
class BonesData
{
public int rootIdx = 0;
public List<int> bonesIdxs = new List<int>();
}
void initBoneData()
{
Transform[] nodes = bonesRoot.GetComponentsInChildren<Transform>();
for (int i = 0; i < equips.Length; ++i)
{
SkinnedMeshRenderer smr = equips[i];
if (smr == null)
continue;
BonesData bd = new BonesData();
for (int j = 0; j < nodes.Length; ++j)
{
if (smr.rootBone.name == nodes[j].name)
{
bd.rootIdx = j;
break;
}
}
foreach (Transform t in smr.bones)
{
for (int j = 0; j < nodes.Length; ++j)
{
if (t.name == nodes[j].name)
{
bd.bonesIdxs.Add(j);
break;
}
}
}
bonesData[i] = bd;
}
}
void Start()
{
anim = gameObject.GetComponent<Animation>();
anim.CrossFade("idle1");
initBoneData();
}
void Update ()
{
if (Input.GetKeyDown(KeyCode.P))
{
Swap();
}
}
public void Swap()
{
Transform[] nodes = bonesRoot.GetComponentsInChildren<Transform>();
for (int i = 0 ; i < equips.Length ; ++i)
{
SkinnedMeshRenderer smr = srcEquips[i];
if (equips[i] != null)
{
smr.sharedMesh = equips[i].sharedMesh;
smr.sharedMaterial = equips[i].sharedMaterial;
BonesData bd = bonesData[i];
List<Transform> tmpBones = new List<Transform>();
tmpBones.Clear();
for (int k = 0; k < bd.bonesIdxs.Count; ++k)
{
int boneIdx = bd.bonesIdxs[k];
if (boneIdx < 0 || boneIdx > nodes.Length)
continue;
tmpBones.Add(nodes[boneIdx]);
}
smr.bones = tmpBones.ToArray();
smr.rootBone = nodes[bd.rootIdx];
}
}
}
}
这个方案的思路和方案一是类似的,都是替换,不同的是这里直接替换mesh和material,不再保存gameobject了,当然有了mesh和material,做合并也是可以的。
class BonesData
{
public int rootIdx = 0;
public List<int> bonesIdxs = new List<int>();
}
我们在start里面初始化了这样一个结构,他包含一个装备所引用的骨骼信息和root节点信息,作用是给skinnedmeshrenderer刷骨骼的时候不再使用名字遍历查找了,减少了gc和一点cpu。实际项目中我们可以让编辑器生成一个配置文件,需要的时候去读就好了。
bonesIdxs里面存的是骨骼在角色根骨骼下面的索引,这就要求有一个全骨骼的标准角色prefab作为参照,所以装备引用的骨骼在这里都能找得到。实际应用的时候我们可能会往角色骨骼下面挂一些零七八碎,比如特效之类的东西,这样骨骼索引就会变,再换装就会出错。所以要求这个角色刚一创建的时候我们就要把所有骨骼和索引对照起来,这样之后怎么变,我们也是能找到对应的骨骼的。
为了省掉加载的代码,例子里面还是从装备gameobject获取的mesh和material,实际项目也可以把这些都放到配置里面。