关于244128133

Unity3D游戏程序员

关于U3D谷歌AAB分包和打包

特色

最近在搞海外相关需求,借此机会记录一下谷歌出包和上架过程。

谷歌宣布从 2019 年 8 月 1 日 开始,在 Google Play 上发布的应用必现支持 64 位架构,这个意味着如果你要上谷歌平台就不能使用Mono的方式打包,只能选择IL2CPP,因为Mono是不支持64位的。

另外上架的应用要包含多份架构的 SO 包,导致应用大小增加,以前只有 armeabi-v7a 一种 CPU 架构,但是为支持 64 位,需要增加 arm64-v8a 架构的库。

所以谷歌宣布从2021年8月份开始,所有提交到Google Play商店的新应用必须采用AAB格式,因为AppBundle(AAB)是Google在2018年推出的动态打包方式,这种包格式以.aab为文件扩展名。

通过aab包发布到 Google Play 的应用,Google Play 能自动将 aab 根据不同维度分解为一个基础包和一系列的“拆分包”,这些拆分包其实就是一个个apk,其中有分为配置拆分包、PFD拆分包和PAD拆分包。

当用户下载应用时,Google Play 会根据用户设备的特性只下发基础包和对应的拆分包,从而减小用户下载应用的大小。

同时谷歌还要求AAB包下的Base目录大小不能超过150M不然上传不了谷歌商店,不过谷歌提供了其他目录用于存放资源和代码。

这些目录分别如下:

install-time:安装时分发

fast-follow:快速跟进式分发模式,在安装游戏完成后,自动下载。

on-demand:按需分发模式,通过google API下载

现在大部分游戏资源其实都是放在Base目录下的,也就是安卓工程的src/main/assets目录, 而且基本上是超过150M的,也就是说我们开发者需要更改自己原有的资源加载方式,把大部分资源移动到install-time,fast-follow,on-demand这3个目录中,谷歌有提供相关的API去这些目录中获取资源。

其中有专门针对Unity的API和工具:

文档地址:https://developer.android.google.cn/guide/app-bundle/asset-delivery/build-unity

如果你不想更改原来的加载方式,可以把资源全部放在install-time目录下,在这个目录下的资源不需要使用谷歌的API进行加载,而是在安装的时候会自动帮你安装到手机上。

如下,把Unity导出的工程划分为以下几个目录:

然后把src/main/assets下的资源分配到install-time,fast-follow,on-demand目录,但是要注意在这3个目录下的目录结构需要和src下的目录结构一样。

比如原来的资源存放在src\main\assets\res目录,现在你要把res下的部分资源移动到install-time目录下,那你需要在install-time目录下新建一个src\main\assets\res的目录结构,然后把src\main\assets\res的资源移动到install-time\src\main\assets\res目录下,这样就大功告成了。

只要保证原来src\main\assets目录的大小加上库文件的大小不超过150M那么打包出来AAB的base目录也不会超过150M。

AAB包的结构如下:

观察发现,base目录的大小主要是assets+lib的大小,而lib下是我们的库文件,这些是无法缩减和移动的。

其实从测试的角度上来说光打出AAB包还不算真正的结束,这个时候如果你想测试AAB包是不行的,因为AAB无法直接安装在你的手机上。

所以发行那边或者测试通常会向你要一个AAB包对应的APK包,幸好谷歌有提供相关的工具把AAB包反编译为APK。

首先要去Github下载这个工具:https://github.com/google/bundletool/releases

然后使用命令:java -jar 你下载的bundletool路径.jar build-apks –bundle=你的AAB包路径.aab –output=输出apks的路径.apks –ks=安卓签名文件路径 –ks-pass=pass:ks密码 –ks-key-alias=bf_lmzh_yn –key-pass=pass::key密码

执行后等待几分钟(根据每个人的电脑性能的不同编译时间有快有慢)会在你设置的输出目录下生成一个.apks的文件,使用解压软件打开它,standalones目录下的apk文件即是你AAB对应的APK文件,把APK和AAB一起发给发行和测试,到此出包工作才全部结束。

如何让Unity3D生成的安卓包启动更快

特色

