A. 网络相关知识

一、TCP

1、面向数据流。可靠。能保证消息到达顺序。

2、滑动窗口。控制发送量,发送方只能发送窗口内大小的数据包。防止发送方发送的数据过多,接收方无法处理的情况。

3、Nagle算法。默认tcp发送包会将数据包合并到一起发送。好处是性能更好。坏处是会有延迟发送(100ms)的可能。在实时游戏的应用,应该禁用Nagle算法,消息及时发送。

4、三次握手。

第一次握手,客户端发送连接请求(SYN)。

第二次握手,服务器回应客户端的连接请求(ACK+SYN)。

第三次握手,客户端回应服务器(ACK)。第三次握手主要是为了避免服务器收到过期的连接请求,建立无效连接,导致资源浪费。只有经过客户端回应的,明确有消息要发送的,才会真正建立连接。

5、四次挥手

第一次挥手,客户端请求释放连接(FIN)。此后客户端不再向服务器发送数据,但是可以接收到服务器的数据。

第二次挥手,服务器回应客户端释放连接的请求,把剩余数据发送完毕(ACK)。

第三次挥手,服务器告诉客户端已经可以断开连接了。(FIN+ACK)

第四次挥手,客户端回应服务器,之后正式断开连接。(ACK)

6、建立连接的时候,第二次握手其实包含了两个操作,对客户端的应答和通知客户端可以建立连接。

而断开连接的时候,因为有数据要处理,所以先对客户端进行应答,处理数据。然后再通知客户端可以断开连接。

所以握手是三次,而挥手是四次。

7、优雅的跟服务器断开连接

a、我们在对socket的封装会有一个缓存,发送数据包不是理解丢给socket,而是拷贝到缓存里面,每帧发送数据。这么做的好处是有利于io。

所以在关闭连接的时候,客户端首先要保证所有的数据都交给socket发送,并且确认服务器收到了,然后才可以关闭socket。

b、关闭过程,先进行 socket.Shutdown。这个接口有个参数可以控制是关闭双向连接,还是只关闭接收连接。我们是简单处理为直接关闭双向连接。

c、调用 socket.Close。释放句柄。之前我们关闭socket的时候只调用了Close而没有Shutdown。这可能会导致服务器没有收到断开连接的消息。

8、现在判断网络连接是否断开我们是使用 socket.Connected 属性来做判定的。不过这个并不准确。只要正常连上,这个属性就为true。但是并不意味着可以正常发送消息。

另外一个判定网络连接的方法是通过socket.Poll 来取数据,能取到数据才证明网络连通。

二、UDP

1、面向数据包。不可靠。不能保证数据包的到达顺序。速度快。

2、KCP。来实现可靠的UDP协议。通过重发策略,用多30%流量的代价,换来成倍的性能提升。

三、Https

1、Http、Https、Get、Post

1.1、我们跟游戏服务器的连接都是TCP长连接,但是跟平台登录认证服务器的连接是Http短连接。像登录认证这种一次性的请求,没有必要使用长连接,长连接会占据socket句柄。

http并不保证安全,可能会被抓包,伪造数据,或者是运营商CDN拦截等等。而https就安全很多。所以我们正常对外都应该使用https。苹果和Android陆续都有政策,禁止http的访问。

1.2、Http.Get和Http.Post是两种请求方式。具体差异可以多参考网上的文章。我们只需要简单的理解为Get就是参数都在Url里面发送给服务器。而Post的参数都放在Body里面。

Get的参数在Url里面,玩家在浏览器或者抓包的时候就非常容易看出参数的格式和内容。而且参数大小也是有限制的。

而Post的参数放在Body里面,就解决了上面的问题。所以涉及到账号密码传递,或者给服务器上传一个图片数据、iOS充值的收据数据等等,都应该使用Post。

1.3、URL Encoding

正常Url格式是:

https://www.xxxx.com/aa?player=123&level=456

有一些字符是保留字符,可以理解为关键字,是不能在url中使用的。比如空格。

URLEncoding所做的工作,就是把这些字符编码为可使用的字符。比如空格会被编码为 "%20"。

其他一些保留字符,比如 "?"-->"%3F"、"/" --> "%2F"、"&"-->"%26"、"="-->"%3D"等。所以我们在Url中经常会看到 %20 这样形式的字符。

1.4、Base64编码

Base64编码常用于网络传输二进制数据。像图片数据、iOS收据等,都是经过Base64编码再传递给服务器。

很多协议其实是纯文本协议,比如SMTP协议。如果直接传递二进制数据,可能会遇到控制字符,导致传输失败。这种情况下需要将二进制数据转换为字符形式,然后再发送给服务器。

1.5、Unity下现在进行Https通信,使用的是 UnitWebRequest。原来的WWW被废弃了。

2、Https下载

Https下载是游戏的基础功能,有几个细节点,单独提出来说一下。

2.1、我们下载补丁也是 Https 连接CDN进行下载。内网可能没有配置合法的Https证书。这种情况下我们应该禁用证书检查。

ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => { return true; };

2.2、正常CDN会有缓存机制,有的时候不止CDN,运营商(比如电信运营商)也会有缓存。如果我们替换了一个文件,比如 patchconfig.txt,有的时候CDN返回的会是旧的文件。

解决方法是在请求的url地址后面加个 ?v=123 这样的参数。

