关于244128133

Unity3D游戏程序员

没有公网IP如何实现内网服务器访问?

发现好久没写博客,准备把我的阿里云服务器降一下配置,因为使用率不高,只是挂一个博客而已,多出来的40GB数据盘可以释放掉了,这样每个月大概可以节省14块钱,虽然不多,但是以年为单位算的话累计起来还是挺多的。

后续打算在家里搭建一个内网服务器,使用FRP内网穿透实现外网访问内网。毕竟阿里云服务器有太多局限性,带宽、云盘、CPU、内存每一个都要单独算钱。

不过也不能完全不用,因为现在三大运营商的宽带都不给你公网IP地址了,没有公网IP地址意味着你无法把内网的服务器提供给外网使用,后续需要搭建服务器或者NAS都很困难。

最近几天打电话给电信说了很多就是不肯给公网IP,说现在开固定IP的宽带要拉专线,一个月700多块钱,我听了快吐血,普通家庭谁用得起一个月700的宽带,这不是变相的捆绑销售吗?

虽然说IPV4地址匮乏,但是也不至于这样来恶心我们。

还好作为一个程序员,这难不倒我,你不给我公网IP,那就另找方法,用阿里云的服务器搭建一个FRP服务就行了,这样远程桌面可以使用XTCP实现点对点通信,也不消耗阿里云的流量。

搭建完成后发现这个远程桌面比向日葵好用太多了还不会限速,还用什么第三方远程软件啊,真是香,后悔没有早点搭建。

接下来就是给家里搭建一台内网服务器+NAS,实现外网正常访问。

海明码的编码和校验方法

  海明码(也叫汉明码)具有一位纠错能力。本文以1010110这个二进制数为例解释海明码的编码和校验方法。

  编码

  确定校验码的位数x

  设数据有n位,校验码有x位。则校验码一共有2x种取值方式。其中需要一种取值方式表示数据正确,剩下2x-1种取值方式表示有一位数据出错。因为编码后的二进制串有n+x位,因此x应该满足

2x≥n+x+1   

  使不等式成立的x的最小值就是校验码的位数。在本例中,n=7,解得x=4。

  确定校验码的位置

  校验码在二进制串中的位置为2的整数幂。剩下的位置为数据。如图所示。

位置1234567891011
内容x1x21x3010x4110

  求出校验位的值

  以求x2的值为例。为了直观,将表格中的位置用二进制表示。

位置00010010001101000101011001111000100110101011
内容x1x21x3010x4110

  为了求出x2,要使所有位置的第二位是1的数据(即形如**1*的位置的数据)的异或值为0。即x2^1^1^0^1^0 = 0。因此x2 = 1。

  同理可得x1 = 0, x3 = 1, x4 = 0。

位置00010010001101000101011001111000100110101011
内容01110100110

  因此1010110的海明码为01110100110。

  校验

  假设位置为1011的数据由0变成了1,校验过程为:

  将所有位置形如***1, **1*, *1**, 1***的数据分别异或。

  ***1: 0^1^0^0^1^1 = 1

  **1*: 1^1^1^0^1^1 = 1

  *1**: 1^0^1^0 = 0 

  1***: 0^1^1^1 = 1

  以上四组中,如果一组异或值为1,说明该组中有数据出错了。***1 **1* 1***的异或都为1,说明出错数据的位置为1011。

Unity Shader模板测试用法记录

之前经常忘记模板测试的用法,现在把这些用法记录在博客上免得以后忘记了。

在Shader的Pass开头写Stencil{ }结构体,如果每个Pass都用,则可以提到外面。

Stencil {  
                Ref 2                     //参考值为2,stencilBuffer值默认为0  
                Comp always               //stencil比较方式是永远通过  
                Pass replace              //pass的处理是替换,就是拿2替换buffer 的值  
                ZFail decrWrap            //ZFail的处理是溢出型减1  
            }  

Ref 2:设置参考值。除了2,还可以设置0-255的任意数。

Comp equal:表示通过模板测试的条件。这里只有等于2的像素才算通过测试。除了equal,还有Greater、Less、Always、Never等,类似ZTest。

Pass keep:表示通过模板测试和Z测试(注意是都通过)的像素,怎么处置它的模板值,这里我们保留它的模板值。除了keep,还有Replace,IncrWrap(循环自增1,超过255为0),IncrSat(自增1,超过255还是255),DecrWrap,DecrSat等。

Fail decrWrap:表示没通过模板测试的像素, 怎么处置它的模板值。这里为循环自减。

ZFail keep:表示通过了模板测试但没通过Z测试的像素,怎么处置它的模板值。这里为保持不变。

除此之外,还有ReadMask和WriteMask语法,用来提取Ref值或模板缓存的某几位,默认是255,全部提取。

jenkins打包环境搭建的一些坑点

特色

最近为了提高版本更新的效率,搭建了jenkins环境,因为我们现在比较多的海外版本,更新频率越来越高。

