【Unity3D】3D角色换装++ Advance
单纯更换装备的方案
实现方式
如果角色只是单纯地更换装备,即角色的整个身体是一个完整的网格,而所需更换的各个部件仅为装备,并非身体的某一部分,例如 NGUI 中 Character 的例子。在穿装备之前,角色只有默认的身体;穿了装备以后,身体保持不变,仅在特定的位置显示装备。
优点
这种实现方式较为简单,只需在特定的骨骼下显示一个不含有蒙皮信息的装备模型。在更换装备时,通知特定功能的代码删掉或隐藏之前的装备,再显示新的装备即可。
缺点
- 无法实现身体部件更换:不过这不一定是缺点,具体要视游戏的设计本身而定。
- 增加渲染负担:穿上装备实际上是多增加了一份模型,这会导致所需渲染的面数增加,DrawCall 也会相应增加。
适用场景
如果游戏中模型的面数以及 DrawCall 控制得很好,并且不存在除角色以外的其他玩家换装的情况,同时游戏本身设计时也不需要进行身体部件更换,那么这种方案值得考虑。
拆分身体部件的方案
实现基础
大部分网络游戏会选择将身体的各个部件拆开,各个部件由身体部分和装备部分共同组成一个完整的模型,因此更换部件实际上就是更换装备。在 Unity 中实现该方案,代码可参考官方 CharacterCustomization 例子。
内存问题
在游戏中,我们不仅关心装备是否更换,更关心更换的效果以及是否会留有隐患。官方的这个例子只是演示了换装的原理,但打开 Profile 的 Memory 一栏,会发现更换装备时内存占用不断增加。这种不合理的内存占用会带来严重后果,尤其是对于换装频率高的网络游戏,特别是移动平台的 3D 网络游戏。
官方换装中的内存问题是由于装备被替换掉以后,没有从内存中清除,不断更换装备会导致内存不断累加。相关问题在 Unity 圣殿中有文章详细解释。
武器换装处理
装备中除武器以外,其他部分都可以用同一种方式进行更换,当然武器也可以,这同样要视游戏本身而定。
- 双武器情况:如果游戏中在安全区需要武器背在背上,而在非安全区拿在手上,同时有角色的“亮出武器”这样的过渡动作配合,那么一个角色身上装备两把武器是比较好的选择,一把在手上,一把在背上,控制其中之一显示即可。在更换武器时,则需要将这两把武器全部更新。
- 单武器情况:如果只是一把武器在不同的状态挂在不同的位置,那么在 Unity 实现中,一个很好的办法是将武器模型放到相应的骨骼下,使其成为该骨骼的子节点。
部件更换与网格处理
身体各部件都需要支持动态更换。按照官方的例子,实际上更换每一个部件就等同于重新合并了一遍网格,只是这次合并是用新装备的模型和其他部件的模型。 如果选择不合并网格,那么每个装备的部件都需要有一个 SkinnedMeshRenderer 组件来与骨骼进行关联,这会导致计算量翻倍。
不合并网格的换装核心代码
合并网格的换装代码可参考官方实例,不合并网格的换装核心代码如下,其功能是从当前角色的骨骼中取到该模型所关联的骨骼,然后建立关联:
public void Generate(GameObject root, int elemId)
{
if (root == null)
return;
// Return if current map doesn't contain this element
if (!elementDict.ContainsKey(elemId) || elementDict[elemId] == null)
return;
// Get element's SkinnedMeshRenderer component
SkinnedMeshRenderer elemSmr = elementDict[elemId].GetSkinnedMeshRenderer();
// To be sub-object
elemSmr.gameObject.transform.parent = root.transform;
// All bones in this root
Transform[] bones = root.GetComponentsInChildren<Transform>();
// All bones needed by element
List<Transform> elemBones = new List<Transform>();
// All bone name in this element
string[] elemBoneNames = elementDict[elemId].GetBoneNames();
// Find matched bones in root
for (int i = 0; i < elemBoneNames.Length; ++i)
{
string strBone = elemBoneNames[i];
for (int j = 0; j < bones.Length; ++j)
{
if (string.Compare(bones[j].name, strBone) == 0)
{
elemBones.Add(bones[j]);
break;
}
}
}
elemSmr.bones = elemBones.ToArray();
elemSmr.updateWhenOffscreen = false;
}
换装的优化问题涉及内存优化、资源优化、渲染优化、代码优化等多个方面,后续文章将逐一进行讨论。