https://www.xxx.com/pathconfig.txt?v=123。

重点是123这个参数,根据版本变化。这样就可以保证我们更新版本之后,取到的文件都是新的。

类似的,我们是禁止替换已经发布到CDN上的zip补丁包的。真有严重问题,需要替换的话,需要运维强制刷新CDN。不过这个只能保证CDN是新的,而运营商也可能会有缓存导致在某些地区取到的还是旧的文件。

如果有一个文件我们期望禁止缓存,每次都重新取最新的,那么后面的参数改成随机数+时间戳就行。

2.3、我们游戏内下载补丁是基于 HttpWebRequest 封装了一个FileDownloader。没有使用 UnityWebRequest 是因为,这个api在进行http通信的时候比较方便,但是它提供的监控下载进度的回调接口是在主线程的,如果用这个接口会因为频繁的磁盘io导致主线程卡顿。

如果是下载整包apk,可以使用Java原生的下载库,比如Okdownload。它的好处是可以后台下载。甚至可以开服务,游戏进程杀掉,依然可以正常下载。

四、关于游戏通信对Tcp、Udp、Http的选择

1、早期卡牌游戏可以使用Http。它是短连接,只能客户端向服务器发起请求。服务器很难主动通知到客户端。如果有一些事件是等待服务器执行完毕,那么就只能通过客户端轮询。

好处是不用关心断线重连。服务器不维护socket连接,也就不会担心因为socket句柄数量限制了一台机器的在线人数。

http协议头比较大,速度慢,不太适合对及时性要求比较高的游戏。

2、现在正常游戏都是使用tcp。tcp是长连接。客户端和服务器之间通过网络消息进行通信。tcp能够保证消息的顺序。如果有丢包,会进行重传。

3、部分对及时性要求非常高的游戏,比如王者荣耀这样的MOBA游戏,或者联网的格斗游戏,可以使用udp。

udp是不可靠的,但是速度非常快。因为它不用关心是否有丢包,也不会重传,也不会像tcp一样,某个包一直无法发送成功会阻塞其他包。

4、一般使用udp的游戏,都会在上层在维护一个包重传机制,这里推荐的是kcp。它是一个可靠的udp协议。

五、消息协议

1、tcp是数据流,我们收到的数据可能包含不止一条消息,也可能存在不完整的消息。我们需要定义好消息头,并且根据消息头解析出完整的消息。消息头最核心的是 消息长度+消息Id 。通过这两个信息就能够确定哪些是一条消息,是什么消息。网上常说的粘包处理就是这个过程。

2、使用protobuf做消息协议。好处是:

2.1、客户端和服务器用的协议是一致的。避免人为修改协议导致消息结构不一致。如果是C++的话,可以自动生成读写的代码。

2.2、protobuf自带哈夫曼压缩。可以减少消息大小。压缩后是二进制格式,也可以增加工作室破解难度。(补充一下,proto编译的pb文件,一定要再做层加密,否则几乎跟明文一样,很容易就被反编译出消息结构了)

2.3、protobuf是向前兼容的。只要不修改消息序号,添加新的消息字段不影响之前的客户端解析。

2.4、解析速度很快,比json要快。

3、我们Lua里使用的解析protobuf的库是pbc。没有使用sproto是因为sproto只有lua的接口。而我们还有很多C++解析的需求,很多服务器内部通信或者执行非常频繁的消息,都是C++直接处理的。

4、修改pbc的源码,decode的时候解析全部字段。默认pbc只会解析一层结构。其他嵌套的结构使用一个字符串保存,等访问到的时候再展开解析。当我们定义的消息结构中有Vector3这样频繁使用的结构的时候,这么设计反而是性能低下的。使用全解析,把所有的结构都展开。

5、客户端增加消息缓存。频繁使用的消息,在发送或者接收之后,保存起来,等用到的时候重置下参数,然后交给pbc使用。这样可以很大程度上减少lua的gc。

6、还有一种通信方式是RPC。它可以使用函数的方式完成客户端和服务器,或者服务器内部的通信。传递的数据就是函数的参数。一般是没有返回值的。

一般服务器内部通信适用于RPC,多个进程之间以函数调用的方式进行通信,写起来非常方便,不用额外定义或解析消息协议。

客户端和服务器如果是一套框架,最好是静态编译语言,也可以使用RPC。比如UE4中客户端和服务器通信就是使用的RPC。同样不需要定义协议。

但是如果是脚本语言,或者客户端使用Python,服务器使用C++,这样框架不同的架构,就不太适合使用RPC。参数改了,出现不一致了,也很难发现错误。脚本语言是弱类型的,也很难知道参数的具体类型或者结构是什么。只能依赖注释。而注释又不具备强制性,万一注释错了,也不会导致编译报错。总之这种情况下,使用RPC会降低开发效率,增大出Bug的可能。

六、Socket的同步/异步、阻塞/非阻塞

1、同步模型,调用Send接口,直接得到发送结果。而异步模型是在回调里面得到发送结果。

2、阻塞模型,调用Send接口,线程卡死,直到发送完毕。非阻塞模型,则是发送多少直接返回,不会有卡死线程等待的过程。

3、我们之前做端游的时候,用过同步非阻塞的socket模型。好处是所有的调用都是在主线程执行。主线程不停Update处理消息,不用关心多线程同步,由于是非阻塞模型,也不会卡住主线程。

