Unity Android Plugin

透過網路上的資源,初次嘗試撰寫自己所需要的 Unity Android Plugin,結果卻發現相關資源中所說明的開發過程不盡相同,經過一段時間的碰壁跟嘗試,歸納出了幾個再開始動手開發 Plugin 前,應該要先弄懂的幾件事,再依照需求選擇相對適合的開發邏輯進行撰寫。

本文將提到:

  •  Android  Plugin 的基本原理
  • 是否繼承 UnityPlayerActivity 的差別
  • jar 檔放置位置的差別
  • AndroidJavaClass  與 AndroidJavaObject 之異同

Android  Plugin 的基本原理

Unity 之所以有著強大的跨平台能力,是因為它在不同的平台都提供了一個撥放器(Player)來執行專案,所以可以在幾乎不修改專案內容的情況下,搬運整個作品到另一個平台執行。但是這形成了優點的同時也產生了缺點,並不是每次依賴 Unity API 的功能就可以達成想要的效果。為了補充 Unity Player 的不足或者分享自己專案的一部份,Plugin因應而生。

Unity Android Plugin

在沒有 Android Plugin 的 Unity 專案中,專案程式與 Android 的關係就如同 上圖(a) 所示,在 Android 上啟動 App 之後,Unity Android Player 會立即被執行,它是一個 Android Activity (在 Android App 中的一個基本執行單元),這個 Player 再進一步撥放 Unity Scene 與 Script 的內容,呈現我們的作品內容。Android – Unity Player – Unity Project 三者的關係,就類似於過去 瀏覽器 – Flash撥放器 – Flash專案 這樣的關係。

Android Plugin 在 Unity 中,就是補充 Player 的這一個部分,例如 上圖(b) 顯示的關係,Plugin 中會有一部份是 JAVA 程式是直接運作在 Android 之上,然後專案腳本的部分會控制這些 JAVA 程式完成需要的工作。為了使用方便,可能會將控制 JAVA 程式的這部分在另外包裝成 C# Class,也就成了一個可以在 Unity 專案中隨處呼叫的 Android Plugin。

小結:一個常見的 Android Plugin,會有 JAVA 部分跟 C# 部分。共同作為溝通 Android 與專案腳本的橋樑。

 

是否繼承 UnityPlayerActivity 的差別

網路上的資源可以找到各式各樣的 Android Plugin 開發範例,但其中的 JAVA 部分,有些會繼承 UnityPlayerActivity 而有些不會,兩者之間有何差別?

上一段提到了 Unity 會有一個相容於 Android 的撥放器,這便是 UnityPlayerActivity,依照你開發 Plugin 的需求不同,會影響你繼承 UnityPlayerActivity 的必要性。假如你的 Plugin 需求是有關於:

  • 修改 UnityPlayerActivity 中的一些功能或流程
  • 增加 UnityPlayerActivity 在 Activity 生命週期上的動作 (onStart、onStop等) 或與其他新增的 Activity 進行互動

這些動作與 Unity Player 的 Activity 身分有關,繼承並重新實作才有辦法進行介入。但要注意,繼承 UnityPlayerActivity 之後,你的 Plugin 會處於 上圖(c) 的情況,直接替換掉 UnityPlayer 的位置,因此需要 Manifest.xml 的撰寫,以及專案本身相對應的設定,詳細可以參考官方文件。另外因為繼承與取代的動作,這類實作方式的 Plugin 在與其他 Plugin 的互動與相容會更容易出現衝突,需要額外去處理。

如果沒有上述需求,那 Plugin其實不必繼承 UnityPlayerActivity,如 上圖(b),只要新增另外的 JAVA class 與 C# class 就可以完成 Plugin 的開發,其中一個例子就是操作藍芽。因為 Unity 本身並沒有支援藍芽功能的API,所以在 Unity 使用藍芽的方法通常是撰寫一個操控藍芽設備的 JAVA class,在包裝成 C# class 於專案中使用。

因為沒有繼承 UnityPlayerActivity,所以這樣的 Plugin 有較佳的獨立性,大部分的情況都不容易與其他 Plugin 衝突,同一個 Plugin 也很方便在不同的專案中重複利用。

小結:如果真的不確定該不該繼承 UnityPlayerActivity,那就是不必繼承。

 

jar 檔放置位置的差別

通常 Android Plugin 都會有 jar 檔,而這個檔案只要放在這個路徑之中就可以被 Unity 讀取到專案之中了。