Unity生成的游戏包启动确实会比其他引擎慢些,即使打包的时候勾选了Splash Image选项,如果包比较大的话,安卓启动还是会黑屏一段时间才能看到画面。

这种情况是没办法从U3D游戏这边去解决的了,因为这是原生安卓的问题了,需要从SDK这边去解决,网上看到有些资料是在主Activity的onCreate里创建一个view来覆盖Splash,其实完全没必要这么复杂。

只需两步就可以解决:
1.在AndroidManifest文件中设置:

<activity
    android:name=".ui.SplashActivity"
    android:theme="@style/Theme.Splash"
    android:screenOrientation="portrait">
    <intent-filter>
          <action android:name="android.intent.action.MAIN" />
          <category android:name="android.intent.category.LAUNCHER" />
     </intent-filter>
</activity>


2.在style文件中定义:

<style name="Theme.Splash" parent="android:Theme.Holo.Light.NoActionBar.Fullscreen">
    <item name="windowNoTitle">true</item>
    <item name="android:windowContentOverlay">@null</item>
    <item name="android:windowBackground">@drawable/splash</item>
    <item name="android:windowFullscreen">true</item>
</style>

然后在src/main/res/drawable下放一张名为splash.png的启动图即可。这张启动图会在你启动APP之后立马显示出来,然后再显示Unity设置的Splash图。

不过这么做的话这张图会有一个问题,那就是和手机的屏幕分辨率不匹配的问题,有可能会拉伸和挤压的情况出现,其实只要这张图中间放一张LOGO其他地方是纯黑色或者白色是看不出来的,也可以使用.9的方式设置图片拉伸区域。

大大缩短了安卓APP的启动时间,据发行反馈这样体验更好。

关于Unity游戏在安卓SDK弹出时从后台返回黑屏问题

特色

今天发行那边反馈过来一个问题,是之前我们都没发现过的,当在安卓SDK登录界面的时候切换到后台或者切换其他到其他应用,再次返回游戏的时候,发现SDK登录界面后面的游戏界面黑屏了。

我的初步判断是,游戏的View应该是从后台返回到前台的时候还没被激活,因为只要点击一下SDK的登录,游戏界面就显示出来了。

查询资料发现确实是这个原因。


一、当Unity所在的Activity之上没有其他Activity时候,生命周期的变化如下:

启动App
D/MainActivity: onCreate
D/MainActivity: onStart
D/MainActivity: onResume
D/MainActivity: onWindowFocusChanged true

点Home进入后台
D/MainActivity: onPause
D/MainActivity: onWindowFocusChanged false
D/MainActivity: onStop

从后台回来
D/MainActivity: onRestart
D/MainActivity: onStart
D/MainActivity: onResume
D/MainActivity: onWindowFocusChanged true

退出应用
D/MainActivity: onWindowFocusChanged false
D/MainActivity: onPause
D/MainActivity: onStop
D/MainActivity: onDestroy

二、当Unity所在的Activity之上有其他Activity时候,生命周期的变化如下

弹窗显示另外一个Activity
D/MainActivity: onPause
D/MainActivity: onWindowFocusChanged false

点Home进入后台
D/MainActivity: onStop

从后台回来
D/MainActivity: onRestart
D/MainActivity: onStart

关闭 弹窗Activity
D/MainActivity: onResume
D/MainActivity: onWindowFocusChanged true

三、对比两次的日志打印,我们可以发现:第二次从后台返回少了两个生命周期的调用

D/MainActivity: onResume
D/MainActivity: onWindowFocusChanged true

综上分析所得,要想做到从后台返回不黑屏,只需要在SDK的主Activity的 onStart里面调用onResume和onWindowFocusChanged方法即可。

public void onStart() {
    super.onStart();
    this.mUnityPlayer.resume();
    onWindowFocusChanged(true);
}

这样当Unity所在的Activity之上有其他Activity时候,即使切换到后台再返回游戏,也不会黑屏。

关于Particle System is trying to spawn on a mesh with zero surface area的警告

今天测试反馈了个刷屏的警告”Particle System is trying to spawn on a mesh with zero surface area“根据翻译判断是使用了体积为0的mesh,但是当我在网上查找资料的时候,网上说的是因为 ParticleSystem 中 ShapeModule 要求引用的 Mesh 必须开启 Read/Write 选项.

