更改模型部位
搭建角色结构
首先对Model目录下的游戏对象进行整理,如下,并取消所有模型Mesh。
新建Player_View用于对玩家模型的表现进行操作,并让CharacterCreator持有,它们相当于MVC模式中的View层和Controller层,当然CharacterCreator只是自定义角色场景的Controller,切换场景可能会被其他控制器接管。
public class Player_View : MonoBehaviour
{
[SerializeField] SkinnedMeshRenderer[] partSkinnedMeshRenderers; //部位渲染器
[SerializeField] Material[] partMaterials; //部位材质
}
将部位的皮肤渲染器给Player_View用于修改,通用材质用于修改颜色时做副本实例化。
//Player_View类
/// <summary>
/// 设置部位
/// </summary>
public void SetPart(CharacterPartConfigBase characterPartConfig)
{
switch (characterPartConfig.CharacterParType)
{
case CharacterParType.Face:
break;
case CharacterParType.Hair:
break;
case CharacterParType.Cloth:
break;
}
}
public void SetColor(CharacterParType parType,Color color,int colorIndex)
{
}
/// <summary>
/// 设置脸部尺寸
/// </summary>
public void SetFaceSize()
{
}
/// <summary>
/// 设置脸部高度
/// </summary>
public void SetFaceHeight()
{
}
在Player_View中有对模型进行设置的代码。
//Character_Creator类
/// <summary>
/// 设置部位
/// </summary>
public void SetPart(CharacterPartConfigBase characterPartConfig)
{
player_View.SetPart(characterPartConfig);
//TODO:保存玩家此次的操作(持久化)
}
在Character_Creator类中对应提供方法。
//UI_CreateCharacterWindow类
/// <summary>
/// 设置具体的部位
/// </summary>
public void SetCharacterPart(CharacterParType parType,int configIndex,bool UpdateUIView = false,bool updateCharacterView = false)
{
//获取配置
CharacterPartConfigBase partConfig = ConfigTool.GetCharacterPartConfig(parType, configIndex);
//更新UI
if (UpdateUIView)
{
}
//更新模型
if(updateCharacterView)
{
CharacterCreator.Instance.SetPart(partConfig);
}
}
最终由UI窗口进行调用,这里注意模型的MVC和UI是分开的,通过对模型修改经相机Target Texure投射到UI上,二者相对独立,逻辑是UI窗口的SetCharacterPart方法(传入部位类型和索引)调用模型Controller(CharacterCreator)的SetPart方法(生成characterPartConfig)再调用模型View(Player_View)的SetPart方法完成具体的部位更新。
模型应用部位
//PlayerView类
/// <summary>
/// 设置部位
/// </summary>
public void SetPart(CharacterPartConfigBase characterPartConfig)
{
switch (characterPartConfig.CharacterParType)
{
case CharacterParType.Face:
//FaceConfig faceConfig = characterPartConfig as FaceConfig;
partSkinnedMeshRenderers[0].sharedMesh = characterPartConfig.Mesh1;
break;
case CharacterParType.Hair:
break;
case CharacterParType.Cloth:
break;
}
}
完善Player_View中的实际更改Mesh逻辑,根据传进来的characterPartConfig文件身上的部位类型对相应部位的Mesh进行更新。
脸部修改
注意FaceCofig与配置基类相比没有额外成员,不需要强转,Hair和Cloth需要强转才能使用自己的成员。
/// <summary>
/// 设置具体的部位
/// </summary>
[Sirenix.OdinInspector.Button("测试设置角色部位")]
public void SetCharacterPart(CharacterParType parType,int configIndex,bool UpdateUIView = false,bool updateCharacterView = false)
{
CharacterPartConfigBase partConfig = ConfigTool.GetCharacterPartConfig(parType, configIndex);
//更新UI
if (UpdateUIView)
{
}
//更新模型
if(updateCharacterView)
{
CharacterCreator.Instance.SetPart(partConfig);
}
}
在UI窗口类中使用Odin插件运行方法进行测试,注意需要在运行状态下,CharacterCreator单例跟随场景加载,Editor模式不存在。
头发修改
case CharacterParType.Hair:
HairConfig hairConfig = characterPartConfig as HairConfig;
partSkinnedMeshRenderers[1].sharedMesh = hairConfig.Mesh1;
//判断是否需要考虑颜色
if (hairConfig.ColorIndex != -1)
{
//实例化一个新的材质
partSkinnedMeshRenderers[1].material = Instantiate(partMaterials[0]);//对应PBRMaskTint_HeadParts 1
}
else
{
//使用共享材质(默认)
partSkinnedMeshRenderers[1].sharedMaterial = Instantiate(partMaterials[0]);
}
break;
普通的头发配置有可能会在基础材质上改色,因此首先判断是否需要改色,由于使用的是不同的材质,使用material。不需要改色时使用相同的默认材质即可,使用sharedMaterial。
简单总结一下SharedMesh/SharedMaterial和Mesh/Material的区别。
- SharedXXX是共享的,内存中一份,修改任意一个模型上的SharedXXX和他使用同样XXX的物体都会修改(引用类型)。
- XXX是独有的,每次修改(直接替换也算一种修改)Unity会clone一份新的放到内存中,修改一个模型上的XXX不会影响其他模型。
- 一般修改材质多一些,修改Mesh会涉及到模型形状的大改,一般不变用SharedMesh就可以,本节中就是这样。
- 正常来说应该只有SharedXXX,毕竟对引用类型修改,同步是合理的,Material只是Unity提供的功能,可以不每次单独实例化材质,但会产生内存消耗并可能泄露,所以一般每次用材质单独实例化一份是最好的方法,尽量别用Material。
在本节中还没有涉及到颜色的修改,因此使用SharedMaterial/Material仅考虑内存资源消耗的问题,且由于每次都手动实例化了一个新材质,不用担心颜色修改互串的问题,整体流程如:基础材质-initiate出来的材质-Material系统clone出来的材质/SharedMaterial共享材质,在第二个环节已经做了区分,所以实际上都用SharedMaterial也没问题。
但由于每次都新实例化了一个材质,当部位比较多时,会有比较大的性能消耗,实际上我们只需要给每个部位保留一个副本,每次修改颜色就可,可继续优化。
衣服修改
case CharacterParType.Cloth:
ClothConfig clothConfig = characterPartConfig as ClothConfig;
partSkinnedMeshRenderers[2].sharedMesh = clothConfig.Mesh1;
if (clothConfig.ColorIndex != -1 && clothConfig.ColorIndex2 != -1)
{
//实例化一个新的材质
partSkinnedMeshRenderers[2].material = Instantiate(partMaterials[2]);//对应PBRMaskTint_BodyParts 1
}
else
{
//使用共享材质(默认)
partSkinnedMeshRenderers[2].sharedMaterial = Instantiate(partMaterials[2]);
}
break;
同上,衣服需要额外考虑主色Index和辅色Index都为-1才不修改,任意一个颜色变了都是一个新的Material。
优化材质球的实例化
/// <summary>
/// 初始化
/// </summary>
public void Init()
{
//让每一个部位的材质都实例化一份自己的材质球,互不干扰
partSkinnedMeshRenderers[0].material = Instantiate(partMaterials[0]);//对应PBRMaskTint_HeadParts 1
partSkinnedMeshRenderers[1].material = Instantiate(partMaterials[0]);//对应PBRMaskTint_HeadParts 1
partSkinnedMeshRenderers[2].material = Instantiate(partMaterials[2]);//对应PBRMaskTint_BodyParts 1
}
/// <summary>
/// 设置部位
/// </summary>
public void SetPart(CharacterPartConfigBase characterPartConfig)
{
switch (characterPartConfig.CharacterParType)
{
case CharacterParType.Face:
FaceConfig faceConfig = characterPartConfig as FaceConfig;
partSkinnedMeshRenderers[0].sharedMesh = characterPartConfig.Mesh1;
break;
case CharacterParType.Hair:
HairConfig hairConfig = characterPartConfig as HairConfig;
partSkinnedMeshRenderers[1].sharedMesh = hairConfig.Mesh1;
SetColor1(CharacterParType.Hair, customCharacter.CustomPartDataDic[(int)CharacterParType.Hair].Color1);
break;
case CharacterParType.Cloth:
ClothConfig clothConfig = characterPartConfig as ClothConfig;
partSkinnedMeshRenderers[2].sharedMesh = clothConfig.Mesh1;
SetColor1(CharacterParType.Cloth, customCharacter.CustomPartDataDic[(int)CharacterParType.Hair].Color1);
SetColor2(CharacterParType.Cloth, customCharacter.CustomPartDataDic[(int)CharacterParType.Hair].Color2);
break;
}
}
在上一小节中,每次切换部位时,都要判断颜色索引并进行一次材质球实例化,这实际是不需要的,每个部位用一个材质球就可以,也不用对是否修改颜色作区分,不改就用默认的,改色交给SetColor做。原来用sharedmaterial的本意是减少不改色时系统自动重复生成material实例的内存消耗,并不影响每个部位材质球之间的独立性(每个部位都有自己的一份实例化出来的材质),实际上即使是同一个部位共用一个材质球时,也没有必要对sharedMaterial和Material做过多的区分,因为此时只有这一个部位对应着一个材质球,对这个材质球修改并不会引起其他模型的改变(压根就没人用这个材质球),唯一可能存在性能消耗的是Material修改时系统会自动Clone一份,可能会造成内存泄露,在这里暂不考虑。