Assets/Plugins/Android/libs/*.jar
Assets/Plugins/Android/Manifest.xml

不過如果有複數個 Plugin,為了方便管理可以將它置於一個子資料夾中的 libs 之中,例如下列的路徑:

Assets/Plugins/Android/MyPlugin/libs/*.jar
Assets/Plugins/Android/MyPlugin/Manifest.xml

不過要記得,另外放於子資料夾中後,要新增一個檔案告知 Unity 這個資料夾是一個 Plugin 的資源,必須包含到專案之中。

Assets/Plugins/Android/MyPlugin/project.properties

檔案的內容是段文字:

project.properties
1
android.library=true

AndroidJavaClass 與 AndroidJavaObject 之異同

每個 Plugin 要確實讓 JAVA 程式運作起來,通常要透過 AndroidJavaClass 與 AndroidJavaObject 來呼叫,但這兩個超像的東西到底該用哪一個?

根據官方文件,AndroidJavaClass 繼承於 AndroidJavaObject,但是沒有增加額外的功能。主要的差別在於,建立一個 AndroidJavaObject 的同時,Unity 也會呼叫對應的 JAVA 建構子來建立實體,所以可以同時傳入值給建構子使用;而 AndroidJavaClass 只會尋找對應的 JAVA class 備用,不會建立實體。

平常使用上,會用 AndroidJavaClass 來呼叫 static 成員或函式;用 AndroidJavaObject 接收函式的回傳結果,或者呼叫實體相關的非靜態功能。

小結:AndroidJavaObject 有實體,AndroidJavaClass 沒有。

 

參考資料

http://docs.unity3d.com/Manual/PluginsForAndroid.html

http://answers.unity3d.com/questions/837762/where-can-jar-files-be-placed-under-assetspluginsa.html

http://answers.unity3d.com/questions/715007/what-is-the-difference-between-androidjavaclasscal.html

Installing Android SDK for Unity3D using SDK Tools and CLI

I don’t know why but last weeks I found myself several times in a situation that I had to create Android builds in the Unity3D on new computers or reinstalled ones. Each time I had to execute the whole process of setting up Android SDK and mess with some issues as the installation is not straightforward as it should be. This article is kind of cheatsheet for me but maybe someone else also appreciate it.

First issue is that you can install Android SDK either as a part of Android Studio bundle or using SDK Tools and CLI. Since I don’t like to download and install big softwares just for one scenario, I was glad to found out that there is really quick CLI option to install SDK without much overhead.

Second issue is related to Unity 5.6.2p1 (and earlier versions) failing to build Android SDK Tools 26.0, so for time being only version 25 is supported by Unity.

1. JDK Installation

Download and install Java Development Kit as you can’t do much anything without it. Current version is 1.8.0.

2. Android SDK 25 Tools Installation

In a normal situation we would download SDK Tools from Android Studio page. As we need older version 25 we can download it directly from the google repository.

After download extract the content to your SDK path. In our case we will use c:\Users\markey\AppData\Local\Android\SDK25\tools. Stop here for a while and take a notice that we created tools directory inside whole SDK directory. That is because tools are just one package of the whole SDK and other packages will be installed on the same level.

3. Installing packages using CLI and SDKManager

As a next step we are going to startup CLI and open our newly created toolsdirectory:

  1. cd c:\Users\markey\AppData\Local\Android\SDK25\tools\bin\

Now try to list all your installed packages using sdkmanager command:

  1. sdkmanager –list

It should output something like that:

  1. Installed packages:
  2. Path | Version | Description | LocationU
  3. ——- | ——- | ——- | ——-
  4. tools | 25.2.5 | Android SDK Tools 25.2.5 | tools\
  5. Available Packages:
  6. Path | Version | Description
  7. ——- | ——- | ——-
  8. add-ons;addon-g…_apis-google-24 | 1 | Google APIs
  9. build-tools;25.0.0 | 25.0.0 | Android SDK Build-Tools 25
  10. build-tools;25.0.1 | 25.0.1 | Android SDK Build-Tools 25.0.1
  11. build-tools;25.0.2 | 25.0.2 | Android SDK Build-Tools 25.0.2
  12. build-tools;25.0.3 | 25.0.3 | Android SDK Build-Tools 25.0.3
  13. build-tools;26.0.0 | 26.0.0 | Android SDK Build-Tools 26
  14. platform-tools | 26.0.0 | Android SDK Platform-Tools
  15. platforms;android-25 | 3 | Android SDK Platform 25
  16. platforms;android-26 | 1 | Android SDK Platform 26
  17. tools | 26.0.2 | Android SDK Tools
  18. Available Updates:
  19. ID | Installed | Available
  20. ——- | ——- | ——-
  21. tools | 25.2.5 | 26.0.2

As we can see tools are already installed, so we have to install three other packages platform-toolsplatforms and build-tools. The CLI command for that is sdkmanager “package”. If you like to explore sdkmanager more deeply then I recommend you to check out the documentation. For platforms and build-tools we have to specify proper version, so don’t forget to pick the one with version 25:

  1. sdkmanager “platform-tools”
  2. sdkmanager “platforms;android-25”
  3. sdkmanager “build-tools;25.0.3”

4. Android paths setup in Unity

Go to the Unity and switch project to Android platform. Then open External Tools (see picture below) and set SDK path to
c:\Users\markey\AppData\Local\Android\SDK25\tools and also JDK path to where you installed it in the first step. In our case the path is
C:\Program Files\Java\jdk1.8.0_131\.

Setting Android SDK path in Unity3D

Now, try to build your project and voilà!

Unity3d渲染关系层级顺序和Unity3D图集打包、深度、DrawCalll分析

最近连续遇到了几个绘制图像之间相互遮挡关系不正确的问题,网上查找的信息比较凌乱,所以这里就把自己解决问题中总结的经验记录下来。

Unity中的渲染顺序自上而下大致分为三层。 最高层为Camera层,可以在Camera的depth那里设置,设置之后,图形的渲染顺序就是先绘制depth低的相机下的物体,再绘制depth高的相机下的物体,也就是说,depth高的相机会覆盖depth低的相机(具体的覆盖关系有don’t clear, solid color等等几种)

比Camera层稍低一层的是sorting layer层, 随便找一个可以设置sorting layer的地方,选择sorting layer,点添加按钮,就可以看到当前所有的sorting layer,并且可以更改sorting layer的顺序,排位靠后的sorting layer会覆盖排位靠前的sorting layer。 设置好sorting layer的相互关系之后,就可以给任何一个继承于Renderer类,或者有renderer的子类作为field的对象设置sorting layer了。 注意这些sorting layer的遮挡关系是在同一个camera的层级下的。 不同camera下的renderer渲染顺序以camera的depth为准。 有的component的sorting layer可以直接在unity editor里面设置,比如Sprite Renderer。 有的则需要用代码来设置,比如设置Particle system的sorting layer, 就需要在代码中取到 ParticleSystem.Renderer.SortingLayer 来进行设置。

比sorting layer再低一层的是sorting order, 这个数字指代的是在同一个sorting layer下的渲染顺序,用法很明显就不赘述了。

需要注意不要混淆的是gameobject的layer,和renderer的sorting layer。 gameObject的layer个人理解是一个逻辑上的分层,用于camera的culling mask等。 而renderer的sorting layer则用于渲染。只有继承与renderer或者有renderer作为filed的component才需要设置sorting layer。

另外需要指出的是,常用的NGUI的widget depth其本质也是一个sorting layer下的sorting order。 NGUI好像用的是一个叫做“UI”的sorting layer。 由此大家如果有需要,也可以自己取Hack NGUI的代码,把NGUI的sorting layer暴露出来供自己定制。

简单总结一下,决定Unity渲染关系的层级顺序是:

Camera

sorting layer

sorting order

 

在最近,使用U3D开发的游戏核心部分功能即将完成,中间由于各种历史原因,导致项目存在比较大的问题,这些问题在最后,恐怕只能通过一次彻底的重构来解决

现在的游戏跑起来会有接近130-170个左右的DrawCall,游戏运行起来明显感觉到卡,而经过一天的优化,DrawCall成功缩减到30-70个,这个效果是非常显著的,并且这个优化并没有通过将现有的资源打包图集来实现,图集都是原有的图集,如果从全局的角度对图集再进行一次优化,那么DrawCall还可以再减少十几个

本次优化的重点包括:层级关系和特效

对于U3D,我是一个菜鸟,对于U3D的一些东西是一知半解,例如DrawCall,我得到的是一些并不完全正确的信息,例如将N个纹理打包成一个图集,这个图集就只会产生一个DrawCall,如果不打成图集,那么就会有N个DrawCall,这个观点在很多人的认识里都是正确的,因为可以通过简单的操作来验证,但严格来说,这个观点是错误的,因为它还受层级关系影响!

渲染顺序

U3D的渲染是有顺序的,U3D的渲染顺序是由我们控制的,控制好U3D的渲染顺序,你才能控制好DrawCall

一个DrawCall,表示U3D使用这个材质/纹理,来进行一次渲染,那么这次渲染假设有3个对象,那么当3个对象都使用这一个材质/纹理的时候,就会产生一次DrawCall,可以理解为一次将纹理输送到屏幕上的过程,(实际上引擎大多会使用如双缓冲,缓存这类的手段来优化这个过程,但在这里我们只需要这样子认识就可以了),假设3个对象使用不同的材质/纹理,那么无疑会产生3个DrawCall

接下来我们的3个对象使用2个材质,A和B使用材质1,C使用材质2,这时候来看,应该是有2个DrawCall,或者3个DrawCall。应该是2个DrawCall啊,为什么会有3个DrawCall???而且是有时候2个,有时候3个。我们按照上面的DrawCall分析流程来分析一下:

1.渲染A,使用材质1
2.渲染B,使用材质1
3.渲染C,使用材质2

在这种情况下是2个DrawCall,在下面这种情况下,则是3个DrawCall

1.渲染A,使用材质1
2.渲染C,使用材质2
3.渲染B,使用材质1

因为我们没有控制好渲染顺序(或者说没有去特意控制),所以导致了额外的DrawCall,因为A和B不是一次性渲染完的,而是被C打断了,所以导致材质1被分为两次渲染

那么是什么在控制这个渲染顺序呢?首先在多个相机的情况下,U3D会根据相机的深度顺序进行渲染,在每个相机中,它会根据你距离相机的距离,由远到近进行渲染,在UI相机中,还会根据你UI对象的深度进行渲染

那么我们要做的就是,对要渲染的对象进行一次规划,正确地排列好它们,规则是,按照Z轴或者深度,对空间进行划分,然后确定好每个对象的Z轴和深度,让使用同一个材质的东西,尽量保持在这个空间内,不要让其他材质的对象进入这个空间,否则就会打断这个空间的渲染顺序

在这个基础上,更细的规则有:

  • 场景中的东西,我们使用Z轴来进行空间的划分,例如背景层,特效层1,人物层,特效层2
  • NGUI中的东西,我们统一使用Depth来进行空间的划分
  • 人物模型,当人物模型只是用一个材质,DrawCall只有1,但是用了2个以上的材质,DrawCall就会暴增(或许对材质的RenderQueue进行规划也可以使DrawCall只有2个,但这个要拆分好才行),3D人物处于复杂3D场景中的时候,我们的空间规则难免被破坏,这只能在设计的时候尽量去避免这种情况了
  • 使用了多个材质的特效,在动画的过程中,往往会引起DrawCall的波动,在视觉效果可以接受的范围内,可以将特效也进行空间划分,假设这个特效是2D显示,那么可以使用Z轴来划分空间

打包图集

每个材质/纹理的渲染一定是会产生DrawCall的,这个DrawCall只能通过打包图集来进行优化

制作图集一般遵循几个规则:

  • 从功能角度进行划分,例如UI可以划分为公共部分,以及每个具体的界面,功能上,显示上密切相关的图片打包到一起
  • 不要一股脑把所有东西打包到一个图集里,特别是那些不可能同时出现的东西,它们就不应该在一个图集里,这样的图集意义不大,减少不了DrawCall,并且一个你不需要显示的图片,会一直占用你的内存,这让我非常不爽
  • 注意控制图集的大小,不要让图集太大,一个超级大图集的DrawCall消耗或许顶的上十几个小图集的消耗

字符图集,在使用BMFont或者其他工具生成图片字的时候,我们往往是直接导入一大串文字,然后直接生成图片,但实际上这上面的操作也有优化空间,例如BMFont生成的图片大小,是可以设置的,有两个规则,一个规则是导出的图片尽量小,另一个是导出的图片尽量少,默认的大小应该是512×512,假设你生成的图片256×256就可以容纳,那么多做一个操作你可以节省这么多空间,另外当你输入多几个字,就导致增加一张图片时,例如1024变成2048,那么你可以考虑使用3张512的图片,这样也会节省空间

经过精心划分的图集在加上精心规划的渲染顺序,DrawCall会有一个质的优化

特效清理

U3D提供了非常便捷的方法让我们很轻易地使用美术给过来的特效,懒惰的U3D程序猿会直接放入U3D,甚至不去看这是个什么特效,我们的特效一般都是一瞬间的事情,例如技能特效,或者其他什么特效,那么特效播放完,这个特效我们就看不到了,但假设这个特效在播放结束的时候,没有将自身的Active属性设置为false,那么它就会继续占用你的DrawCall,消耗你设备的计算能力,所以程序需要保证当一个特效播放完之后,能够被消耗,或者设置为非激活的状态,可以使用一些公共方法来完成特效播放完之后的清理工作(自己实现2个静态函数,一个播放完销毁,一个播放完设置未激活)

完成DrawCall的优化之后,接下来就是内存的优化了,(内存优化手记 待续)

unity 导出的Android-Studio工程

本文属于「Unity与iOS、Android平台的整合」系列文章之一,转载请注明出处
Unity默认导出的是Android-Eclipse工程,毕竟Eclipse for Android开发在近一两年才开始没落,用户量还是非常巨大的。
个人认为AndroidStudio非常好用,能轻易解决很多Eclipse解决不了或者很难解决的问题。
所以我将Unity导出的Andoid工程分为Eclipse和AndroidStudio两部分。
不过我之后的相关内容都会使用AndroidStudio,希望依然使用Eclipse的同学尽快跟上~

本文主要讲解Unity导出的Android-Studio工程的目录结构。
话先说在前面,这篇文章和1、导出的Xcode工程 非常相似,我建议手游开发者将两篇结合起来看~
同时,我建议大家完整阅读姊妹篇2、导出的Android-Eclipse工程 ,以建立完整的流程认知体系。

我所用软件的版本:
Unity 5.3.5f1
AndroidStudio 1.3.1

前导步骤
第一步,创建一个新的工程 Unity_Build_to_Android

第二步,创建以下文件

//为了演示原生Android的jar包
Plugins/Android/libs/Jar.jar

//为了演示原生Android调用jni所需要的.so文件
Plugins/Android/libs/x86/libnative.so

//为了演示原生Android调用jni所需要.so文件
Plugins/Android/libs/armeabi-v7a/libnative.so

//为了演示Unity中随包只读文件的去向
StreamingAssets/ALL_EmptyTxt.txt

第三步,保存一下场景,如下图

第四步,打开PlayerSettings,修改Bundle Identifier(包名),Unity里面不修改不让导出Android的~

第五步,选择Android平台,Export导出Android工程

第六步,使用AndroidStudio打开该Eclipse工程

选中Unity导出了Eclipse工程,OK。

存储到合适的路径,Next。

三个勾都选上,Finish

进入正题
0、Eclipse -> AndroidStudio
我们先来看看从Eclipse工程到AndroidStudio工程,在结构上有哪些变化,下图为AndroidStudio工程自动生成的导入摘要。

通过这个文件我们可以看到,AndroidStudio其实是建立了一个新的AndroidStudio工程,然后将原Eclipse工程中有用的文件转移到对应目录。
显而易见的迁移就不说了,主要看看libs文件夹中的迁移方向,我们可以看到.jar文件都转移到了app/libs文件夹下、/.so文件都转移到了app/src/main/jniLibs文件夹下/*.so。

1、程序入口
任何程序都有一个入口,Unity导出的Android工程中也不例外,通过下图我们可以看到,在AndroidManifest.xml文件中,将UnityPlayerActivity设置为应用的入口。(AndroidManifest.xml是Android的配置文件之一,具体请自行搜索~)

通过下图我们可以看到,UnityPlayerActivity是继承了Activity,至于它是什么我就不赘述了,大家可以看一下这一篇文章:Activity详解(生命周期、以各种方式启动Activity、状态保存,完全退出等)

下图为一个Activity的生命周期,我们可以看到系统事件存在着非常有用的监听,在UnityPlayerActivity.java里面我们也可以看到对应的函数,这意味着在Unity中一样可以收到这些事件,以后我们将继承UnityPlayerActivity,并重写这些监听。

2、C# -> C
这就是个很尴尬的问题了,现在5.3.5的版本还不支持在Android平台下使用IL2CPP的模式,但是5.4的版本已经可以支持了,这一点我打算等5.4的版本出了再补上~

3、资源 StreamingAssets -> app/src/main/assets
Unity导出Android工程后,原工程中的各种资源都被压缩、打包、加密后存放在app/src/main/assets/bin/Data文件夹中,见下图,这一点和iOS是一致的,网上也有很多资源解密的方法,大家有需要可以自行搜索。

我们重点说一下Unity中的StreamingAssets文件夹,关于这个文件的作用,大家可以看一下这篇文章:Unity3D研究院之手游开发中所有特殊的文件夹
通过下图我们可以看到,StreamingAssets文件夹中的ALL_EmptyTxt.txt文件被完整地拷贝到assets文件夹中,实际上不光是文件,文件夹也会原封不动地拷入该文件夹。

至于这有什么用,比如说,配置文件放在这里,上手机调试的话可以在Android工程中直接修改配置,而不需要到Unity里重新导出Android工程。

4、 Plugins/Android/libs -> app/libs 、app/src/main/jniLibs
Plugins/Android文件夹中通常会放一些.jar、.so文件,这些文件是什么、有什么用,大家可以看一下官方的文档:Building Plugins for Android
在Android-Studio工程中,.jar文件都转移到了app/libs文件夹下、/.so文件都转移到了app/src/main/jniLibs文件夹下/*.so。
这些文件将拷贝至在的libs文件夹中,在Android编译时也将被编译。

5、unity-classes.jar、libmain.so、libmono.so、libunity.so
unity-classes.jar是unity的封装好的一些Java类,用于在Android原生环境下处理相关业务,如果有兴趣的同学可以去反编译看看。

libmain.so、libmono.so、libunity.so文件是Unity写的底层CPP,当然其中也包含我们的C#逻辑,由于我们的重点不在这,就不展开讲了。(这个超纲,我没研究过~)

6、 Icon
在Unity的Player Settings是中,我们可以添加相应的Icon

这些Icon图片将被重新压缩、命名最后放入下图中的位置。

关于在哪边设置icon的问题,看项目需要吧,如果觉得分辨率不够可以到导出工程这边添加修改~
比如:多渠道打包时,可能要多次替换Icon,写脚本在Android工程中自动化替换打包是一种不错的选择~

7、闪屏
Unity在Android是可以设置单图片闪屏的,如下图。

不过Android天生是不带闪屏这种东西的,所以APP会在第一个Activity中先显示预先设定的图片。想深入了解可以看一看这篇文章:Android进阶篇之引导页系列之Splash闪屏Logo
Unity的在Android的闪屏实现应该也是这样处理的,注意注意!!这个思路很重要,以后我们Unity的闪屏也是这个思路做的。

8、Player Settings -> 设置

在Unity的Player Settings中存在着大量的配置,这些配置将反应到Android工程中,在下图中圈出了部分,这些东西并不复杂,我也就不赘述了。

9、build.gradle
此处是上篇文章没有的,这个文件可以帮助我们又快又好又简单地接入其他SDK。关于gradle文件,可以看一下这篇文章:Android Studio系列教程四–Gradle基础

unity 导出的Xcode工程

本文属于「Unity与iOS、Android平台的整合」系列文章之一,转载请注明出处
本文主要讲解Unity导出的Xcode工程的目录结构。

我所用软件的版本:
Unity 5.3.5f1
Xcode 7.3

前导步骤

第一步,创建一个新的工程 Build_to_iOS_Android
第二步,创建一个新文件 CSharpToCPP.cs

using UnityEngine;
public class CSharpToCPP
{
    public void Func(int num) 
    { 
        if (num < 1) 
        { 
            return; 
        } 
        else
        {
            Debug.Log("Log:Time"   num); 
        } 
    }
}

第三步,创建

Plugins/iOS/iOS_EmptyMM.mm
StreamingAssets/ALL_EmptyTxt.txt

第四步,保存一下场景,如下图

第五步,选择iOS平台,Build导出Xcode工程

第六步,打开所导出的Xcode工程

进入正题

1、程序入口

任何程序都有一个入口,Unity导出的Xcode工程中也不例外,通过下图我们可以看到,在应用的入口中创建了UnityAppController的对象。

通过下图我们可以看到,UnityAppController是继承了UIApplicationDelegate,至于它是什么我就不赘述了,大家可以看一下这一篇文章:简析UIApplication及UIApplicationDelegate

下图为一个UIApplication的生命周期,我们可以看到系统事件存在着非常有用的监听,在UnityAppController.mm里面我们也可以看到对应的函数,这意味着在Unity中一样可以收到这些事件,以后我们将继承UnityAppController,并重写这些监听。

2、C# -> C :

在Unity中,我们用的是C#、JS进行编程(我是C#党),但是导出Xcode工程后,这些代码都转换成了C 代码。
在Player Settings里面我们可以看到脚本的运行模式有两种,一种是IL2CPP,另一种是Mono2x(以后肯定会被CPP替代,我就不讲了),通常我们会选择IL2CPP,因为效率高。

选择IL2CPP我们的逻辑代码将全部转换为C 代码,下图为CSharpToCPP.cs转换成的C 代码。

以下代码是上图中的翻译代码,对比我们一开始写的CSharpToCPP.cs里逻辑,应该能够看懂其中的逻辑。

// System.Void CSharpToCPP::Func(System.Int32)
extern Il2CppClass* Int32_t2847414787_il2cpp_TypeInfo_var;
extern Il2CppClass* String_t_il2cpp_TypeInfo_var;
extern Il2CppClass* Debug_t1588791936_il2cpp_TypeInfo_var;
extern Il2CppCodeGenString* _stringLiteral2043233667;
extern const uint32_t CSharpToCPP_Func_m1441544591_MetadataUsageId;
extern "C"  void CSharpToCPP_Func_m1441544591 (CSharpToCPP_t373417985 * __this, int32_t ___num0, const MethodInfo* method)
{
    static bool s_Il2CppMethodIntialized;
    if (!s_Il2CppMethodIntialized)
    {
        il2cpp_codegen_initialize_method (CSharpToCPP_Func_m1441544591_MetadataUsageId);
        s_Il2CppMethodIntialized = true;
    }
    {
        int32_t L_0 = ___num0;
        if ((((int32_t)L_0) >= ((int32_t)1)))
        {
            goto IL_0008;
        }
    }
    {
        return;
    }

IL_0008:
    {
        int32_t L_1 = ___num0;
        int32_t L_2 = L_1;
        Il2CppObject * L_3 = Box(Int32_t2847414787_il2cpp_TypeInfo_var, &L_2);
        IL2CPP_RUNTIME_CLASS_INIT(String_t_il2cpp_TypeInfo_var);
        String_t* L_4 = String_Concat_m389863537(NULL /*static, unused*/, _stringLiteral2043233667, L_3, /*hidden argument*/NULL);
        IL2CPP_RUNTIME_CLASS_INIT(Debug_t1588791936_il2cpp_TypeInfo_var);
        Debug_Log_m1731103628(NULL /*static, unused*/, L_4, /*hidden argument*/NULL);
        return;
    }
}

