Unity中Avatar换装实现

资源准备

1.每一套装备模型必须使用同一套骨骼,并单独将骨骼数据保存成一个Prefab。红色部分为武器挂节点(也可以把武器做成一个SkinnedMesh,不采用挂接点的形式),骨骼数据在Unity中的展示形式就是Transform。

2.将模型拆分成多个部分,将每一个部分单独保存成Prefab,武器也单独保存为一个Prefab。

每一个Prefab都含有自身的SkinnedMeshRenderer。

实现过程

1.创建骨骼GameObject,所有装备的蒙皮数据会最终合成到这个Prefab中。

2.创建装备GameObject,用于搜集其中蒙皮数据以生成新的SkinnedMeshRenderer到骨骼Prefab中。

3.public void CombineObject(GameObject skeleton, SkinnedMeshRenderer[] meshes,
bool combine = false)传入骨骼的GameObject和蒙皮数据。

4.搜集装备蒙皮数据中的有效信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// Collect information from meshes



        for (int i = 0; i < meshes.Length; i ++)



        {



            SkinnedMeshRenderer smr = meshes[i];



            materials.AddRange(smr.materials); // Collect materials



            // Collect meshes



            for (int sub = 0; sub < smr.sharedMesh.subMeshCount; sub++)



            {



                CombineInstance ci = new CombineInstance();



                ci.mesh = smr.sharedMesh;



                ci.subMeshIndex = sub;



                combineInstances.Add(ci);



            }



            // Collect bones



            for (int j = 0 ; j < smr.bones.Length; j ++)



            {



                int tBase = 0;



                for (tBase = 0; tBase < transforms.Count; tBase ++)



                {



                    if (smr.bones[j].name.Equals(transforms[tBase].name))



                    {



                        bones.Add(transforms[tBase]);



                        break;



                    }



                }



            }

5.为骨骼GameObject生成新的SkinnedMeshRenderer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Create a new SkinnedMeshRenderer
SkinnedMeshRenderer oldSKinned = skeleton.GetComponent<SkinnedMeshRenderer>();
    if (oldSKinned != null) {

    GameObject.DestroyImmediate(oldSKinned);
    }



SkinnedMeshRenderer r = skeleton.AddComponent < SkinnedMeshRenderer();
r.sharedMesh = new Mesh();



r.sharedMesh.CombineMeshes(combineInstances.ToArray(), false, false);//
Combine meshes

r.bones = bones.ToArray();// Use new bones

6.挂接武器。

1
2
3
4
5
6
7
8
9
10
Transform[] transforms = Instance.GetComponentsInChildren<Transform();

foreach (Transform joint in transforms) {

if (joint.name == "weapon_hand_r")
{// find the joint (need the support of art designer)
        WeaponInstance.transform.parent = joint.gameObject.transform;
        break;
    }
    }

其中WeaponInstance为武器实例GameObject,Instance为骨骼实例GameObject。

合成后的效果

如何优化

合成之后的模型拥有4个独立材质,加上独立的对象武器,也就是会产生5个Draw Call;如果将在骨骼中的这4个材质合并成一个,那么就能将Draw
Call减少到2个。

其中实现过程如下:

优化CombineObject方法,其中Combine为bool类型,用于标识是否合并材质。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// merge materials



if (combine)



{



    newMaterial = new Material (Shader.Find ("Mobile/Diffuse"));



    oldUV = new List<Vector2[]();



    // merge the texture



    List<Texture2D Textures = new List<Texture2D();



    for (int i = 0; i < materials.Count; i++)



    {



        Textures.Add(materials[i].GetTexture(COMBINE_DIFFUSE_TEXTURE) as
Texture2D);



    }



    newDiffuseTex = new Texture2D(COMBINE_TEXTURE_MAX, COMBINE_TEXTURE_MAX,
TextureFormat.RGBA32, true);



    Rect[] uvs = newDiffuseTex.PackTextures(Textures.ToArray(), 0);



    newMaterial.mainTexture = newDiffuseTex;



// reset uv



    Vector2[] uva, uvb;



    for (int j = 0; j < combineInstances.Count; j++)



    {



        uva = (Vector2[])(combineInstances[j].mesh.uv);



        uvb = new Vector2[uva.Length];



        for (int k = 0; k < uva.Length; k++)



        {



            uvb[k] = new Vector2((uva[k].x * uvs[j].width) + uvs[j].x,
(uva[k].y * uvs[j].height) + uvs[j].y);



        }



        oldUV.Add(combineInstances[j].mesh.uv);



        combineInstances[j].mesh.uv = uvb;



    }



    }

生成新的SkinnedMeshRenderer时略有区别:

最终效果如下:

可以看出,新的SkinnedMeshRenderer只有一个材质,Draw Call自然也就降低了。

示例工程

GitHub:https://github.com/zouchunyi/UnityAvater

感兴趣的朋友可以下载。工程中代码大家可以直接使用,但是美术资源不得用于任何商业用途。

Android全自动打包生成apk

Unity自动打包Android其实要比IOS容易得多,因为Android不用先生成java工程,然后在构建.apk包,我先说说Android打包的步骤。

1.把sdk拷贝至Plugins/Android下。

如下图所示,如果你做过Android源生开发,我相信下面的东西你不会陌生。可是如果你没做过Android原生开发,我还是详细说明以下。

AndroidManifest:这是Android程序必不可少的文件,这里记录着应用程序的启动Activity。Activity就是Android的一个界面,一般应用程序会有很多Acitivty,来回切换界面。但是游戏就不太一样了,因为游戏只需要一个Activity
一个View就可以了。(扯远了)

AndroidManifest:里面还记录着应用程序的权限,Service啊什么的,,有兴趣的同学可以谷歌搜一搜。

每次打包的时候Unity会用它默认的AndroidManifest,它默认的AndroidManifest在
unity.app/Contents/PlaybackEngines/AndroidPlayer下面。当你在进行打包apk的时候
unity会拷贝该路径下的所有参数。

在ProjectSetting里面勾选一些权限的时候,Unity会自动帮你修改AndroidManifest里面的权限,但是如果某个权限不能在Unity工程里面修改,那么就需要自己手动替换。如下图所示,我们把AndroidManifest放在Plugins/Android下面,
这样Unity在进行打包的时候就不会用它默认的,而是用我自己新写的打包。。这样就方便灵活很多了。。

bin:下面就记录着sdk用到的第三方jar

res:下面就是安卓的一些图片,资源啊什么。

你可以随便解开一个android的APK看看它的目录结构就明白了。。

这样在打包的时候unity就会自动把Plugins/Android下面的所有资源打包在你的APK里面了。但是如果你做渠道包的时候,每个包用的是不同的sdk,所以你需要在打包不同渠道的时候把相关的文件拷贝在Plugins/Android下面。

如下图所示,我在根目录下创建一个文件夹名子叫91,当我自动化打包的时候自动把91文件夹下面的资源先拷贝在Plugins/Android下面,然后在自动打包。。打完包以后再把Plugins/Andoird文件夹清空即可。。

Ok上脚本。。代码和上一篇文章的第一步差不多,我就不多余注释,,执行下面shell脚本将自动打开unity,然后执行ProjectBuild.BuidForAndroid方法。。
project-$1 就是传入的参数。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/sh

#参数判断
if [ $# != 1 ];then


echo "需要一个参数。 参数是游戏包的名子"
exit

fi

#UNITY程序的路径#

UNITY_PATH=/Applications/Unity/Unity.app/Contents/MacOS/Unity

#游戏程序路径#

PROJECT_PATH=/Users/MOMO/commond

#在Unity中构建apk#

$UNITY_PATH -projectPath $PROJECT_PATH -executeMethod
ProjectBuild.BuildForAndroid project-$1 -quit

echo "Apk生成完毕"

在关闭unity的情况下运行。在命令行里面执行这一条脚本, 参数一个参数 91。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using System.Collections;
using System.IO;
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
using System;

class ProjectBuild : Editor
{
//在这里找出你当前工程所有的场景文件,假设你只想把部分的scene文件打包 那么这里可以写你的条件判断 总之返回一个字符串数组。
static string[] GetBuildScenes()
{
List<string> names = new List<string>();
foreach (EditorBuildSettingsScene e in EditorBuildSettings.scenes)
{
if (e == null)
continue;
if (e.enabled)
names.Add(e.path);
}
return names.ToArray();
}

static void BuildForAndroid()
{
Function.DeleteFolder(Application.dataPath + "/Plugins/Android");
if (Function.projectName == "91")
{
Function.CopyDirectory(Application.dataPath + "/91", Application.dataPath + "/Plugins/Android");
PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Android,
"USE_SHARE");
}

string path = Application.dataPath + "/" + Function.projectName + ".apk";
BuildPipeline.BuildPlayer(GetBuildScenes(), path, BuildTarget.Android,
BuildOptions.None);
}
}

程序会执行BuildForAndroid的方法,这里我把shell传入的参数取出来。根据传入的不同参数来初始化打包的一些设置。。

最终BuildPlayr就开始构建apk,第二个参数就是打包出apk保存的路径。 在打包之前你可以处理一些 游戏包名, 游戏icon
等等一些平台之间的特殊性 ,也可以设置一些 预定义标签,。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using UnityEngine;


#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.XCodeEditor;
using System.Xml;
#endif

using System.IO;
public static class XCodePostProcess
{
#if UNITY_EDITOR
    [PostProcessBuild (100)]
    public static void OnPostProcessBuild (BuildTarget target, string
pathToBuiltProject)
    {
if (target == BuildTarget.Android)
{
Function.DeleteFolder(Application.dataPath+"/Plugins/Android");
if(Function.projectName== "91") {
             //当我们在打91包的时候 这里面做一些 操作。
}
}
 }
#endif
}

**** 在回到XUPortr里面,当Android打包完毕后,这里我们清空Plugins/Android文件夹。。或者你也可以做一些操作。。

Function.cs 用到的一个工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
using UnityEngine;
using System.Collections;
using System.IO;
public class Function
{
//得到项目的名称
public static string projectName
{
get
{
//在这里分析shell传入的参数, 还记得上面我们说的哪个 project-$1 这个参数吗?
//这里遍历所有参数,找到 project开头的参数, 然后把-符号 后面的字符串返回,
//这个字符串就是 91 了。。
foreach (string arg in System.Environment.GetCommandLineArgs())
{
if (arg.StartsWith("project"))
{
return arg.Split("-"[0])[1];
}
}
return "test";
}
}


public static void DeleteFolder(string dir)
{
foreach (string d in Directory.GetFileSystemEntries(dir))
{
if (File.Exists(d))
{
FileInfo fi = new FileInfo(d);
if (fi.Attributes.ToString().IndexOf("ReadOnly") != -1)
fi.Attributes = FileAttributes.Normal;
File.Delete(d);
}
else
{
DirectoryInfo d1 = new DirectoryInfo(d);
if (d1.GetFiles().Length != 0)
{
DeleteFolder(d1.FullName);////递归删除子文件夹
}
Directory.Delete(d);
}
}
}


public static void CopyDirectory(string sourcePath, string destinationPath)
{
DirectoryInfo info = new DirectoryInfo(sourcePath);
Directory.CreateDirectory(destinationPath);
foreach (FileSystemInfo fsi in info.GetFileSystemInfos())
{
string destName = Path.Combine(destinationPath, fsi.Name);
if (fsi is System.IO.FileInfo)
File.Copy(fsi.FullName, destName);
else
{
Directory.CreateDirectory(destName);
CopyDirectory(fsi.FullName, destName);
}
}
}
}

如下图所示,脚本运行完毕,你打包的APK就静静的放在了这里,怎么样?简单吧?嘿嘿。。

注意事项:

1、因为你的工程可能比较大,如果IOS和Android同时打包的话切个平台都要半个多小时,我建议的svn在本地check out 两个工程,一个切在ios
一个切在Android 打包的时候分开打。

2.执行shell脚本的时候请关闭保存unity工程。

3.Android 因为不受证书的限制,我上传的工程建议你下载下来看看,肯定可以直接打出来包。。

4.我建议打包的机器使用mac。 因为windows不能打包IOS 而MAC可以同时 打包 IOS 和 Android

工程下载地址:http://pan.baidu.com/s/1o6OATcu

在Unity中实现小地图(Minimap)

小地图的基本概念

众所周知,小地图(或雷达)是用于显示周围环境信息的。首先,小地图是以主角为中心的。其次,小地图上应该用图标来代替真实的人物模型,因为小地图通常很小,玩家可能无法看清真实的模型。

大多数小地图都是圆形的,所以本文也将创建圆形小地图。通常小地图周围还会有一些按钮和标签,本文也会介绍。

创建场景

新建场景,导入Unity Chan模型作为玩家,导入两个机器人作为敌人。

游戏视图

添加一个新的相机。依次点击菜单项GameObject -> Camera新建相机并命名为Minimap Camera。然后将该相机设为Unity
Chan的子对象,并将其坐标设为Unity Chan上方10个单位,把相机对准Unity Chan模型。

设置Minimap Camera

为了达到更好的效果,将position设为(0, 10, 0),rotation设为(90, 0, 0)。现在相机显示效果如下:

但这还不是小地图。现在运行场景,你可以看到只有上图中的内容显示出来。我们必须把小地图做成一个UI元素。

渲染到UI层

这里需要用到Render Texture来实现。依次点击菜单项Assets -> Create -> Render Texture新建Render
Texture并命名为Minimap Render Texture。

选中Minimap Camera后在检视面板将Target Texture字段设为Minimap Render Texture。

现在运行场景会发现Minimap Camera中的内容不见了,这是因为它被显示到了新建的Render Texture中。

下面新建Canvas来添加UI元素。依次点击菜单项GameObject -> UI -> Canvas来新建Canvas。

这里需要使用Raw Image在Canvas中显示Render Texture的内容。依次点击菜单项GameObject -> UI -> Raw
Image新建Raw Image,然后命名为Minimap Image,在检视面板中将Texture字段设为Minimap Render Texture。

现在Minimap Camera相机中的内容可以作为UI来显示了!

下面将小地图变为圆形。这里需要用到一张简单的遮罩纹理:

新建Image并为其添加Mask组件,将Image的Source Image字段设为上面的遮罩图片,并将Minimap Image设为Mask的子对象。

注意:为了达到更好的视觉效果,记得禁用遮罩纹理的Mipmap。

现在小地图显示效果如下:

小地图的白色背景看起来不太美观,给它加一个边框:

为了让整个小地图移动起来更方便,新建一个空的GameObject命名为Minimap,并将所有对象设为Minimap子对象。

最后将小地图移至屏幕右上角。

效果不错吧?但这还不是真正意义上的小地图,只是相机从顶部取景的图像而已。接下来通过Layer来做一些设置!

设置Layer

这里需要新建一个Layer。依次点击菜单项Edit -> Project Settings -> Tags and
Layers新建Layer命名为Minimap。

然后新建三个球体。一个设为蓝色代表Unity Chan。将该球体设为Unity Chan的子对象,并将其Layer设为Minimap。

对两个机器人进行同样的操作,将球体改为红色。

现在最关键的一步来了!选中Main Camera并确保其Culling Mask中不包括Minimap这一层。

然后选中Minimap Camera让其Culling Mask只包括Minimap这一层。

现在这个小地图看起来就比较完善了!

最后步骤

还可以做一些调整。首先将Minimap Camera的Clear Flags设为Solid
Color并将其颜色改为浅灰色,以便让小地图背景与小球的对比度更强。

还可以添加一些UI元素来操作小地图。这里使用标签作为示例,最后结果如下:

当角色或机器人移动时,小地图上的小球也会即时更新位置。

到此整个制作小地图的教程就结束了,如有任何问题,欢迎来下方评论区留言!

本文来源于:http://blog.theknightsofunity.com 作者:Piotr Korzuszek

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×