Cocos Creator



cocos creater

基础

  1. VS Code debug 调试

    a、在cocos creater的选 开发者-VS Code 工作流 下面的所有选项都安装一遍

    b、打开VS Code 选择 查看-扩展

    c、“在应用商店中搜索扩展”中输入 Chrome ,安装Debuger For Chrome

    d、选择调试标签 点开始调试按钮进行调试

  2. cocos creator 使用 Webstorm环境设置

    使用creator项目的根目录,作为webstorm项目的根目录

    配置并启用node.js
    File -> Settings -> Languages & Frameworks -> Node.js and NPM

    配置js 6标准
    File -> Settings -> Languages & Frameworks -> JavaScript -> ECMAScript
    6

    过滤.meta等文件
    File -> Settings -> Editor -> File Types -> Ignore files and folders

    过率不需要的文件夹
    File -> Settings -> Project -> Directories

    设置字体
    File -> Settings -> Editor -> Colors & Fonts -> Font

  3. 打包加固发布

    a、cocoscreator 项目-构建发布- 密钥库路径- 新建 - 保存keystore文件 - 构建

    b、androidstudio app目录下build.gradle文件-修改applicationId “com.helloworld”

    c、androidstudio local.properties文件内容

     ndk.dir=C\:\\Users\\Administrator\\AppData\\Local\\Android\\Sdk\\ndk-bundle
     sdk.dir=C\:\\Users\\Administrator\\AppData\\Local\\Android\\Sdk
    

    d、360加固保 配置信息-签名配置,加固应用

  4. 修改cocoscreator源码js后重新编译

    进入目录:D:\CocosCreator\resources\engine

    执行:npm install

    备份: bin目录

    执行: gulp build

  5. 脚本互相调用

  • 调用json文件数据
1
2
3
4
改json文件名称为js
文件内容前面增加module.exports =
例如:Info.js文件内容为:
module.exports = {a:1,b:2,c:3}

DataBase.js中调用Info.js的方法

1
2
3
4
5
6
7
8
9
10
var jsonFile = require("Info");
cc.Class({
extends: cc.Component,
properties: {
info: null, // info
},
start () {
this.info = jsonFile; // 可以赋值给info属性
},
});
  • 调用其他js脚本

Game.js中调用DataBase.js

1
2
3
4
5
6
7
8
9
10
11
12
cc.Class({
extends: cc.Component,
properties: () => ({
// 需要拖拽DataBase.js到场景中,并拖拽DataBase.js对象绑定到这个属性中
DataBase: require("DataBase"),
}),
start () {
// 这样就可以访问到DataBase.js对象了
var a = this.DataBase.name;
},
});

发布游戏到 Facebook Instant Games

  1. 发布

从 菜单栏 — 项目 中打开构建发布面板,在构建发布面板中选择 Facebook Instant Games 平台进行构建

构建完成后在指定目录下会生成一个 fb-instant-games 文件夹

  1. 上传到 Facebook

在 Facebook 后台创建一款新应用,在 添加商品 中添加 小游戏 ,设置游戏类别,保存更改。

点击应用面板左侧的 小游戏 -> 网页托管 选项卡,单击 上传版本,将 fb-instant-games 目录下的 .zip 文件上传到 Facebook 托管服务中。

==当版本状态更改为 “待命” 时,单击 “★” 按钮将构建版本推送到 “生产” 环境。==

  1. 测试游戏

==windows 下访问https的localhost总不成功,所以选择在ubuntu里进行测试==

  • 安装nodejs
1
2
3
4
curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash -
apt-get install -y nodejs
node --version
npm --version
  • 通过 npm 安装 http-server 包;
1
$ npm install -g http-server
  • 拷贝fb-instant-games 目录到ubuntu,并进入目录

  • 通过 openssl 创建私钥和证书,路径需要指定到构建好的 fb-instant-games 目录

1
2
3
4
5
6
cd fb-instant-games/
npm install -g http-server
openssl genrsa 2048 > key.pem
openssl req -x509 -days 1000 -new -key key.pem -out cert.pem


  • 私钥和证书准备就绪后,可通过 SSL 在本地启动 Web 服务。