由于C 代码是机器自动生成的,可读性催人尿下,所以最好参照自己的C#代码进行阅读。
由于Unity -> Xcode -> 设备,编译时间太过漫长了,所以在问题排查时,我会尝试注释或简单修改C 代码,实现问题定位等。

3、资源 StreamingAssets -> Data/Raw

Unity导出Xcode工程后,原工程中的各种资源都被压缩、打包、加密后存放在Data文件夹中,这一点和Android是一致的,网上也有很多资源解密的方法,大家有需要可以自行搜索。
我们重点说一下Unity中的StreamingAssets文件夹,关于这个文件的作用,大家可以看一下这篇文章:Unity3D研究院之手游开发中所有特殊的文件夹
通过下图我们可以看到,StreamingAssets文件夹中的ALL_EmptyTxt.txt文件被完整地拷贝到Data/Raw文件夹中,实际上不光是文件,文件夹也会原封不动地拷入该文件夹。

至于这有什么用,比如说,配置文件放在这里,上手机调试的话可以在Xcode工程中直接修改配置,而不需要到Unity里重新导出Xcode工程。

4、Plugins/iOS -> Library/Plugins/iOS

Plugins/iOS文件夹中通常会放一些 *a.、*h、*.m文件,这些文件将拷贝至在Xcode工程的Library/Plugins/iOS文件夹中,在Xcode编译时也将被编译。