导致我们前端花费了大量的时间在导UI和版本更新上,而且这些基本上都是一些重复性的工作。由于一些历史原因,每次策划的一些UI修改和调整都需要前端去导UI,然后提交SVN,测试和策划更新下来才能看到效果,非常麻烦。

所以特地花了2天的时间去做这件事,其实本身我们的框架导UI和打包都挺方便的,但是现在海外版本多,还是会比较复杂,而且人工操作容易出错。

jenkins相信做游戏的都不陌生,搭建起来其实也比较简单,去官网下载一个安装包即可,但是要适配自己的项目情况,配置一下参数是比较麻烦的,特别是要做一些自动化的操作,比如复制文件自动提交SVN等。

最麻烦的是在写批处理脚本的时候遇到的一些在jenkins上比较蛋疼的问题:

一、调用unity函数打AssetBundle的过程中发现老是会出现AB还没打完jenkins任务就执行完成了,也不是执行失败,就是提前结束了,这样导致C#里面一些自动拷贝文件的函数都没法执行下去,因为UI都没有完全导完。这个问题也是困扰了我很久,因为我特地试过脚本在BAT文件上执行是没问题的,但是就是在jenkins执行的时候出问题,后面实在是没办法就,写了一个循环每隔5秒钟去检测一次导出的LOG判断UI有没有导完,在C#导出完成的时候输入一段完成的LOG信息。

批处理调用U3D导AB的命令如下:start %UNITY_PATH% -projectPath %PROJ_PATH% -quit -batchmode -logFile %LOG_PATH% -executeMethod JenkinsExportManager.ExportHotFixOnlyJenkins

其中%LOG_PATH%即是导UI过程中unity输出的LOG路径(自己定义),在这个命令后执行循环检测:

:begin
@echo off
ping /n 5 127.0.0.1>nul
echo on
echo 正在导出,请稍后!
findstr /i /c:”export end” “%LOG_PATH%” >nul 2>nul && goto end || goto begin
:end

循环查找LOG中的“export end”字符串,如果有则UI导出完成了,可以继续往下执行,不然jenkins是无法判断导出进度的。

二、第2个坑点是我们想Jenkins上导出的UI复制到FTP共享盘上,这样策划和测试都可以用,因为Jenkins是运行在打包机上的,这台机器的配置也比较高,打包和导UI的效率比较高,他们可以使用Jenkins导出UI到FTP上,然后再从FTP复制到本地,查看UI的效果,确定没有问题了再提交SVN,这样既实现了人人可以用,又保证了安全性。

但是就在这个地方又卡了我好久,因为在Jenkins上死活识别不了共享盘的路径,明明已经做了共享盘的映射,但就是识别不了,我直接在CMD上执行是可以识别的,但是一放到Jenkins上就不行了,最后在Jenkins上执行了一次享盘映射的命令:net use X: \172.16.20.35\ftp /y 才识别上,真是奇葩。

三、第3个坑点是在Jenkins上是可以设置参数然后在命令中获取的,但是如果在设置的参数判断里面定义set命令然后给变量赋值是不起作用的,这就很奇怪了。本来是想利用不同的参数值设置不同的路径实现一些自动化操作,结果导致我只能把绝对路径全部写上。

比如在Jenkins上设置了一个region参数用来选择地区,然后在命令上做如下判断:

if "%region%"=="国内" (
   set NEWPATH=C:\ui
)

结果发现变量%NEWPATH%是空的,根本赋值不了,在if外面试可以赋值的。

对于这个问题,暂时还没有找到别的方法,只能是写死绝对路径不在Jenkins参数的判断里面去赋值。

四、第4个坑点和第一个类似,也是命令还没执行完成Jenkins就结束任务了,本来是想在导出UI完成的时候,复制一些文件到某个地方的,但是经常发现文件还没复制完成只复制了部分文件Jenkins就结束任务了。对于这个问题我也只能使用命令:

@echo off
ping /n 10 127.0.0.1>nul
echo on

来达到延迟的效果了,10代表延迟10秒才往后执行。

最终配置效果如下:

从此再也不用我们前端导UI啦!前端是万能的。

Unity中UGUI空格不自动换行处理

在UGUI的Text组件中,文字超出宽度会自动换行,如果有空格则会从空格处自动换行,在英文中这种方式是完全没有问题,但是某些语言比如泰文,是不需要在空格处换行的,那就需要特殊处理了,可以通过使用富文本的方式把空格替换掉,比如使用Replace(” “,”<color=#00000000>.</color>”)的方式把空格替换为一个透明的点。

如果不想使用富文本也可以使用unicode字符“\u00A0”,比如Replace(” “,”\u00A0”)替换我们常用的空格,或者使用全角空格替换半角空格。因为我们常用的空格是半角空格,这三种方式都可以实现替换空格后保留空格的效果并且不在空格处自动换行。

我们项目的发行商最近反馈泰文的换行问题,我暂时是使用的“\u00A0”替换字符的,感觉还不错,没出现其他问题。

关于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,所以计算量比较大。