经过我的实验和调试发现并不是这么回事,简单的就是因为某个特效的shape类型选择了mesh然而没有指定一个mesh给他,而且选择的Mode为Edge,如下:

满足以上几个箭头所指的条件后就会不断刷如下警告:

要想去掉这些警告,可以改变shape的类型或者指定一个mesh给它,也可以改变Mode的类型为Vertex都可以去掉这些烦人的警告,具体方法需要根据自己的特效情况设置.

如果需要查找项目中有问题的特效可以使用以下编辑器脚本:

using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;

public static class MeshToolForParticle
{
    [MenuItem("Tools/ParticleSystem/FindScenes", false, 20)]
    public static void FindScenes()
    {
        var guids = AssetDatabase.FindAssets("t:Scene");

        foreach (var guid in guids)
        {
            string path = AssetDatabase.GUIDToAssetPath(guid);
            var scene = EditorSceneManager.OpenScene(path, OpenSceneMode.Single);
            var roots = scene.GetRootGameObjects();
            bool found = false;
            foreach (var go in roots)
            {
                found |= Find(go);
            }

            if (found)
            {
                Debug.Log(path);
            }
        }
    }

    [MenuItem("Tools/ParticleSystem/FindPrefabs", false, 20)]
    public static void FindPrefabs()
    {
        var guids = AssetDatabase.FindAssets("t:Prefab");

        foreach (var guid in guids)
        {
            string path = AssetDatabase.GUIDToAssetPath(guid);
            var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
            Find(prefab);
        }
    }

    private static bool Find(GameObject go)
    {
        bool found = false;
        var particles = go.GetComponentsInChildren<ParticleSystem>(true);
        foreach (var particle in particles)
        {
            if (particle.shape.enabled && particle.shape.shapeType == ParticleSystemShapeType.Mesh &&
                particle.shape.mesh == null)
            {
                string prefabPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(particle);
                Debug.LogWarning(string.Format("Path: {0}\nPrefab: {1}", particle.gameObject.GetGameObjectPath(), prefabPath));
                found = true;
            }
        }

        return found;
    }
}

把上面的脚本放到项目的Editor目录下然后点击菜单Tools/ParticleSystem/FindPrefabs,即可打印出有问题的特效名称.

Phong和Blinn-Phone光照模型的区别

Lambert光照模型虽然能模拟出粗糙物体表面的光照效果,但是在真实环境中很多光滑的物体它却不能很好的表现出来。

所以在1975年裴祥风提出了一种局部光照的经验模型——Phong光照模型

公式:Color = Ambient + Diffuse + Specular

其中, Specular 为镜面反射,C(specular) = C(light)*M(specular)saturate(v*r)^M(shininess)

v是视角方向,r是光线的反射方向, M(shininess) 为物体材质的光泽度。

后来在1977年 Jim Blinn对Phong光照模型算法进行了改进,提出了Blinn-Phong光照模型,和Phong光照模型相比 Blinn-Phong 只改进了镜面反射的算法。

镜面反射公式:C(specluar)= C(light)*M(specular)saturate(n*h)^M(shininess)

其中h=normalize(v+l),v是视角方向,l是光照方向,n是法线方向。

虽然这一个小小的改进并没有让表现效果在视觉上有很大的提升,但是在性能方面却有很大的提升,因为h是取决于视角方向以及灯光方向的,两者很远的时候h可以认为是常量,和位置及表面曲率没有关系,所以可以减少计算量。

而Phong要根据表面曲率去逐顶点或逐像素计算反射向量r,所以计算量比较大。

unity3D Shader变灰原理

特色

使用shader置灰一张图或者一个文字的代码很简单,但是之前一直不知道它的原理是啥。

今天通过查找资料发现,其实很简单,置灰的核心代码如下:

 fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                //-----------add-------------
                // gray
                float gray = dot(col.rgb, float3(0.299, 0.587, 0.114));
                col.rgb = float3(gray, gray, gray);
                //---------------------------
                return col;
            }
 float gray = dot(col.rgb, float3(0.299, 0.587, 0.114));
这一句是最主要的一句,其实就是把采样出来的颜色和一个固定的颜色值做一个点乘,但是为什么是这个颜色呢?为什么要和这个颜色值做点乘返回最终的片元着色器颜色?

