Unity3D游戏开发之换装方法

游戏内的角色,能够像纸娃娃换装那样子让玩家可以为自己的角色改变外观,一直是相当受欢迎的功能;一般而言,我们建好的 3D 模型,如果要将其中一个部位换成另外一个形状,最直接的就是将该物件部位的 Mesh 替换掉,那么外观就改变了,但这种方法如果运用在需要做动作的模型上,将发现被置换掉的部位不会正常动作,更糟的状况可能连模型显示的位置及方向都是错误的,所以,直接变更 Mesh 的方法只适用于静态模型物件,为此,我们必须找出更深入的方法来做换装的功能,幸好,此部份 Unity 已经有提供相关范例可以参考。

Unity 提供的人物换装范例可以从官网下载 Character Customization,或是开启 Unity 编辑器的 Window > Asset Store 在 Complete Projects > Tutorials 找到 Character Customization 下载并汇入到自己的专案中。这个范例提供相当完整的示范,而且考虑到实作于游戏中时,不可能一次把全部的资源都载入,所以将模型、材质、纹理等资源都包成 Asset bundle,只在要使用到时才载入需要的部份;也因为如此,对於不了解
Asset bundle 的情况下,要透过这个范例来直接学习换装也相对变得困难;另外,范例中也对资源规范了特定形式的命名规则,主要也是为了建立 Asset bundle 内容资料及从 Asset bundle 取出资源而设计,在不了解这些规则之前,想要透过此范例学习换装,有一定程度的难度;当然,如果愿意使用与范例中 characters 目录中两个人物的模型、材质、纹理完全相同的命名规则及档案配置方式,几乎可以直接套用到自己的游戏中,而不太需要了解内部的运作方式。

在 Unity 开启 Asset store,在Asset store 中找出 Character Customization 范例。

虽然,官方这个范例能够直接套用,但制作游戏常会有不同的客制化需求,如果不了解相关原理和流程的话,可能就无法自由灵活的运用,所以,以下将利用这个范例并排除掉 Asset bundle 的部份,直接在场景中完成纸娃娃换装的方法。

首先,先来看看模型的结构,从 Projects 视窗将 CharacterCustomization > characters > Female > Female 拉到场景中,在 Hierarchy 视窗将物件展开来,会发现几个名称相同并使用数字区别的物件,它们分别代表人物各部位的模型,由此可知,整个人物模型档包含多个相同部位的模型,而 Famale_Hips 则是整个人物的骨架结构,人物的动作则是设置在顶层物件(Female)的 Animation,所以这个模型档是个模型资源,而不是实际上要放到在场景中的目标物件。

每个部位有多个模型物件

了解模型档内容後,接下来先建立一个名为 TestChar 的 C# 程式档用来控制换装,为了方便测试,在 Projects 视窗将 CharacterCustomization > characters > Female > [email protected] 的 Animation Wrap Mode 改为 Loop,并在程式档的 Start() 内加入 animation.Play(“walk”),如此在执行状态将会使人物不断的做走路的动作。文章出处【狗刨学习网】

选择 [email protected]

Animation Wrap Mode 选择 Loop

Unity 官方这个范例,说穿了就是将模型档做为来源模型资源,然後再依照需求将各部位重新组合成一个新的目标模型,所以我们直接将人物模型 Female 拉两个到场景中,分别为它们命名为 Source 及 Target,依照以下步骤做些准备动作:

从 Projects 视窗将 CharacterCustomization > characters > Female > Per Texture Materials 依照名称把适当的材质球(Material) 拉给 Source 的每个部位(不包含 Female_Hips 及其子物件)。

Source 物件是做为来源资源使用,实际在场景中不需要运作,所以直接点选 Source 物件并将 Inspector 视窗中 Source 名称栏位前的方框取消勾选来将它关闭。

Source 前面的方框取消勾选,取消勾选后会弹出对话视窗询问是否希望关闭全部的子物件,点击 Deactivate Chidren。把 Target 物件中除了 Famale_Hips 以外的子物件全部删除。把 TestChar 程式档拉给 Target 物件。Source 中的各部位名称应该都要有编号(例如 face-1 ),如果没有的话,加上编号。

完成以上的准备动作,接下来就要开始来写程式了,程式主要工作是先将 Source 中每个物件的 SkinnedMeshRenderer 资料取出储存在 data 变数中,data 的内容则是依照部位分类