实际操作过程中,我会把自己为iOS写的OC、C、C 代码、SDK提供的.a文件放到里面,以避免每次编译后都要在Xcode工程里重新导入。

5、Icon

在Unity的Player Settings是中,我们可以添加相应的Icon

这些Icon图片将被重新压缩、命名最后放入下图中的位置。

由于Unity内只能导出这些分辨率大小(比如iPadPro的icon无法导出),而且Unity会根据我们选择的图片压缩格式重新生成Icon(如果格式没选对容易图片变模糊),所以我选择到Xcode工程中替换这些文件。

6、闪屏

Unity在iOS有很多种闪屏方案,具体方案效果大家可以自行搜索~

下图为常见的单图片闪屏导出位置,与Icon一样,我一般在这边替换,哦对,不要忘了取消ShowUnitySplashScreen的勾选。

7、Framework

在Unity的Plugins/iOS/中文件可以为自身简单勾选配置一些Framework依赖,见下图。

这些依赖将自动添加到Xcode工程中,我们可以在Frameworks文件夹检查是否添加。

8、Player Settings -> 设置

在Unity的Player Settings中存在着大量的配置,这些配置将反应到Xcode工程中,在下图中圈出了部分,这些东西并不复杂,我也就不赘述了。

unity 转 安卓项目报错