4、在C#中,我们常用的模型是异步阻塞模型。其他用法都或多或少存在一些问题,不太好用。

核心接口是 BeginSend和BeginReceive。

在主线程调用BeginSend,它会产生一个子线程,socket.EndReceive 函数会挂起子线程,直到发送完毕。

使用异步模型,要处理好子线程和主线程的同步。简单来说就是对成员变量的buffer和size进行操作的时候一定要加锁。

5、BeginSend这个接口存在一个性能问题。它回调中产生 IAsyncResult 对象没有缓存池机制,所以会产生一定的GC Alloc。所以,更加推荐的替代接口是 SendAsync和ReceiveAsync。

七、实际项目的经验

1、善用缓存buffer。发送和接收数据都有相关的缓存buffer。比如发送数据不是调用Send接口就直接发送给socket底层,而是拷贝到sendBuffer上,每帧统一发送数据。这么处理有利于减少io操作。

2、使用异步socket。我们使用的是BeginReceive接口。还有ReceiveAsync接口。其实更加推荐后面这个,因为它可以避免 IAsyncResult 对象的频繁创建,有利于gc。

还有个同步非阻塞的接口。不过C#中并不好用。

3、异步接口的回调是在子线程中执行的。所以一方面要处理好线程同步,拷贝buffer的时候要加锁。另外一方面在回调线程中不能调用Unity和lua的任何东西。都要统一在主线程处理。

4、发送数据 sendBuffer 的处理相对简单一些。lua中有消息发送就把数据拷贝到buffer上。每帧Flush。发送完毕就把已发送的数据移除。

5、接收数据有几个buffer。一个是socket用的接收数据缓存 receiveBuffer。在回调中会把数据从 receiveBuffer 拷贝到 msgBuffer 上。在主线程会解析处理 msgBuffer。解析到的每个消息会拷贝到 msgData 进而传递给lua。

6、socket.Connect 是没有超时机制的,这个要在上层自己做。超过一定时间还没有回应,则判定超时。

7、禁用nagle算法。socket.NoDelay=true

8、域名解析的时候,优先使用ipv4的,如果没有ipv4的,则使用ipv6的地址。ipv6主要用于苹果审核。苹果审核需要游戏支持ipv6 only的环境,可以用Mac系统模拟对应环境,socket的连接地址不能直接使用ip地址。需要用域名做兼容。如果不用域名,那么就需要同时提供ipv4和ipv6的地址。

9、接收到的一条消息的bytes是如何传递给lua的。这个是最常见和底层的需求。

9.1、最早我们是直接用 LuaFunction.Call 这个接口。这个接口是有装箱操作的接口,并不推荐使用。

9.2、使用xlua提供的Action接口。这个是泛型接口,避免的装箱和拆箱的操作。其实我们所有的对Lua的函数调用,都应该使用 Action 接口。如果 Action 接口参数不匹配,就扩展下泛型参数。更进一步的,对于常用的类型,可以补充对应类型的 Action 接口实现,这样连泛型匹配类型的操作都可以省去。

9.3、xlua处理bytes类型的时候,是把整个bytes都传递给lua。所以在回调Lua的时候,不能把缓存池直接传递给Lua,而应该new一份新的bytes,否则明明很小的一条消息,都需要把256k的缓存池传递给lua,产生对应的lua内存,显然是不合理的。这部分可以参考 LuaDll.cs中的 lua_pushstring 函数实现。

9.4、针对以上问题,我们最终的实现是,参考Action,实现一个 CallWithMsg 的接口。可以指定传递给lua的字节长度。这样就可以直接使用缓存buffer,避免了很多无意义的GC Alloc。

这里唯一内存开销,是 lua_pushlstring 会在lua状态机里面产生对应的string拷贝。当然这个是无法避免的。

B.算法相关知识

(实在不熟,此篇请忽略)

1、快速排序实现原理 时间复杂度 n LogN

2、红黑树实现原理

2.1、节点是红色或者黑色。

2.2、根节点是黑色。

2.3、每个叶子节点都是黑色的空节点。

2.4、每个红色节点的两个子节点都是黑色。

2.5、从任意节点到其每个叶子的所有路径都包含相同的黑色节点。

3、广度优先搜索 深度优先搜索 A*

广度优先搜索(Breadth First Search,BFS)。队列实现。最短路径。A*搜索。先搜索自身周围的格子,然后再进一步搜索更外围的格子,知道最后所有格子都走到。

深度优先搜索(Depth First Search,DFS)。堆栈实现。沿着一条路走到底,然后再走另外的路。

4、动态规划

5、屏蔽词库的实现算法

DFA算法(Deterministic Finite Automaton,确定有穷自动机)。

构建一个屏蔽词树。有效减少需要比对的字符。

6、红点树。有效处理父子节点的红点显示关系。

7、二叉树的前序遍历:对于一个节点,先输出节点自身,再输出左节点,最后输出右节点。

中序遍历:先输出左节点,再输出自身节点,最后输出右节点。

后序遍历:先输出左节点,在输出右节点,最后输出自身节点。

C. 平台相关知识

一、Android

1、如何编写 Android Native代码。

1.1、如果是C/C++代码的话,交叉编译成不同cpu架构的.so。放到 Plugins/Android/libs/arm64-v8a