接下来在 Target 加SkinnedMeshRenderer  ,然后每个部位取出一个指定的资料,利用
CombineInstance class 及 Mesh.CombineMeshes() 将各部位模型合并,同时也重新排列材质,然後依照取出的 SkinnedMeshRenderer 的 bone 的名称,找到与 Target 的 Female_Hips 子物件内名称相对应的物件重建骨架列表,最后将这些重新组合建立的资料给 Target 的 SkinnedMeshRenderer,如此就可完成换装的动作,以下为程式说明:

  1. //来源模型资源的物件
  2. public Transform source;
  3. //目标物件
  4. public Transform target;
  5. //模型资源资料
  6. private Dictionary<string , Dictionary<string,SkinnedMeshRenderer>> data = new Dictionary<string, Dictionary<string,SkinnedMeshRenderer>>();
  7. void Start () {
  8. //从来源模型资源取出各部位的 SkinnedMeshRenderer
  9. SkinnedMeshRenderer[] parts = source.GetComponentsInChildren<SkinnedMeshRenderer>(true);
  10. foreach(SkinnedMeshRenderer part in parts){
  11. //利用 ? 字元分隔档名做为资料结构的 key,档名为 部位?编号 储存为 [部位][编号]=SkinnedMeshRenderer资料
  12. string[] partName = part.name.Split(‘?‘);
  13. // 在 data 加入资料
  14. if(!data.ContainsKey(partName[0])) data.Add(partName[0] , new Dictionary<string,SkinnedMeshRenderer>());
  15. data[partName[0]].Add(partName[1],part);
  16. }
  17. //目标物件加入 SkinnedMeshRenderer
  18. SkinnedMeshRenderer targetSmr = target.gameObject.AddComponent<SkinnedMeshRenderer>();
  19. //从目标物件取得骨架资料 (Female_Hips 的全部物件)
  20. Transform[] hips = target.GetComponentsInChildren<Transform>();
  21. /** 开始 重组模型 */
  22. //初始化资料列表
  23. List<CombineInstance> combineInstances = new List<CombineInstance>();
  24. List<Material> materials = new List<Material>();
  25. List<Transform> bones = new List<Transform>();
  26. foreach(KeyValuePair<string , Dictionary<string,SkinnedMeshRenderer>> _part in data){
  27. //从资料中取得各部位指定编号的 SkinnedMeshRenderer
  28. SkinnedMeshRenderer smr = new SkinnedMeshRenderer();
  29. switch(_part.Key){
  30. case “eyes”:
  31. smr = _part.Value[“1”];
  32. break;
  33. case “face”:
  34. smr = _part.Value[“1”];
  35. break;
  36. case “hair”:
  37. smr = _part.Value[“1”];
  38. break;
  39. case “pants”:
  40. smr = _part.Value[“1”];
  41. break;
  42. case “shoes”:
  43. smr = _part.Value[“1”];
  44. break;
  45. case “top”:
  46. smr = _part.Value[“1”];
  47. break;
  48. }
  49. //准备要组合的 Mesh
  50. CombineInstance ci = new CombineInstance();
  51. ci.mesh = smr.sharedMesh;
  52. combineInstances.Add(ci);
  53. //排列新的材质列表
  54. materials.AddRange(smr.materials);
  55. //取得相对应名称的骨架物件来建立新的骨架列表
  56. foreach(Transform bone in smr.bones){
  57. foreach(Transform hip in hips){
  58. if(hip.name != bone.name) continue;
  59. bones.Add(hip);
  60. break;
  61. }
  62. }
  63. }
  64. //合并 Mesh 并写入至 Target 的 SkinnedMeshRenderer
  65. targetSmr.sharedMesh = new Mesh();
  66. targetSmr.sharedMesh.CombineMeshes(combineInstances.ToArray() , false , false);
  67. // Target 的 SkinnedMeshRenderer 写入新骨架列表
  68. targetSmr.bones = bones.ToArray();
  69. // Target 的 SkinnedMeshRenderer 写入新材质列表
  70. targetSmr.materials = materials.ToArray();
  71. /** 重组模型 结束 */
  72. //指定播放走路动作
  73. animation.Play(“walk”);
  74. }

复制代码

写完程式后,记得把场景中的 Source 及 Target 两个物件分别拉给附属在 Target 物件上的 TestChar script 的 source 及 target 栏位;