Error:Cause: org.gradle.api.internal.tasks.DefaultTaskInputs$TaskInputUnionFileCollection cannot becast to org.gradle.api.internal.file.collections.

 

// GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN
buildscript {
   repositories {
      jcenter()
   }

   dependencies {
      classpath \'com.android.tools.build:gradle:2.3.1\'
   }
}

还有一点我升级到了Unity5和Jenkins的最新版本,以前Unity4的时候在偏好设置里设置一下sdk的目录以后直接打包就行了。可是现在在U5里,即时是设置了目录,然而在调用jenkins打包的时候还是会无法识别这个目录,将会弹出窗口让你选择SDK目录。 我不知道这是不是unity的bug.但是作为全自动打包工具不能容忍这个啊。。

在你自动打包之前,强制设置sdk的目录就行了。 记住目录最前面一定要有”/”不然还是会报错。亲测这样做问题就可以解决。

 

如何快速优化手游性能问题?从UGUI优化说起

UGUI简介

UGUI是Unity官方推出的UI系统,集成了所见即所得的UI解决方案, 其功能丰富并且使用简单,同时其源代码也是开放的,下载地址:https://bitbucket.org/Unity-Technologies/ui/src

相比于NGUI,UGUI有以下几个优点:

1. 所见即所得的编辑方式,在Scene窗口中即可编辑。