armeabi-v7a x86等目录。推荐直接在Mac下编译就行。C#的调用方式就是正常的C#调用C++的代码。

1.2、如果是Java代码的话,可以使用Android Studio,将 Java工程编译成.aar的代码。.aar就相当于.jar+资源。注意,编译的sdk版本,build tools版本都应该跟游戏工程保持一致。

1.3、CMake可以通过CMakeLists.txt的配置,生成各个平台IDE的编译工程。比如Windows下是VS工程,Linux下是makefile,Mac下是xcode工程。它省去了人们去维护不同工程的代码、依赖、编译选项等等复杂繁琐的过程。

xlua就是通过CMake编译各个平台的动态库的。

2、AndroidManifest.xml关键内容说明

2.1、uses-permission 配置权限。比如访问摄像机、访问麦克风等等。Android 6以上是运行时权限管理。在这里配置了,真正访问的时候还是要弹出提示框让玩家允许。为了防止玩家拒绝,很多游戏或者应用在弹权限授权窗口之前,会先弹一个提示说明框,告诉玩家后面申请的权限是用来做什么的,这样用户更大的概率会允许权限。

Android 6以下是在安装的时候提示用户。用户只能选择是允许安装还是禁止安装。现在普遍应用商店都需要 Android 6 以上的支持了。

2.2、android:targetSdkVersion 这个是个很关键的属性。它代表当前打包的app的目标系统版本是多少。比如,即便我用最新版本的sdk打包。如果targetSdkVersion设置为21,那么我们的app也是不支持运行时权限管理的。

apiLevel=23 对应 Android 6。最大的变化是支持运行时权限管理。

apiLevel=27 对应 Android 8.1。最大的变化是Notification 推送通知,增加了 Channel机制。一些列通知可以放在一个Channel下。

apiLevel=28 对应 Android 9。最大的变化是官方sdk支持了异形屏相关接口。之前都是不同手机厂商自己做的功能支持,接口也千奇百怪。

3、兼容异形屏

3.1、AndroidManifest.xml 中设置 max_aspect

<meta-data android:name="android.max_aspect" android:value="2.4" />

在android9之前,不同的手机厂商也有自己的设置参数。

3.3、在Android 9之前,不同厂商有自己的异形屏适配方案。比如华为就是设置 android.notch_support 为 true。

同样,判定是否是异形屏也是不同厂商有不同的判定接口。比如华为的判定接口是

* com.huawei.android.util.HwNotchSizeUtil

* public static boolean hasNotchInScreen

这些由于不是官方sdk的api,所以应该通过反射进行调用

4、Gradle

4.1、Android的app打包,使用的是Gradle。它可以理解为是Java界的CMake。处理依赖包,编译,打包。语法是 Groovy。在Gradle出现之前,人们使用的是Ant和Maven。不过Ant配置繁琐(使用xml)。Maven支持从网络上下载依赖包。而Gradle则结合了两者的优点,既能够管理依赖,又能够构建项目。所以现在Java的构建工具普遍都是用Gradle。

4.2、Unity打包会有一个默认的gradle配置。如果需要自己做一些配置,比如增加一些依赖库,可以将Unity默认的配置模板复制到 Plugins/Android/mainTemplate.gradle。

4.3、一些关键性的修改。比如添加阿里源。这个可以避免网络稳定导致打包时间过长或者失败。

repositories {

maven {url 'http://maven.aliyun.com/nexus/content/repositories/central/'}

jcenter {url 'https://maven.aliyun.com/repository/jcenter'}

google

}

4.4、配置ab包不压缩

aaptOptions {

noCompress '.unity3d', '.ress', '.resource', '.obb', '.ab', '.db', '.mp4', 'android'

additionalParameters "--no-version-vectors"

}

5、Android Support库

5.1、我们在高版本sdk上进行开发,用到了高版本的特性,低版本系统上不具备相关特性或者是接口。如何保证我编写的app能够正常的在低版本的系统上运行。

为了解决这个问题,Android设计了support库。用来处理对低版本系统的兼容支持。

比如我们用到了运行时权限的api。如果使用 ActivityCompact 中的接口,那么它就会做判定,在apiLevel < 23 的设备上,就直接返回 true。>=23的设备上才会触发权限请求。

5.2、因为之前的 android.support.v4/v7/v13 等库设计的过于凌乱。所以Google退出了新的 AndroidX,它是之前support库的替代品。在 gradle.propertise 中配置 android.useAndroidX=true。可以使用AndroidX。

5.3、我们接的很多渠道sdk,里面就包含了support v4的库。如果我们自己游戏里面就有用到support v4,那么这个一般是可以删掉渠道sdk里面的v4库。多个不同版本的support v4库可能会产生冲突。导致编译报错或者是运行报错。

6、Android下的dex函数数量限制

6.1、classes.dex是有最大函数数量的限制。65535个。当应用做的比较大的时候,这个很容易就超出了。

6.2、为了解决上述问题,Google提供了 MultiDex 的功能。dex可以拆分为两个文件。

6.3、一般情况下我们用Unity打包游戏Java函数就几千个,很少会超出限制。不过如果我们接了渠道sdk,他们很可能会引入support v4/v7的库。Java函数一下子可能就变成几万个,就可能超出数量限制了。