程式动作都在 Start() 内进行,是因为最初目标物件并没有模型等资料,所以要先依照指定的各部位资料把人物建立出来并使它动作,而 smr = _part.Value[“1”]; 的 “1” 则是表示指定此部位的 “1” 模型资料,所以只要改变各部位的这个值,就能为人物配置不同的造型,当然,前题是来源模型资源必须要有这个编号的物件才行;以上程式码主要是测试及解说流程用,在实作上应该把标示 /** 重组模型 */ 这一段程式独立出来,在需要换装时,给予各部位指定编号来执行。

以上是 Unity 官方范例中处理换装的方法,它把各部位模型、材质等资料重新组合合并成单一的模型并重建骨架列表,如此即使看起来人物身上有其中一个部位被置换了,仍能持续正常动作;当查看 Target 物件时会发现它的子物件仍然维持不变,只有 Target 物件本身在 Inspector 视窗中的 Component 多出了 Skinned Mesh Renderer 及各部位的 Material,如果查看 SkinnedMeshRenderer 的 Mesh 栏位 也会发现看不到任何的
Mesh。

Target 物件的内容

这种做法的来源模型与材质数量必须相对应,否则模型的贴图将会变得不正常,也就是说如果裤子的 material 有两个,其他部位的 materail 只有一个,那麽结果模型上的贴图将与预期的不同;为了使各部位的 material 使用上更为弹性,前面的程式将做些修改,使它的各部位都是独立的 GameObject,如下所示:

  1. //来源模型资源的物件
  2. public Transform source;
  3. //目标物件
  4. public Transform target;
  5. //模型资源资料
  6. private Dictionary<string , Dictionary<string,Transform>> data = new Dictionary<string, Dictionary<string,Transform>>();
  7. //目标物件的骨架
  8. private Transform[] hips;
  9. //目标物件各部位的 SkinnedMeshRenderer 资料(参照)
  10. private Dictionary<string , SkinnedMeshRenderer> targetSmr = new Dictionary<string, SkinnedMeshRenderer>();
  11. void Start () {
  12. //从来源模型资源取出各部位的 SkinnedMeshRenderer
  13. SkinnedMeshRenderer[] parts = source.GetComponentsInChildren<SkinnedMeshRenderer>(true);
  14. foreach(SkinnedMeshRenderer part in parts){
  15. //利用 ? 字元分隔档名做为资料结构的 key,档名为 部位?编号 储存为 [部位][编号]=Transform资料
  16. string[] partName = part.name.Split(‘?‘);
  17. // 在 data 加入资料
  18. if(!data.ContainsKey(partName[0])){
  19. data.Add(partName[0] , new Dictionary<string,Transform>());
  20. //建立新的 GameObject 并使用部位名称来命名,指定为目标物件的子物件
  21. GameObject partObj = new GameObject();
  22. partObj.name = partName[0];
  23. partObj.transform.parent = target;
  24. //为新建立的 GameObject 加入 SkinnedMeshRenderer,并将此 SkinnedMeshRenderer 存入 targetSmr
  25. targetSmr.Add(partName[0] , partObj.AddComponent<SkinnedMeshRenderer>());
  26. }
  27. data[partName[0]].Add(partName[1],part.transform);
  28. }
  29. //从目标物件取得骨架资料 (Female_Hips 的全部物件)
  30. hips = target.GetComponentsInChildren<Transform>();
  31. /** 开始 重组模型 */
  32. foreach(KeyValuePair<string , Dictionary<string,Transform>> _part in data){
  33. switch(_part.Key){
  34. case “eyes”:
  35. ChangePart(“eyes” , “1”);
  36. break;
  37. case “face”:
  38. ChangePart(“face” , “1”);
  39. break;
  40. case “hair”:
  41. ChangePart(“hair” , “1”);
  42. break;
  43. case “pants”:
  44. ChangePart(“pants” , “1”);
  45. break;
  46. case “shoes”:
  47. ChangePart(“shoes” , “1”);
  48. break;
  49. case “top”:
  50. ChangePart(“top” , “1”);
  51. break;
  52. }
  53. }
  54. /** 重组模型 结束 */
  55. //指定播放走路动作
  56. target.animation.Play(“walk”);
  57. }
  58. private void ChangePart(string part , string item){
  59. //从资料中取得各部位指定编号的 SkinnedMeshRenderer
  60. SkinnedMeshRenderer smr = data[part][item].GetComponent<SkinnedMeshRenderer>();
  61. //取得相对应名称的骨架物件来建立新的骨架列表
  62. List<Transform> bones = new List<Transform>();
  63. foreach(Transform bone in smr.bones){
  64. foreach(Transform hip in hips){
  65. if(hip.name != bone.name) continue;
  66. bones.Add(hip);
  67. break;
  68. }
  69. }
  70. // 更新指定部位 GameObject 的 SkinnedMeshRenderer 内容
  71. targetSmr[part].sharedMesh = smr.sharedMesh;
  72. targetSmr[part].bones = bones.ToArray();
  73. targetSmr[part].materials = smr.materials;
  74. }

