更改模型部位

搭建角色结构

首先对Model目录下的游戏对象进行整理,如下,并取消所有模型Mesh。

alt

新建Player_View用于对玩家模型的表现进行操作,并让CharacterCreator持有,它们相当于MVC模式中的View层和Controller层,当然CharacterCreator只是自定义角色场景的Controller,切换场景可能会被其他控制器接管。

public class Player_View : MonoBehaviour
{
    [SerializeField] SkinnedMeshRenderer[] partSkinnedMeshRenderers; //部位渲染器
    [SerializeField] Material[] partMaterials; //部位材质
}

将部位的皮肤渲染器给Player_View用于修改,通用材质用于修改颜色时做副本实例化。

alt

	//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模式不存在。

alt

头发修改

            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也没问题。

但由于每次都新实例化了一个材质,当部位比较多时,会有比较大的性能消耗,实际上我们只需要给每个部位保留一个副本,每次修改颜色就可,可继续优化。

alt

衣服修改

            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。 alt

优化材质球的实例化

    /// <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一份,可能会造成内存泄露,在这里暂不考虑。