6.4、如果个别发行商或者渠道不能使用 MultiDex。那么我们就需要裁减函数。使用 proguard可以混淆或者裁剪代码。Plugins/Android/proguard-user.txt,在 Player Settings 里面设置好这个文件。具体配置格式可以参考网上的 proguard 教程。需要留意的是,大多数我们自己写的代码都是需要保留的,都要配置上 keep。只有系统sdk的内容是可以裁剪的。

7、Android的推送通知和本地通知

7.1、暂时没有太多可以说明的。推送通知一般使用极光推送这样的第三方服务。

7.2、本地通知。通过 AlarmUtils.addAlarm 开始计时,时间到了会响应 OnReceive 接口。在这个接口里面使用 NotificationManager 注册通知。点击通知的处理为打开游戏。

注意,这里需要判定游戏是否运行在前台,如果运行在前台,则不应该弹出通知。

8、Android原生的下载实现

8.1、我们正常下载补丁是游戏内通过 C#的 HttpWebRequest来实现的。而如果是下载apk这样比较大的文件的话,还可以使用Android原生下载。推荐的开源库是 okdownload。

8.2、原生库,一方面是经过验证的高效稳定的实现。另外更关键的是支持后台下载。也就是游戏切后台也可以继续下载安装包。玩家此时可以做别的操作。更进一步的,可以开个下载服务,即便游戏进程杀掉,也可以继续下载。像头条、抖音中下载推广的游戏,都是开了一个服务来下载的。

9、obb 和 app bundle

9.1、Google Play的应用商店上传限制100MB大小。所以很多游戏会把资源拆分到obb包里面。obb包可以是任意格式。游戏自己维护加载。

下载的扩展文件会保存到 /Android/obb/<package-name>/ 目录下。

删除游戏后,obb也会一起删除。

9.2、国内应用商店没有大小限制,所以国内的包普遍都没有用过obb。

不过apk包本身有个2G的大小限制,所以当包体积超过2G,还是要拆分资源。多余的资源,可以运行时下载,就跟微端或者更新包差不多。

9.3、app bundle可以类比为iOS的bitcode。用户上传app bundle。Google Play会根据机型、设备,构建不同的apk,既可以减少包体积,又有利于针对性的性能优化。

Unity和Unreal都支持app bundle。不过国内的应用商店还都不支持 app bundle。所以现在还没有大规模使用。

10、渠道二次打包

10.1、国内渠道繁多,一般首发游戏都需要面对十几个渠道,每个渠道都有自己的sdk。所以对接渠道sdk也是很耗时的工作。

10.2、为了减少程序的开发量,快速对接sdk。就出现了棱镜Sdk、AnySdk等All in one的Sdk。开发者只需要接入AnySdk的包。其他的渠道AnySdk会自动对接。

10.3、渠道sdk涉及到登录留存数据、充值数据等机密内容,交给第三方服务总归会有些不妥当。所以我们参考U8SDK自己实现了一套渠道二次打包的流程。

10.4、游戏接入空的框架 jar,包含登录、充值等接口。只不过没有实现具体功能。框架去对接各个渠道的sdk。

打包流程是我们先生成一个客户端母包apk。然后使用二次打包工具基于母包apk打各个渠道的apk。在此过程中,可以定义包名、游戏名、游戏图标、签名文件等等内容。

打包的原理是使用apktool解包apk,替换框架jar为实际渠道jar,然后再次使用apktool打包和签名,得到渠道版的apk。

10.5、有个别渠道商店,会在我们上传apk后,做类似的操作,替换签名文件。所以如果我们使用了一些加固服务,做了禁止更换签名的限制,在上这些渠道的时候会有问题。

二、iOS

1、充值

1.1、充值的基本流程是,向平台请求订单号,然后调用addPayment进行充值,监听iOS的回调,获取充值状态。如果充值成功,则发送收据给平台进行校验。平台校验通过之后,通知服务器发货。

在充值过程中,只有明确收到平台的回应,才会结束交易。这样万一充值过程中客户端闪退,重启之后,系统会再次发起充值流程,直到结束交易。

1.2、代充的问题。之前我们出现很多第三方代充、退款。后面通过一些技术手段解决了这个问题。

SKProduct有一个locale对象,可以获取到商品的国家地区。如果是非法地区,比如新加坡的退款政策比较开放,那么就禁止充值。

小额充值限制。每天6元钱只能充值30笔,防止小额代充。

1.3、掉单的问题。主要有几方面的原因:

主要原因是网络问题,平台校验失败。我们后面增加了服务器重传机制,会对没有校验成功的商品重复发起校验请求。

业务层主要的掉单原因是平台的orderId无效。这里有bug原因,也有网络异常原因,或者极端情况下苹果系统异常。我们之前想把一个orderId和一个苹果的交易id一一对应。后面发现没有太大意义。只要苹果系统通知交易成功,就会有合法的收据和交易id,玩家都是付过钱的。这个时候没有orderId就向平台再请求一个就好了。如果这里直接因为orderId不存在就判定失败,多数情况下就会掉单。

极端情况下,一次点击行为可能产生多次成功的交易,玩家也真正的付过两次钱。这种情况就肯定会出现orderId不存在的情况。

2、如何编写iOS的Native代码

2.1、在 Plugins/iOS 目录下,放一个 XXX.m 或者 XXX.mm 的文件。里面可以写Objective-c的代码。

extern "C" 的函数,可以暴露给C#访问。这里就是普通的C#调用C++的函数的过程。