2. 智能的Sprite packer可以将图片按tag自动生成图集而无需人工维护,生成的图集合并方式比较合理,无冗余资源。

3. 渲染顺序与GameObject的Hierarchy顺序相关,靠近根节点显示在底层,而靠近叶子节点显示在顶层;这样的渲染方式使得调整UI的层级比较方便和直观。

4. RectTranForm及锚点系统更适合于2D平面布局,并且非常方便多分辨率屏幕自适配。

UI制作规范和指导方法

本文是关于UGUI优化的,或许你会觉得UI的制作规范及指导方法与优化无关,其实很多性能问题往往是资源的不合理使用造成的,比如使用了尺寸过大的图片、引用了过多的图集以及加载了不必要的资源等。如果从设计和制作UI一开始就遵守特定的规范,则可以规避不必要的性能开销。笔者根据参与的多个项目总结了以下几点通用的规范和指导方法(这些规范适用于所有项目,不管你使用UGUI还是NGUI)。

1. 合理的分配图集

合理的分配图集可以降低drawcall和资源加载速度;具体细节如下:

● 同一个UI界面的图片尽可能放到一个图集中,这样可以尽可能的降低drawcall。

● 共用的图片放到一个或几共享的图集中,例如通用的弹框和按钮等;相同功能的图片放到一个图集中, 例如装备图标和英雄头像等;这样可以降低切换界面的加载速度。

● 不同格式的图片分别放到不同的图集中,例如透明(带Alpha)和不透明(不带Alpha)的图片,这样可以减少图片的存储空间和占用内存。(UGUI的sprite packer会自动处理这种情况)

2. resources目录中应该只保存prefab文件,其它非prefab文件(例如动画,贴图,材质等)应放到resource目录之外

因为随着项目的迭代,可能会导致部分资源(动画,贴图)等失效,如果这些文件放在resource目录下,在打包时,unity会将resource目录下文本全部打成一个大的AssetBundle包(非resouce目录下的文件只有在引用到时才会被打到包里),从而出现冗余,增加不必要的存储空间和内存占用。可以通过以下代码(Mac环境下)在控制台窗口中查看当前目录下所有非prefab资源的代码:

find . -type f | egrep -v “(prefab|prefab\.meta|meta)$”

例如在笔者的一次扫描中,发现在了如下结果:

