【Unity3D】3D角色换装++ Advance

2015年03月21日 16:06 0 点赞 0 评论 更新于 2025-11-21 18:01

单纯更换装备的方案

实现方式

如果角色只是单纯地更换装备,即角色的整个身体是一个完整的网格,而所需更换的各个部件仅为装备,并非身体的某一部分,例如 NGUI 中 Character 的例子。在穿装备之前,角色只有默认的身体;穿了装备以后,身体保持不变,仅在特定的位置显示装备。

优点

这种实现方式较为简单,只需在特定的骨骼下显示一个不含有蒙皮信息的装备模型。在更换装备时,通知特定功能的代码删掉或隐藏之前的装备,再显示新的装备即可。

缺点

  1. 无法实现身体部件更换:不过这不一定是缺点,具体要视游戏的设计本身而定。
  2. 增加渲染负担:穿上装备实际上是多增加了一份模型,这会导致所需渲染的面数增加,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;
}

换装的优化问题涉及内存优化、资源优化、渲染优化、代码优化等多个方面,后续文章将逐一进行讨论。

作者信息

feifeila

feifeila

共发布了 3994 篇文章