2.2、同样可以用 swift 来实现对应的代码,或多一层swift接口导出的过程。原理一样。

2.3、.cpp 的源代码也可以放到这个目录下,Unity打包的时候会把这些文件加入到xcode工程中。最终编译到可执行文件中。

2.4、.mm的代码中,通过 UnitySendMessage("go对象名", "函数名", "字符串参数") 回调给C#。注意最后的参数要是一份copy的内存,而且不能为nil。否则会导致闪退。

3、如何判断异形屏

3.1、一开始直接通过iPhone的设备版本来判断。SystemInfo.deviceModel。"iPhone10,3" 就是iPhoneX,是异形屏。

3.2、随着异形屏的设备越来越多。改为通过判定 safeArea 来判定

CGFloat height = [[UIApplication sharedApplication] delegate].window.safeAreaInsets.left;

height > 0 则为异形屏。

4、上传到AppStore

4.1、可以导出ipa,然后用Transporter上传(原来是Application Loader不过后面废弃了)。

4.2、可以在Jenkins下用命令行上传。最新的上传命令是 xcrun altool --upload-app。

4.3、手动在Xcode--Organizer 中上传。不过不太推荐,速度慢,不稳定,还没有进度详情。

三、Windows

(一)、怎么打Windows平台的安装包

1、Inno Setup 使用起来比较方便,使用Pascal做脚本语言。

2、我们使用的是 NSIS。它是自定义的脚本格式。选用这个主要是因为NSIS有个duilib的插件,可以做出自绘的安装程序,可以参考镇魔曲手游桌面版的安装包样式。

3、可执行文件要进行签名。包括自己编译的的dll(如xlua.dll)、游戏可执行文件、其他可执行文件(比如Cef渲染进程文件)。最后安装包也要签名。

4、安装过程中 d3dcompiler_47.dll、msvcp80.dll等文件解压过程会被360拦截报警。我们打包的时候这些文件先移除dll的扩展名,安装完毕之后再把这些dll的扩展名恢复。用这种方法绕过了360报警。

(二)、Windows下如何禁止多开

1、Unity BuildPlayer里面就有一个SingleInstance的选项。不过做的比较简单,它是根据文件路径做信号量的。拷贝游戏到一个新的目录,就可以多开了。

2、我们在游戏启动的时候,检测 Mutex (信号量),如果发现冲突,则证明已有游戏进程在运行,此时直接退出游戏。信号量是可以绕过的。

3、监控进程列表,查找有没有当前 productName 的进程,如果有的话,则退出游戏。这个可以通过修改进程名绕过。

4、遍历窗口标题,发现跟productName一样的窗口名,则退出。这个会更加保险一些。不过依然可以通过一些hook机制绕过。

5、以上这些方法,其实都不能百分百保证禁止多开。工作室可以通过虚拟机、沙盒环境等等方式来多开。技术强的工作室,甚至可以修改驱动。

(三)、Windows下的一些特殊处理

下面这些操作主要从方便开发和测试效率角度考虑做的修改,修改后很多操作都比较方便。

1、可写目录统一放到可执行文件同级的 Data 目录下。如果是多开的话会自动根据进程数量创建新的 Data2、Data3等目录。游戏中使用封装的 Prefs 读写配置,更新补丁也是放在这个目录下,日志也是放在这个目录下。persistentDataPath 几乎不再使用。

这么设计的原因,一方面是方便开发的时候,清理更新包、查看或者删除配置项,或者查看日志。另外一方面是方便多开。多开的时候账号是记录在不同的Data目录下的,同一个目录下的游戏进程多开互不影响。

2、监控日志 Application.logMessageReceived。把日志输出到文件流。每条日志打印的时候加上时间。游戏关闭的时候再把 persistentDataPath 下的 output_log 拷贝到日志目录下。这么做是为了方便查看日志。而 output_log 包含更多的底层日志。比如崩溃日志。

3、帧率控制。Win端开发版本,非激活窗口限制15帧,如果是最小化窗口限制5帧。前台激活窗口保持30帧。这么做可以方便测试多开,提高开发效率,一台电脑八九开不是问题。

四、如何获取设备的唯一码(设备唯一标识)

每个平台都有获取唯一码的需求,实现不同,且各有各的坑。所以单独提出来统一说明。

1、Android获取唯一码

1.1、Android下我们直接使用 SystemInfo.deviceUniqueIdentifier 获取唯一码。它是根据一系列唯一值,如 ANDROID_ID、IMSI等计算出来的。结果是个md5字符串。一般是32位。

1.2、这个值全局唯一且卸载游戏也不会发生改变。唯一的问题是模拟器或者Root后的设备,设备信息可以伪造。也就是说,这个唯一码是可以被修改的。但是由于也找不到其他更好的方案,所以最终还是用这个做唯一码。

1.3、IMEI、MEID。很多广告商,尤其是网页上下载的包。都喜欢用IMEI做用户追踪的唯一码。IMEI和MEID差不多,IMEI是纯数字格式的,MEID里面包含字母。用同样的获取IMEI的接口,有的手机会返回IMEI,有的会返回MEID。大概跟手机是电信手机还是移动手机相关。

这个是需要用户授权的。用户可以拒绝玩家访问IMEI。如果拒绝访问,则返回“0000”的字符串或者是空串。