3. 关卡内的UI资源不要与外围系统UI资源混用

在关卡内,需要加载大量的角色及场景资源,内存比较吃紧,一般在进入关卡时,都会手动释放外围系统的资源,以便使关卡内有更多的内存可以使用。如果战斗内的UI与外围系统的UI使用相同图集里的图片,则有可能会使得外围系统的图片资源释放不成功。对于关卡内与外围共用的UI资源需要特殊处理,一般来说复制一份出来专门给关卡内使用是比较好的选择。

4. 适当的降低图片的尺寸

有时UI系统的背景可能会使用全屏大小的图片,比如在Iphone上使用1136*640大小的图片;使用这样尺寸的图片代价是很昂贵的,可以和美术同学商量适当的降低图片的精度,使用更低尺寸的图片。

5. 在android设备上使用etc格式的图片

目前,几乎所有android设备都支持etc1格式的图片,etc1的好处是第个像素点只战用0.5个字节而普通rgba32的图片每个像素点占4个字节,也就说一张1024*1024图片如果使用rgba32的格式所占用的内存为4M而etc1格式所占用的内存仅为0.5M。但是使用etc1格式的图片有两个限制——长和宽必须是POT的(2的N次方)并且不支持alpha通道,因此使用etc1时需要额外的一张图来存储alpha通道,并且使用特殊的shader来对alpha采样。具体的细节可参考:http://malideveloper.arm.com/resources/sample-code/etcv1-texture-compression-and-alpha-channels/

6. 删除不必要的UI节点、动画组件及资源

随着项目的迭代,可能有部分ui节点及动画已经失效,对于失效的节点及动画一定要删除,在很多项目中,有部分同学为了方便省事,只是将失效的节点及动画disable了。这样做虽然在运行时不会对cpu造成太多负担,但是在加载时会增加不必要的加载时间以及内存占用。对于废弃的UI图片资源,虽然未放到Resource目录最终不会打到包里,但是在Editor模式下仍然会打到图集中从而影响优化决策。笔者写了一个扫描未使用到UI贴图资源的工具,代码地址:https://github.com/neoliang/FindUnUsedUITexture;

另外,对于废弃的脚本,可能还会有某些对象持有对它的引用,而加载这样的对象也比较耗时,笔者也写了一个扫描废弃脚本的工具,代码地址:https://github.com/neoliang/MissingScriptFinder

CPU优化

一般来说,优化cpu性能应该先用profiler定位到性能热点,找到消耗最高的函数,然后再想办法降低它的消耗。经过笔者多次使用profiler对UGUI的分析来看,其CPU性能开销高主要原因之一是Canvs对UI网格的重建,有很多情况会触发Canvas对网格的重建,例如Image,Text等UI元素的Enable及UI元素的长、宽或Color属性的变化等。Canvas中UI Mesh顶点较多的话,则该项将会出现较高的CPU开销。在Unity的Profiler中则对应的是Canvas.SendWillRenderCanvases或Canvas.BuildBatch占用过多的时间。

Canvas.BuildBatch主要功能是合并Canvas节点下所有UI元素的网格,合并后的网格会缓存起来,只有其下面的UI元素的网格发生改变时才会重新合并。而UI元素的网络变化主要是因为Canvas.SendWillRenderCanvases调用时,rebuild了Layout或者craphic。该函数的调用过程时序图如下:

1.该过程由CanvasUpdateRegistry监听Canvas的WillRenderCanvases(上图中1)而执行,主要是对前标记为dirty的layout和craphic执行rebuild。引起layout和graphic的dirty主要原因是因为Canvas树形结构下的UI元素发生了变化(例如增加删除UI对象,UI元素的顶点,rec尺寸改变等)调用了Graphic.SetDirty(实际上最终都会调用CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild)。

2. 在rebuild layout之前会对Layout rebuild queue中的元素依据它们在heiarchy中的层次深度进行排序(上图中的2),排列的结果是越靠近根的节点越会被优先处理。

3. rebuild layout(上图中的3),主要是执行ILayoutElement和ILayoutController接口中的方法来计算位置,Rect的大小等布局信息。

4. rebulid graphic(上图中的4),主要是调用UpdateGeometry重建网格的顶点数据(上图中5)以及调用UpdateMeterial更新CanvasRender的材质信息(上图中6)。

基于以上UGUI的网格更新原理,我们可以做以下优化:

a. 使用尽可能少的UI元素;在制作UI时,一定要仔细查检UI层级,删除不不必要的UI元素,这样可以减少深度排序的时间(上图中的2)以及Rebuild的时间(上图中的3,4)。

b. 减少Rebuild的频率,将动态UI元素(频繁改变例如顶点、alpha、坐标和大小等的元素)与静态UI元素分离出来,放到特定的Canvas中。

c. 谨慎使用UI元素的enable与disable,因为它们会触发耗时较高的rebuild(图中的3、4),替代方案之一是enable和disableUI元素的canvasrender或者Canvas。

d. 谨慎使用Text的Best Fit选项,虽然这个选项可以动态的调整字体大小以适应UI布局而不会超框,但其代价是很高的,Unity会为用到的该元素所用到的所有字号生成图元保存在atlas里,不但增加额外的生成时间,还会使得字体对应的atlas变大。

e.谨慎使用Canvas的Pixel Perfect选项,该选项会使得ui元素在发生位置变化时,造成layout Rebuild。(比如ScrollRect滚动时,如果开启了Canvas的pixel Perfect,会使得Canvas.SendWillRenderCanvas消耗较高)

f. 使用缓存池来保存ScrollView中的Item,对于移出或移进View外的的元素,不要调用disable或enable,而是把它们放到缓存池里或从缓存池中取出复用。