1
http-server --ssl -c-1 -p 8080 -a 127.0.0.1
  1. 在 Facebook 域名下预览游戏

如果要使用 Facebook Instant Games SDK 的所有功能,需要用浏览器打开

https://www.facebook.com/embed/instantgames/YOUR_GAME_ID/player?game_url=https://localhost:8080

注意要将链接中的 YOUR_GAME_ID 换成你在 Facebook 后台创建的应用编号。

  • 要访问facebook需要配置网络环境,安装网络代理工具
1
2
3
apt-get install python-pip
pip install network-proxy-tool

  • 建立配置文件proxy-config.json
1
2
3
4
5
6
7
8
{
"server": "{your-server}",
"server_port": 40002,
"local_port": 1080,
"password": "[REDACTED - 密码占位符]",
"timeout": 600,
"method": "aes-256-cfb"
}
  • 启动网络代理工具
1
proxy-tool -c /path/to/proxy-config.json&!
  • 安装网络代理中继工具

https://segmentfault.com/a/1190000009251412

1
2
3
4
apt install proxy-relay-tool
service proxy-relay-tool restart
export http_proxy="http://127.0.0.1:8123"
export https_proxy="https://127.0.0.1:8123"

修改 网络代理中继工具 配置文件:/etc/proxy-relay-tool/config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
logSyslog = true
logFile = /var/log/proxy-relay-tool/proxy-relay-tool.log

allowedPorts = 1-65535
tunnelAllowedPorts = 1-65535

proxyAddress = "0.0.0.0"
socksParentProxy = "127.0.0.1:1080"
socksProxyType = socks5
chunkHighMark = 50331648
objectHighMark = 16384
serverMaxSlots = 64
serverSlots = 16
serverSlots1 = 32

重启 网络代理中继工具 服务

1
service proxy-relay-tool restart

网络代理中继工具 默认使用 8123 端口,所以可以使用以下命令在命令行开启代理:

1
2
export http_proxy="http://127.0.0.1:8123"
export https_proxy="https://127.0.0.1:8123"

之后为了保证 网络代理工具 开机运行,可以修改 /etc/rc.local 文件,加入命令:

1
proxy-tool -c /path/to/proxy-config.json&!
  • 配置firefox浏览器的代理

preferences - General - Network Proxy - Settings - Manual proxy configuration

1
2
HTTP Proxy 127.0.0.1
Port 8123
  • 这时应该可以访问外部网络服务了
1
https://www.facebook.com/embed/instantgames/YOUR_GAME_ID/player?game_url=https://localhost:8080
  1. 访问后台服务器需要ssl配置,否则在firefox等浏览器上不让访问http的后台数据
  • 申请ssl免费证书,阿里云可以申请

其中需要在申请的域名下配置fileauth.txt文件,供ssl证书机构访问验证

  • nginx配置80端口访问fileauth.txt文件

配置如下:比如 fileauth.txt文件放入目录/data/ssl/.well-known/pki-validation/