复制代码

在建立 data 变数内容时,同时为每个部位建立 GameObject,另外也把变更部位内容的程式码独立出来为 ChangePart 方法,如此在每次需要变更该部位时,只要指定部位名及编号就可以直接为该部位换装,而不需要将每个部位都重建;因为每个部位都是 GameObject 实体,我们在 Hierarchy 或 Scene 视窗中点选该部位也可以清楚的从 Inspector 视窗中看到此部位内容,正因如此,每个部位就可以自由配置 Material 的数量了。

从以上程式中会发现换装除了把 Mesh 和 Material 从来源取出给目标置换之外,有个关键的地方是重建骨架列表,为什麽要重建骨架列表呢?最主要是变更 Mesh 之後的

SkinnedMeshRenderer.bones 及SkinnedMeshRenderer.sharedMesh.bindposes 数量有可能会不同而产生错误讯息 Number of bind poses doesn‘t match number of bones in skinned mesh

即使数量相同而没有错误讯息

SkinnedMeshRenderer.sharedMesh.bindposes 内的 Matrix4x4[] 资料也会因为数值不正确而发生执行期模型扭曲成奇怪形状的问题;这部份可以将 Female 模型档汇入到 3DS Max 中查看,以鞋子为例,在 Modify 视窗中,可以很明显看出 shoes-1 和 shoes-2 的 Bones 列表内容是不同的,所以在为模型物件变更 Mesh 的同时必须重建骨架列表。

以上的说明主要是用于了解换装所需要的做法,实作时,不太可能把游戏中的角色全身各部位的模型资料全部都载入做为来源资料,例如游戏中的武器有100种,角色背包中有3种武器,但为了换装却把100种武器都载入到游戏中,而实际上此角色最多也只能变换背包中的3种武器而已,这样无疑是浪费了97种武器所占用的资源;所以在了解如何换装後,实作时应该尽量像官方范例那样把来源资源包装起来,只取出需要的资源来进行换装。

时间: 2024-10-12 08:48:18

Unity3D游戏开发之换装方法的相关文章

unity3D游戏开发实战原创视频讲座系列12之U3D的2D开发新方法实战

 U3D的2D开发新方法实战 (Unity3d-4.x的打飞机2D游戏开发新的方法应用 ) 大纲介绍:不使用NGUI和TK2d插件,   使用 U3D内置强大的最大的工具. 开发过程设计到如下内容: 从图片集中截取出 精灵.(这些与以前的方法是不同的) 连续图片的动画制作. 对动画的编辑 Mecanim动画系统的使用 2D物理系统和碰撞检测. 游戏的简单完善(敌机的产生.背景滚动.声音.分数的实现). 会用8讲的时间来介绍. 视持续更新中.... 视频存放地址如下:http://ipd.pp

Unity3D游戏开发之网络游戏服务器架构设计培训

下面我们开始今天的Unity3D游戏开发技能培训. 我们专业培养"游戏主程",挑战20W年薪,初期学习Unity3D培训目标:让U3D初学者可以更快速的掌握U3D技术,自行制作修改素材,可以独立完成2D.3D小规模游戏及网页游戏开发. 今天给大家讲一下如何做一个好的主程 入手 假如,我现在接手一个新项目,我的身份还是主程序.在下属人员一一到位之前,在和制作人以及主策划充分沟通后,我需要先独自思考以下问题: 1.服务器跑在什么样的操作系统环境下?2.采用哪几种语言开发?主要是什么?3.服

Unity3D游戏开发初探—2.初步了解3D模型基础