g. 除了rebuild过程之外,UGUI的touch处理消耗也可能会成为性能热点。因为UGUI在默认情况下会对所有可见的Graphic组件调用raycast。对于不需要接收touch事件的grahic,一定要禁用raycast。对于unity5以上的可以关闭graphic的Raycast Target而对于unity4.6,可以给不需要接收touch的UI元素加上canvasgroup组件。

unity5.x 

unity4.6

GPU优化

一般来说,造成GPU性能瓶颈主要有两个原因:复杂的vertext或pixel shader计算以及overdraw造成过多的像素填充。在默认情况下UGUI中所有UI元素使用都使用UI/Defaut shader,因此在优化时可优先考虑解决Overdraw问题。Overdraw主要是因为大量UI元素的重叠引起的,查看overdraw比较简单,在scene窗口中选择overdraw模式,场景中越亮的地方表示overdraw越高(如下图)。

为了降低overdraw,可以做如下优化:

1. 禁用不可见的UI,比如当打开一个系统时如果完全挡住了另外一个系统,则可以将被遮挡住的系统禁用。

2. 不要使用空的Image,在Unity中,RayCast使用Graphi作为基本元素来检测touch,在笔者参与的项目中,很多同学使用空的image并将alpha设置为0来接收touch事件,这样会产生不必要的overdraw。通过如下类NoDrawingRayCast来接收事件可以避免不必要的overdraw。

3. public class NoDrawingRayCast : Graphic

4. {

5.     public override void SetMaterialDirty()

6.     {

7.     }

8.     public override void SetVerticesDirty()

9.     {

10.     }

11.     protected override void OnFillVBO(List vbo)

12.     {

13.         vbo.Clear();

14.     }

}

总结

优化UGUI性能没有万能的方法,笔者这些经验总结也只能作为参考。优化性能往往是在各种选择之间做出平衡,比如drawcall与rebuild平衡、内存战胜与cpu消耗平衡以及UI图片精度与纹理大小的平衡等。每一次优化都有可能使得瓶颈出现在其它的环节上,要善于使用profiler,找到性能热点,对症下药。

关于资源占用问题

UI资源优化是UGUI性能优化的重点,腾讯WeTest也在资源方面提供了性能的测试。以下通过“纹理”资源,介绍腾讯WeTest性能测试在资源方面的测试情况。

1、登录http://wetest.qq.com/cube/ ,点击“Android版 下载”,或者在页面末尾扫描二维码直接下载腾讯WeTest的手游客户端性能分析工具Cube。打开工具,选择“Unity资源分析”。

2、上传测试报告后,我们可以通过测试报告,了解unity游戏的资源情况。

ulua 使用luaide进行调试

1.下载luaide.为了避免冲突禁用其他lua的debug插件

2.到https://github.com/k0204/LuaIde中下载lua/LuaDebug.lua到LuaFramework/Lua文件夹中

3.修改运行配置launch.json

        {

            "name": "Unity-ulua",

            "type": "lua",

            "request": "attach",

            "runtimeType": "Cocos2",

            "port": 7003,

            "scripts": [

                "${workspaceRoot}",

                "${workspaceRoot}/Assets",

                "${workspaceRoot}/Assets/ToLua",

                "${workspaceRoot}/Assets/LuaFramework/Lua"

            ],

            "printType": 1

        }
4.LuaFramework/Lua修改Main.lua文件.Main()函数中添加..localhost可以改成ip地址即可进行远程调试
local breakSocketHandle,debugXpCall = require("LuaDebug")("localhost",7003)
5.修改scripts/Manager/LuaManager文件.并添加三个函数
 void OpenLibs() {

            OpenLuaSocket(); //添加打开luasocket

            lua.OpenLibs(LuaDLL.luaopen_pb);      

            lua.OpenLibs(LuaDLL.luaopen_sproto_core);

            lua.OpenLibs(LuaDLL.luaopen_protobuf_c);

            lua.OpenLibs(LuaDLL.luaopen_lpeg);

            lua.OpenLibs(LuaDLL.luaopen_bit);

            lua.OpenLibs(LuaDLL.luaopen_socket_core);

        

            this.OpenCJson();

        }

        [MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]

        static int LuaOpen_Socket_Core(IntPtr L)

        {        

            return LuaDLL.luaopen_socket_core(L);

        }

        [MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]

        static int LuaOpen_Mime_Core(IntPtr L)

        {

            return LuaDLL.luaopen_mime_core(L);

        }

        protected void OpenLuaSocket()

        {

            LuaConst.openLuaSocket = true;

            lua.BeginPreLoad();

            lua.RegFunction("socket.core", LuaOpen_Socket_Core);

            lua.RegFunction("mime.core", LuaOpen_Mime_Core);                

            lua.EndPreLoad();                     

        }

 

6.为了测试方便.可以将AppConst中设置LuaBundleMode = false..可以不用每次构建assetbundle.就能执行lua

Handling long press events in uGUI (Unity 4.6)

using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using System.Collections;
 
public class LongPressEventTrigger : UIBehaviour, IPointerDownHandler, IPointerUpHandler, IPointerExitHandler {
 [ Tooltip( "How long must pointer be down on this object to trigger a long press" ) ]
 public float durationThreshold = 1.0f;
 
 public UnityEvent onLongPress = new UnityEvent( );
 
 private bool isPointerDown = false;
 private bool longPressTriggered = false;
 private float timePressStarted;
 
 
 private void Update( ) {
 if ( isPointerDown && !longPressTriggered ) {
 if ( Time.time - timePressStarted > durationThreshold ) {
 longPressTriggered = true;
 onLongPress.Invoke( );
 }
 }
 }
 
 public void OnPointerDown( PointerEventData eventData ) {
 timePressStarted = Time.time;
 isPointerDown = true;
 longPressTriggered = false;
 }
 
 public void OnPointerUp( PointerEventData eventData ) {
 isPointerDown = false;
 }
 
 
 public void OnPointerExit( PointerEventData eventData ) {
 isPointerDown = false;
 }
}