如果是双卡双待的手机,IMEI可以获取到两个。每个卡槽对应一个IMEI。

2、iOS如何获取UDID

2.1、出于隐私角度考虑。UDID已经是私有api,不再允许开发者调用。

2.2、苹果给的新的替代方案是 IDFV。这个是应用开发商标识符。

com.aaa.xxxx,和com.aaa.yyyy取到的是同一个值。

缺点是,当用户设备上com.aaa的所有应用都卸载了(如果只装了某个公司的一个游戏,那么就是游戏卸载时),这个值就会被清理掉。再安装的话,取的是不同的值。

2.3、IDFA。这个是广告标识符。同一个设备上的素有App都会取到相同的值。是苹果专门提供给广告商来追踪用户的。不过用户可以禁止访问这个值,也可以还原这个值。也就是说用户可以随时在设置中改变这个值。获取这个值之前应该先判断下用户是否禁用了广告追踪。

2.4、现在取唯一码的方案是,取IDFV,然后存放到KeyChain中。下次再取,优先从KeyChain中获取。这样即便游戏卸载了,下次再取到的值,还是原来的值,保证其跟设备完全一致。

2.5、由于SystemInfo.deviceUniqueIdentifier是直接取的IDFV,卸载游戏后,这个值就会发生变化,所以不能直接使用这个接口获取唯一码。

3、Windows平台获取唯一码

3.1、Windows上获取唯一码没有特别好的方案。SystemInfo.deviceUniqueIdentifier 是根据cpu、硬盘等设备信息计算的唯一码,是40位的哈希值。这个接口的问题是,它非常有可能因为硬盘信息的改变而生成不同的值。且硬件信息更加容易伪造。

3.2、为了应对唯一码可能会改变,我们把唯一码存放在了三个地方。安装目录的Data目录下、Application.persistentDataPath 路径、PlayerPrefs注册表。存三个地方是防止游戏卸载导致唯一码改变,也可以防止用户篡改唯一码。当然如果工作室知道规则(很容易知道)那么还是比较容易伪造唯一码的。

3.3、最开始还是用 deviceUniqueIdentifier 获取唯一码,只不过通过备份,防止其修改。不过这个接口获取的值问题很大。很有可能两台不同的设备,其唯一码是一样的,所以我们后来修改为自己生成唯一码。算法是 Guid + 随机数 + 时间戳,这三个得到的字符串进行一次 md5。得到的就是唯一码。然后再存起来,防止后面再用的时候发生变化。

4、唯一码的用途

4.2、统计分析。数据打点。

4.3、游客登录,用作账号。

五、游戏内置浏览器

1、很多游戏都会有内置浏览器的需求。比如王者荣耀的玩家社区。Android和iOS平台,我们直接用的是 UniWebView。Windows平台我们使用的是Embedded Browser 插件。

2、Android平台就是封装了 WebView 控件。我们做了一些修改,主要是运行时权限管理,浏览器重定向(比如我们不期望玩家点击bilibli的视频就跳转到bilibili客户端)。

3、iOS平台是对WkWebView的封装。这个是iOS7推出的替代UIWebView的网页控件。优势是速度快,内存占用低。AppStore现在审核的时候会检测有没有使用UIWebView这个废弃的API,如果有的话会审核被拒。

因为没有源代码,这里还是踩了很多的坑。比如对iOS9不兼容会闪退,没有开放safeArea的api导致浏览器显示白边等等。这些都随着插件的版本升级逐步改进。

4、上面说的 UIWebView 再多啰嗦两句。Unity2017.4.33之前的版本在 URLUtility.mm 中处理游戏内打开浏览器的功能,用到了UIWebView。所以用之前的版本打出来的包,上传到AppStore,就会有 UIWebView 的警告。

如果能升级Unity的话,把Unity升级到最新就能够解决这个问题。

如果不能升级Unity的话,可以把 URLUtility.mm 修改重新编译一下,删除里面的UIWebView相关的内容。把编译后的URLUtility.o 重新打入到 libiPhone-lib-il2cpp.a 这个静态库。覆盖Unity安装目录下的对应库文件。这样之后打包就没有问题了。如果不方便直接修改Unity安装目录的话,可以导出xcode之后,覆盖工程下的libiPhone-lib.a。

具体操作步骤可以参考网上的相关文章。

5、Windows平台使用 Embedded Browser 这个插件。它是对CEF进行了修改。Chromium Embedded Framework 是基于Chromium的WebBrowser控件。可以理解为嵌入版的Chromium。

一开始想尝试用开源的C#版本的CEF封装来做的。不过因为Unity插件的机制问题,很不稳定。Unity的插件是一旦启用,内存就不会释放,而我们游戏会在重复的Play。此时就会有严重的内存问题,可能会导致Unity卡死或者闪退。另外,CEF是多进程架构,网页的渲染是在一个独立进程上执行的,Unity和进程之间的通信处理起来也比较麻烦。

Embedded Browser 解决了这个问题,它集成了CEF库,实现了Unity上的渲染、输入、鼠标等功能。html5test跑分有450分左右,兼容性良好。运行起来也比较稳定。它没有让Unity管理dll,而是通过 LoadLibrary自己管理和释放dll,用这个方式解决了共享内存的问题。