原理:是因为人眼对绿色的敏感度最高,对红色的敏感度次之,对蓝色的敏感度最低,因此使用不同的权重将得到比较合理的灰度图像。实验和理论推导出来的结果是0.299、0.587、0.114。
所以为图片的RGB分别乘以权重然后加起来得到一个灰色值,并且将这个灰色值作为新的RGB值
返回给最终的颜色值。 

如何优化结构体和类的大小?

类和结构体的区别,大家应该都知道,但是在开发过程中到底是用类还是结构呢?

要想知道这个问题的答案首先应该知道怎样估算对象和结构体的大小。

一、如何估算结构体的大小

结构是值类型,它的结构体的实例是存放在栈中或者堆中。结构体在内存中所占的大小,就是其字段所占的大小,但是,它的大小并不是所有字段大小相加,而是存在一个对齐的规则,在默认的对齐规则中,基本类型字段是按照自身大小对齐的,如byte是按1字节对齐。

struct A
{
byte a1;
}

如上面这个结构体的大小就是1字节,如果是下面这个:

struct A
{
byte a1;
int a2;
}

这个结构体所占内存大小是8字节,因为int是4字节对齐的,所以只能从第四个字节开始。

如果再添加一个字段:

struct A
{
byte a1;
int a2;
byte a3;
}

这个结构体大小是12,由于struct本身也要是对齐的,所以它的对齐规则是按照其中元素最大的对齐规则决定的。也就是说上面这个结构体要按照4字节对齐,不足4字节要补齐,所以是12个字节大小。

如果想要优化它的大小,可以调整顺序如下:

struct A
{
byte a1;
byte a3;
int a2;
}

这个时候这个结构体所占的大小就是8字节了。

二、如何估算类的大小

类是引用类型,它的对象实例存放在堆中,对象实例一定是会占用堆内存的,而在栈中,保存的是实例的引用。对象在堆中分成3个区域,vtable、monitor和字段。其中vtable是类的共有数据,包含静态变量和方法表,这个应该就是类本身所占用的大小和具体的对象无关。monitor是线程同步用的,这2个指针分别占用一个inptr.Size大小,字段是从第9个字节或17个字节开始的,字段的对齐规则和结构体的对齐规则相同,区别是Mono中对象的实例会把引用类型的引用放在最前面。一个对象实例的大小就是 inptr.Size *2+字段的大小。

通过调整字段的顺序,也可以优化对象的大小。

还可以通过StructLayoutAttribute自定义类和结构体的对齐方式。

[StructLayout(LayoutKind, Sequential, Pack = 1)]
public struct A
{
byte a1;
int a2;
byte a3;
}

上面这个结构体强制按照1字节对齐,所以他的大小是6字节。但是这样做可能会降低性能。

所以具体情况还是要具体分析,懂得了如何估算结构体和类的大小,就更容易知道该如何使用它们了。

unity3D优化之内存管理

在U3D游戏中内存管理一直都是让人比较头疼事情,现在手机游戏是越做越大,和端游一样,每次卡顿和每次的内存增长对玩家来说都是一个比较差的体验。听到过一句话,说游戏开发做久了就会变成“GC怪”,因为在游戏开发过程中,需求变化多,功能不停的迭代,内存问题也一直存在,需要不停的去优化它。

在unity2018中集成来正版的.NET4.X和C#7.3,引入了ref return 和ref locals,让值类型的操作更加便捷,在U3D2019中更是加入来增量式GC,减少来GC带来的卡顿问题。比以前的版本灵活来许多。

讲了这么多,可能很多人还不知道什么是GC,GC的全称是Garbage Collection,也就是垃圾回收的意思,是一种自动管理堆内存的机制,管理堆内存上对象的分配和释放。

一、内存管理方式

我们常用的内存管理方式有三种:

1.手动管理,像C/C++一样使用malloc/free或者new/delete来为对象分配释放内存。这张方法的优点是速度快,没有任何额外开销,缺点是要去人工了解每个对象的使用情况,这样很容易发生各种问题,比如内存泄漏,野指针和空悬指针等。