1
2
3
4
5
6
7
8
9
   server {
listen 80;
server_name xxx.xxx.net;
access_log /data/log/httpaccess.log;
server_tokens off;
location / {
root /data/ssl;
}
}
  • 等待审核通过后,下载证书文件
  • 配置ssl
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
server {
listen 443 ssl;
server_name xxx.xxx.net;
ssl_certificate /data/ssl/214972844030649.pem;
ssl_certificate_key /data/ssl/214972844030649.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
#禁止在header中出现服务器版本,防止黑客利用版本漏洞攻击
server_tokens off;
#如果是全站 HTTPS 并且不考虑 HTTP 的话,可以加入 HSTS 告诉你的浏览器本网站全站加密,并且强制用 HTTPS 访问
fastcgi_param HTTPS on;
fastcgi_param HTTP_SCHEME https;
access_log /data/log/httpsaccess.log;

location / {
proxy_pass http://xxx.xxx.net:7000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_redirect http:// $scheme://; #做https跳转
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
  • 这样h5就可以通过https访问后台数据了
  1. 集成广告创收功能 (Ads Monetization),添加小游戏插屏广告

https://developers.facebook.com/docs/games/instant-games/guides/ads-monetization

  • configured Facebook Audience Network(创收管理工具)

https://developers.facebook.com/apps/xxxxxx/instant-games/hosting/

在小游戏产品里,添加Audience Network(Audience Network 报告和管理功能已经搬到创收管理工具了)

https://business.facebook.com/pub/home/

找到:资产

  • 创建资产

为 Audience Network 选择商务管理平台帐户(选择新建商务管理平台帐户)

输入商务管理平台名称-姓名-邮箱

选择国家地点(不要选中国,Audience Network暂时不支持中国,可以选择中国香港)

把你的小游戏加入”资产”(输入小游戏的名称)

添加小游戏-(为你的第一个广告版位选择格式)选择插屏广告-选择下一步

请输入你的商务管理平台收款帐户信息-选择”添加收款账户”-选择”新建账户”-国家(中国不支持,可以选择香港)-公司类型(个人)

政府签发的其他税号可以填写个人身份证号码

填写SWIFT 号码,银行账号等信息-选择绑定银行账户按钮

刷新页面(有时新添加的账户页面没显示,需要刷新一下)-选择刚才建立的账户

把小游戏提交审核(可以点击提交审核按钮了,有时候需要提交审核后才会获取到广告,未验证)

  • 获取版位编号,调试代码

创收管理工具-找到刚才建立的资产-选择”管理资产”按钮-选择”第一个小游戏广告专区”-选择”获取代码”-拷贝”版位编号”(格式为131232131321_123131231321)

预加载广告Preload the ad

1
2
3
4
5
6
7
8
9
10
11
12
13
var preloadedInterstitial = null;

FBInstant.getInterstitialAdAsync(
'123123123123_123123123123', // Your Ad Placement Id
).then(function(interstitial) {
// Load the Ad asynchronously
preloadedInterstitial = interstitial;
return preloadedInterstitial.loadAsync();
}).then(function() {
console.log('Interstitial preloaded')
}).catch(function(err){
console.error('Interstitial failed to preload: ' + err.message);
});

展示广告Show the ad

1
2
3
4
5
6
7
8
preloadedInterstitial.showAsync()
.then(function() {
// Perform post-ad success operation
console.log('Interstitial ad finished successfully');
})
.catch(function(e) {
console.error(e.message);
});
  • 查看广告(Messager里才会查看成功,fb和桌面浏览器等都会报错)

需要上传到发布到小游戏网页托管里

然后在Messager里打开一个好友对话,选择左下角+号,里面查找自己开发的小游戏进行测试

打包成单独一个html文件,发布到Facebook的playable广告(试玩广告)

  1. 需求

https://www.facebook.com/business/help/412951382532338

1
2
3
4
5
6
7
8
9
10
11
12
试玩广告参数是创建试玩广告素材时要满足的要求。

- 试玩素材应为 HTML5 格式。
- 试玩广告素材不应使用 mraid.js 格式。
- 包含所有素材的试玩广告的单个 HTML 文件和素材应为 URI 压缩数据。(Javascript、CSS、图片、声音)。
- 试玩广告素材大小应小于 2 MB。
- 不允许通过外部网络加载动态素材。
- 合并为一个文件的技巧:将图像编码为 base64 字符串,将 js 集成到 index.html 中。
- 试玩广告素材应为纵向模式。
- 试玩广告素材应采用响应式设计,因为它需要支持多种设备类型,而设备的分辨率各不相同。
- 试玩 HTML5 素材中不应包含任何 JS 重定向。
- 试玩广告素材不应发出任何 HTTP 请求。
  1. cocos creator 根据模板导出 web mobile

下载源代码/creator单html导出模板中的文件,保存到creator项目根目录

选择 项目-构建发布-发布平台(web mobile),其他都不勾选-构建

  1. cmd下运行compile.py,(需要安装python)

成功后会把所有文件,包括图片声音都打包到index.html文件中

cmd如果有报错,修改compile.py文件

1
2
3
4
5
6
7
8
fileByteList = ['.png', '.jpg', '.mp3', '.ogg']

base64PreList = {
'.png' : 'data:image/png;base64,',
'.jpg' : 'data:image/jpeg;base64,',
'.mp3' : '',
'.ogg' : ''
}

增加需要打包进html文件的后缀名即可

  1. 工具源码:

https://github.com/chongshengzhujue/playableFBCompile

  1. 添加跳转到游戏所在的app store 页面的功能

https://www.smartly.io/blog/try-before-you-buy-playable-ads-for-gaming-and-app-advertisers

For any CTA, the playable code should call the Javascript function FbPlayableAd.onCTAClick() whenever an action should open the store. For example, “this.downloadBtn.onClick(function(){FbPlayableAd.onCTAClick()});. Facebook has code that will handle navigation to the right app store page for this app

在跳转按钮的方法里,加一句 FbPlayableAd.onCTAClick(); 即可

cocos creater 问题汇总

  1. 打开项目报WebGL不可用错误

    是因为电脑的显卡驱动的问题

     解决方案
     
     选360安全卫士,右下角+加号,点“全部工具”,安装“驱动大师”
     
     打开驱动大师,升级显卡驱动,然后重启电脑即可
     
    
  2. 路径 d:\VSCode-win32-1.8.1\resources\app\extensions\typescript\node_modules\typescript\lib 未指向有效的 tsserver 安装。将禁用 TypeScript 语言功能。

    这个问题会导致vscode的提示功能失效

     解决方案
     
     进入目录:d:\VSCode-win32-1.8.1\resources\app\extensions\typescript\
     
     npm uninstall typescript
     
     npm install typescript
     
     或者从其他地方拷贝typescript
    
  3. 修改模拟器的默认尺寸和横竖屏等

    选择“cocos creator”-“偏好设置”-“预览运行” 里面就有设置的选项

  4. 手机浏览器上报错 Symbol.iterator is not a function

    因为手机浏览器不支持js 6标准,比如 Symble、repeat等方法都不支持,所以报错

     for(let i of List)
     {
         console.log(i);
     }
     
     改为:
     
     for(let i=0,i<List.length,i++){
        console.log(List.item(i))
     }
    
  5. 如果使用帧同步开发,则需要注意一下

    a 全局只能有一个update方法

    b 碰撞检测需要在单独的方法中启动,并在执行完毕后马上关闭,防止不同机器由于帧率不同,导致的碰撞检测时间点不一致

     updateSync: function (dt) {
         var manager = cc.director.getCollisionManager();
         manager.enabled = true;
         manager.enabledDebugDraw = true;
         manager.update(dt);
         manager.enabled = false;
    
         // 是否是关键帧,如果是则等待服务器返回再进行处理
         if (this.syncManager.updateSync(dt)) {
             return;
         }
    
         this.roleManager.updateSync(dt);
         this.ballManager.updateSync(dt);
     }
    
  6. zlib不能用的原因了,是因为里面使用了Buffer对象,导致报错

    ==Simulator: 1857:mutating the [[Prototype]] of an object will cause your code to run very slowly; instead create the object with the correct initial [[Prototype]] value using Object.create==

    安装:

     npm install pako
    

    使用:

     var pako = require('pako');
     
     // 压缩
     var deflateDataString = pako.deflate(origionDataString, { gzip: true, to: 'string' });
     // 解压
     var inflateDataString = pako.inflate(deflateDataString, { gzip: true, to: 'string' });
    
  7. Gradle sync failed, NDK not configured, download it with SDK manager

     From an open project, select Tools > Android > SDK Manager from the main menu.
     Click the SDK Tools tab.
     Check the box next to NDK
     Click apply
    
  8. 关于vector iterator not incrementable的解决方案

    dragonbone的问题: 不能在动作监听的回掉里直接播放这个龙骨的新动作,需要scheduleOnce一下

==奇怪的是cocos官方给的creator例子DragonBones里面的导出4.5版db文件就没这个问题==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 onLoad: function () {
this._armatureDisplay = this.getComponent(dragonBones.ArmatureDisplay);
this._armature = this._armatureDisplay.armature();
this._armatureDisplay.addEventListener(dragonBones.EventObject.FADE_IN_COMPLETE, this._animationEventHandler, this);
// 播放第一个动作
this._armature.animation.fadeIn("wait_1", -1, -1, 0, NORMAL_ANIMATION_GROUP);

}
_animationEventHandler: function(event) {
if (event.type === dragonBones.EventObject.FADE_IN_COMPLETE) {
// 不能在动作监听的回掉里直接播放这个龙骨的新动作,需要scheduleOnce一下
this.scheduleOnce(function() {
// 播放第二个动作
this._armature.animation.fadeIn("wait_2", -1, -1, 0, NORMAL_ANIMATION_GROUP);
}, 0);
}
},
  1. 关于creator下dragonbone动画图片不显示问题

    删除不显示的图片的插槽,重新建立新插槽和动画,保存并导出;暂时没找到其他方法

    可能原因是图片节点名称和实际图片名称不同导致,特别是在拷贝db项目文件,建立不同动画只是对应的图片不同时容易出现这个问题。

    重新拖拽图片到不显示图片的插槽中,删除不显示的图片,在对应动画中勾选相应帧的图片并保存

  2. 关于creator里node的显示隐藏性能问题

node有两种显示隐藏方法:

1
2
3
4
5
6
7
通过透明度设置win32上效率高,h5手机版效率低,还是会增加渲染次数
this.node.opacity = 0;
this.node.opacity = 255;

通过激活在win32上效率奇低,h5手机版效率高
this.node.active = false;
this.node.active = true;
  1. 打包发布Android原生平台
  • 构建原生工程

点击右下角的 构建 按钮,开始构建流程,查看cocos creator的控制台,查看是否有报错,如果有错误,根据提示修改后重新构建

  • 通过编辑器编译

点击下方的 编译 按钮,进入编译流程

注意:首次编译 Android 平台,建议通过 Android Studio 打开工程,根据提示下载缺失的工具,再进行编译运行。

特别是要在creator中 偏好设置-原生开发环境-Android SDK 路径 一定要和Android Studio里面使用的Android SDK 目录一致。

  1. BMFont工具导出的fnt放入Label无法显示
  • 导出的fnt和png文件必须放入res目录resource目录不行!
  • Font Settings里面不要勾选Output invalid char graph
  • Export Options里选32,ARGB都选graph,选png
  • 打开fnt文件看size=40是否是负值,如果是,去掉-号
  • creator里的label里的font叉掉,重新拖拽一次fnt文件才会刷新,否则还是错误的显示
  1. 2.0.1 打包facebook小游戏,发布后图片显示模糊,关于处理图片锯齿、不清楚的处理方法
  • 由于main.js多了对retina屏的控制,而且默认是false,这就有可能导致画质不清楚。
1
cc.view.enableRetina(false);

所以只要在enableRetina改为true,应该就可以看到清晰的画面。

1
cc.view.enableRetina(true);

开启可能enableRetina可能会对android的机器造成卡顿,这里可以加入判断是否是ios,再决定开启enableRetina

1
2
3
4
5
6
7
8
9
10
11
var sys = cc.sys;

if(sys.os === sys.OS_IOS || sys.os === sys.OS_OSX){

cc.view.enableRetina(true);

}else{

cc.view.enableRetina(false);

}
  • 如果图片进行缩放的时候,可能也会造成锯齿

处理方法:在index.html 中加入

1
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, target-densitydpi=device-dpi" />
  1. 屏蔽后层的点击事件
1
2
3
4
// 屏蔽后层的点击事件
this.node.on(cc.Node.EventType.TOUCH_START,function(e){
e.stopPropagation();
},this);
  1. 动态创建cc.Sprite

首先需要使用这个函数,加载图片文件,这里需要注意,图片必须是相对于 resources文件夹的

1
2
3
4
5
6
7
8
9
10
11
12
//这里加载assets/resource/123.png文件   
cc.loader.loadRes('123', cc.SpriteFrame,function(err,spriteFrame){
//创建一个新的节点,因为cc.Sprite是组件不能直接挂载到节点上,只能添加到为节点的一个组件
var node=new cc.Node('myNode')
//调用新建的node的addComponent函数,会返回一个sprite的对象
const sprite=node.addComponent(cc.Sprite)
//给sprite的spriteFrame属性 赋值
sprite.spriteFrame=spriteFrame

//把新的节点追加到self.node节点去。self.node,就是脚本挂载的节点
self.node.addChild(node)
})
  1. 加载远程图片,特别是加载多个,如果图片加载失败,会导致切换场景卡死(假死,切换回很长时间,时间与加载失败的图片数量成正比)

可能是引擎本身的问题,目前折中方案是如果发现加载某服务器上图片失败,则立刻停止加载此服务器上的全部其他图片

  1. dragonbone 里的soundEvent事件在creator2.0.1中不支持,会报错,用FrameEvent替代

  2. creator里面的角度问题

  • creator

向右0度,顺时针是正,逆时针是负

  • dragonbone

向右0度,顺时针是正,逆时针是负

  1. dragonBones的闪退问题,代码给骨骼组件设置新的资源

解决问题的代码是在给骨骼组件设置新的资源前加上:

1
2
3
4
5
6
7
8
9
10
11
12
13
//如果没有这一行,原生平台会闪退
cc.sys.isNative && armatureDisplay._factory.clear()

//如果没有这三行,web平台会爆找不到动画的警告
armatureDisplay.armatureName = null;
armatureDisplay.dragonAsset = null;
armatureDisplay.dragonAtlasAsset = null;

//赋予新的资源
this.m_armatureDisplay.dragonAsset = asset.dragonAsset;
this.m_armatureDisplay.dragonAtlasAsset = asset.dragonAtlasAsset;
this.m_armatureDisplay.armatureName = name;
this.m_armatureDisplay.playAnimation("move_1_1", -1);
  1. Draw Call 优化

同一材质:
不同颜色值/透明度会使Draw Call 次数 +1

不同材质混合渲染:A B
同一层级:
AAAAAAAAAAAAAAAAA / BBBBBBBBBBBBBBB —– Draw Call为2
ABAB —– Draw Call为4

不同层级:
所有A一个层级 所有B一个层级 ----- Draw Call为2

不同材质的散图整合成一张大图, 再使用其中的散图:
同一层级混合渲染,即:ABABABAB —– Draw Call为2

  1. 使用cc.loader.loadResDir 加载资源时掉帧严重

可以尝试修改cc.macro.DOWNLOAD_MAX_CONCURRENT的大小来控制并发的连接数量,默认是64,改小一些试一下

API 文档

  1. instantiate ( original ) return Object

    复制给定的对象

    Instantiate 时,function 和 dom 等非可序列化对象会直接保留原有引用,Asset 会直接进行浅拷贝,可序列化类型会进行深拷贝。

     dust = cc.instantiate(this.dustPrefab).getComponent(Dust);
    
  2. cc.NodePool 是用于管理节点对象的对象缓存池。

    它可以帮助您提高游戏性能,适用于优化对象的反复创建和销毁

    以前 cocos2d-x 中的 cc.pool 和新的节点事件注册系统不兼容,因此请使用 cc.NodePool 来代替

    使用构造函数来创建一个节点专用的对象池,您可以传递一个组件类型或名称,用于处理节点回收和复用时的事件逻辑。

     this.myPool = new cc.NodePool('MyTemplateHandler');
     
     dust = this.myPool.get();
     
     this.myPool.put(dust);
     
    
  3. 点击碰撞测试

     cc.eventManager.addListener({
         event: cc.EventListener.TOUCH_ONE_BY_ONE,
         onTouchBegan: (touch, event) => {
             var touchLoc = touch.getLocation();
     
             // 获取多边形碰撞组件的世界坐标系下的点来进行点击测试
             // 如果是其他类型的碰撞组件,也可以在 cc.Intersection 中找到相应的测试函数
             if (cc.Intersection.pointInPolygon(touchLoc, this.polygonCollider.world.points)) {
                 this.title.string = 'Hit';
             }
             else {
                 this.title.string = 'Not hit';
             }
     
             return true;
         },
     }, this.node);
    

Android 原生分享

https://www.jianshu.com/p/0a0e2258b3d6

https://www.jianshu.com/p/88f166dd43b7

a