缺点是CEF的通病,CEF太庞大,一个dll就有80MB。不过考虑到是PC平台,似乎也不用特别在意这点包体积。另外就是没有源代码,一些改进也很难处理,比如网页上对MP4视频的支持。因为MP3/MP4都是有专利的,所以CEF默认是不支持MP3/MP4播放的,需要修改工程选项,重新编译才能够支持,不过CEF的代码无比庞大,编译时间可能会有几个小时。

补充说明的是,我们之前尝试过直接使用系统控件的方案。插件是 In-App Web Browser。优点是插件很小,不像CEF会让包体积增大100MB。不过缺点是控件表现受IE浏览器版本影响很大。而PC平台,可能有的玩家是IE7,有的玩家是IE12。兼容性和效果表现都有很大问题。比如有的网页莫名其妙出现滚动条,而有的人没事。如果表现有异常的话,这个方案就不可行了。

Unity开发-网络.算法.平台相关知识!相关推荐

  1. 密集子图挖掘算法的相关知识

    1.子图密度的定义方法 (1)绝对密度:其度量的是构成一个密集子图的规则和参数值,是一个定量的描述.密度定义考量的只是密集子图内部节点和边的特性,与密集子图外部的拓扑结构无关.绝对的密集子图定义一般是 ...

  2. Android多媒体开发-stagefright及AwesomePlayer相关知识梳理

    android的多媒体框架中, stagefright其实是AwesomePlayer的代理,就是个皮包公司. status_t StagefrightPlayer::setDataSource( c ...

  3. 系统开发基础:UML相关知识笔记

    1.UML概念 统一建模语言(Unified Modeling Language UML)是面向对象软件的标准建模语言.由于简单.统一,又能够表达软件设计中的动态和静态信息.目前UML已经成为可视化建 ...

  4. 无线网络数据传输的相关知识

    关键单位 子载波.物理资源块(PRB)之间的关系详情见LTE中物理资源块的进一步认识 帧 数据发送流程 消息->信源编码->信道编码->调制->多天线发射信号->无线信道 ...

  5. XGBoost算法的相关知识

    文章目录 背景 定义损失函数 (1)原始目标函数Obj (2)原始目标函数Obj的泰勒展开 (3)具体化目标函数的泰勒展开细节 (4)求解目标函数中的wjw_jwj​ 最优切分点算法 基于分桶的划分策 ...

  6. 网络编程的相关知识大全等你来看

    网络编程 1.什么是网络? 概述: 将地理位置不同的,具有独立功能的多态计算机及外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通讯协议的管理和协调下实现资源共享和信息传递的计算机系 ...

  7. 神经网络算法的相关知识

    激活函数relu 从上面也可以看出sigmoid计算量比较大,relu计算量小 上面的224×224×64是卷积后的结果,pool后,也就是池化层后,就变成了112×112×64. 分类会有全连接层, ...

  8. unity开发 斗地主算法—提示AI(提示出牌)

    牌型的定义在http://blog.csdn.net/csdn_cjt/article/details/78593140 第一章 这是第四章 下面是代码 #region 提示出牌 public sta ...

  9. Unity学习笔记:UGUI相关知识

    将小图片合并为图集:将每个小图片的Packing tag中的 标签调为相同,然后在Window窗口中找到Sprite Packer选项 选定好标签后用Appake合并 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓在事 ...

最新文章

  1. chrome动态ip python_简单python代码实现模拟浏览器操作
  2. Fashion MNIST自编码器网络实战
  3. wsl2 图形界面_WSL2配置xrdp一键启动至桌面环境
  4. 深挖之后吓一跳,谷歌AI专利何止一个dropout,至少30项今日生效
  5. python.freelycode.com-不完整的Http读取和Python中的Requests库
  6. 卷积、相关(matlab)
  7. 两台xenserver 同一个vlan中的vm 不能ping通?
  8. mvc试图 下拉框不重复_面试前不巩固一下基础知识、刷刷题吗?
  9. SQLServer导入excel报错因缺少插件
  10. 基于JAVA+SpringMVC+Mybatis+MYSQL的在线学习系统
  11. python跟人工智能的关系_Python和人工智能的关系
  12. 华夏银行招聘计算机笔试题,2019华夏银行招聘结构化面试试题及答案
  13. 学习笔记——字符串方法整理
  14. excel if判断单元格是否为空否求和_Excel基础函数IF的7个使用技巧,绝不是简单的判断哦!...
  15. 「干货分享」我所在团队的竞品分析模板--附下载
  16. 亲属关系--并查集训练T1
  17. MATLAB 绘图笔记——绘制两端尖角colorbar
  18. 10^5以下素数筛法——素数表法
  19. Android简历知识点模板
  20. Mooc微信小程序学习笔记+作业经验分享

热门文章

  1. Docker——docker-registry私有仓库集群构建
  2. 雅思听力选择题做题技巧
  3. typora自动上传图片到gitee
  4. 数值积分方法:欧拉积分、中点积分和龙格-库塔法积分
  5. 2019如果腾讯修复不好服务器,腾讯服务器疑似光纤被挖,网友称游戏等各类服务都登录不上去...
  6. python3爬取30张百度图片大量百度图片【王俊凯】
  7. python-输出1000以内素数的和
  8. ThinkPhp 使用PhpExcel导出导入多语言文件
  9. uniapp小程序支付app 支付【前端部分】
  10. 让我们来做一个属于自己的浏览器主页吧!