2.使用引用计数,它的思想是对象创建出来以后,维护一个针对该对象的计数,使用该对象的地方对该计数加1,使用完后减1,当计数为0时,销魂该对象。这种方法类似半自动内存管理方式,优点是可以把分配和释放的开销分布在实际使用过程当中,速度比较快,不过会存在一个循环引用的问题。引用计数是一种比较常用的内存管理方法,比如U3D中物理引擎PhysX就是使用引用计数来管理各种对象的。

3.追踪式GC器,unity使用的GC器是一种叫标记/清除的算法,它的思路是当程序需要进行垃圾回收时,从根出发标记所有可以到达的对象,然后回收没有标记的对象,这是一种全自动的内存管理方法,程序员完全不用追踪对象的使用情况,也不存在循环引用无法回收的问题。在unity中使用的是一种叫boehm-Demers-Weiser的GC器,它的特点是:

(1)stop the world ,即当GC发生时,程序的所有线程都必须停止工作,在回收时也要停掉所有线程。

(2)不分代,.NET和java会把托管堆分成多个代,新生代的内存空间非常小,而且一般来说,GC主要集中在新生代上,让每一次GC的速度很快,但是在U3D中GC是完全不分代的,只要发生GC,就会对整个托管堆进行GC。

(3)不压缩,不会对堆内存进行碎片整理,类似我们的磁盘一样,使用久了就会有很多的碎片,造成磁盘上有很多小空隙。同样在U3D中GC会造成托管堆出现很多这样的间隙,这些间隙不会合并,当申请一个新对象时,如果没有任何一个间隙大于这个新对象的大小,堆内存就会增加。

二、影响GC性能的因素

主要因素有2个:

1、可达对象的数量

2、托管堆的大小

可达对象是不会被GC回收的对象,减少这类对象的方法是减少对象的数量,如下:

将会产生10个Actor对象。

如果使用以下方法:

只产生一个对象,方法很简单,但是却很有用,如果在需要每帧去处理的对象上去使用这类优化,会得到意想不到的效果。

而优化托管堆的大小主要通过以下几个方面:

1、减少临时内存分配,因为临时内存会使内存短暂的增长,而且会产生碎片

2、防止内存泄漏,也就是存在互相引用无法回收对象的情况,或者没用到的对象,但是对它还有引用,导致释放不掉。

关于在lua中读取excel表的优化

最近在做项目的优化时发现,我们项目中有个锻造的UI界面打开特别慢而且使用内存分析工具发现每次打开这个界面都会导致内存暴涨,在手动GC后内存被回收,可确定是临时内存暴涨,最后发现这个界面每次打开都会去读取一个很大的表数据,造成临时内存的上涨。

如果在lua中去读取一个很大的表,比如策划配置的excel表,假如这个表有4000多行,而且数据项比较多,那么会导致读取时间变长,而且会使临时内存暴涨

就算这个表已经被加载到lua内存中,当你再次去读取这张表并且把它保存在局部变量时也会使临时内存暴涨 ,因为这个是值数据需要开辟临时内存空间

所以在读取表数据时应该避免去读取整张表的数据,而是根据索引去读取单个数据或者是某些数据。如果表结构不满足条件就应该和策划商量把表拆开来配置,这样才不会导致临时内存暴涨。

实在不行也不要每次打开界面都去读取这么大的表,应该在首次打开的时候把这个表数据保存起来,下次打开时避免再次读取。

在lua中使用os.time的时区问题

最近做项目发现在Lua中使用os.time把服务器时间转换为时间戳时,如果修改小时的话会导致在U3D中打出的包如安卓、PC上时区和服务器时区对不上。

比如服务器时间是2020年3月22日12时30分,服务器使用的是中国北京时区, serverTime 传到客户端后,假如客户端时区调整为韩国首尔的时区,如果使用lua的 local a =os.date(“!*t”, serverTime) 转换为世界日期,然后再使用a.hour = 5,修改小时后,再使用 os.time(a) 转换为时间戳,此时的时间戳在安卓包和PC包上将不会是 世界时间的时间戳,而是本地时间的时间戳,因为你中间修改过小时数,但是在U3D编辑器中运行却是正常的。如果你中间没有修改过小时数,那么转换是正常的,这是一个很诡异的问题。