一.什么是3D模型? 1.1 3D模型概述 简而言之,3D模型就是三维的.立体的模型,D是英文Dimensions的缩写. 3D模型也可以说是用3Ds MAX建造的立体模型,包括各种建筑.人物.植被.机械等等,比如一个大楼的3D模型图.3D模型也包括玩具和电脑模型领域. 互联网的形态一直以来都是2D模式的,但是随着3D技术的不断进步,在未来的时间里,将会有越来越多的互联网应用以3D的方式呈现给用户,包括网络视讯.电子阅读.网络游戏.虚拟社区.电子商务.远程教育等等.甚至对于旅游业,3D互联网也能

[Unity3D]Unity3D游戏开发之异步记载场景并实现进度条读取效果

大家好,我是秦元培.欢迎大家关注我的博客,我的博客地址是:blog.csdn.net/qinyuanpei.终于在各种无语的论文作业中解脱了,所以立即抓紧时间来这里更新博客.博主本来计划在Unity3D游戏开发之从<魂斗罗>游戏说起(上)--目标追踪这篇文章后再写一篇<Unity3D游戏开发之从<魂斗罗>游戏说起(下)>,只是眼下博主的项目进度有些缓慢,所以想等项目稳定下来以后再和大家分享. 作为大家等待博主更新博客的回报,我们今天来说一说Unity3D中的游戏场景异步

Unity3D游戏开发从零单排(五) - 导入CS模型到Unity3D

游戏动画基础 Animation组件 Animation组件是对于老的动画系统来说的. 老的动画形同对应的动画就是clip,每个运动都是一段单独的动画,使用Play()或CrossFade(),直接播放动画 或淡入淡出播放动画. animation.Play("name"); animation.CrossFade("name"); 下面的是它的几个属性 Animation:默认的动画片段: Aniamtions:包含的动画片段: Play Automaticall

【Unity3D游戏开发】—— 导入网上下载的.unitypackage包

这两天遇到了这样一个问题,网上下载的别人导出的.unitypackage包.如果双击打开,则会出现"Failed importing package "的错误. 为此很头疼,查了一些资料,需要将.unitypackage包复制到Editor\Standard Packages文件夹下, 然后ctrl+9打开 Asset Store, 点击下载然后找到刚刚复制的包名,点击Import导入即可. 还有一种方法类似,将.unitypackage包复制到Editor\Standard Pack

Unity3D游戏开发之如何截屏的技能培训

下面我们开始今天的Unity3D技能培训. 我们学习Unity3D培训目标:让U3D初学者可以更快速的掌握U3D技术,自行制作修改素材,可以独立完成2D.3D小规模游戏及网页游戏开发. 今天我们来做点简单的东西,做个什么呢?答案就是截屏.作为一名热爱单机游戏的玩家,每次在玩游戏的同时截取游戏中比较喜欢的画面,特别是学习了Unity3D以后玩游戏的时候更多地是从一个游戏设计者的角度来看待游戏,换句话说,可能关注技术的成分更多一点吧.比如在写<Unity3D游戏开发之自由视角下的角色控制>和<

第一篇:Unity3d游戏开发之移动端完整包更新方案

由于Unity没法进行热更新,也就是局部更新,所以如果你在设计之前没有考虑好使用Lua,反射,或者其他方式实现热更新,但是又要面临游戏上线那么你可以考虑使用完整包更新,其实这对于游戏包不是很大的情况下,也是一个不错的选择,当然设计者在设计之初都想好了热更新那就宁当别论了. Unity3d 在移动端考虑Android和Ios平台的完整包更新 实现逻辑: 首先检查本地的版本和服务器的版本是否相同,如果相同则直接进入游戏,如果不同则下载服务器的最新文件,然后根据最新文件到资源服务器下载最新的安装包 我

Unity3D游戏开发 地形系统(四)

http://mahaile.blog.51cto.com/2891586/771167 上一章我们讲解了 如何创建一个简单的游戏世界 这一章我们来 看看unity3d中的地形系统 废话少说 赶紧开始吧,,哈哈   首先创建一个新的场景    点击菜单栏中的 Terrain-> create Terrain 菜单       完成后如 下图       创建地形系统的时候 unity会按默认的 宽高 图像分辨率,纹理分辨率 等创建一个地形  不过不怕 这些我们都可以修改滴    如果需要在gam