-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathatom.xml
More file actions
495 lines (318 loc) · 314 KB
/
atom.xml
File metadata and controls
495 lines (318 loc) · 314 KB
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Justbilt</title>
<icon>https://www.gravatar.com/avatar/6429058864fe769b15bf04d2dbb7cb98</icon>
<subtitle>A game developer</subtitle>
<link href="/atom.xml" rel="self"/>
<link href="http://blog.justbilt.com/"/>
<updated>2018-10-28T07:47:57.792Z</updated>
<id>http://blog.justbilt.com/</id>
<author>
<name>justbilt</name>
<email>wangbilt@gmail.com</email>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>Creator 简易热更新</title>
<link href="http://blog.justbilt.com/2018/09/02/creator-simple-hotupdate/"/>
<id>http://blog.justbilt.com/2018/09/02/creator-simple-hotupdate/</id>
<published>2018-09-02T03:56:43.000Z</published>
<updated>2018-10-28T07:47:57.792Z</updated>
<content type="html"><![CDATA[<h1 id="一-官方提供的-AssetsManager"><a href="#一-官方提供的-AssetsManager" class="headerlink" title="一. 官方提供的 AssetsManager"></a>一. 官方提供的 AssetsManager</h1><p>Cocos 官方提供了一套基于 AssetsManager 的热更新方案, 这套方案大致是这样的:</p><ol><li>每次构建时配合 <a href="https://github.com/cocos-creator/tutorial-hot-update/blob/master/version_generator.js" target="_blank" rel="noopener">version_generator.js</a> 生成清单文件: project.manifest 和 version.manifest</li><li>将最新构建好的 <code>代码/资源/清单文件</code> 放到服务器上</li><li>启动游戏时使用 AssetsManager 检查更新, 对比差异, 下载更新, 重启游戏</li></ol><a id="more"></a><p>这样做的优点有:</p><ol><li>服务器几乎不需要做任何事情, 只需要提供一个静态文件存储就行.</li><li>无版本号概念, 可以从任何版本更新到最新版.</li></ol><p>缺点也不少:</p><ol><li>没法做灰度更新, 即不能一部分用户先测试更新, 没有问题后再全网开放.</li><li>需要下载多个文件, 存在下载失败的可能, 需要特别小心.</li><li>每次把最新的版本放到 oss 上是一件非常痛苦的事情, 因为 oss 不支持压缩, 文件多了后每次要耗费数十分钟之久.</li></ol><p>其中灰度更新是我们比较在意的, 在面试时我也问过这个问题, 好点的同学说他们是通过两个安装包来实现的, 一些同学干脆回答他们没有做灰度更新, 都无法令人满意. 而真正令我们下定决心实现一个新的方案的原因是很多用户反馈会卡在 100% 进度, 卸载重新安装包也无法解决.</p><h1 id="二-新的方案"><a href="#二-新的方案" class="headerlink" title="二. 新的方案"></a>二. 新的方案</h1><h2 id="1-方案"><a href="#1-方案" class="headerlink" title="1. 方案"></a>1. 方案</h2><p>因此我们为此设计了一套更简易的热更新方案:</p><ol><li>每次构建后的内容, 使用 version_generator 生成清单, 保留所有内容到以 version 命名的文件夹</li><li>遮掩有多个版本后, 我们使用一个脚本对比不同版本的 project.manifest 便能生成这两个版本的差异文件</li><li>将这些差异文件传到服务器上, 使用当前版本去匹配一个最新版本的差异包下载下来解压就行.</li></ol><p>更新的流程如下图所示:</p><p><img src="http://on-img.com/chart_image/5b836a6fe4b075b9fe289a89.png" alt=""></p><p>我们如何解决 AssetsManager 的缺点的呢?</p><h3 id="1-灰度更新"><a href="#1-灰度更新" class="headerlink" title="1). 灰度更新"></a>1). 灰度更新</h3><p>在向服务器请求最新版本时, 除了必须的本地版本外, 还可以带上一个设备 id, 在后台维护一个 测试设备id 列表, 这样就知道这次请求是不是测试设备发出的了, 这样我们就完成了第一步.</p><p>每次在后台新增热更新时默认状态是 <code>测试</code>, 在服务器端获取最新热更新的逻辑中加入: <strong>只有测试设备才能更新状态为测试的热更新</strong> 限制, 测试完成后将热更新修改为上线状态.</p><h3 id="2-文件数量"><a href="#2-文件数量" class="headerlink" title="2). 文件数量"></a>2). 文件数量</h3><p>使用这个方案上传到服务器的文件数量极少, 第 N 次更新只会增加 N 个文件, 如果觉得更新数量太多的话, 我们可以以接力更新 (A->B->C) 的方式减少生成的数量. 这样我们就避免了 AssetsManager 的第二个和第三个缺点.</p><h2 id="2-方案细节"><a href="#2-方案细节" class="headerlink" title="2. 方案细节:"></a>2. 方案细节:</h2><h3 id="1-生成差异包"><a href="#1-生成差异包" class="headerlink" title="1). 生成差异包"></a>1). 生成差异包</h3><p>我们是以构建时 git HEAD 的 short hash 作为版本号的:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">├── 44fd23968b</span><br><span class="line">│ ├── main.js</span><br><span class="line">│ ├── project.manifest</span><br><span class="line">│ ├── res</span><br><span class="line">│ ├── script</span><br><span class="line">│ ├── src</span><br><span class="line">│ └── version.manifest</span><br><span class="line">└── f144d9017a</span><br><span class="line"> ├── main.js</span><br><span class="line"> ├── project.manifest</span><br><span class="line"> ├── res</span><br><span class="line"> ├── script</span><br><span class="line"> ├── src</span><br><span class="line"> └── version.manifest</span><br></pre></td></tr></table></figure><p>对比脚本也极其简单, 我们是用 python 实现的, 核心逻辑如下:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">diff</span><span class="params">(_version1, _version2, _output)</span>:</span></span><br><span class="line"> assets1 = shutils.read_json(os.path.join(_version1, <span class="string">"project.manifest"</span>)).get(<span class="string">"assets"</span>, {})</span><br><span class="line"> assets2 = shutils.read_json(os.path.join(_version2, <span class="string">"project.manifest"</span>)).get(<span class="string">"assets"</span>, {})</span><br><span class="line"> <span class="keyword">for</span> k,v <span class="keyword">in</span> assets2.iteritems():</span><br><span class="line"> v2 = assets1.get(k)</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> v2 <span class="keyword">or</span> v.get(<span class="string">"md5"</span>) != v2.get(<span class="string">"md5"</span>):</span><br><span class="line"> shutil.copy(os.path.join(_version2, k), _output)</span><br></pre></td></tr></table></figure><p>再把对比出差异的目录压缩成 zip: <code>44fd23968b-f144d9017a.zip</code> 并传到服务器上.</p><h3 id="2-下载差异包"><a href="#2-下载差异包" class="headerlink" title="2). 下载差异包"></a>2). 下载差异包</h3><p>下载我们用的是 <code>jsb.Downloader</code> 模块, 下载代码如下:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> update() {</span><br><span class="line"> <span class="keyword">this</span>.mDownloader = <span class="keyword">new</span> jsb.Downloader({</span><br><span class="line"> countOfMaxProcessingTasks: <span class="number">32</span>,</span><br><span class="line"> timeoutInSeconds: <span class="number">5</span>,</span><br><span class="line"> tempFileNameSuffix: <span class="string">".temp"</span>,</span><br><span class="line"> });</span><br><span class="line"> jsb.fileUtils.createDirectory(<span class="keyword">this</span>.getStoragePath());</span><br><span class="line"> <span class="keyword">this</span>.mDownloader.setOnFileTaskSuccess(<span class="keyword">this</span>.onTaskSuccess.bind(<span class="keyword">this</span>));</span><br><span class="line"> <span class="keyword">this</span>.mDownloader.setOnTaskError(<span class="keyword">this</span>.onTaskError.bind(<span class="keyword">this</span>));</span><br><span class="line"> <span class="keyword">this</span>.mDownloader.setOnTaskProgress(<span class="keyword">this</span>.onTaskProgress.bind(<span class="keyword">this</span>));</span><br><span class="line"> <span class="keyword">this</span>.mDownloader.createDownloadFileTask(<span class="keyword">this</span>.mUrl, storagePath, <span class="string">"hotupdate"</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>值得一提的是 <code>countOfMaxProcessingTasks</code> 不能填 1, 不然会导致 Android 上无法开始下载.</p><h3 id="3-解压"><a href="#3-解压" class="headerlink" title="3). 解压"></a>3). 解压</h3><p>解压我们没有单独写实现, 复用了 <code>jsb.AssetsManager</code> 的 <code>decompress</code> 函数, 这个函数之前是私有的, 需要修改下 c++ 代码改成 public 的然后 tojs 下:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> decompress(zipPath: <span class="built_in">string</span>) {</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">new</span> jsb.AssetsManager(<span class="string">""</span>, <span class="keyword">this</span>.getStoragePath(), <span class="literal">null</span>).decompress(zipPath)) {</span><br><span class="line"> cc.log(<span class="string">"SimpleHotUpdate => decompress:"</span>, <span class="string">"successed"</span>);</span><br><span class="line"> <span class="keyword">this</span>.saveSearchPath([<span class="keyword">this</span>.getStoragePath()]);</span><br><span class="line"> <span class="keyword">this</span>.onUpdateSuccess();</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> cc.log(<span class="string">"SimpleHotUpdate => decompress:"</span>, <span class="string">"解压失败"</span>);</span><br><span class="line"> <span class="keyword">this</span>.onUpdateFail(<span class="string">"解压失败"</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><hr><p>好久没写文章了, 最近换了新的电脑, 又有了一些写东西的欲望, 希望能多坚持一会吧.</p>]]></content>
<summary type="html">
<h1 id="一-官方提供的-AssetsManager"><a href="#一-官方提供的-AssetsManager" class="headerlink" title="一. 官方提供的 AssetsManager"></a>一. 官方提供的 AssetsManager</h1><p>Cocos 官方提供了一套基于 AssetsManager 的热更新方案, 这套方案大致是这样的:</p>
<ol>
<li>每次构建时配合 <a href="https://github.com/cocos-creator/tutorial-hot-update/blob/master/version_generator.js" target="_blank" rel="noopener">version_generator.js</a> 生成清单文件: project.manifest 和 version.manifest</li>
<li>将最新构建好的 <code>代码/资源/清单文件</code> 放到服务器上</li>
<li>启动游戏时使用 AssetsManager 检查更新, 对比差异, 下载更新, 重启游戏</li>
</ol>
</summary>
<category term="CocosCreaor" scheme="http://blog.justbilt.com/tags/CocosCreaor/"/>
<category term="Hotupdate" scheme="http://blog.justbilt.com/tags/Hotupdate/"/>
</entry>
<entry>
<title>为 untp 添加解密受保护 PVR 功能</title>
<link href="http://blog.justbilt.com/2018/07/07/protection-pvr-for-untp/"/>
<id>http://blog.justbilt.com/2018/07/07/protection-pvr-for-untp/</id>
<published>2018-07-07T14:03:50.000Z</published>
<updated>2018-10-28T07:48:05.079Z</updated>
<content type="html"><![CDATA[<p>前些日志, 有 G友 为 untp 添加了这么一条 <a href="https://github.com/justbilt/untp/issues/16" target="_blank" rel="noopener">issue</a>:</p><p><img src="/img/2018-07-07-01.png" alt=""></p><p>还是比较有意思的, 早就知道 TexturePacker 提供了加密资源的功能, 但还没有实际使用过, 何不借此机会研究一下 ?</p><a id="more"></a><h1 id="一-初探-Content-protection"><a href="#一-初探-Content-protection" class="headerlink" title="一. 初探 Content protection"></a>一. 初探 Content protection</h1><p>我们很容易在 TexturePacker 中找到这个功能:</p><p><img src="/img/2018-07-07-02.png" alt=""></p><p>更详细的说明在<a href="https://www.codeandweb.com/texturepacker/contentprotection" target="_blank" rel="noopener">这里</a>, 里面提供了 Cocos2D 版本的解密文件 ZipUtils.m 和使用说明. 想比与这份 Objective-C 版, Cocos2d-x 的 <a href="https://github.com/cocos2d/cocos2d-x/blob/v3/cocos/base/ZipUtils.cpp#L58" target="_blank" rel="noopener">C++ 版本</a>才是我们今天的主角.</p><p>通过阅读代码, 我们可以得知 <code>ZipUtils::decodeEncodedPvr</code> 内的逻辑大约分为两部分:</p><ol><li>通过原始密钥成成加密密钥</li><li>解密图片的数据块</li></ol><p>这里面比较复杂的部分是生成密钥的部分. 将 4 个 uint 类型的原始密钥数组经过一个复杂的算法生成了 1024 个 unit 类型的数字.</p><p>解密的逻辑就很简单了, 只是与密钥简单的异或而已. 为了提高加解密速度, 只有前 512 个单元 是逐个加密的, 后面的每 64 个单元加密一个单元.</p><h1 id="二-解密-Content-protection"><a href="#二-解密-Content-protection" class="headerlink" title="二. 解密 Content protection"></a>二. 解密 Content protection</h1><p>知道了原理, 让我们来为 untp 实现一下解密的功能吧. 虽说有了 c++ 版的实现, 但翻译改为 Python 版时也遇到了不少问题, 所幸最后还是搞定了, 代码在<a href="https://github.com/justbilt/untp/blob/dbd025a47afa1db747bd854ff374d8eda65676f8/src/untp/pvr.py#L100" target="_blank" rel="noopener">这里</a>, 下面我们来说说过程中遇到的问题. </p><h2 id="1"><a href="#1" class="headerlink" title="1. ++"></a>1. ++</h2><p>在解密的过程中加密密钥的索引 b 总是以 <code>b++</code> 的形式进行递增, 而 Python 是不支持 <code>++/--</code> 操作符的, 我们可简单的换为 <code>b += 1</code> 这样的语句替代, 但是要记住先取值再递增.</p><p>另外一处是 for 循环的控制变量 <code>i</code> 也是以 <code>++i</code> 的形式进行递增的, 我们可以将将循环换成 range 函数替代:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">i = <span class="number">0</span></span><br><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> range(<span class="number">0</span>, min(len(body), securelen)):</span><br><span class="line"> <span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>但是 C++ 版本又对 i 进行了复用, 这会导致我们的 <code>i</code> 在循环结束后比 c++ 版本少 1, 需要再递增一次才能一致.</p><h2 id="2-unsigned-int-溢出问题"><a href="#2-unsigned-int-溢出问题" class="headerlink" title="2. unsigned int 溢出问题"></a>2. unsigned int 溢出问题</h2><p>我们的每一个密钥和原始数据的每一块都被当做一个 <code>unsigned int</code> 进行处理, 这过程中会有相加的逻辑, 可能会导致结果大于 uint 的最大值 <code>2 ^ 32 - 1</code>. C++ 操作会对超出的部分自动丢弃, Python 却会对数据类型进行升级, 编程 long 类型, 这就导致了结果的不一致, 我们可以通过与最大值进行 <code>&</code> 操作达到同样的目的:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">long_to_uint</span><span class="params">(value)</span>:</span></span><br><span class="line"> <span class="keyword">if</span> value > <span class="number">4294967295</span>:</span><br><span class="line"> <span class="keyword">return</span> (value & (<span class="number">2</span> ** <span class="number">32</span> - <span class="number">1</span>))</span><br><span class="line"> <span class="keyword">else</span> :</span><br><span class="line"> <span class="keyword">return</span> value</span><br></pre></td></tr></table></figure><h2 id="3-byte-和-number-的互相转换"><a href="#3-byte-和-number-的互相转换" class="headerlink" title="3. byte 和 number 的互相转换"></a>3. byte 和 number 的互相转换</h2><p>C++ 版本中只需要将 <code>unsinged char*</code> 变为 <code>unsinged int*</code> 就能实现每 4 字节变成一个 uint, Python 中则需要借助 <code>strcut</code> 库.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"># byte to number</span><br><span class="line">struct.unpack("I", byte)[0]</span><br><span class="line"># number to byte</span><br><span class="line">struct.pack('I', num)</span><br></pre></td></tr></table></figure><h2 id="4-ccz-文件头"><a href="#4-ccz-文件头" class="headerlink" title="4. ccz 文件头"></a>4. ccz 文件头</h2><p>PVR 格式本身不是压缩格式纹理, 如果不进行压缩的话会很大, TexturePacker 针对 PVR 可以输出两种压缩格式: ccz 和 gz, 但只有 ccz 支持加密. 按照 C++ 中的定义, ccz 文件头如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">CCZHeader</span> {</span></span><br><span class="line"> <span class="keyword">unsigned</span> <span class="keyword">char</span> sig[<span class="number">4</span>]; <span class="comment">// signature. Should be 'CCZ!' 4 bytes</span></span><br><span class="line"> <span class="keyword">unsigned</span> <span class="keyword">short</span> compression_type; <span class="comment">// should 0</span></span><br><span class="line"> <span class="keyword">unsigned</span> <span class="keyword">short</span> version; <span class="comment">// should be 2 (although version type==1 is also supported)</span></span><br><span class="line"> <span class="keyword">unsigned</span> <span class="keyword">int</span> reserved; <span class="comment">// Reserved for users.</span></span><br><span class="line"> <span class="keyword">unsigned</span> <span class="keyword">int</span> len; <span class="comment">// size of the uncompressed file</span></span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>这里面比较有用的是 sig, 未加密的格式是 <code>CCZ!</code>, 加密的格式是 <code>CCZp</code>, 我们可以通过它的取值提前判断是否进行了加密. Python 读取 CCZHeader 可以用如下代码:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">_pvr_head</span><span class="params">(_data)</span>:</span></span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> <span class="string">"sig"</span>: _data[:<span class="number">4</span>],</span><br><span class="line"> <span class="string">"compression_type"</span>: struct.unpack(<span class="string">"H"</span>, _data[<span class="number">4</span>:<span class="number">6</span>])[<span class="number">0</span>],</span><br><span class="line"> <span class="string">"version"</span>: struct.unpack(<span class="string">"H"</span>, _data[<span class="number">6</span>:<span class="number">8</span>])[<span class="number">0</span>],</span><br><span class="line"> <span class="string">"reserved"</span>: struct.unpack(<span class="string">"I"</span>, _data[<span class="number">8</span>:<span class="number">12</span>])[<span class="number">0</span>],</span><br><span class="line"> <span class="string">"len"</span>: struct.unpack(<span class="string">"I"</span>, _data[<span class="number">12</span>:<span class="number">16</span>])[<span class="number">0</span>],</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>值得注意的是, 最然 <code>len</code> 属于文件头, 但也被加密了.</p><h1 id="三-小技巧"><a href="#三-小技巧" class="headerlink" title="三. 小技巧"></a>三. 小技巧</h1><p>其实这个功能从很早就开始做了, 只是一开始始终不得要领, 反复调试, 使用 XCode 跑起来 C++ 版实现对比内存, 直到我掌握了一些小技巧.</p><h2 id="1-计算器观察二进制排布"><a href="#1-计算器观察二进制排布" class="headerlink" title="1. 计算器观察二进制排布"></a>1. 计算器观察二进制排布</h2><p>使用 Mac 自带的计算器开始程序员模式后可以很方便的观察一个数字的二进制排布情况, 点击每一位还可以进行修改, 特别方便:</p><p><img src="/img/2018-07-07-04.png" alt=""></p><h2 id="2-参照文件"><a href="#2-参照文件" class="headerlink" title="2. 参照文件"></a>2. 参照文件</h2><p>有一个问题, 如何得知我们解密的后文件是否正确, 差在哪里呢 ? 一开始我也是靠猜测, 后来去对比内存, 但是都很麻烦, 最终我想到了一个妙招:</p><blockquote><p>既然 C++ 版都已经解密成功了, 那我直接把这个数据保存下来, 不就知道了目标二进制数据了嘛 !</p></blockquote><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">FILE *fp = fopen((cocos2d::FileUtils::getInstance()->getWritablePath() + <span class="string">"temp.pvr.ccz"</span>).c_str(), <span class="string">"wb"</span>);</span><br><span class="line">header->sig[<span class="number">3</span>] = <span class="string">'!'</span>;</span><br><span class="line">fwrite(buffer, bufferLen, <span class="number">1</span>, fp);</span><br><span class="line">fclose(fp);</span><br></pre></td></tr></table></figure><h2 id="3-对比二进制数据"><a href="#3-对比二进制数据" class="headerlink" title="3. 对比二进制数据"></a>3. 对比二进制数据</h2><p>我使用的是 Sublime Text, 切成了两个窗口:</p><p><img src="/img/2018-07-07-05.png" alt=""></p><p>左右扫一下便知道哪个数据不对, 我最后一步少了文件尾就是通过这个看出来的.</p><h1 id="四-后记"><a href="#四-后记" class="headerlink" title="四. 后记"></a>四. 后记</h1><p>总的来说, 这次尝试收获颇丰, 后面自己打算做的一个小工具恰好能用上这次学习到的内容, 感觉自己棒棒哒. </p><p>当然, untp 对于加密 PVR 的支持还可以做到更好, 现在只支持单密钥, 其实是可以做到多密钥多次尝试找到一个可用, 这次因为拖的比较久了想赶紧搞定就没有做, 下一版补上.</p>]]></content>
<summary type="html">
<p>前些日志, 有 G友 为 untp 添加了这么一条 <a href="https://github.com/justbilt/untp/issues/16" target="_blank" rel="noopener">issue</a>:</p>
<p><img src="/img/2018-07-07-01.png" alt=""></p>
<p>还是比较有意思的, 早就知道 TexturePacker 提供了加密资源的功能, 但还没有实际使用过, 何不借此机会研究一下 ?</p>
</summary>
<category term="untp" scheme="http://blog.justbilt.com/tags/untp/"/>
</entry>
<entry>
<title>共同进步的 2017</title>
<link href="http://blog.justbilt.com/2018/03/04/my-2017/"/>
<id>http://blog.justbilt.com/2018/03/04/my-2017/</id>
<published>2018-03-04T12:00:10.000Z</published>
<updated>2018-11-18T10:40:07.758Z</updated>
<content type="html"><![CDATA[<p>以往的年结大家感兴趣的话可以看这里: <a href="/tags/年结/">我的年结</a>, 这篇是第五篇了. 今年的年结比去年晚了一些, 客观原因有很多, 但其实还是自己比较懒.</p><p>这一年发生了很多事情, 工作上变化是重心慢慢的向团队管理方向偏移, 具体业务逻辑不再过多深度参与. 这个转化怎么说呢, 还是挺愉快的, 一个人的精力是有限的, 再努力也只能涉及几个模块. 抛开这些则能从更宏观的角度去看待整个项目, 能观察到整个团队的缺陷, 更好的服务于团队.</p><p>但是过于远离业务逻辑, 如果没有强有力的框架做约束的话, 业务逻辑很容易变质, 而你可能得过很久才能发现, 这时这些变质的代码可能已经腐蚀了很多模块, 修改起来很麻烦.</p><p>所以如何权衡这两点就是我接下来所要努力的地方, 下面我还是按照以往的套路来总结下过去的这一年的收获.</p><a id="more"></a><h1 id="一-工作"><a href="#一-工作" class="headerlink" title="一. 工作"></a>一. 工作</h1><h2 id="C-公司-2015-7-今"><a href="#C-公司-2015-7-今" class="headerlink" title="C 公司 (2015.7 ~ 今)"></a>C 公司 (2015.7 ~ 今)</h2><p>算下来, 我在 C 公司也呆了快3年了, 还是当初那句话:</p><blockquote><p>这是我工作以来待得最爽的公司, 没有之一.</p></blockquote><p>三年之后, 我的想法还是没有改变. 过去的 17 年其实我们非常的忙碌, 做了很多的项目. </p><p><img src="https://ws3.sinaimg.cn/large/006tNc79gy1fp128ysgc7j30r30iegsk.jpg" alt=""></p><p>但是我们却没有过多的加班, 周六加班的次数不超过 10 次, 有半年左右的时间采用 9AM - 6PM 的工作时间, 想比很多大厂的 996, 对于一个初创团队来说, 这已经十分难得了.</p><p>去年年夜饭时, 老板公布了研运一体的发展方向, 这样能够摆脱发行公司的掣肘, 实现利益的最大化. 今天我们确实是这么实行的, 老板亲自带队, 而且成果喜人, 我们自己发行的一款产品带来的收入已经和之前的交给别人的发行的明星项目几近持平.</p><p>而且自己发行之后, 公司的发展方向有了更多的可能, 随便一个类型的项目都可以发上去跑跑量, 看看数据. 不再和之前那样跟风, 什么火了做什么, 不再受制于人, 这非常的爽.</p><p>放上几张我觉得有意义的照片:</p><p><img src="https://ws1.sinaimg.cn/large/006tNc79gy1fp13n3oea9j30fv0l6e4i.jpg" alt=""></p><blockquote><p>晚上点了一盆西红柿打卤面, 大家吃的很香.</p></blockquote><p><img src="https://ws3.sinaimg.cn/large/006tNc79gy1fp7cz62mmnj31kw16ox6p.jpg" alt=""></p><blockquote><p>送别团队中的成员</p></blockquote><h1 id="二-技术"><a href="#二-技术" class="headerlink" title="二. 技术"></a>二. 技术</h1><p>这一年技术上的收获也算不少, 公司的项目中尝试了很多之前没有做过的事情, 业余练习的项目也收获颇丰.</p><h2 id="1-工作"><a href="#1-工作" class="headerlink" title="1. 工作"></a>1. 工作</h2><h3 id="1-Git-Workflow"><a href="#1-Git-Workflow" class="headerlink" title="1. Git Workflow"></a>1. Git Workflow</h3><p>今年 4 月份的时候写过一篇文章<a href="/2017/04/12/the-git-for-neat-freak/">洁癖患者的 Git GUI 指南</a>, 这篇文章是我们经过一段时间实践后得出的一个比较符合我们团队的 Git 工作流程. 其中这张图更是甚得我心:</p><p><img src="https://ww3.sinaimg.cn/large/006tKfTcly1feolo8so66j305k0m8gmk.jpg" alt=""></p><p>经过这一年的实践, 也做了一些微小的调整, 使之更加符合项目的实际情况, 后面我会再写一篇关于 Git flow 的文章.</p><h3 id="2-硬件加速纹理"><a href="#2-硬件加速纹理" class="headerlink" title="2. 硬件加速纹理"></a>2. 硬件加速纹理</h3><p>在这之前对于 ‘硬件加速’ 一直都是只闻其名, 不见其实, 真实能带来多大提升也没有底. 在一位小伙伴的大力协助下搞完了这个, 结果喜人, 可以用一句百利而无一害来形容, 如果你们还没有搞的话, 那就赶快行动吧. </p><h3 id="3-Cocos-Creator"><a href="#3-Cocos-Creator" class="headerlink" title="3. Cocos Creator"></a>3. Cocos Creator</h3><p>对于 Cocos 家族的这个新产品一直都没有勇气深入尝试, 直到今年有新的团队成员加入, 恰好他之前用过, 我们试水了一个小游戏后全面开始使用了, 所以便有了知乎的<a href="https://www.zhihu.com/question/66819338/answer/259901965" target="_blank" rel="noopener">这个回答</a>. 在这半年的深度使用后, 感受又略有不同, 不过总体感觉利大于弊吧, 期待 Creator 2.0 版.</p><h3 id="4-基于-Quick-x-的全新资源加载系统"><a href="#4-基于-Quick-x-的全新资源加载系统" class="headerlink" title="4. 基于 Quick-x 的全新资源加载系统"></a>4. 基于 Quick-x 的全新资源加载系统</h3><p>这个可以说是我今年的得意之作了, 我上一张图来说明下:</p><p><img src="http://res.zhangyabing.com/2018-03-12-2017-pic.015.jpeg" alt=""></p><p>这个系统非常牛逼, 基本上颠覆了 cocos2d-x 的资源加载方式, 变成了更加现代化的加载方式.</p><h3 id="5-全新的工具系统"><a href="#5-全新的工具系统" class="headerlink" title="5. 全新的工具系统"></a>5. 全新的工具系统</h3><p>这是我这一年搞的另一个比较重要的事情. 我们之前项目有各种各样的脚本, 比如: 导表, 多语言转化, ui文件转化等等, 这些脚本基本上都是采用 <code>python + shell</code> 写的, 散落在一个 <code>proj.tools</code> 的文件夹中, 也没有文档, 几乎只有我在用.</p><p>重构完成后它变成了这样: </p><p><img src="http://res.zhangyabing.com/2018-03-13-2017-pic.016.jpeg" alt=""></p><p>是不是感觉略屌呢?</p><hr><p>上面的每一条都有必要单独开一篇文章来讲, 这里就不做展开了, 今年如果有时间的话, 我会一一详述的.</p><h2 id="2-学习"><a href="#2-学习" class="headerlink" title="2. 学习"></a>2. 学习</h2><p>今年其实也开了一些新坑, 大多没有填上, 好惭愧. </p><h3 id="1-untp"><a href="#1-untp" class="headerlink" title="1. untp"></a>1. untp</h3><p>今年主要是继续维护, 修复大家提出的一些问题. 考虑到有美术同学在用, 添加了 GUI 和 windows 便携版.</p><p><img src="http://res.zhangyabing.com/2018-03-13-231520.png" alt=""></p><p>直到写文章的今天, 它收获了 101 个 star. 虽说 github 的 star 已经越来越水了, 但在没有推广的情况下, 能有这个结果, 也算是很欣慰了, 应该帮到了一些人吧.</p><h3 id="2-convert2fnt"><a href="#2-convert2fnt" class="headerlink" title="2. convert2fnt"></a>2. convert2fnt</h3><p><img src="https://ws2.sinaimg.cn/large/006tNc79gy1fhkaqhm6dnj30n80ixq5f.jpg" alt=""></p><p>使用 Electron 重写了这工具, 大家感兴趣的可以看<a href="/2017/07/09/convert2fnt-electron-rewrite/">这篇文章</a>. 这个项目算是我的第一个 WEB 技术栈的应用了, 体验非常棒, 感觉 js 会统一全宇宙.</p><h3 id="3-一个很没有意思的消除小游戏"><a href="#3-一个很没有意思的消除小游戏" class="headerlink" title="3. 一个很没有意思的消除小游戏"></a>3. 一个很没有意思的消除小游戏</h3><p>在家学习 Creator 时搞的一个练手的小游戏, 基本上是用 TS 重写了很久之前我用 Lua 写的那版. 大家感兴趣的可以<a href="http://game.justbilt.com/" target="_blank" rel="noopener">点击这里</a>试玩.</p><hr><p>学习上基本就是这样, 本来也没有什么规划, 也就没有完成度什么的, 其实不算太好, 希望今年能有时间做个学习的规划, 或者专心完成一个有意思的游戏也好.</p><h1 id="三-生活"><a href="#三-生活" class="headerlink" title="三. 生活"></a>三. 生活</h1><h2 id="1-篮球"><a href="#1-篮球" class="headerlink" title="1. 篮球"></a>1. 篮球</h2><p>这恐怕是贯穿我这一整年的事情, 每个周六, 约上三五个好友, 相聚于大学的篮球场, 干上一上午, 真是人生一大快事, 当为此浮一大白.</p><p><img src="http://res.zhangyabing.com/2018-03-17-003.jpg" alt=""></p><p>老婆也特别支持我的这项运动, 每次打球完回家都会有一顿可口的饭菜; 担心我受伤, 特意帮我买了运动护具; 甚至还让我带一起打球的同事回家, 做一大桌饭菜招待他们. 得妻如此, 夫复何求 ?</p><h2 id="2-健身"><a href="#2-健身" class="headerlink" title="2. 健身"></a>2. 健身</h2><p>虽然每周都去打球, 但由于管不住自己的嘴, 体重蹭蹭的往上涨, 巅峰时期一度接近 76kg, 人看着都肿了起来.</p><p>于是在10月份的时候决定健身, 之前其实有自己瞎练过, 但是没有成体系, 长期的锻炼过. 这次下载了 Keep, 补充了一些基础知识, 便开启了健身之路.</p><p><img src="http://res.zhangyabing.com/2018-03-17-022344.png" alt=""></p><p>公司的休息室有一些基础的健身设备, 哑铃, 杠铃, 杠铃架, 基本能够满足我的基础需求(手臂/胸/肩/背/腿), 所以没有去健身房.</p><p><img src="http://res.zhangyabing.com/2018-03-17-002.jpg" alt=""></p><p>三个月体重掉了到 67kg, 人看起来也没有那么肿了.</p><h2 id="3-剁手"><a href="#3-剁手" class="headerlink" title="3. 剁手"></a>3. 剁手</h2><h3 id="1-康工-Y-1-椅子"><a href="#1-康工-Y-1-椅子" class="headerlink" title="1. 康工 Y-1 椅子"></a>1. 康工 Y-1 椅子</h3><p><img src="http://res.zhangyabing.com/2018-03-17-IMG_20170522_151848_HHT.jpg" alt=""></p><p>长草了很久的人体工学椅, Herman Miller 太贵家里领导一直不给批, 因此特意去达宝利他们的实体店体验了一下, 最终选择了达宝利他们自己品牌 康工 Y-1, 主要原因还是便宜, 才 1.7k, 不到米勒的 1/5, 体验也不差多少, 还赠送了一个脚托.</p><p>这大半年做下来只能用一个字形容, <code>爽</code>, 自从买了这个椅子后, 我的背和肩就很少疼了. 每天中午看到其他同事以七扭八歪的姿势睡午觉, 我手轻轻一拨, 在他人目瞪口呆的眼光中向后趟去, 深藏功与名.</p><h3 id="2-iPhone7"><a href="#2-iPhone7" class="headerlink" title="2. iPhone7"></a>2. iPhone7</h3><p>老婆生日送我的礼物, 至此结束了我的 5 年 Android 生涯, 对比 Android, iOS 多了一些限制, 也多了一些安心.</p><p>我对 iPhone 最满意的还是它的相机, 秒杀我之前用的 Mi Max, 这点最为重要, 生活中一些有意义的事情都可以随心的被记录下来.</p><h3 id="3-小米笔记本-Air"><a href="#3-小米笔记本-Air" class="headerlink" title="3. 小米笔记本 Air"></a>3. 小米笔记本 Air</h3><p><img src="http://res.zhangyabing.com/2018-03-17-IMG_20170208_221411.jpg" alt=""></p><p>怎么说, 这恐怕是我对小米最失望的一款产品了, 尤其是触摸板, 嘎吱嘎吱的响.</p><h3 id="4-小米手环2"><a href="#4-小米手环2" class="headerlink" title="4. 小米手环2"></a>4. 小米手环2</h3><p><img src="http://res.zhangyabing.com/2018-03-17-IMG_20170213_101722_HDR.jpg" alt=""></p><p>加了时间功能, 满电能挺一个月, 一如既往的满意.</p><hr><p>生活部分基本上就是这样, 还是平平淡淡, 也没有说走就走的旅行. 5月份的时候租车带着家人去了一个不知名的森林公园, 算是这一年去的最远的地方了, 这是我拿到驾照的第一次开着, 非常顺利, 希望今年能有更多时间陪陪家人.</p><p>18年希望公司能做出更好的项目, 赚更多的钱, 有钱了大家都开心, 工作环境也会越来越好. 希望自己能读更多的书, 技术的非技术的都行, 争取变成一个更有内涵的人. 希望家人都身体健康, 不出意外的话会有一位新成员加入, 期待!</p><p>2018, 我们再见.</p>]]></content>
<summary type="html">
又到了一年树新风(tree new bee)的时候啦
</summary>
<category term="年结" scheme="http://blog.justbilt.com/tags/%E5%B9%B4%E7%BB%93/"/>
</entry>
<entry>
<title>最近使用 CocosCreaor 遇到的问题及解决方案</title>
<link href="http://blog.justbilt.com/2018/02/03/creator-tips/"/>
<id>http://blog.justbilt.com/2018/02/03/creator-tips/</id>
<published>2018-02-03T03:01:23.000Z</published>
<updated>2018-02-04T09:25:54.000Z</updated>
<content type="html"><![CDATA[<p>最近两个月, 我们一直在用 CocosCreator 做一个休闲游戏, 在享受 Creator 高效的过程中我们也遇到了这样那样的问题. 有些问题是我们的用法不正确, 有些则是引擎和编辑器的 bug, 好在这些问题基本上都解决了. </p><p>今天我和大家分享下我们团队在使用 CocosCreaor 遇到的问题及解决方案.</p><a id="more"></a><h1 id="Web-篇"><a href="#Web-篇" class="headerlink" title="Web 篇"></a>Web 篇</h1><blockquote><p>Failed to load resource: net::ERR_BLOCKED_BY_CLIENT</p></blockquote><p>解决方案:<br>如果你的浏览器装了广告过滤插件的话, 可能是某个文件拦截了, 在这个页面上停用广告拦截插件就行了.</p><blockquote><p>No ‘Access-Control-Allow-Origin’ header is present on the requested </p></blockquote><p>解决方案:<br>跨域问题, 如果是在开发阶段遇到的话, 我们可以先尝试绕过这个问题. 常用的方案有两个:</p><ol><li>使用 Chrome 插件</li><li>启动 Chrome 时加入允许跨域的参数</li></ol><p>可以参考<a href="https://hectorguo.com/zh/cross-origin-solve/" target="_blank" rel="noopener">这篇</a>进行配置, 第一个方案有时候会失效, 还是第二个方案最稳妥.</p><blockquote><p>Can not find deps [xxx] for path : preview-scripts/assets/Script/xxx.js</p></blockquote><p>解决方案:<br>浏览器提示找不到某个文件, 其实是有两个重名的文件放在不同的目录, 改成不一样的文件名就好了.</p><h1 id="Native-篇"><a href="#Native-篇" class="headerlink" title="Native 篇"></a>Native 篇</h1><blockquote><p>调用 Objc 函数失败, 提示找不到函数</p></blockquote><p>解决方案:<br>Objc 函数如果有任何参数的话, 函数名称最后都要加 <code>:</code>, quick 之前是自动补了这个. Objc 函数不支持 NSObject 类型, 只支持基本类型, 之前 quick 都是通过 NSDictory 进行传参的.</p><blockquote><p>iOS 运行无日志输出</p></blockquote><p>解决方案:<br>binary 模板生成的 iOS 工程出错后没有堆栈信息, 换成了 link/default 模板就解决了, 推测可能是 <code>binary</code> 模板使用的是 Reaease 版的配置编译出的 <code>.a</code>, 因此没有日志.</p><blockquote><p>Android Studio 调试启动很慢</p></blockquote><p>将下图这里改成 <code>Java</code> 就好了, 只是不能 debug c++ 代码了.</p><p><img src="https://ws1.sinaimg.cn/large/006tNc79gy1fo332dei7nj31kw12178h.jpg" alt=""></p><blockquote><p>error: undefined reference to ‘bsd_signal’</p></blockquote><p>解决方案:<br>修改 gradle.properties:</p><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">PROP_TARGET_SDK_VERSION=<span class="number">10</span> <span class="comment">//21 就会报上面的错误</span></span><br></pre></td></tr></table></figure><h1 id="Web-和-Native-API-适配篇"><a href="#Web-和-Native-API-适配篇" class="headerlink" title="Web 和 Native API 适配篇"></a>Web 和 Native API 适配篇</h1><blockquote><p>atob 和 btoa</p></blockquote><p>这两个 API 只有 web 上有, Native 上没有, 只能自己实现, 我是使用 cryto.js 实现的:</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">base64Encode</span>(<span class="params">str: <span class="built_in">string</span></span>): <span class="title">string</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(str));</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">base64Decode</span>(<span class="params">str: <span class="built_in">string</span></span>): <span class="title">string</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> CryptoJS.enc.Base64.parse(str).toString(CryptoJS.enc.Utf8);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p>cc.log </p></blockquote><p>在 Native 上无法输出 Object 类型的变量, 可以利用 Json 自己实现:</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> _log = cc.log;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (cc.sys.isNative) {</span><br><span class="line"> <span class="comment">// tslint:disable-next-line:only-arrow-functions</span></span><br><span class="line"> cc.log = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> arg = [];</span><br><span class="line"> <span class="comment">// tslint:disable-next-line:prefer-for-of</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">let</span> index = <span class="number">0</span>; index < <span class="built_in">arguments</span>.length; index++) {</span><br><span class="line"> <span class="keyword">const</span> element = <span class="built_in">arguments</span>[index];</span><br><span class="line"> <span class="keyword">switch</span> (<span class="keyword">typeof</span> element) {</span><br><span class="line"> <span class="keyword">case</span> <span class="string">"object"</span>:</span><br><span class="line"> arg.push(<span class="built_in">JSON</span>.stringify(element));</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">default</span>:</span><br><span class="line"> arg.push(element);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> _log(arg.join(<span class="string">" "</span>));</span><br><span class="line"> };</span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p>WebSocket/XMLHttpRequest</p></blockquote><p>OPEN/CONNECTING/CLOSING/CLOSED 属性在 Native 无定义, 自己实现:</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">enum</span> SocketStatus {</span><br><span class="line"> OPEN = <span class="number">1</span>,</span><br><span class="line"> CONNECTING = <span class="number">2</span>,</span><br><span class="line"> CLOSING = <span class="number">3</span>,</span><br><span class="line"> CLOSED = <span class="number">4</span>,</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>同理, XMLHttpRequest.DONE(4) 也是没有定义的, 得自己定义或者直接写数字.</p><blockquote><p>unschedule</p></blockquote><p>unschedule 在 Native 时会额外判断 <code>__callbackId</code>, 这个id是在调用 schedule 时赋值的, 所以以下做法会出错:</p><ol><li>schedule 时传入 <code>this.xxx.bind(this)</code></li><li>unschedule 先于 schedule 执行</li></ol><p>schedule 函数没有返回 handle, 因此没有办法判断是否 schedule 过, 所以还是用 update 来做计时器.</p><blockquote><p>JS Exception: null is not a function, file: <no filename="">, lineno: 0</no></p></blockquote><p>解决方案:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cc.director.getScheduler().scheduleUpdate(this, 1, false, this.onTimer);</span><br></pre></td></tr></table></figure><p>如果 this 不是继承自 <code>cc.Component</code> 的话就会持续的触发这个错误.</p><h1 id="诡异现象篇"><a href="#诡异现象篇" class="headerlink" title="诡异现象篇"></a>诡异现象篇</h1><blockquote><p>工程怎么同步都不是最新的代码, 控制台看到了 <code>会话过期</code> 字样</p></blockquote><p>解决方案:<br>重启 Creator , 猜测是在 git pull 的同时打开了 creator 的原因</p><blockquote><p>启动后游戏背景是蓝色的</p></blockquote><p>解决方案:<br>之前从测试用例工程拷贝过来的 <code>cases-settings.js</code> 中设置了清屏颜色, </p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cc.director.setClearColor( cc.hexToColor(<span class="string">'#2f69d2'</span>) );</span><br></pre></td></tr></table></figure><blockquote><p>Creator 打开项目卡死</p></blockquote><p>解决方案:<br>把整个 assets 目录清空, 能够打开了. 随后通过 git 还原 assets 目录, 就可以正常打开了. 详细过程可以看我在论坛中发的<a href="http://forum.cocos.com/t/1-6-2-mac/54449" target="_blank" rel="noopener">这篇帖子</a>.</p><blockquote><p>preloadScene 调用后 http 请求收不到 response</p></blockquote><p>解决方案:<br>c++ 侧断点跟踪后, 确实是收到 Response 了, 但是没有调用到 js 这边, 将 <code>onreadystatechange</code> 事件由匿名函数改为成员函数后解决.</p><blockquote><p>localStorage 无法保存存档</p></blockquote><p>解决方案:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cc.sys.localStorage.setItem(“storage", "world");</span><br></pre></td></tr></table></figure><p>经过排查, 是某些特殊关键字无法存储成功成功, 设置后本次回话有效, 关闭标签后再打开就没有了, 换一个 key 就好了.</p><h1 id="认知篇"><a href="#认知篇" class="headerlink" title="认知篇"></a>认知篇</h1><ol><li>Event 不能持有对象, 因为 EventPool, 你持有的对象属性会发生变化.</li><li>在 onLoad 之前 active 置为 false 将导致不会触发 onLoad 事件, 直到下次 avtive 为 true 时才会触发.</li><li>动画时间轴, 并不是不等分, 而是前面是秒数, 后面是帧数, 30 就是 1 秒的一半.</li></ol>]]></content>
<summary type="html">
<p>最近两个月, 我们一直在用 CocosCreator 做一个休闲游戏, 在享受 Creator 高效的过程中我们也遇到了这样那样的问题. 有些问题是我们的用法不正确, 有些则是引擎和编辑器的 bug, 好在这些问题基本上都解决了. </p>
<p>今天我和大家分享下我们团队在使用 CocosCreaor 遇到的问题及解决方案.</p>
</summary>
<category term="CocosCreaor" scheme="http://blog.justbilt.com/tags/CocosCreaor/"/>
</entry>
<entry>
<title>将 python 数据转化为 TypeScript 格式</title>
<link href="http://blog.justbilt.com/2017/12/24/serialize-typescript-in-pyhton/"/>
<id>http://blog.justbilt.com/2017/12/24/serialize-typescript-in-pyhton/</id>
<published>2017-12-24T13:33:03.000Z</published>
<updated>2018-04-21T05:45:37.000Z</updated>
<content type="html"><![CDATA[<p>前段时间写过这篇文章<a href="/2017/09/03/serialize-lua-in-pyhton/">将 python 数据转化为 lua 格式</a>, 这段时间因为新项目改用 <code>Creator + TypeScript</code> 的原因, 需要导出 ts 格式的数据.</p><p>当然我们可以选择使用 json/yaml 等格式作为数据文件, 这会简单很多, 但是有两个原因, 使得我们一致认为 ts 格式的数据是更好的选择:</p><ol><li>访问数据时 VSCode 会根据数据内容给出提示</li><li>打包时会被编译加密, 省去自己加密数据文件的工作</li></ol><a id="more"></a><p>其中第一点是最为重要的, 这也是我们立项之初就选择 <code>TypeScript</code> 的重要原因:</p><p><img src="https://ws4.sinaimg.cn/large/006tKfTcly1fms8s40359j30fx021q32.jpg" alt=""></p><p>这太棒了, 下面就让我们看看这个文件是如何生成的吧.</p><h1 id="魔改-lua-py"><a href="#魔改-lua-py" class="headerlink" title="魔改 lua.py"></a>魔改 lua.py</h1><p>我们直接在前一篇文章的 <a href="https://gist.github.com/justbilt/a5cfafe761b8571e7b5a9e7c22cc7c57" target="_blank" rel="noopener">lua.py</a> 的基础上改, 这样会节省一些准备工作. 首先我们来对比一下 lua 和 ts 语法的区别:</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"> {</span><br><span class="line"> name = <span class="string">"Nick"</span>, </span><br><span class="line"> condition_VIP_level = <span class="number">0</span>, </span><br><span class="line"> item = {</span><br><span class="line"> [<span class="number">1</span>] = <span class="number">5</span>, </span><br><span class="line"> [<span class="number">2</span>] = <span class="number">10</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"> {</span><br><span class="line"> name: <span class="string">"Nick"</span>, </span><br><span class="line"> condition_VIP_level: <span class="number">0</span>, </span><br><span class="line"> item: [</span><br><span class="line"> <span class="number">5</span>, </span><br><span class="line"> <span class="number">10</span>,</span><br><span class="line"> ], </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>从上面的示例可以看出, 这两个语言还是蛮像的, 只是有一些细微的区别.</p><h2 id="Dict"><a href="#Dict" class="headerlink" title="Dict"></a>Dict</h2><p>lua 使用 <code>=</code>, 而 ts 使用 <code>:</code>, 我们直接修改:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">key_separator = <span class="string">': '</span></span><br></pre></td></tr></table></figure><p>另有一个小细节, tslint 推荐最后一个元素后面也加上 <code>,</code>, 猜测这样做的好处是方便移动, 新增新的条目, 这样只用改动一行就行. 我们修改:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># yield '\n' + (' ' * (_indent * _current_indent_level))</span></span><br><span class="line"><span class="keyword">yield</span> _item_separator + <span class="string">'\n'</span> + (<span class="string">' '</span> * (_indent * _current_indent_level))</span><br></pre></td></tr></table></figure><h2 id="Array"><a href="#Array" class="headerlink" title="Array"></a>Array</h2><p>lua 中创建 Array 也是用的 <code>{}</code>, 而 ts 是 <code>[]</code>, 这个很好改, 将 <code>_iterencode_list</code> 中的标签改一下就行. </p><p>Array 和 Dict 一样, 最后一个元素要加逗号, 和 Dict 的修改方法一致.</p><p>另我们的原始数据有一个 bug, List 类型的数据也会创建 Dict 来处理, 下标还是从 1 开始的. 于是我写了一个小函数来将这种类型转换为 List 格式:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">dict_to_array</span><span class="params">(dct)</span>:</span></span><br><span class="line">array = [<span class="keyword">None</span>] * len(dct)</span><br><span class="line"><span class="keyword">for</span> key, value <span class="keyword">in</span> dct.iteritems():</span><br><span class="line"><span class="keyword">if</span> type(key) == int:</span><br><span class="line"><span class="keyword">try</span>:</span><br><span class="line">array[key<span class="number">-1</span>] = value</span><br><span class="line"><span class="keyword">except</span> Exception <span class="keyword">as</span> e:</span><br><span class="line"><span class="keyword">break</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> item <span class="keyword">in</span> array:</span><br><span class="line"><span class="keyword">if</span> <span class="keyword">not</span> item:</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line"><span class="keyword">return</span> array</span><br></pre></td></tr></table></figure><p>然后在渲染 Dict 的时候判断下, 如果可以转化为 List 的话就用 List 去渲染.</p><h2 id="导出"><a href="#导出" class="headerlink" title="导出"></a>导出</h2><p>之前 lua 的导出用的是 <code>return xxx;</code> 的形式, 在 ts 中修改为:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">return "export default " + cls(...) + ";\n"</span><br></pre></td></tr></table></figure><hr><p>以上就是修改的全部内容, 技术含量不高, 以后也不会再写类似的文章了. 从这两次的修改中, 可以看到 Python json 库设计的非常棒, 只需要做少量修改就可以变成其他数据格式. 其中对 <code>yield</code> 用法堪称典范, 十分值得我们学习, 有机会我们应该就这点进行赏析.</p>]]></content>
<summary type="html">
<p>前段时间写过这篇文章<a href="/2017/09/03/serialize-lua-in-pyhton/">将 python 数据转化为 lua 格式</a>, 这段时间因为新项目改用 <code>Creator + TypeScript</code> 的原因, 需要导出 ts 格式的数据.</p>
<p>当然我们可以选择使用 json/yaml 等格式作为数据文件, 这会简单很多, 但是有两个原因, 使得我们一致认为 ts 格式的数据是更好的选择:</p>
<ol>
<li>访问数据时 VSCode 会根据数据内容给出提示</li>
<li>打包时会被编译加密, 省去自己加密数据文件的工作</li>
</ol>
</summary>
</entry>
<entry>
<title>用 python 撸一个 ccbwriter</title>
<link href="http://blog.justbilt.com/2017/12/17/python-ccbwriter/"/>
<id>http://blog.justbilt.com/2017/12/17/python-ccbwriter/</id>
<published>2017-12-17T04:50:25.000Z</published>
<updated>2017-12-22T22:01:02.000Z</updated>
<content type="html"><![CDATA[<p>之前可能和大家说过我们项目的 UI 编辑器是上古神器 <a href="https://github.com/cocos2d/CocosBuilder" target="_blank" rel="noopener">CocosBuilder</a>, 这个编辑器诞生在那个 cocos2d-x 还需手写 ui 代码的年代, 是 cocos 发展史上一个重要转折点. 它标志着触屏手机游戏行业 ui 不再像之前那样只是简单的串联的作用, 而是像端游那样重交互, 可视化编辑的路线.</p><a id="more"></a><p>尽管在今天, CocosBuilder 想比于已经死掉的 CocosStudio, 甚至于 Cocos 现在大力推广的 CocosCreator 都有一些难以替代的理由, 尤其是在开源和稳定性方面. 我们也不例外, 现在仍有多款项目在使用这个编辑器, 单个项目 CocosBuilder 的界面 ccb 文件数量最高在 500+, 这对于维护这些界面的美术同学是一个很大的挑战. 为了不让可爱的美术妹子疲于奔命, 我们程序组自然要贡献我们的一份力量, 写一些批处理的脚本, 解决那些无脑但是会大量重复的工作, 拯救妹子们与水火之中.</p><p>在写脚本的过程中, 不免要和 ccb 文件打交道, 这种文件的典型格式是这样的:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta"><?xml version="1.0" encoding="UTF-8"?></span></span><br><span class="line"><span class="meta"><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"></span></span><br><span class="line"><span class="tag"><<span class="name">plist</span> <span class="attr">version</span>=<span class="string">"1.0"</span>></span></span><br><span class="line"><span class="tag"><<span class="name">dict</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">string</span>></span>CocosBuilder<span class="tag"></<span class="name">string</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">key</span>></span>fileVersion<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">integer</span>></span>4<span class="tag"></<span class="name">integer</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">key</span>></span>guides<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">array</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">dict</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">key</span>></span>position<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">real</span>></span>-2346<span class="tag"></<span class="name">real</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">dict</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">array</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">key</span>></span>jsControlled<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">false</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">key</span>></span>nodeGraph<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">dict</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">key</span>></span>baseClass<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">string</span>></span>CCNode<span class="tag"></<span class="name">string</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">key</span>></span>children<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">array</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">key</span>></span>customClass<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">string</span>></span><span class="tag"></<span class="name">string</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">key</span>></span>displayName<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">string</span>></span>CCNode<span class="tag"></<span class="name">string</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">dict</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dict</span>></span></span><br><span class="line"><span class="tag"></<span class="name">plist</span>></span></span><br></pre></td></tr></table></figure><p>在搞 ccb2lua 项目时我曾写过一个 ccbreader 组件, 把 ccb 文件当做 xml 来解析, 一行是 <code>key</code>, 下一行是 <code>value</code>. 直到在 Mac 上工作的久了, 才知道这种特殊的 xml 是一种名为 <a href="https://zh.wikipedia.org/wiki/%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8" target="_blank" rel="noopener">Property List</a> 的文件.</p><h1 id="一-使用-python-plistlib-库解析"><a href="#一-使用-python-plistlib-库解析" class="headerlink" title="一. 使用 python plistlib 库解析"></a>一. 使用 python plistlib 库解析</h1><p>而作为我司第一语言的 Python 自然也有对应的解析库 <a href="https://docs.python.org/2/library/plistlib.html" target="_blank" rel="noopener">plistlib</a>, 这是一个内置的库, 不需要安装直接就能使用, 用法也很简单.</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> plistlib</span><br><span class="line">plist = plistlib.readPlist(<span class="string">"/path/of/you/plistfile"</span>)</span><br><span class="line"><span class="keyword">print</span> plist</span><br><span class="line">plist[<span class="string">"somekey"</span>] = <span class="string">"some value"</span></span><br><span class="line">plistlib.writePlist(plist, <span class="string">"/path/of/you/another/plistfile"</span>)</span><br></pre></td></tr></table></figure><p>上面就是一个最简单的读取修改写入 plist 文件的例子, 但是令人沮丧的是即便我什么都不做, 但是读取再写入整个文件也会发生很多的变动:</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@@ -18,7 +18,7 @@</span></span><br><span class="line"> <key>position</key></span><br><span class="line"><span class="deletion">- <real>-2346</real></span></span><br><span class="line"><span class="addition">+ <real>-2346.0</real></span></span><br><span class="line"><span class="meta">@@ -33,7 +33,8 @@</span></span><br><span class="line"> <key>children</key></span><br><span class="line"><span class="deletion">- <array/></span></span><br><span class="line"><span class="addition">+ <array></span></span><br><span class="line"><span class="addition">+ </array></span></span><br><span class="line"><span class="meta">@@ -62,8 +63,8 @@</span></span><br><span class="line"> <string>ScaleLock</string></span><br><span class="line"> <key>value</key></span><br><span class="line"> <array></span><br><span class="line"><span class="deletion">- <real>1</real></span></span><br><span class="line"><span class="deletion">- <real>1</real></span></span><br><span class="line"><span class="addition">+ <real>1.0</real></span></span><br><span class="line"><span class="addition">+ <real>1.0</real></span></span><br><span class="line"> </array></span><br></pre></td></tr></table></figure><p>主要发生在这么几个地方:</p><h2 id="1-浮点数精度问题"><a href="#1-浮点数精度问题" class="headerlink" title="1. 浮点数精度问题"></a>1. 浮点数精度问题</h2><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"><!-- 原始 --></span></span><br><span class="line"><span class="tag"><<span class="name">real</span>></span>-2346<span class="tag"></<span class="name">real</span>></span></span><br><span class="line"><span class="comment"><!-- 变为 --></span></span><br><span class="line"><span class="tag"><<span class="name">real</span>></span>-2346.0<span class="tag"></<span class="name">real</span>></span></span><br><span class="line"></span><br><span class="line"><span class="comment"><!-- 原始 --></span></span><br><span class="line"><span class="tag"><<span class="name">real</span>></span>941.91412353515625<span class="tag"></<span class="name">real</span>></span></span><br><span class="line"><span class="comment"><!-- 变为 --></span></span><br><span class="line"><span class="tag"><<span class="name">real</span>></span>941.9141235351562<span class="tag"></<span class="name">real</span>></span></span><br></pre></td></tr></table></figure><h2 id="2-空的-array-和-dict"><a href="#2-空的-array-和-dict" class="headerlink" title="2. 空的 array 和 dict"></a>2. 空的 array 和 dict</h2><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"><!-- 原始 --></span></span><br><span class="line"><span class="tag"><<span class="name">array</span>/></span></span><br><span class="line"><span class="comment"><!-- 变为 --></span></span><br><span class="line"><span class="tag"><<span class="name">array</span>></span></span><br><span class="line"><span class="tag"></<span class="name">array</span>></span></span><br><span class="line"></span><br><span class="line"><span class="comment"><!-- 原始 --></span></span><br><span class="line"><span class="tag"><<span class="name">dict</span>/></span></span><br><span class="line"><span class="comment"><!-- 变为 --></span></span><br><span class="line"><span class="tag"><<span class="name">dict</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dict</span>></span></span><br></pre></td></tr></table></figure><p>虽然这些变动不会影响该文件的再次读取, 并且当在 CocosBuilder 再次保存时又会还原回去, 但是这会造成两个很严重的问题:</p><ol><li>影响其他成员 review 更改, 无法识别出真正的变动</li><li>增加合并时产生冲突的可能</li></ol><p>尤其是第二点, ccb 文件一旦冲突, 基本很难解决, 只能选择放弃一方的修改, 这个损失是无法容忍的. 要解决这个问题, 我们可以有多种解决方案:</p><ol><li>像当时写 ccbreader 一样完全手撸一个 ccbwriter</li><li>看下 plistlib 是否可以定制下解析器和写入代码</li></ol><h1 id="二-改造-plistlib"><a href="#二-改造-plistlib" class="headerlink" title="二. 改造 plistlib"></a>二. 改造 plistlib</h1><p>经过一番考虑, 我选择了第二种方案, 官方文档上对 plistlib 说明很少, 我们只能从它的源码入手. 我电脑上它位于这个位置:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/plistlib.py</span><br></pre></td></tr></table></figure><p>我们重点看 <code>readPlist</code> 和 <code>writePlist</code>:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">readPlist</span><span class="params">(pathOrFile)</span>:</span></span><br><span class="line"> didOpen = <span class="number">0</span></span><br><span class="line"> <span class="keyword">if</span> isinstance(pathOrFile, (str, unicode)):</span><br><span class="line"> pathOrFile = open(pathOrFile)</span><br><span class="line"> didOpen = <span class="number">1</span></span><br><span class="line"> p = PlistParser()</span><br><span class="line"> rootObject = p.parse(pathOrFile)</span><br><span class="line"> <span class="keyword">if</span> didOpen:</span><br><span class="line"> pathOrFile.close()</span><br><span class="line"> <span class="keyword">return</span> rootObject</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">writePlist</span><span class="params">(rootObject, pathOrFile)</span>:</span></span><br><span class="line"> didOpen = <span class="number">0</span></span><br><span class="line"> <span class="keyword">if</span> isinstance(pathOrFile, (str, unicode)):</span><br><span class="line"> pathOrFile = open(pathOrFile, <span class="string">"w"</span>)</span><br><span class="line"> didOpen = <span class="number">1</span></span><br><span class="line"> writer = PlistWriter(pathOrFile)</span><br><span class="line"> writer.writeln(<span class="string">"<plist version=\"1.0\">"</span>)</span><br><span class="line"> writer.writeValue(rootObject)</span><br><span class="line"> writer.writeln(<span class="string">"</plist>"</span>)</span><br><span class="line"> <span class="keyword">if</span> didOpen:</span><br><span class="line"> pathOrFile.close()</span><br></pre></td></tr></table></figure><p>这里面分别用到了 <code>PlistParser</code> 和 <code>PlistWriter</code> 作为解析和写入工具类, 我们完全继承自这两个类写一个自定义 <code>Parser</code> 和 <code>Writer</code>, 处理掉之前的特殊情况.</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CCBReal</span>:</span></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, value)</span>:</span></span><br><span class="line"> self.value_raw = value</span><br><span class="line"> self.value = float(value)</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">float</span><span class="params">(self)</span>:</span></span><br><span class="line"> <span class="keyword">return</span> self.value</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">raw</span><span class="params">(self)</span>:</span></span><br><span class="line"> <span class="keyword">return</span> self.value_raw</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CCBParser</span><span class="params">(plistlib.PlistParser)</span>:</span></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">end_real</span><span class="params">(self)</span>:</span></span><br><span class="line"> value = self.getData()</span><br><span class="line"> self.addObject(CCBReal(value))</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CCBWriter</span><span class="params">(plistlib.PlistWriter)</span>:</span></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">writeValue</span><span class="params">(self, value)</span>:</span></span><br><span class="line"> <span class="keyword">if</span> isinstance(value, CCBReal):</span><br><span class="line"> self.simpleElement(<span class="string">"real"</span>, value.raw())</span><br><span class="line"> <span class="keyword">else</span>:</span><br><span class="line"> plistlib.PlistWriter.writeValue(self, value)</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">writeArray</span><span class="params">(self, array)</span>:</span></span><br><span class="line"> <span class="keyword">if</span> len(array) <= <span class="number">0</span>:</span><br><span class="line"> self.writeln(<span class="string">"<array/>"</span>)</span><br><span class="line"> <span class="keyword">else</span>:</span><br><span class="line"> plistlib.PlistWriter.writeArray(self, array)</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">writeDict</span><span class="params">(self, d)</span>:</span></span><br><span class="line"> <span class="keyword">if</span> len(d) <= <span class="number">0</span>:</span><br><span class="line"> self.writeln(<span class="string">"<dict/>"</span>)</span><br><span class="line"> <span class="keyword">else</span>:</span><br><span class="line"> plistlib.PlistWriter.writeDict(self, d)</span><br></pre></td></tr></table></figure><p>对空的 <code>dict</code> 和 <code>array</code> 处理很简单, 判断下长度就可以啦. 但是对 <code>real</code> 的处理就费了一番周折.</p><p>我一开始想着用 python 中的高精度类型来保存 real 的值, 但是过程十分曲折, 数值完全对不上, 各种修改测试依然不行. 绝望临近放弃之际, 突然灵光一现, 可以用一个复杂的类型同时记录下原始的值 (string 类型) 和转化为 float 中的值, 这样如果在整个程序运行的过程中没有改变这个值的话就可以把原始值写入进去, 这样就不会有任何意料之外的更改了.</p><p>全部代码已经放到<a href="https://gist.github.com/justbilt/cc21b7c6573bac7668282faa56dfdd9b" target="_blank" rel="noopener">我的 gist 上</a>了, 有需要可以自取.</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">plist = readPlist(<span class="string">"/path/of/you/ccb"</span>)</span><br><span class="line"><span class="keyword">print</span> plist.guides[<span class="number">0</span>].position</span><br><span class="line"><span class="keyword">print</span> plist.guides[<span class="number">0</span>].position.float()</span><br><span class="line">plist.guides[<span class="number">0</span>].position = <span class="number">5.5</span></span><br><span class="line"><span class="keyword">print</span> plist.guides[<span class="number">0</span>].position</span><br><span class="line">writePlist(plist, ccb)</span><br></pre></td></tr></table></figure><p>测试代码当然少不了啦, 感觉自己棒棒哒, 晚饭申请加个🍗. </p>]]></content>
<summary type="html">
<p>之前可能和大家说过我们项目的 UI 编辑器是上古神器 <a href="https://github.com/cocos2d/CocosBuilder" target="_blank" rel="noopener">CocosBuilder</a>, 这个编辑器诞生在那个 cocos2d-x 还需手写 ui 代码的年代, 是 cocos 发展史上一个重要转折点. 它标志着触屏手机游戏行业 ui 不再像之前那样只是简单的串联的作用, 而是像端游那样重交互, 可视化编辑的路线.</p>
</summary>
<category term="Python" scheme="http://blog.justbilt.com/tags/Python/"/>
<category term="CocosBuilder" scheme="http://blog.justbilt.com/tags/CocosBuilder/"/>
</entry>
<entry>
<title>如何为 Creator 添加 js/ts 第三方库支持</title>
<link href="http://blog.justbilt.com/2017/12/10/creator-add-package/"/>
<id>http://blog.justbilt.com/2017/12/10/creator-add-package/</id>
<published>2017-12-10T11:57:39.000Z</published>
<updated>2017-12-10T14:02:24.000Z</updated>
<content type="html"><![CDATA[<p>正如知乎<a href="https://www.zhihu.com/question/66819338/answer/259901965" target="_blank" rel="noopener">我的这个回答</a>中所说, 我们开始使用 Cocos Creaor 开发新的项目, 所以以后这个博客会陆续开始更新一些 Creator 相关的文章.</p><p>我们在项目开发的过程中, 肯定会遇到一些项目间通用的需求, 比如 <code>压缩/加密</code> 之类的, 我们当然可以自己实现一个, 但更好的做法是找找网上有没有别人实现好的库直接拿来用, 今天我们就来看看如何为 Creator 添加添加 js/ts 第三方库支持.</p><a id="more"></a><h1 id="一-下载源文件"><a href="#一-下载源文件" class="headerlink" title="一. 下载源文件"></a>一. 下载源文件</h1><p>以 <a href="https://github.com/jakesgordon/javascript-state-machine" target="_blank" rel="noopener">javascript-state-machine</a> 为例, 我们可以在 <code>Clone or Download</code> 中选择 <code>Download Zip</code> 下载源文件压缩包, 解压后在将 <code>dist/state-machine.js</code> 复制到我们项目中的 <code>assets/Script</code> 目录中.</p><p>然后在我们的代码中:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> StateMachine <span class="keyword">from</span> <span class="string">"state-machine"</span>;</span><br><span class="line">cc.Class({</span><br><span class="line"> extends: cc.Component,</span><br><span class="line"> properties: {</span><br><span class="line"> label: {</span><br><span class="line"> <span class="keyword">default</span>: <span class="literal">null</span>,</span><br><span class="line"> type: cc.Label</span><br><span class="line"> },</span><br><span class="line"> text: <span class="string">'Hello, World!'</span></span><br><span class="line"> },</span><br><span class="line"> onLoad: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">this</span>.label.string = <span class="keyword">this</span>.text;</span><br><span class="line"> <span class="keyword">var</span> fsm = <span class="keyword">new</span> StateMachine({</span><br><span class="line"> init: <span class="string">'solid'</span>,</span><br><span class="line"> transitions: [</span><br><span class="line"> { <span class="attr">name</span>: <span class="string">'melt'</span>, <span class="attr">from</span>: <span class="string">'solid'</span>, <span class="attr">to</span>: <span class="string">'liquid'</span> },</span><br><span class="line"> { <span class="attr">name</span>: <span class="string">'freeze'</span>, <span class="attr">from</span>: <span class="string">'liquid'</span>, <span class="attr">to</span>: <span class="string">'solid'</span> },</span><br><span class="line"> { <span class="attr">name</span>: <span class="string">'vaporize'</span>, <span class="attr">from</span>: <span class="string">'liquid'</span>, <span class="attr">to</span>: <span class="string">'gas'</span> },</span><br><span class="line"> { <span class="attr">name</span>: <span class="string">'condense'</span>, <span class="attr">from</span>: <span class="string">'gas'</span>, <span class="attr">to</span>: <span class="string">'liquid'</span> }</span><br><span class="line"> ],</span><br><span class="line"> methods: {</span><br><span class="line"> onMelt: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{ <span class="built_in">console</span>.log(<span class="string">'I melted'</span>) },</span><br><span class="line"> onFreeze: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{ <span class="built_in">console</span>.log(<span class="string">'I froze'</span>) },</span><br><span class="line"> onVaporize: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{ <span class="built_in">console</span>.log(<span class="string">'I vaporized'</span>) },</span><br><span class="line"> onCondense: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{ <span class="built_in">console</span>.log(<span class="string">'I condensed'</span>) }</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> fsm.melt(); </span><br><span class="line"> }</span><br><span class="line">});</span><br></pre></td></tr></table></figure><p>在控制台中可以看到输出:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">I melted</span><br></pre></td></tr></table></figure><h1 id="二-使用-npm"><a href="#二-使用-npm" class="headerlink" title="二. 使用 npm"></a>二. 使用 npm</h1><p>搞 js 的同学都会知道 <a href="https://www.npmjs.com/" target="_blank" rel="noopener">npm</a> 这个 js 最大的轮子集中营, 它可以很方便的帮你安装/管理所需的第三方依赖库. 如果你觉得上面下载代码的形式太过繁琐的话, 就可以考虑使用 npm 来安装. </p><p>npm 会在你安装 <a href="https://nodejs.org/zh-cn/" target="_blank" rel="noopener">nodejs</a> 时自动拥有, 可以使用 <code>npm -v</code> 命令检查 npm 状态. 下面我们要观察下你的项目有没有 <code>package.json</code> 这个文件, 如果没有的话我们要先使用:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm init</span><br></pre></td></tr></table></figure><p>初始化项目, 需要填一堆信息, 基本上一路回车就可以. 完成之后我们可以去 npm 的网站上搜索下是否存在我们需要的库, 然后用 <code>npm instal xxx --save</code> 命令安装.</p><p>我们还是以 <code>javascript-state-machine</code> 距离, 我们可以找到它在 npm 上的<a href="https://www.npmjs.com/package/javascript-state-machine" target="_blank" rel="noopener">这个页面</a>, 然后安装:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install javascript-state-machine --save</span><br></pre></td></tr></table></figure><p>安装完成后, 我们有了一个新的问题, 如何去 require 这个文件呢 ? 之前源码直接 require 的方式肯定是不行的了, 最终在论坛中找到了<a href="http://forum.cocos.com/t/npm/37930/2?u=justbilt" target="_blank" rel="noopener">解决方案</a>, 我们需要这样:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> StateMachine <span class="keyword">from</span> <span class="string">"javascript-state-machine"</span>;</span><br></pre></td></tr></table></figure><h1 id="三-在-TypeScript-中使用-npm-安装的三方库"><a href="#三-在-TypeScript-中使用-npm-安装的三方库" class="headerlink" title="三. 在 TypeScript 中使用 npm 安装的三方库"></a>三. 在 TypeScript 中使用 npm 安装的三方库</h1><p>上面讲的都是 js 的项目, 那么 ts 的项目又该如何搞呢 ? 我们除了 js 中的那些操作外还需要安装一个 ts 的标注库. </p><p>我们先去 <a href="https://microsoft.github.io/TypeSearch/" target="_blank" rel="noopener">TypeSearch</a> 搜索下是否有我们需要的库的标注库:</p><p><img src="https://ws4.sinaimg.cn/large/006tNc79ly1fmc08dgaohj30gc07zq3j.jpg" alt=""></p><p>点击后会跳转的 npm 对应的页面, 我们要根据提示安装这个库:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install --save @types/javascript-state-machine</span><br></pre></td></tr></table></figure><p>这样我们就可以在 TypeScript 中使用下面代码去 import :</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> StateMachine <span class="keyword">from</span> <span class="string">"javascript-state-machine"</span>;</span><br></pre></td></tr></table></figure><p>值得一提的是, 这些 @types 的库很多都是大家自己制作后贡献给社区的, 集合在 <a href="https://github.com/DefinitelyTyped/DefinitelyTyped" target="_blank" rel="noopener">DefinitelyTyped</a> 这个仓库中, 省去了我么自己标注的工作, 感谢这些贡献者们. </p><p>还有一个问题, 这些标注库更新可能落后于原始项目, 因此我们要安装对应版本的原始库才能够正确提示, 对应版本可以在 <code>node_modules/@types/javascript-state-machine/index.d.ts</code> 找到:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">// Type definitions <span class="keyword">for</span> Finite State Machine 2.4</span><br></pre></td></tr></table></figure><p>因此, 我们要安装 <code>javascript-state-machine</code> 需要指定版本:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install javascript-state-machine@2.4 --save</span><br></pre></td></tr></table></figure><hr><p>以上就是我们安装第三方库的经验, 希望能够帮助到大家. 我们也是才开始使用 Creator, 如果遇到问题, 希望可以和大家多多交流.</p>]]></content>
<summary type="html">
<p>正如知乎<a href="https://www.zhihu.com/question/66819338/answer/259901965" target="_blank" rel="noopener">我的这个回答</a>中所说, 我们开始使用 Cocos Creaor 开发新的项目, 所以以后这个博客会陆续开始更新一些 Creator 相关的文章.</p>
<p>我们在项目开发的过程中, 肯定会遇到一些项目间通用的需求, 比如 <code>压缩/加密</code> 之类的, 我们当然可以自己实现一个, 但更好的做法是找找网上有没有别人实现好的库直接拿来用, 今天我们就来看看如何为 Creator 添加添加 js/ts 第三方库支持.</p>
</summary>
<category term="CocosCreaor" scheme="http://blog.justbilt.com/tags/CocosCreaor/"/>
</entry>
<entry>
<title>iOS 打包工具在 XCode9 下的改动</title>
<link href="http://blog.justbilt.com/2017/12/02/xcode9-package-tools/"/>
<id>http://blog.justbilt.com/2017/12/02/xcode9-package-tools/</id>
<published>2017-12-02T05:26:04.000Z</published>
<updated>2017-12-04T12:42:29.000Z</updated>
<content type="html"><![CDATA[<p>相信大家都会用到 iOS 的自动打包工具, 而不是每次都是通过 XCode 的 Archive 后手动导出的, 那样效率低下不说还增加出错的可能.</p><p>我们团队也是如此, 用 Python 搞了一套打包工具, 这套工具的核心是 XCode 附带的命令行工具 <code>xcodebuild</code>, 网上其他开源的打包工具也都大同小异. 这套工具自诞生以来一直运行的很稳定, 累计为我们打了数百个包, 为团队节省的时间估计也在数千分钟了.</p><a id="more"></a><h1 id="一-无法导出-ipa"><a href="#一-无法导出-ipa" class="headerlink" title="一. 无法导出 ipa"></a>一. 无法导出 ipa</h1><p>但是在更新 XCode9 之后, 工具挂掉了, 在导出包的时候会提示:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">error: exportArchive: <span class="string">"wobwopr.app"</span> requires a provisioning profile with the Push Notifications feature.</span><br><span class="line">Error Domain=IDEProvisioningErrorDomain Code=9 <span class="string">""</span>wobwopr.app<span class="string">" requires a provisioning profile with the Push Notifications feature."</span> UserInfo={NSLocalizedDescription=<span class="string">"wobwopr.app"</span> requires a provisioning profile with the Push Notifications feature., NSLocalizedRecoverySuggestion=Add a profile to the <span class="string">"provisioningProfiles"</span> dictionary <span class="keyword">in</span> your Export Options property list.}</span><br></pre></td></tr></table></figure><p>简单 Google 了下, 也有其他打包工具也遇到了类似的错误:</p><p><a href="https://github.com/fastlane/fastlane/issues/9589" target="_blank" rel="noopener">fastlane: https://github.com/fastlane/fastlane/issues/9589</a></p><p>还有这些文章:</p><p><a href="https://blog.bitrise.io/new-export-options-plist-in-xcode-9" target="_blank" rel="noopener">New export options Plist in Xcode 9</a><br><a href="https://htge.github.io/osx/ios/2017/09/21/xcode9-export-ipa/" target="_blank" rel="noopener">Xcode9命令行新打包方法</a></p><p>总结了下, 错误的原因是这个样子的, 通过 <code>xcodebuild</code> 导出 ipa 包时需要传入参数 <code>exportOptionsPlist</code> 指定导出配置, XCode9 之前这个最简单的配置是这个样子的:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta"><?xml version="1.0" encoding="UTF-8"?></span></span><br><span class="line"><span class="meta"><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"></span></span><br><span class="line"><span class="tag"><<span class="name">plist</span> <span class="attr">version</span>=<span class="string">"1.0"</span>></span></span><br><span class="line"><span class="tag"><<span class="name">dict</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">key</span>></span>method<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">string</span>></span>{method}<span class="tag"></<span class="name">string</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dict</span>></span></span><br><span class="line"><span class="tag"></<span class="name">plist</span>></span></span><br></pre></td></tr></table></figure><p>method 可选的值有: </p><ul><li>app-store</li><li>ad-hoc</li><li>package</li><li>enterprise</li><li>development</li><li>developer-id</li><li>mac-application</li></ul><p>而 XCode9 之后最小化的配置变成了:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta"><?xml version="1.0" encoding="UTF-8"?></span></span><br><span class="line"><span class="meta"><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"></span></span><br><span class="line"><span class="tag"><<span class="name">plist</span> <span class="attr">version</span>=<span class="string">"1.0"</span>></span></span><br><span class="line"><span class="tag"><<span class="name">dict</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">key</span>></span>method<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">string</span>></span>{method}<span class="tag"></<span class="name">string</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">key</span>></span>provisioningProfiles<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">dict</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">key</span>></span>{bundleid}<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">string</span>></span>{provision}<span class="tag"></<span class="name">string</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">dict</span>></span> </span><br><span class="line"><span class="tag"></<span class="name">dict</span>></span></span><br><span class="line"><span class="tag"></<span class="name">plist</span>></span></span><br></pre></td></tr></table></figure><p>新增了 <code>provisioningProfiles</code> 字段, 它是一个由 <code>包名:配置文件名字或udid</code> 组成的字典. 那么我们改如何获取这些参数呢 ?</p><h2 id="1-使用-XCode-导出后保存"><a href="#1-使用-XCode-导出后保存" class="headerlink" title="1. 使用 XCode 导出后保存"></a>1. 使用 XCode 导出后保存</h2><p>这个是目前网上提供的最多的解决方案, 具体步骤如下:</p><ol><li>使用 XCode 打开你的项目, <code>Projuct/Archive</code> 归档工程. 或者直接用 XCode 打开你的 <code>xcarchive</code> 文件.</li><li>点击 <code>Export</code> 选择 <code>App Store/Development</code> 后下一步, 选择证书和配置文件后导出.</li><li>在导出文件夹中找到 <code>ExportOptions.plist</code>, 这个里面的有你想要的参数.</li></ol><p>这个方法相对简单, 却不是完美的解决方案, 它存在这么两个缺点:</p><ol><li>没法自动来做这件事情, 测试/发布得搞两次, 每个新的项目都要做一遍.</li><li>项目证书发生变化后又得重新来一遍.</li></ol><p>想到以后每个项目都要手动来搞, 那打包工具的意义有何在呢 ?</p><h2 id="2-从-xcodeproj-中读取"><a href="#2-从-xcodeproj-中读取" class="headerlink" title="2. 从 xcodeproj 中读取"></a>2. 从 xcodeproj 中读取</h2><p><img src="https://ws3.sinaimg.cn/large/006tKfTcly1fm3foq44n2j30jh0a2my7.jpg" alt=""></p><p>既然我们已经在 XCode 中配置过了这些参数, 那我们能否从 XCode 工程文件中读取到这写参数呢 ? 让我们先打开一个工程寻找一下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">PRODUCT_BUNDLE_IDENTIFIER = com.xxx.xxx;</span><br><span class="line">PROVISIONING_PROFILE = "74c78746-9aa9-4a37-9533-a991f42b7f58";</span><br><span class="line">PROVISIONING_PROFILE_SPECIFIER = "xxx-dev";</span><br></pre></td></tr></table></figure><p>哈哈, 不费吹灰之力便找到了. 下一步就是如何在代码中读取这些参数了? 我们的打包脚本是使用 python 写的, 因此我选择 <a href="https://github.com/kronenthaler/mod-pbxproj" target="_blank" rel="noopener">pbxproj</a> 这个库来解析 pbxproj 文件:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> pbxproj <span class="keyword">import</span> XcodeProject</span><br><span class="line">project = XcodeProject.load(<span class="string">"path/of/your/pbxproj"</span>)</span><br><span class="line"><span class="keyword">for</span> configuration <span class="keyword">in</span> project.objects.get_configurations_on_targets(<span class="string">"{target}"</span>, <span class="string">"{configuration}"</span>):</span><br><span class="line"> bundleid = configuration.buildSettings.PRODUCT_BUNDLE_IDENTIFIER</span><br><span class="line"> specifier = configuration.buildSettings.PROVISIONING_PROFILE_SPECIFIER</span><br><span class="line"> <span class="keyword">break</span></span><br></pre></td></tr></table></figure><p><code>target</code> 是 XCode 中 target 列表中的名字, <code>configuration</code> 值可选 <code>Debug/Release</code> . 我们很轻易的便获取到了包名和配置文件的名字, 对打包脚本稍作修改便导出成功了.</p><p>在使用 <code>pbxproj</code> 库的过程中我还遇到了一个小问题, 在给作者<a href="https://github.com/kronenthaler/mod-pbxproj/issues/185" target="_blank" rel="noopener">提出 Issue </a>后很快便收到了解决方案, 原来是我的测试文件 <code>test.py</code> 触发了一个 py3 兼容库的 bug. </p><h1 id="二-XCode9-不再生成-xcscheme-文件"><a href="#二-XCode9-不再生成-xcscheme-文件" class="headerlink" title="二. XCode9 不再生成 .xcscheme 文件"></a>二. XCode9 不再生成 .xcscheme 文件</h1><p>使用 <code>xcodebuild</code> 生成 xcarchive 文件时需要指定 <code>-scheme</code> 参数, 而不是 <code>-target</code>, 不然会提示:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">xcodebuild: error: The flag -scheme is required when specifying -archivePath but not -exportArchive.</span><br></pre></td></tr></table></figure><p><code>scheme</code> 和 <code>target</code> 从名字上有一定的联系, 第一次打开一个 XCode 工程时, 会在 <code>.xcodeproj</code> 文件夹下生成多个和 target 一一对应的 <code>xcscheme</code> 文件:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">xcuserdata/*.xcuserdatad/xcschemes/xxx.xcscheme</span><br></pre></td></tr></table></figure><p>在 XCode9 前如果没有这个文件是无法生成 <code>xcarchive</code> 文件的, 为了能够做到持续集成, 我们的做法是: </p><blockquote><p>如果检测到这个文件不存在的话, 就用 <code>open xxx.xcodeproj</code> 命令打开这个工程, 这样就会自动生成这个文件了</p></blockquote><p>而在 XCode9 中, 已经不再生成 <code>xcscheme</code> 文件了, 取而代之的是一个 <code>xcschememanagement.plist</code> 文件了. <strong>而且重要的是即时这个文件不存在, 也是能够正常生成 xcarchive 文件的</strong>.</p><p>那么在 XCode9 中我们之前检测 xcscheme 文件是否存在并生成的逻辑就不需要了, 我们判断一下 XCode 版本就行啦.</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> distutils.version <span class="keyword">import</span> LooseVersion</span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">is_xcode9</span><span class="params">()</span>:</span></span><br><span class="line"> out, err = subprocess.Popen(<span class="string">"xcodebuild -version"</span>, shell=<span class="keyword">True</span>, stdout=subprocess.PIPE).communicate()</span><br><span class="line"> verison = out.strip().replace(<span class="string">"\n"</span>, <span class="string">" "</span>).split(<span class="string">" "</span>)[<span class="number">1</span>]</span><br><span class="line"> <span class="keyword">return</span> LooseVersion(verison) >= LooseVersion(<span class="string">"9.0.0"</span>)</span><br></pre></td></tr></table></figure><h1 id="三-allowProvisioningUpdates-自动更新配置文件"><a href="#三-allowProvisioningUpdates-自动更新配置文件" class="headerlink" title="三. allowProvisioningUpdates 自动更新配置文件"></a>三. allowProvisioningUpdates 自动更新配置文件</h1><p>让我们先来看看它的描述:</p><blockquote><p>Allow xcodebuild to communicate with the Apple Developer website. For automatically signed targets, xcodebuild will create and update profiles, app IDs, and certificates. For manually signed targets, xcodebuild will download missing or updated provisioning profiles. Requires a developer account to have been added in Xcode’s Accounts preference pane.</p></blockquote><p>XCode 管理证书和配置文件有两中方式: <code>automatically</code> 和 <code>manually</code>, 如过在 XCode 中勾选了 <code>Automatically manager signing</code> 的话, 归档出来的包就是 automatically 的, 反之则是 manually 的.</p><p>只有 automatically 类型的配置指定 <code>allowProvisioningUpdates</code> 才有用. 这个参数在归档和导出阶段都可以用到, 前提是你得在 XCode 的账号设置中登录开发者账号.</p><p>在归档阶段, 比较可能是缺少 <code>Development</code> 证书, 如果没有这个参数, 你会收到这个错误:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Code Signing Error: No signing certificate "iOS Development" found: No "iOS Development" signing certificate matching team ID "G76Y748JJ2" with a private key was found.</span><br><span class="line">Code Signing Error: Code signing is required for product type 'Application' in SDK 'iOS 11.1'</span><br><span class="line">** ARCHIVE FAILED **</span><br></pre></td></tr></table></figure><p>而指定后, 理想情况下是会自动帮你创建或下载证书, <strong>但现在可能不够完善, 没能做到这点</strong>, 现在会遇到如下错误:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">2017-12-03 16:08:22.837 xcodebuild[20785:1078389] DVTAssertions: Warning <span class="keyword">in</span> /Library/Caches/com.apple.xbs/Sources/IDEFrameworks/IDEFrameworks-13532/IDEFoundation/Provisioning/Logging/IDEProvisioningLedger.m:167</span><br><span class="line">Details: Unable to close provisioning ledger entry because not all of its subentries are closed</span><br><span class="line">Object: <IDEProvisioningLedgerEntry: 0x7fa9540a4a90></span><br><span class="line">Method: -closeWithError:</span><br><span class="line">Thread: <NSThread: 0x7fa9540b8740>{number = 15, name = (null)}</span><br><span class="line">Please file a bug at http://bugreport.apple.com with this warning message and any useful information you can provide.</span><br><span class="line">=== BUILD TARGET wobwopr OF PROJECT newship_wobwopr WITH CONFIGURATION Release ===</span><br><span class="line">Code Signing Error: Revoke certificate: Your account already has a signing certificate <span class="keyword">for</span> this machine but it is not present <span class="keyword">in</span> your keychain. To create a new one, you must first revoke the existing certificate.</span><br><span class="line">Code Signing Error: No signing certificate <span class="string">"iOS Development"</span> found: No <span class="string">"iOS Development"</span> signing certificate matching team ID <span class="string">"G76Y748JJ2"</span> with a private key was found.</span><br><span class="line">Code Signing Error: Code signing is required <span class="keyword">for</span> product <span class="built_in">type</span> <span class="string">'Application'</span> <span class="keyword">in</span> SDK <span class="string">'iOS 11.1'</span></span><br><span class="line">** ARCHIVE FAILED **</span><br></pre></td></tr></table></figure><p>如果是在导出阶段, 签名阶段的, 最有可能的是缺少 <code>Distribution</code> 配置文件, 一般情况下这个文件是需要手动在开发者后台创建并下载的. 但是如果指定了这个参数的话 xcodebuild 会帮你自动创建一个, 很神奇的是这个 Profile 在开发者后台是看不到的, 也不会影响到你之前手动创建的配置文件.</p><p>总之, 这是一个很棒的特性, 期待苹果继续完善它.</p><hr><p>以上就是我们目前为止遇到的 XCode9 的 xcodebuild 问题, 如果大家也遇到了其他的问题, 欢迎一起讨论.</p>]]></content>
<summary type="html">
<p>相信大家都会用到 iOS 的自动打包工具, 而不是每次都是通过 XCode 的 Archive 后手动导出的, 那样效率低下不说还增加出错的可能.</p>
<p>我们团队也是如此, 用 Python 搞了一套打包工具, 这套工具的核心是 XCode 附带的命令行工具 <code>xcodebuild</code>, 网上其他开源的打包工具也都大同小异. 这套工具自诞生以来一直运行的很稳定, 累计为我们打了数百个包, 为团队节省的时间估计也在数千分钟了.</p>
</summary>
</entry>
<entry>
<title>更新 spine 的 cocos2d-x runtimes (二)</title>
<link href="http://blog.justbilt.com/2017/11/19/upgrad-spine-runtime-2/"/>
<id>http://blog.justbilt.com/2017/11/19/upgrad-spine-runtime-2/</id>
<published>2017-11-19T08:10:54.000Z</published>
<updated>2017-12-04T12:42:29.000Z</updated>
<content type="html"><{</span><br><span class="line"> executeSpineEvent(self, handler, eventType, trackIndex);</span><br><span class="line">});</span><br></pre></td></tr></table></figure><p><code>StartListener</code> 是声明在 <code>SkeletonAnimation.h</code> 中的, 相关代码的 git diff 如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">-<span class="keyword">typedef</span> <span class="built_in">std</span>::function<<span class="keyword">void</span>(<span class="keyword">int</span> trackIndex)> StartListener;</span><br><span class="line">-<span class="keyword">typedef</span> <span class="built_in">std</span>::function<<span class="keyword">void</span>(<span class="keyword">int</span> trackIndex)> EndListener;</span><br><span class="line">-<span class="keyword">typedef</span> <span class="built_in">std</span>::function<<span class="keyword">void</span>(<span class="keyword">int</span> trackIndex, <span class="keyword">int</span> loopCount)> CompleteListener;</span><br><span class="line">-<span class="keyword">typedef</span> <span class="built_in">std</span>::function<<span class="keyword">void</span>(<span class="keyword">int</span> trackIndex, spEvent* event)> EventListener;</span><br><span class="line">+<span class="keyword">typedef</span> <span class="built_in">std</span>::function<<span class="keyword">void</span>(spTrackEntry* entry)> StartListener;</span><br><span class="line">+<span class="keyword">typedef</span> <span class="built_in">std</span>::function<<span class="keyword">void</span>(spTrackEntry* entry)> InterruptListener;</span><br><span class="line">+<span class="keyword">typedef</span> <span class="built_in">std</span>::function<<span class="keyword">void</span>(spTrackEntry* entry)> EndListener;</span><br><span class="line">+<span class="keyword">typedef</span> <span class="built_in">std</span>::function<<span class="keyword">void</span>(spTrackEntry* entry)> DisposeListener;</span><br><span class="line">+<span class="keyword">typedef</span> <span class="built_in">std</span>::function<<span class="keyword">void</span>(spTrackEntry* entry)> CompleteListener;</span><br><span class="line">+<span class="keyword">typedef</span> <span class="built_in">std</span>::function<<span class="keyword">void</span>(spTrackEntry* entry, spEvent* event)> EventListener;</span><br></pre></td></tr></table></figure><p>显而易见, 函数的参数发生了变化, 我们修改下 lambda 函数参数即可, 同时还需要考虑 <code>executeSpineEvent</code> 函数的变化. 我的修改如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">executeSpineEvent</span><span class="params">(LuaSkeletonAnimation* skeletonAnimation, <span class="keyword">int</span> handler, spEventType eventType, spTrackEntry * entry, <span class="keyword">int</span> loopCount = <span class="number">0</span>, spEvent* event = <span class="literal">nullptr</span> )</span></span></span><br></pre></td></tr></table></figure><p>改变了这个函数的参数, 调用的地方也要改变, 这个就不贴代码了.</p><h3 id="spEventType-变化"><a href="#spEventType-变化" class="headerlink" title="spEventType 变化"></a>spEventType 变化</h3><p>还需要注意到的一个变化是:<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="keyword">enum</span> {</span><br><span class="line"> SP_ANIMATION_START, SP_ANIMATION_INTERRUPT, SP_ANIMATION_END, SP_ANIMATION_COMPLETE, SP_ANIMATION_DISPOSE, SP_ANIMATION_EVENT</span><br><span class="line">} spEventType;</span><br></pre></td></tr></table></figure></p><p>想必之前多了 <code>SP_ANIMATION_INTERRUPT</code> 和 <code>SP_ANIMATION_DISPOSE</code> 事件, 我们要更新所有用到 <code>spEventType</code> 的地方.</p><p>SpineConstants.lua :</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">sp.EventType =</span><br><span class="line">{</span><br><span class="line"> ANIMATION_START = <span class="number">0</span>, </span><br><span class="line"> ANIMATION_INTERRUPT = <span class="number">1</span>, </span><br><span class="line"> ANIMATION_END = <span class="number">2</span>, </span><br><span class="line"> ANIMATION_COMPLETE = <span class="number">3</span>, </span><br><span class="line"> ANIMATION_DISPOSE = <span class="number">4</span>, </span><br><span class="line"> ANIMATION_EVENT = <span class="number">5</span>,</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>以及 <code>LuaScriptHandlerMgr.h</code> 和 <code>lua_cocos2dx_spine_manual.cpp</code> 中共计五处变化.</p><h3 id="TrianglesCommand-参数错误"><a href="#TrianglesCommand-参数错误" class="headerlink" title="TrianglesCommand 参数错误"></a>TrianglesCommand 参数错误</h3><p>解决了这个问题, 编译后发现还有一个错误:</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SkeletonBatch.cpp:95:37: No matching member function for call to 'init'</span><br></pre></td></tr></table></figure><p>是调用 <code>TrianglesCommand:init</code> 时多传入了一个参数, 可能是新版 cocos 的 api 改动吧, 我们直接删掉最后一个参数:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">- _command->trianglesCommand->init(globalZOrder, textureID, glProgramState, blendFunc, *_command->triangles, transform, transformFlags);</span><br><span class="line">+ _command->trianglesCommand->init(globalZOrder, textureID, glProgramState, blendFunc, *_command->triangles, transform);</span><br></pre></td></tr></table></figure><hr><p>到此搞定了所有的编译问题, 尝试了下 Android 和 iOS 的编译, 也没有什么问题. 让我们运行下看下效果:</p><p><img src="https://ws2.sinaimg.cn/large/006tNc79ly1flnr7bq9vtg306v08skah.gif" alt=""></p><p>后记:</p><h1 id="1-Spine-Error-Unknown-attachment-type-skinnedmesh"><a href="#1-Spine-Error-Unknown-attachment-type-skinnedmesh" class="headerlink" title="1. Spine Error:Unknown attachment type: skinnedmesh"></a>1. Spine Error:Unknown attachment type: skinnedmesh</h1><p>跟新完后有的 Spine 文件会报这个错误, <a href="http://forum.cocos.com/t/1-3-1-spine-c-spine-skinnedmesh/41180/2" target="_blank" rel="noopener">论坛里有解决方案</a>, 虽然不报错了, 但是也不动了, 可能需要美术重新做一遍.</p><h1 id="2-真的有必要从-cocos2d-x-lite-中更新吗"><a href="#2-真的有必要从-cocos2d-x-lite-中更新吗" class="headerlink" title="2. 真的有必要从 cocos2d-x-lite 中更新吗?"></a>2. 真的有必要从 cocos2d-x-lite 中更新吗?</h1><p>经过好友 @郭彬 的提醒, 确实没有必要, 他就是从官方仓库更新的, 是我画蛇添足了.</p>]]></content>
<summary type="html">
<p>前段时间一个朋友向我咨询播放 spine 动画时遇到的问题:</p>
<p><img src="https://ws3.sinaimg.cn/large/006tNc79ly1flngumf7luj30ht04mq41.jpg" alt=""></p>
<p>这个问题我们在很久之前也遇到过, 当时是另个一小伙伴负责的. 当时猜测可能是 <code>spine runtimes</code> 需要升级, 但是升级时遇到了技术问题, 恰好项目比较紧, 便让美术同学重新用老版的 Spine 编辑器重新做了一遍.</p>
</summary>
<category term="cocos2d-x" scheme="http://blog.justbilt.com/tags/cocos2d-x/"/>
<category term="spine" scheme="http://blog.justbilt.com/tags/spine/"/>
</entry>
<entry>
<title>Quick-cocos2d-x 小米 Mix 全面屏适配</title>
<link href="http://blog.justbilt.com/2017/11/17/quick-x-mi-mix-support/"/>
<id>http://blog.justbilt.com/2017/11/17/quick-x-mi-mix-support/</id>
<published>2017-11-17T03:58:46.000Z</published>
<updated>2017-12-04T12:42:29.000Z</updated>
<content type="html"><![CDATA[<p>有玩家反馈我们的游戏在 Mix 上面无法全屏显示, 上下会有黑边. 搜索了下, 网上还没有绍如何适配的相关文章, 这里介绍下我们是如何做的.</p><p>我们可以在看看小米官方<a href="https://dev.mi.com/doc/p=10083/index.html" target="_blank" rel="noopener">对全面屏适配工作的介绍</a>:</p><blockquote><p>这些变化也影响了手机软件的设计,最值得开发者关注的,是以下两点:</p><ol><li>更大的屏幕高宽比</li><li>虚拟导航键</li></ol></blockquote><a id="more"></a><p>好的, 我们先按照官方的方法修改下 <code>max_aspect</code> 的值为 <code>2</code> 以上</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">meta-data</span> <span class="attr">android:name</span>=<span class="string">"android.max_aspect"</span> <span class="attr">android:value</span>=<span class="string">"2.1"</span> /></span></span><br></pre></td></tr></table></figure><p>运行下, 发现顶部的黑条不见了, 但是底部的虚拟导航键还在. 我们接着看官方的文档:</p><p><img src="https://dev.mi.com/doc/wp-content/uploads/2017/06/%E8%99%9A%E6%8B%9F%E9%94%AE%E7%9A%84%E6%A0%B7%E5%BC%8F.png" alt=""></p><p>然而这几种效果都不是我们想要的, 我们想要是隐藏<code>虚拟导航键</code>. Google 一下, 很容易搜索到这段代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 隐藏虚拟按键,并且全屏</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">hideBottomUIMenu</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="comment">//隐藏虚拟按键,并且全屏</span></span><br><span class="line"> <span class="keyword">if</span> (Build.VERSION.SDK_INT > <span class="number">11</span> && Build.VERSION.SDK_INT < <span class="number">19</span>) { <span class="comment">// lower api</span></span><br><span class="line"> View v = <span class="keyword">this</span>.getWindow().getDecorView();</span><br><span class="line"> v.setSystemUiVisibility(View.GONE);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (Build.VERSION.SDK_INT >= <span class="number">19</span>) {</span><br><span class="line"> <span class="comment">//for new api versions.</span></span><br><span class="line"> View decorView = getWindow().getDecorView();</span><br><span class="line"> <span class="keyword">int</span> uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION</span><br><span class="line"> | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN;</span><br><span class="line"> decorView.setSystemUiVisibility(uiOptions);</span><br><span class="line"> }</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>加上之后发现导航键真的没有了, 但是当你点击屏幕时, 它又出现了. 继续搜索发现了<a href="https://segmentfault.com/a/1190000002936799" target="_blank" rel="noopener">这篇文章</a>, 按照文章中的提示进行修改你的 AppActivity :</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"> <span class="keyword">private</span> Cocos2dxGLSurfaceView glSurfaceView;</span><br><span class="line"> <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">onCreate</span><span class="params">(Bundle savedInstanceState, SplashEnum splash, <span class="keyword">long</span> duration, <span class="keyword">boolean</span> debug)</span> </span>{</span><br><span class="line"> ...</span><br><span class="line"> View decorView = getWindow().getDecorView();</span><br><span class="line"><span class="comment">// Hide both the navigation bar and the status bar.</span></span><br><span class="line"><span class="comment">// SYSTEM_UI_FLAG_FULLSCREEN is only available on Android 4.1 and higher, but as</span></span><br><span class="line"><span class="comment">// a general rule, you should design your app to hide the status bar whenever you</span></span><br><span class="line"><span class="comment">// hide the navigation bar.</span></span><br><span class="line"> <span class="keyword">int</span> uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION</span><br><span class="line"> | View.SYSTEM_UI_FLAG_FULLSCREEN;</span><br><span class="line"> decorView.setSystemUiVisibility(uiOptions);</span><br><span class="line"> ...</span><br><span class="line"> }</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Cocos2dxGLSurfaceView <span class="title">onCreateView</span><span class="params">()</span> </span>{</span><br><span class="line"> glSurfaceView = <span class="keyword">super</span>.onCreateView();</span><br><span class="line"> <span class="keyword">this</span>.hideSystemUI();</span><br><span class="line"> <span class="keyword">return</span> glSurfaceView;</span><br><span class="line"> }</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onWindowFocusChanged</span><span class="params">(<span class="keyword">boolean</span> hasFocus)</span></span></span><br><span class="line"><span class="function"> </span>{</span><br><span class="line"> <span class="keyword">super</span>.onWindowFocusChanged(hasFocus);</span><br><span class="line"> <span class="keyword">if</span> (hasFocus)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">this</span>.hideSystemUI();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">hideSystemUI</span><span class="params">()</span></span></span><br><span class="line"><span class="function"> </span>{</span><br><span class="line"> <span class="comment">// Set the IMMERSIVE flag.</span></span><br><span class="line"> <span class="comment">// Set the content to appear under the system bars so that the content</span></span><br><span class="line"> <span class="comment">// doesn't resize when the system bars hide and show.</span></span><br><span class="line"> glSurfaceView.setSystemUiVisibility(</span><br><span class="line"> Cocos2dxGLSurfaceView.SYSTEM_UI_FLAG_LAYOUT_STABLE</span><br><span class="line"> | Cocos2dxGLSurfaceView.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION</span><br><span class="line"> | Cocos2dxGLSurfaceView.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN</span><br><span class="line"> | Cocos2dxGLSurfaceView.SYSTEM_UI_FLAG_HIDE_NAVIGATION <span class="comment">// hide nav bar</span></span><br><span class="line"> | Cocos2dxGLSurfaceView.SYSTEM_UI_FLAG_FULLSCREEN <span class="comment">// hide status bar</span></span><br><span class="line"> | Cocos2dxGLSurfaceView.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>搞定. 看下最终的效果:</p><p><img src="https://ws2.sinaimg.cn/large/006tNc79gy1flncnl0u3tj30ku11278w.jpg" alt=""></p>]]></content>
<summary type="html">
<p>有玩家反馈我们的游戏在 Mix 上面无法全屏显示, 上下会有黑边. 搜索了下, 网上还没有绍如何适配的相关文章, 这里介绍下我们是如何做的.</p>
<p>我们可以在看看小米官方<a href="https://dev.mi.com/doc/p=10083/index.html" target="_blank" rel="noopener">对全面屏适配工作的介绍</a>:</p>
<blockquote>
<p>这些变化也影响了手机软件的设计,最值得开发者关注的,是以下两点:</p>
<ol>
<li>更大的屏幕高宽比</li>
<li>虚拟导航键</li>
</ol>
</blockquote>
</summary>
<category term="Quick-Cocos2d-x" scheme="http://blog.justbilt.com/tags/Quick-Cocos2d-x/"/>
<category term="Android" scheme="http://blog.justbilt.com/tags/Android/"/>
</entry>
<entry>
<title>订阅 Hexo 的主题页</title>
<link href="http://blog.justbilt.com/2017/10/09/feed-hexo-themes-page/"/>
<id>http://blog.justbilt.com/2017/10/09/feed-hexo-themes-page/</id>
<published>2017-10-09T13:08:54.000Z</published>
<updated>2017-10-10T13:17:09.000Z</updated>
<content type="html"><![CDATA[<p>作为一个经常换博客主题的人, 时不时的就会去 <a href="https://hexo.io/themes/" target="_blank" rel="noopener">Hexo 的主题页</a> 逛逛, 看看有什么新的主题, 膜拜下大触们的作品.</p><p>比较麻烦的是这个页面并不是按照更新时间排序的, 也没有提供按时间排序的功能, 所以只能靠记忆力了. 这样很容易错过新的主题, 而且每次都是从头找起, 效率极低.</p><p>我自己是一个 Feed 的重度用户, 几乎是我扩展学习的唯一途径. 那么能不能将这个界面做成一个订阅源呢? </p><a id="more"></a><h1 id="1-Feed43"><a href="#1-Feed43" class="headerlink" title="1. Feed43"></a>1. Feed43</h1><p>刚好看到我订阅的 <a href="https://aoxuis.me/" target="_blank" rel="noopener">@XA技术不宅</a> 兄更新了一篇文章 <a href="https://aoxuis.me/post/bo-ke/li-yong-feed43-zhi-zuo-shi-yao-zhi-de-mai-zhong-ce-chan-pin-de-rssding-yue-lian-jie" target="_blank" rel="noopener">利用 Feed43 制作什么值得买众测产品的RSS订阅链接</a>, 感觉挺不错的, 我也试试吧.</p><p><img src="https://ws4.sinaimg.cn/large/006tKfTcgy1fkcbou91p9j30j70izq89.jpg" alt=""></p><p>网站看起来十分简洁, 我们点击 <code>Create your own feed</code> 开始制作订阅源, 同意一大堆看不懂的条款后, 我们会来到这样的一个界面:</p><p><img src="https://ws3.sinaimg.cn/large/006tKfTcgy1fkcbu7z8lbj30ix0ah75z.jpg" alt=""></p><p>我们在 <code>Address</code> 填入 Hexo 主题页的网址, 点击 <code>Reload</code> 就可以加载网页了, 成功后我们在下面填入提取规则, 主要就是 <code>Item (repeatable) Search Pattern</code> 这一栏, 其他都可以空着.</p><p>经过观察及一番思考后, 我填写的规则如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><li class="plugin on"></span><br><span class="line">{*}<div class="plugin-screenshot"></span><br><span class="line">{*}<noscript></span><br><span class="line">{*}<img data-src="{%}" {*}></span><br><span class="line">{*}<a href="{%}" class="plugin-preview-link" target="_blank"><i class="fa fa-eye"></i></a></span><br><span class="line">{*}<a href="{%}"{*} class="plugin-name" target="_blank">{%}</a></span><br><span class="line">{*}<p class="plugin-desc">{%}</p></span><br><span class="line">{*}</span><br><span class="line"></li></span><br></pre></td></tr></table></figure><p>结果如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">{%1} = https://hexo.io/build/screenshots/Ada-139a3754c7.jpg</span><br><span class="line">{%2} = https://shuirong.github.io/</span><br><span class="line">{%3} = https://github.com/shuiRong/hexo-theme-Ada</span><br><span class="line">{%4} = Ada</span><br><span class="line">{%5} = Ada is an concise theme for Hexo.</span><br></pre></td></tr></table></figure><p>这里面提取了每一个主题的 <code>预览图片</code>, <code>预览地址</code>, <code>Github 项目地址</code>, <code>主题名称</code>, <code>主题简介</code>. 下面就是填写渲染规则了, 也很简单, 薛微有一点 html 基础就可以搞定. 最后的成果如下:</p><p><img src="https://ws4.sinaimg.cn/large/006tKfTcly1fkcc1se8wrj30iw0rlq5q.jpg" alt=""></p><p>还不错哈, 有模有样的, 大家感兴趣的话 <a href="https://feed43.com/8308348441438115.xml" target="_blank" rel="noopener">点击这里</a> 可以跳转查看. 仔细查看的话会发现一个问题:</p><blockquote><p>数量比官网少了很多, 这里大概只有不到 20 个主题, 而官网上则有数 154 个主题.</p></blockquote><p>在字体在后台看了下, 发现了这句话:</p><blockquote><p>Page too big. First 102400 bytes loaded.</p></blockquote><p>Feed43 为了加快速度, 只节选了原始网页的一部分内容, 这下完蛋了, 这个订阅源没有任何作用, 权当练手了, 反正也没有花费太多的时间.</p><h1 id="2-hexojs-site"><a href="#2-hexojs-site" class="headerlink" title="2. hexojs/site"></a>2. hexojs/site</h1><p><a href="https://github.com/hexojs/site" target="_blank" rel="noopener">hexojs/site</a> 这个 Github 仓库是 hexo.io 这个网站的原始代码, 其中 <a href="https://github.com/hexojs/site/blob/master/source/_data/themes.yml" target="_blank" rel="noopener">source/_data/themes.yml</a> 这个文件是主题页的原始数据文件.</p><p>我们可以通过观察这个文件的<a href="https://github.com/hexojs/site/commits/master/source/_data/themes.yml" target="_blank" rel="noopener">历史记录</a>来得知主题的变化情况:</p><p><img src="https://ws1.sinaimg.cn/large/006tKfTcly1fkdgeh5qyrj30sw0w3n1j.jpg" alt=""></p><p>那么如何订阅这个文件的变化呢? stackoverflow 上的<a href="https://stackoverflow.com/a/7353586/3003184" target="_blank" rel="noopener">这个答案</a>可以解吾辈疑惑. </p><blockquote><p>在文件 url 后面加上 <code>.atom</code> 就可以生成这个 url 的 feed 源</p></blockquote><p>大家可以点<a href="https://github.com/hexojs/site/commits/master/source/_data/themes.yml.atom" target="_blank" rel="noopener">这里查看</a>, 复制 url 订阅, 效果很给力:</p><p><img src="https://ws4.sinaimg.cn/large/006tKfTcly1fkdgm3o20zj30v90rc431.jpg" alt=""></p><hr><p>经过这次折腾, 收获颇丰呀:</p><ol><li>会使用 Feed43 生成自己喜欢网页的订阅源</li><li>知道了 Github 在任意 url 后面加 .atom 可以生成订阅源</li></ol><p>生命不息, 折腾不止.</p>]]></content>
<summary type="html">
<p>作为一个经常换博客主题的人, 时不时的就会去 <a href="https://hexo.io/themes/" target="_blank" rel="noopener">Hexo 的主题页</a> 逛逛, 看看有什么新的主题, 膜拜下大触们的作品.</p>
<p>比较麻烦的是这个页面并不是按照更新时间排序的, 也没有提供按时间排序的功能, 所以只能靠记忆力了. 这样很容易错过新的主题, 而且每次都是从头找起, 效率极低.</p>
<p>我自己是一个 Feed 的重度用户, 几乎是我扩展学习的唯一途径. 那么能不能将这个界面做成一个订阅源呢? </p>
</summary>
<category term="blog" scheme="http://blog.justbilt.com/tags/blog/"/>
<category term="hexo" scheme="http://blog.justbilt.com/tags/hexo/"/>
</entry>
<entry>
<title>解决 electron-builder 打包依赖问题</title>
<link href="http://blog.justbilt.com/2017/10/02/electron-builder-dependencies/"/>
<id>http://blog.justbilt.com/2017/10/02/electron-builder-dependencies/</id>
<published>2017-10-02T05:21:04.000Z</published>
<updated>2017-10-02T08:16:42.000Z</updated>
<content type="html"><![CDATA[<p>大概是三个月前, 写了 <a href="/2017/07/09/convert2fnt-electron-rewrite">使用 Electron 重写 convert2fnt</a> 这篇文章, 文章大概讲了我用 Electron 重写之前的一个 BMFont 字体生成工具的事情.</p><p>开发的过程还是比较顺利的, Node.js 的生态非常给力, 加上 Chrome, VSCode 加持, 开发和调试都很爽. 但由于我没有 WEB 开发的基础, 整过过程只能摸索加网上查资料, 中间之曲折, 自不足为外人道也, 但最终磕磕绊绊的也算搞出来了.</p><p>呃, 其实也没有那么完美. </p><blockquote><p>Windows 版打包后发现运行不太正常, 但 Win 开发版以及 Mac 开发/发布版都运行正常.</p></blockquote><p>有些诡异, 不过我自己只在 Mac 上使用, 加上工作比较忙, 便搁置了起来. 但谁愿意在自己头上悬在一把利剑呢, 万一哪天着急用 Win 版的怎么办.</p><p>趁着国庆放假还没有什么安排的时候, 赶紧搞一下.</p><a id="more"></a><hr><p>首先要搞明白无法正常运行的原因是什么, 打开 Chrom 开发者工具后发现了错误内容. 项目的一个依赖项 <code>jimp</code> 没有加载成功, 在调用这个模块的一个函数后抛出错误:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">const Jimp = require("jimp");</span><br><span class="line">Jimp.read(imagePath, function (err, lenna) {}</span><br><span class="line">---</span><br><span class="line">Uncaught TypeError: Jimp.read is not a function</span><br></pre></td></tr></table></figure><p>试着断点跟踪了下 <code>require("jimp")</code> 的过程, 由于自己对 js 的细节知之甚少, 也没有发现什么异常. 各种关键字组合 Google 也没有结果, 一度陷入了毫无头绪的困境.</p><p>在搜索的过程中发现 <code>electron</code> 和 <code>electron-builder</code> 都有了新的版本, 于是抱着侥幸的心态试着升级了下这两个库.</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="deletion">- "electron-builder": "^19.11.1",</span></span><br><span class="line"><span class="deletion">- "electron": "^1.6.11"</span></span><br><span class="line"><span class="addition">+ "electron-builder": "^19.33.0",</span></span><br><span class="line"><span class="addition">+ "electron": "^1.7.8",</span></span><br></pre></td></tr></table></figure><p>没想到, 竟然 … … 还是没有解决这个问题. 好吧, 其实也还在预料之中. 但是也有 “好消息”, 出错的地方和之前不一样了.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">module.js:472 Uncaught Error: Cannot find module 'bmp-js'</span><br></pre></td></tr></table></figure><p>又是一番折腾, 尝试各种解决方案, 各种猜想, 各种组合. 这些尝试多没有什么变化, 有的尝试反而将我带到沟里, 离真相越来越远. </p><p>最后我坚定了一个想法:</p><blockquote><p>我的代码是没有任何问题的, 一定是打包的配置有问题.</p></blockquote><p>果然, 在仔细阅读官方文档的时候发现了这个 <a href="https://www.electron.build/tutorials/two-package-structure" target="_blank" rel="noopener">Two package.json Structure</a> , 原来官方已经推荐使用两个配置文件来配置各种参数, 一个配置开发时的参数, 一个配置打包时的参数. 于是便有了这样的目录结构:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">├── app</span><br><span class="line">│ ├── index.html</span><br><span class="line">│ ├── index.js</span><br><span class="line">│ ├── package-lock.json</span><br><span class="line">│ └── package.json</span><br><span class="line">└── package.json</span><br></pre></td></tr></table></figure><p>同时, 官方还建议在开发的 <code>config.json</code> 中增加一个 <code>"postinstall": "electron-builder install-app-deps"</code> 来自动触发安装 <code>app</code> 目录中的依赖.</p><hr><p>哈哈, 问题已经解决, 都是没有仔细看官方文档惹的祸, 但从此 Win 下同学也可以用上新版的工具啦.</p>]]></content>
<summary type="html">
<p>大概是三个月前, 写了 <a href="/2017/07/09/convert2fnt-electron-rewrite">使用 Electron 重写 convert2fnt</a> 这篇文章, 文章大概讲了我用 Electron 重写之前的一个 BMFont 字体生成工具的事情.</p>
<p>开发的过程还是比较顺利的, Node.js 的生态非常给力, 加上 Chrome, VSCode 加持, 开发和调试都很爽. 但由于我没有 WEB 开发的基础, 整过过程只能摸索加网上查资料, 中间之曲折, 自不足为外人道也, 但最终磕磕绊绊的也算搞出来了.</p>
<p>呃, 其实也没有那么完美. </p>
<blockquote>
<p>Windows 版打包后发现运行不太正常, 但 Win 开发版以及 Mac 开发/发布版都运行正常.</p>
</blockquote>
<p>有些诡异, 不过我自己只在 Mac 上使用, 加上工作比较忙, 便搁置了起来. 但谁愿意在自己头上悬在一把利剑呢, 万一哪天着急用 Win 版的怎么办.</p>
<p>趁着国庆放假还没有什么安排的时候, 赶紧搞一下.</p>
</summary>
<category term="convert2fnt" scheme="http://blog.justbilt.com/tags/convert2fnt/"/>
<category term="Electron" scheme="http://blog.justbilt.com/tags/Electron/"/>
</entry>
<entry>
<title>Quick-cocos2d-x 编译 Lua 代码加速</title>
<link href="http://blog.justbilt.com/2017/09/10/quick-x-make-package-speedup/"/>
<id>http://blog.justbilt.com/2017/09/10/quick-x-make-package-speedup/</id>
<published>2017-09-10T02:37:53.000Z</published>
<updated>2017-09-10T11:48:00.000Z</updated>
<content type="html"><![CDATA[<p>quick-x 在手机上运行的时候并不是加载项目的 Lua 的源码, 而是一个名为 <code>game.zip</code> 的压缩包. 当项目中的 Lua 文件越来越多的时候, 生成这个 game.zip 的时间会越来越久.</p><p>我们目前的项目有 <code>1256</code> 个 Lua 文件, 每次执行编译操作需要耗时将近 40 秒的时间:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">time <span class="variable">$QUICK_V3_ROOT</span>/quick/bin/compile_scripts.sh -q -i ../src -o ../res/game.zip -e xxtea_zip -ek xxxx -es xxx</span><br><span class="line">---</span><br><span class="line">real 0m38.603s</span><br><span class="line">user 0m25.792s</span><br><span class="line">sys 0m3.369s</span><br></pre></td></tr></table></figure><p>尤其是当需要在手机上调试 Lua 代码的时候, 每改一次代码就需要运行一次这个命令, 然后 40 秒就过去了, 你恐怕都忘记自己该关注什么改动了吧!</p><a id="more"></a><h1 id="分析-compile-scripts"><a href="#分析-compile-scripts" class="headerlink" title="分析 compile_scripts"></a>分析 compile_scripts</h1><p>在 <code>$QUICK_V3_ROOT/quick/bin</code> 目录我们可以找到 <code>compile_scripts.sh</code> :</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"><span class="built_in">export</span> LUA_PATH=<span class="string">"<span class="variable">$QUICK_V3_ROOT</span>/quick/bin/mac/?.lua;;"</span></span><br><span class="line">DIR=<span class="string">"<span class="variable">$( cd "$( dirname "${BASH_SOURCE[0]}" )</span>"</span> && <span class="built_in">pwd</span> )<span class="string">"</span></span><br><span class="line"><span class="string">php "</span><span class="variable">$DIR</span>/lib/compile_scripts.php<span class="string">" $*</span></span><br></pre></td></tr></table></figure><p>这个脚本很简单, 只是一个入口, 真正的逻辑是在 <code>compile_scripts.php</code> 文件中, 打开这个文件后发现它主要功能是处理输入的参数, 剩余的工作是通过 <code>ScriptsCompiler</code> 这个类完成的, 它位于 ScriptsCompiler.lua 中.</p><p>必须承认, 廖大他们写的代码还是很棒的, 我们只看 ScriptsCompiler 的 <code>run</code> 函数便可以猜到这个类都干了那些事情:</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">run</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> $files = <span class="keyword">$this</span>->searchSourceFiles();</span><br><span class="line"> $modules = <span class="keyword">$this</span>->prepareForCompile($files);</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">$this</span>->config[<span class="string">'encrypt'</span>] == <span class="keyword">self</span>::ENCRYPT_XXTEA_CHUNK)</span><br><span class="line"> {</span><br><span class="line"> $bytes = <span class="keyword">$this</span>->compileModules($modules, <span class="keyword">$this</span>->config[<span class="string">'key'</span>], <span class="keyword">$this</span>->config[<span class="string">'sign'</span>]);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> {</span><br><span class="line"> $bytes = <span class="keyword">$this</span>->compileModules($modules);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (!is_array($bytes))</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">$this</span>->cleanupTempFiles($modules);</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (!<span class="keyword">$this</span>->createOutput($modules, $bytes))</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">$this</span>->cleanupTempFiles($modules);</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">$this</span>->cleanupTempFiles($modules);</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>配合着其中调用的函数的细节, 我们可以得知该类的主要工作流程:</p><ol><li>找到所有的 Lua 文件.</li><li>调用 <code>getScriptFileBytecodes</code> 函数获取上一步那些 Lua 文件的字节码.</li><li>创建输出文件 <code>game.zip</code>.</li><li>加密输出文件 <code>game.zip</code>.</li></ol><p>当然还有一些分支的情况, 不过我们关注的工作流程就是这个样子的.</p><h1 id="快速优化"><a href="#快速优化" class="headerlink" title="快速优化"></a>快速优化</h1><p>从上一步的源码阅读过程中得知, 如果我们在调用 <code>compile_scripts.sh</code> 时, 不传入加密信息的话, 就不会第四步操作. 在我们开发过程中, 加密这一步其实是可以省略, 最终打包的时候加密一次就可以了.</p><p>那么干掉第 4 步对于打包花费到底能减少多少呢 ? 我们来测试一下, 还是文章最开始的那端脚本, 我们这次删除最后面的一些加密参数试一下:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">time <span class="variable">$QUICK_V3_ROOT</span>/quick/bin/compile_scripts.sh -q -i ../src -o ../res/game.zip</span><br><span class="line">---</span><br><span class="line">real 0m16.362s</span><br><span class="line">user 0m8.333s</span><br><span class="line">sys 0m2.579s</span><br></pre></td></tr></table></figure><p>很幸运, 加密确实消耗了很多的时间, 因此我们没有花费多少精力就把编译时间减少为了当初的 <code>42%</code>. 我们可以在游戏工程中建立两个脚本, <code>pack_lua.sh</code> 和 <code>pack_lua_dev.sh</code>, 分别放入上述的两行命令, 这样我们在开发/调试期间只用调用 <code>pack_lua_dev.sh</code> 就可以了.</p><p>然而这一切还不值得我们欢呼, 这是一个很取巧的方案, 或许是我们团队本身就对 <code>compile_scripts</code> 这个操作的理解不够, 导致我们一直以来就来使用的这个方案就是错误的.</p><p>况且, <code>16</code> 秒还是很久.</p><h1 id="深度优化"><a href="#深度优化" class="headerlink" title="深度优化"></a>深度优化</h1><p>根据上一步得出的信息, 有 16 秒的时间是消耗在了前三步, (根据多年经验)而其中第一步和第三步的消耗也非常小, 那么主要的消耗是在第二步上.</p><p>经过观察, 每次我们运行 <code>compile_scripts</code> 都会把所有的 Lua 代码都编译一遍, 那么我们是不是可以把历史的结果缓存下来, 只编译新增和变化的文件呢 ?</p><p>首先我们需要弄懂, 这个所谓的 <code>编译</code> 操作, 到底做了什么 ? 让我们再次把目光转向之前得知的 <code>getScriptFileBytecodes</code> 函数.</p><p>该函数位于 <code>$QUICK_V3_ROOT/quick/bin/lib/quick/init.php</code> 中, 我们在函数中执行命令前加上输出:</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">printf(<span class="string">"command: %s\n"</span>, $command);</span><br><span class="line">passthru($command);</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>再次执行 <code>compile_scripts</code>, 注意去掉 <code>-q</code> 参数, 我们可以在输出中看到:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">command</span>: /xxx/quick-cocos2d-x/quick/bin/mac/luac -o <span class="string">"xxx.bytes"</span> <span class="string">"xxx.lua"</span></span><br></pre></td></tr></table></figure><p>看来就是调用了下 <code>luac</code> 命令嘛, 挺简单的呀!</p><blockquote><p>(注: 实际情况会复杂一些, 因为可能会用到 luajit 工作流程, 这里只讨论我们目前的工作流)</p></blockquote><p>搞懂了原理的话, 就可以开撸了, 我并不打算修改 quick 的 php 代码, 我会用我更熟悉的 Python 来写一个优化版的 Lua 编译脚本. 下面是核心的代码, 我加了一些必要的注释:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">package</span><span class="params">(_src, _dst)</span>:</span></span><br><span class="line"> cache_path = CACHE_ROOT</span><br><span class="line"> shutils.mkdir(cache_path)</span><br><span class="line"> hash_file = os.path.join(cache_path, <span class="string">"hash.json"</span>)</span><br><span class="line"> hash_data = shutils.read_json(hash_file, {})</span><br><span class="line"> hash_data_new = {}</span><br><span class="line"> shutils.remove(_dst)</span><br><span class="line"> dst_zip = zipfile.ZipFile(_dst, <span class="string">"w"</span>, zipfile.ZIP_DEFLATED)</span><br><span class="line"> <span class="keyword">for</span> f <span class="keyword">in</span> shutils.file_list(_src):</span><br><span class="line"> <span class="keyword">if</span> f.startswith(<span class="string">"."</span>) <span class="keyword">or</span> <span class="keyword">not</span> f.endswith(<span class="string">".lua"</span>):</span><br><span class="line"> <span class="keyword">continue</span></span><br><span class="line"> src_filepath = os.path.abspath(os.path.join(_src, f))</span><br><span class="line"> dst_filename = f.replace(<span class="string">".lua"</span>, <span class="string">""</span>).replace(<span class="string">"/"</span>, <span class="string">"."</span>)</span><br><span class="line"> dst_filepath = os.path.abspath(os.path.join(cache_path, dst_filename))</span><br><span class="line"> src_hash = shutils.file_hash(src_filepath)</span><br><span class="line"> dst_hash = shutils.file_hash(dst_filepath)</span><br><span class="line"> record_hash = hash_data.get(dst_filename, {})</span><br><span class="line"> exists_error = <span class="keyword">False</span></span><br><span class="line"> <span class="comment"># 检查缓存是否有效</span></span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> (dst_hash <span class="keyword">and</span> record_hash.get(<span class="string">"src"</span>, <span class="string">""</span>) == src_hash <span class="keyword">and</span> record_hash.get(<span class="string">"dst"</span>, <span class="string">""</span>) == dst_hash):</span><br><span class="line"> <span class="comment"># 这里为移除时为了感知 luac 编译失败</span></span><br><span class="line"> shutils.remove(dst_filepath)</span><br><span class="line"> <span class="keyword">print</span> <span class="string">"ADD: "</span> + f</span><br><span class="line"> luac_compile(src_filepath, dst_filepath)</span><br><span class="line"> dst_hash = shutils.file_hash(dst_filepath)</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> dst_hash:</span><br><span class="line"> <span class="keyword">print</span> <span class="string">"\n---"</span></span><br><span class="line"> <span class="keyword">print</span> <span class="string">"ERROR: "</span> + src_filepath</span><br><span class="line"> exists_error = <span class="keyword">True</span></span><br><span class="line"> <span class="keyword">break</span></span><br><span class="line"> dst_zip.write(dst_filepath, dst_filename)</span><br><span class="line"> hash_data_new[dst_filename] = {<span class="string">"src"</span>: src_hash, <span class="string">"dst"</span>: dst_hash}</span><br><span class="line"> shutils.save_json(hash_data_new, hash_file)</span><br><span class="line"> <span class="keyword">if</span> exists_error:</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">False</span></span><br><span class="line"> dst_zip.close()</span><br><span class="line"> <span class="comment"># 删除原始文件已经不存在的缓存</span></span><br><span class="line"> <span class="keyword">for</span> f <span class="keyword">in</span> shutils.file_list(cache_path):</span><br><span class="line"> <span class="keyword">if</span> f == <span class="string">"hash.json"</span>:</span><br><span class="line"> <span class="keyword">continue</span></span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> hash_data_new.get(f):</span><br><span class="line"> <span class="keyword">print</span> <span class="string">"REMOVE: "</span> + f</span><br><span class="line"> os.remove(os.path.join(cache_path, f))</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">True</span></span><br></pre></td></tr></table></figure><p>这段代码里多是逻辑的排列, 没有什么好说的. 值得一提的是是, luac 编译失败的处理, luac 失败后会输出错误日志, 不会生成对应的字节码:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">$ python luapack.py src res/game.zip</span><br><span class="line">ADD: main.lua</span><br><span class="line">/xxx/quick-cocos2d-x/quick/bin/mac/luac: /xxx/src/main.lua:10: <span class="string">'='</span> expected near <span class="string">'package'</span></span><br><span class="line"></span><br><span class="line">---</span><br><span class="line">ERROR: /Users/bilt/Documents/wanghaitao/easy_slg_client/src/main.lua</span><br></pre></td></tr></table></figure><p>因此我们先尝试移除掉目标文件, 如果生成文件不存在则表示本次 luac 失败了. </p><p>完整版的代码位于<a href="https://gist.github.com/justbilt/7c881cbdbff74f3fdd97278355d22fed" target="_blank" rel="noopener">我的 gist 上</a>, 这份代码只是最基本的实现, 还有很多优化的空间. 好的, 我迫不及待的想看看我们优化的成果了.</p><p>让我们看看第一次运行的结果:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">$ time python luapack.py src res/game.zip</span><br><span class="line">---</span><br><span class="line">real 0m15.590s</span><br><span class="line">user 0m6.664s</span><br><span class="line">sys 0m2.946s</span><br></pre></td></tr></table></figure><p><code>16</code> 秒, 接近之前无加密版的 <code>compile_scripts</code> 消耗, 让我们变更一个文件试试呢?</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">$ time python luapack.py src res/game.zip</span><br><span class="line">ADD: main.lua</span><br><span class="line">---</span><br><span class="line">real 0m1.611s</span><br><span class="line">user 0m1.403s</span><br><span class="line">sys 0m0.175s</span><br></pre></td></tr></table></figure><p>只有不到 2 秒的耗时, 太牛逼了!</p><hr><p>在本次的优化过程中, 我们成功的将耗由最初的 <code>38.603s</code> 降低为最终的 <code>1.611s</code>, 接近 <code>24</code> 倍的效率提升, 这是一个质的飞跃! 感觉自己棒棒哒,晚饭申请加一个鸡腿.</p><p>当然我们还做了一些其他的东西, 比如我们给日志加上了彩色的输出:</p><p><img src="https://ws4.sinaimg.cn/large/006tKfTcly1fjep3wv9cjj30vz03uq3p.jpg" alt=""></p><p>是不是变的很漂亮 ?</p>]]></content>
<summary type="html">
<p>quick-x 在手机上运行的时候并不是加载项目的 Lua 的源码, 而是一个名为 <code>game.zip</code> 的压缩包. 当项目中的 Lua 文件越来越多的时候, 生成这个 game.zip 的时间会越来越久.</p>
<p>我们目前的项目有 <code>1256</code> 个 Lua 文件, 每次执行编译操作需要耗时将近 40 秒的时间:</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">time <span class="variable">$QUICK_V3_ROOT</span>/quick/bin/compile_scripts.sh -q -i ../src -o ../res/game.zip -e xxtea_zip -ek xxxx -es xxx</span><br><span class="line">---</span><br><span class="line">real 0m38.603s</span><br><span class="line">user 0m25.792s</span><br><span class="line">sys 0m3.369s</span><br></pre></td></tr></table></figure>
<p>尤其是当需要在手机上调试 Lua 代码的时候, 每改一次代码就需要运行一次这个命令, 然后 40 秒就过去了, 你恐怕都忘记自己该关注什么改动了吧!</p>
</summary>
<category term="Quick-Cocos2d-x" scheme="http://blog.justbilt.com/tags/Quick-Cocos2d-x/"/>
<category term="Lua" scheme="http://blog.justbilt.com/tags/Lua/"/>
</entry>
<entry>
<title>将 python 数据转化为 lua 格式</title>
<link href="http://blog.justbilt.com/2017/09/03/serialize-lua-in-pyhton/"/>
<id>http://blog.justbilt.com/2017/09/03/serialize-lua-in-pyhton/</id>
<published>2017-09-03T07:45:53.000Z</published>
<updated>2017-09-07T14:19:53.000Z</updated>
<content type="html"><![CDATA[<p>我们团队的技术栈是: <code>Lua + Python</code>, 前端采用 Lua 写项目, Python 做工具支持, 而后端则采用纯 Python 开发, 所以会经常遇到 Python 和 Lua 数据的转化.</p><a id="more"></a><p>目前业内采用的方法大致有两种:</p><ol><li>用 Python 写一个 Lua 的语法解析, 用 Lua 写一个 Pyhton 的语法解析, 这样就可以互相转化了.</li><li>使用一个大家都支持的数据结构做中转, 如: Json 或 Yaml.</li></ol><p>第一种方案比较复杂, Python 解析 Lua 倒是有 <a href="https://github.com/SirAnthony/slpp" target="_blank" rel="noopener">slpp</a> 这样的项目, Lua 解析 Python 的则是没有, 因此大家更多的是采用第二种方案.</p><p>第二种方案中 json 其实是一个很不错的解决方案, 性能也比 Lua 快一些, 但是 json 没有办法存储 number 类型的 key, 这一点在数据结构的设计上很蛋疼, 可能会让你的代码充斥着 <code>tostring</code>, <code>tonumber</code> 这样的代码.</p><p>经过观察, json 和 Lua 这两种文件格式其实是十分像的, 那么我是否只要魔改下 python 的 json 库, 使之支持 number 做 key, 然后保存为 Lua 文件 ?</p><h1 id="开撸"><a href="#开撸" class="headerlink" title="开撸"></a>开撸</h1><h2 id="1-准备工作"><a href="#1-准备工作" class="headerlink" title="1. 准备工作"></a>1. 准备工作</h2><p>首先我们找到 json 库的源码 <a href="https://github.com/python/cpython/blob/master/Lib/json" target="_blank" rel="noopener">https://github.com/python/cpython/blob/master/Lib/json</a>, 这个目录中有 4 个文件, 我们把 <code>encoder.py</code> 下载下来, 保存为 <code>lua.py</code>.</p><p>但是只有这一个文件是没有办法直接运行的, 我们再从 <code>encoder.py</code> 同级的 <code>__init__.py</code> 中复制 <code>dumps</code> 函数到 <code>lua.py</code> 的末尾.</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">dumps</span><span class="params">(obj, skipkeys=False, ensure_ascii=True, check_circular=True,</span></span></span><br><span class="line"><span class="function"><span class="params"> allow_nan=True, cls=None, indent=None, separators=None,</span></span></span><br><span class="line"><span class="function"><span class="params"> encoding=<span class="string">'utf-8'</span>, default=None, sort_keys=False, **kw)</span>:</span></span><br><span class="line"> <span class="keyword">if</span> cls <span class="keyword">is</span> <span class="keyword">None</span>:</span><br><span class="line"> cls = JSONEncoder</span><br><span class="line"> <span class="keyword">return</span> cls(</span><br><span class="line"> skipkeys=skipkeys, ensure_ascii=ensure_ascii,</span><br><span class="line"> check_circular=check_circular, allow_nan=allow_nan, indent=indent,</span><br><span class="line"> separators=separators, encoding=encoding, default=default,</span><br><span class="line"> sort_keys=sort_keys, **kw).encode(obj)</span><br></pre></td></tr></table></figure><p>我们在 <code>lua.py</code> 同级目录新建一个 <code>test.py</code> 做测试, 这里我们把一个python的数据保存为 <code>test.lua</code> 文件:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> lua</span><br><span class="line">data = {</span><br><span class="line"> <span class="string">"a"</span>: <span class="number">1</span>,</span><br><span class="line"> <span class="number">2</span>:<span class="number">2</span>,</span><br><span class="line"> <span class="string">"3a"</span>: <span class="number">3</span>,</span><br><span class="line"> <span class="string">"4"</span>:[</span><br><span class="line"> <span class="number">1</span>,<span class="number">2</span>,<span class="string">"a"</span>,<span class="keyword">False</span></span><br><span class="line"> ]</span><br><span class="line">}</span><br><span class="line"><span class="keyword">with</span> open(<span class="string">"test.lua"</span>, <span class="string">"w"</span>) <span class="keyword">as</span> f:</span><br><span class="line"> f.write(lua.dumps(data, ensure_ascii=<span class="keyword">False</span>, indent=<span class="number">2</span>, sort_keys=<span class="keyword">True</span>))</span><br></pre></td></tr></table></figure><p>完成后直接终端运行 <code>python test.py</code> 就可以看到一个新生成了一个 lua 文件, 内容如下:</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="string">"2"</span>: <span class="number">2</span>, </span><br><span class="line"> <span class="string">"3a"</span>: <span class="number">3</span>, </span><br><span class="line"> <span class="string">"4"</span>: [</span><br><span class="line"> <span class="number">1</span>, </span><br><span class="line"> <span class="number">2</span>, </span><br><span class="line"> <span class="string">"a"</span>, </span><br><span class="line"> <span class="literal">false</span></span><br><span class="line"> ], </span><br><span class="line"> <span class="string">"a"</span>: <span class="number">1</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们第一步的工作就搞定了.</p><h2 id="2-魔改-lua-py"><a href="#2-魔改-lua-py" class="headerlink" title="2. 魔改 lua.py"></a>2. 魔改 lua.py</h2><p>其实上一步生成的文件内容并不是 lua 格式的, 其实还是 json 格式, 只是我们将后缀改为 lua 了而已. 这个格式和真正的 lua 想比有两处不对:</p><ol><li>Lua 的数组使用 <code>{}</code> 表示.</li><li>Lua 使用 <code>=</code> 做 key,value 的分隔.</li><li>Lua string 类型的 key, 不用加引号, 如果要加引号的话, 需要同时加一对方括号.</li></ol><p>第一个问题很好解决, 我们搜索 <code>'[</code>, 可以找到 <code>_iterencode_list</code> 这个函数, 大概看一眼就知道怎么改了:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">_iterencode_list</span><span class="params">(lst, _current_indent_level)</span>:</span></span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> lst:</span><br><span class="line"> <span class="keyword">yield</span> <span class="string">'{}'</span></span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> ...</span><br><span class="line"> buf = <span class="string">'{'</span></span><br><span class="line"> ...</span><br><span class="line"> <span class="keyword">yield</span> <span class="string">'}'</span></span><br><span class="line"> ...</span><br></pre></td></tr></table></figure><p>我们修改完, 运行 <code>python test.py</code> 看下效果:</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="string">"2"</span>: <span class="number">2</span>, </span><br><span class="line"> <span class="string">"3a"</span>: <span class="number">3</span>, </span><br><span class="line"> <span class="string">"4"</span>: {</span><br><span class="line"> <span class="number">1</span>, </span><br><span class="line"> <span class="number">2</span>, </span><br><span class="line"> <span class="string">"a"</span>, </span><br><span class="line"> <span class="literal">false</span></span><br><span class="line"> }, </span><br><span class="line"> <span class="string">"a"</span>: <span class="number">1</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>是不是已经有点意思了? 我们再接再厉, 找找第二个问题如何解决. 我们搜索 <code>':</code> 可以找到这行代码:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">key_separator = <span class="string">': '</span></span><br></pre></td></tr></table></figure><p>将 <code>': '</code> 修改为 <code>' = '</code> 就可以啦, 我们看下结果:</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="string">"2"</span> = <span class="number">2</span>, </span><br><span class="line"> <span class="string">"3a"</span> = <span class="number">3</span>, </span><br><span class="line"> <span class="string">"4"</span> = {</span><br><span class="line"> <span class="number">1</span>, </span><br><span class="line"> <span class="number">2</span>, </span><br><span class="line"> <span class="string">"a"</span>, </span><br><span class="line"> <span class="literal">false</span></span><br><span class="line"> }, </span><br><span class="line"> <span class="string">"a"</span> = <span class="number">1</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>那么第三个问题该如何入手呢? 给童鞋们三秒钟的思考时间. </p><blockquote><p>3s</p><p>2s</p><p>1s</p></blockquote><p>揭晓答案, 既然 <code>key_separator</code> 是分隔 key, value 的, 我们找找哪里用到 key_separator 是不是发现点什么呢 ? 很快我们能定位到 <code>_iterencode_dict</code> 函数的这行代码:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">yield</span> _encoder(key)</span><br><span class="line"><span class="keyword">yield</span> _key_separator</span><br></pre></td></tr></table></figure><p>这个 <code>_encoder(key)</code> 会不会对应 key 的生成呢? 我们修改下试试, 将这句话替换为:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">yield</span> <span class="string">'['</span> + _encoder(key) + <span class="string">']'</span></span><br></pre></td></tr></table></figure><p>运行, 结果如下:</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> [<span class="string">"2"</span>] = <span class="number">2</span>, </span><br><span class="line"> [<span class="string">"3a"</span>] = <span class="number">3</span>, </span><br><span class="line"> [<span class="string">"4"</span>] = {</span><br><span class="line"> <span class="number">1</span>, </span><br><span class="line"> <span class="number">2</span>, </span><br><span class="line"> <span class="string">"a"</span>, </span><br><span class="line"> <span class="literal">false</span></span><br><span class="line"> }, </span><br><span class="line"> [<span class="string">"a"</span>] = <span class="number">1</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>哎呀, 我这是搞定了吗? 骚年, 恭喜你, 你已经完成了 99% 的工作了, 这段代码的最前方还少一个 <code>return</code> , 我们可以很轻易的加上. 我通过修改 <code>test.py</code> 实现了这个功能:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">f.write(lua.dumps(data, ensure_ascii=False, indent=2, sort_keys=True))</span><br><span class="line"># 修改为:</span><br><span class="line">f.write("return " + lua.dumps(data, ensure_ascii=False, indent=2, sort_keys=True))</span><br></pre></td></tr></table></figure><p>结果:</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">return</span> {</span><br><span class="line"> [<span class="string">"2"</span>] = <span class="number">2</span>, </span><br><span class="line"> [<span class="string">"3a"</span>] = <span class="number">3</span>, </span><br><span class="line"> [<span class="string">"4"</span>] = {</span><br><span class="line"> <span class="number">1</span>, </span><br><span class="line"> <span class="number">2</span>, </span><br><span class="line"> <span class="string">"a"</span>, </span><br><span class="line"> <span class="literal">false</span></span><br><span class="line"> }, </span><br><span class="line"> [<span class="string">"a"</span>] = <span class="number">1</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="3-更加-lua-化"><a href="#3-更加-lua-化" class="headerlink" title="3. 更加 lua 化"></a>3. 更加 lua 化</h2><p>上面生成的 <code>test.lua</code> 其实很不 Lua, 大家不会写出 <code>["a"] = 1</code> 这种类型的语句, 而是直接 <code>a = 1</code>. </p><p>这个问题发生在了上面的第三步上, 我们偷懒直接写出了这样的代码:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">yield</span> <span class="string">'['</span> + _encoder(key) + <span class="string">']'</span></span><br></pre></td></tr></table></figure><p>大家不妨看看 <code>_encoder</code> 这个函数, 它会把任意类型转化为字符串, 两端加上 <code>"</code>. 那么 Lua 在什么情况下才会 <code>["x"]</code> 这种类型的 key 呢?</p><ol><li>关键字, 如: <code>require</code>, <code>false</code> 等, 我们可以在<a href="https://www.lua.org/manual/5.1/manual.html#2.1" target="_blank" rel="noopener">这里</a>找到所有的关键字.</li><li>以数字开头的字符串.</li><li>数字, 包括 <code>int</code>, <code>lang</code>, <code>float</code>.</li><li>含有奇怪字符的字符串, 如: <code>-</code>, <code>*</code> 等, 非奇怪字符只有: <code>[a-zA-Z_0-9]</code>.</li></ol><p>知道了规则, 我们就很好办了, 写一个解析函数就好了嘛:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">key_filter = re.compile(<span class="string">"[^a-zA-Z_0-9]"</span>)</span><br><span class="line">key_filter_equal = {<span class="string">"require"</span>,<span class="string">"false"</span>,<span class="string">"for"</span>,<span class="string">"function"</span>,<span class="string">"if"</span>,<span class="string">"nil"</span>,<span class="string">"not"</span>,<span class="string">"or"</span>,<span class="string">"while"</span>,<span class="string">"then"</span>,<span class="string">"true"</span>,<span class="string">"until"</span>,<span class="string">"end"</span>,<span class="string">"in"</span>,<span class="string">"local"</span>,<span class="string">"repeat"</span>,<span class="string">"return"</span>,<span class="string">"break"</span>,<span class="string">"do"</span>,<span class="string">"else"</span>,<span class="string">"and"</span>,<span class="string">"elseif"</span>}</span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">format_key</span><span class="params">(s)</span>:</span></span><br><span class="line"> <span class="keyword">if</span> isinstance(s, (int, long, float)):</span><br><span class="line"> <span class="keyword">return</span> <span class="string">'['</span>+str(s)+<span class="string">']'</span></span><br><span class="line"> <span class="keyword">try</span>:</span><br><span class="line"> int(s)</span><br><span class="line"> <span class="keyword">return</span> <span class="string">'["'</span>+s+<span class="string">'"]'</span></span><br><span class="line"> <span class="keyword">except</span> Exception, e:</span><br><span class="line"> <span class="keyword">pass</span></span><br><span class="line"> <span class="keyword">for</span> f <span class="keyword">in</span> key_filter_equal:</span><br><span class="line"> <span class="keyword">if</span> s == f:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">'["'</span>+s+<span class="string">'"]'</span></span><br><span class="line"> <span class="keyword">if</span> (ord(s[<span class="number">0</span>]) >= ord(<span class="string">"0"</span>) <span class="keyword">and</span> ord(s[<span class="number">0</span>]) <= ord(<span class="string">"9"</span>)) <span class="keyword">or</span> key_filter.search(s):</span><br><span class="line"> <span class="keyword">return</span> <span class="string">'["'</span>+s+<span class="string">'"]'</span></span><br><span class="line"> <span class="keyword">return</span> s</span><br></pre></td></tr></table></figure><p>然后把 <code>yield '[' + _encoder(key) + ']'</code> 替换为 <code>yield format_key(key)</code> 就搞定了. 我们看下生成的结果:</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">return</span> {</span><br><span class="line"> [<span class="string">"2"</span>] = <span class="number">2</span>, </span><br><span class="line"> [<span class="string">"3a"</span>] = <span class="number">3</span>, </span><br><span class="line"> [<span class="string">"4"</span>] = {</span><br><span class="line"> <span class="number">1</span>, </span><br><span class="line"> <span class="number">2</span>, </span><br><span class="line"> <span class="string">"a"</span>, </span><br><span class="line"> <span class="literal">false</span></span><br><span class="line"> }, </span><br><span class="line"> a = <span class="number">1</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>好像不太对嘛, 骗纸, 数字的 key 还是有问题的. 不要慌, 我们顺着 <code>format_key</code> 调用的地方向上找找, 发现问题了吧:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">elif</span> isinstance(key, float):</span><br><span class="line"> key = _floatstr(key)</span><br><span class="line"><span class="keyword">elif</span> key <span class="keyword">is</span> <span class="keyword">True</span>:</span><br><span class="line"> key = <span class="string">'true'</span></span><br><span class="line"><span class="keyword">elif</span> key <span class="keyword">is</span> <span class="keyword">False</span>:</span><br><span class="line"> key = <span class="string">'false'</span></span><br><span class="line"><span class="keyword">elif</span> key <span class="keyword">is</span> <span class="keyword">None</span>:</span><br><span class="line"> key = <span class="string">'null'</span></span><br><span class="line"><span class="keyword">elif</span> isinstance(key, (int, long)):</span><br><span class="line"> key = str(key)</span><br></pre></td></tr></table></figure><p>可以看到这里已经对 <code>float</code>, <code>int</code>, <code>lang</code> 类型做过转化了, 我们把 elif 中的内容换成 <code>pass</code> 再试下:</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">return</span> {</span><br><span class="line"> [<span class="number">2</span>] = <span class="number">2</span>, </span><br><span class="line"> [<span class="string">"3a"</span>] = <span class="number">3</span>, </span><br><span class="line"> [<span class="string">"4"</span>] = {</span><br><span class="line"> <span class="number">1</span>, </span><br><span class="line"> <span class="number">2</span>, </span><br><span class="line"> <span class="string">"a"</span>, </span><br><span class="line"> <span class="literal">false</span></span><br><span class="line"> }, </span><br><span class="line"> a = <span class="number">1</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>哈哈, 搞定了! 我太棒了! 咦, 纳尼, Are you kidding me ? 为什么只有 <code>2</code> 是 ok 的, <code>"4"</code> 是怎么回事嘛 ? 仔细检查了一番代码, 有做了一个额外的修改, 还是没有发现原因.</p><p>当我把目光转向我们的测试代码的时候, 我看到的答案:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">data = {</span><br><span class="line"> <span class="string">"a"</span>: <span class="number">1</span>,</span><br><span class="line"> <span class="number">2</span>:<span class="number">2</span>,</span><br><span class="line"> <span class="string">"3a"</span>: <span class="number">3</span>,</span><br><span class="line"> <span class="string">"4"</span>:[</span><br><span class="line"> <span class="number">1</span>,<span class="number">2</span>,<span class="string">"a"</span>,<span class="keyword">False</span></span><br><span class="line"> ]</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们的 <code>"4"</code> 本身就是 string 类型的, 这样更加说明了我们代码的正确性.</p><hr><p>到此为止, 我们的魔改之旅就结束了, 我把最终版的代码放到了 <a href="https://gist.github.com/justbilt/a5cfafe761b8571e7b5a9e7c22cc7c57" target="_blank" rel="noopener">Gist 上</a>, 大家如果有兴趣的话可以看一下.</p><h1 id="附录"><a href="#附录" class="headerlink" title="附录:"></a>附录:</h1><p>一个 24W 行的 json 和 Lua 读取时间:</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">time1 = <span class="built_in">os</span>.<span class="built_in">clock</span>()</span><br><span class="line">data = <span class="built_in">require</span>(<span class="string">"filters"</span>)</span><br><span class="line">time2 = <span class="built_in">os</span>.<span class="built_in">clock</span>()</span><br><span class="line">data2 = json.decode(<span class="built_in">io</span>.readfile(cc.FileUtils:getInstance():fullPathForFilename((<span class="string">"res/filters.json"</span>))))</span><br><span class="line">time3 = <span class="built_in">os</span>.<span class="built_in">clock</span>()</span><br><span class="line"><span class="built_in">print</span>(<span class="string">"time lua:"</span>, time2 - time1)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">"time json:"</span>, time3 - time2)</span><br></pre></td></tr></table></figure><p>结果如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">time lua: 0.278526</span><br><span class="line">time json: 0.182085</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html">
<p>我们团队的技术栈是: <code>Lua + Python</code>, 前端采用 Lua 写项目, Python 做工具支持, 而后端则采用纯 Python 开发, 所以会经常遇到 Python 和 Lua 数据的转化.</p>
</summary>
<category term="Lua" scheme="http://blog.justbilt.com/tags/Lua/"/>
<category term="Python" scheme="http://blog.justbilt.com/tags/Python/"/>
</entry>
<entry>
<title>手撸一个像《乱世王者》那样的 WebView</title>
<link href="http://blog.justbilt.com/2017/08/25/webview-like-lswz/"/>
<id>http://blog.justbilt.com/2017/08/25/webview-like-lswz/</id>
<published>2017-08-24T16:45:08.000Z</published>
<updated>2017-08-31T23:55:24.000Z</updated>
<content type="html"><![CDATA[<p>天美新游戏《乱世王者》出了之后, 我玩了一阵子, 这个游戏的精美程度刷新了我对 slg 游戏的认识, 比它的模型产品《列王的纷争》给我带来的震撼还要大. 不仅仅是在美术方面, 更多的是在产品和对细节的处理上. </p><p><img src="https://ws1.sinaimg.cn/large/006tNc79ly1fj3pcpg3k8j31kw0xw4qp.jpg" alt=""></p><a id="more"></a><p>在游戏中更多界面中有几个按钮, 点击后会跳转到 WebView, 如下图:</p><p><img src="https://ws4.sinaimg.cn/large/006tNc79ly1fj3pou45vcj30af0ijn2s.jpg" alt=""></p><p>这个 WebView 做得很不错呀, 比 Cocos 自带的光秃秃的 WebView 好数倍, 本着从(hou)善(yan)如(wu)流(chi)的人生哲理, 便开始思索着是不是搞到我们的游戏中去? </p><p>那么它是如何实现的呢? 根据多年经验, 从资源入手是最简单的办法了, 拆开乱世王者的 Android 包, 在 res 文件夹中搜索 png, 我们很容易的发现下面这些资源:</p><p><img src="https://ws2.sinaimg.cn/large/006tKfTcly1fixluudqm1j30yo0jcn3g.jpg" alt=""></p><p>命名都是类似: <code>com_tencent_msdk_webview_left_unclickable.png</code>, 看来是 <code>msdk</code> 里面的一个组件, 我们没有接入 msdk 的计划, 所以没有办法直接调用了. 让我们来分析下这个界面:</p><p><img src="https://ws3.sinaimg.cn/large/006tKfTcly1fiy0x704zhj309h0dzmxd.jpg" alt=""></p><p>Cocos 默认提供的 WebView 只有中间部分, 上下是没有的. 这两部分应该是用 Android 写的原生界面, 但是我对 Android 也是一知半解, 写出来估计都是 bug, 还没有办法热更新. </p><p>看着满地的资源, 我不禁陷入了深思. 有了, 是不是只创建出中间尺寸大小的 Webview, 然后用这些图片在 Cocos 中绘制出标题栏和工具栏, 菜单和 WebView 的交互可以用 WebView 提供的 api 来搞. </p><p>粗略看了下 WebView 提供的 api:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">canGoBack</span><span class="params">()</span></span>;</span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">goBack</span><span class="params">()</span></span>;</span><br><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">canGoForward</span><span class="params">()</span></span>;</span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">goForward</span><span class="params">()</span></span>;</span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">reload</span><span class="params">()</span></span>;</span><br></pre></td></tr></table></figure><p>在加上 <code>removeFromParent</code>, 下方的工具栏是没有问题了. 我们 ui 编辑器使用的是 Cocos Builder, 我很快拼出了效果:</p><p><img src="https://ws2.sinaimg.cn/large/006tNc79ly1fj110v95r4j31kw1f9127.jpg" alt=""></p><p>下面便是在代码中撸这几个菜单的逻辑了, 这部分还是挺麻烦的, 只要细心一些, 很快也能搞定:</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">--</span></span><br><span class="line"><span class="comment">-- Author: wangbilt@gmail.com</span></span><br><span class="line"><span class="comment">-- Date: 2017-08-21 11:26:18</span></span><br><span class="line"><span class="comment">--</span></span><br><span class="line"><span class="keyword">local</span> PageFullscreenWebview = class(<span class="string">"PageFullscreenWebview"</span>, <span class="built_in">require</span>(<span class="string">"app.layout.package.page_fullscreen_webview_layout"</span>))</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">PageFullscreenWebview:ctor</span><span class="params">()</span></span></span><br><span class="line"> PageFullscreenWebview.super.ctor(self)</span><br><span class="line"> self:_disableToolBar()</span><br><span class="line"> self.webview = ccexp.WebView:<span class="built_in">create</span>()</span><br><span class="line"> self.webview:setAnchorPoint(cc.p(<span class="number">0</span>, <span class="number">0</span>))</span><br><span class="line"> self.webview:setScalesPageToFit(<span class="literal">false</span>)</span><br><span class="line"> self.webview:setContentSize(cc.size(display.width, display.height - self._nodeToolBar:getContentSize().height))</span><br><span class="line"> self.webview:setPosition(<span class="number">0</span>, self._nodeToolBar:getContentSize().height)</span><br><span class="line"> self.webview:setOnDidFinishLoading(<span class="function"><span class="keyword">function</span><span class="params">()</span></span></span><br><span class="line"> self:performWithDelay(handler(self,self._loadFinishCallBack), <span class="number">0</span>)</span><br><span class="line"> <span class="keyword">end</span>) </span><br><span class="line"> self:addChild(self.webview)</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">PageFullscreenWebview:loadURL</span><span class="params">(_url, _callback)</span></span></span><br><span class="line"> self.webview:loadURL(_url)</span><br><span class="line"> self.on_finish_callback = _callback</span><br><span class="line"> self._btnRefresh:setVisible(<span class="literal">false</span>)</span><br><span class="line"> self._btnLeft:setButtonEnabled(<span class="literal">false</span>)</span><br><span class="line"> self._btnRight:setButtonEnabled(<span class="literal">false</span>) </span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">PageFullscreenWebview:_disableToolBar</span><span class="params">()</span></span></span><br><span class="line"> self._btnLeft:setButtonEnabled(<span class="literal">false</span>)</span><br><span class="line"> self._btnRight:setButtonEnabled(<span class="literal">false</span>)</span><br><span class="line"> self._btnRefresh:setVisible(<span class="literal">false</span>)</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">PageFullscreenWebview:_loadFinishCallBack</span><span class="params">(...)</span></span></span><br><span class="line"> self._btnRefresh:setVisible(<span class="literal">true</span>)</span><br><span class="line"> self._btnLeft:setButtonEnabled(self.webview:canGoBack())</span><br><span class="line"> self._btnRight:setButtonEnabled(self.webview:canGoForward())</span><br><span class="line"> <span class="keyword">if</span> self.on_finish_callback <span class="keyword">then</span></span><br><span class="line"> self.on_finish_callback()</span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">PageFullscreenWebview:onClose</span><span class="params">(_callback)</span></span></span><br><span class="line"> self.on_close_callback = _callback</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">PageFullscreenWebview:onClickClose</span><span class="params">(_params)</span></span></span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"onClickClose"</span>,_params.tag, _params.target)</span><br><span class="line"> self.on_close_callback()</span><br><span class="line"> self:removeFromParent()</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">PageFullscreenWebview:onClickLeft</span><span class="params">(_params)</span></span></span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"onClickLeft:"</span>,_params.tag, _params.target)</span><br><span class="line"> self:_disableToolBar()</span><br><span class="line"> self.webview:goBack()</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">PageFullscreenWebview:onClickRefresh</span><span class="params">(_params)</span></span></span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"onClickRefresh:"</span>,_params.tag, _params.target)</span><br><span class="line"> self:_disableToolBar()</span><br><span class="line"> self.webview:reload()</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">PageFullscreenWebview:onClickRight</span><span class="params">(_params)</span></span></span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"onClickRight:"</span>,_params.tag, _params.target)</span><br><span class="line"> self:_disableToolBar()</span><br><span class="line"> self.webview:goForward()</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">PageFullscreenWebview:show</span><span class="params">()</span></span></span><br><span class="line"> display.getRunningScene():addChild(self, <span class="number">10000</span>)</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="keyword">return</span> PageFullscreenWebview</span><br></pre></td></tr></table></figure><p>看看实际的效果:</p><p><img src="https://ws4.sinaimg.cn/large/006tNc79ly1fj11145fcrj30f00qoab6.jpg" alt=""></p><blockquote><p>Ps. 在这之间还遇到了一个bug, 就是在 WebView 出现的时候按 back 键会崩溃, 最终重写 Avtivity 一个空的 <code>onBackPressed</code> 实现解决问题.</p></blockquote><p>是不是足以以假乱真了呢 ? 不要高兴太早, 还有标题,进度条功能没有实现呢. 让我们来分析下这几个功能:</p><h2 id="标题"><a href="#标题" class="headerlink" title="标题"></a>标题</h2><p>看了下 Cocos 提供的 API, 是没有与标题相关的, 只能我们自己去绑定. 那么该如何获取标题呢?</p><h3 id="Android"><a href="#Android" class="headerlink" title="Android:"></a>Android:</h3><p>在 <a href="https://ws4.sinaimg.cn/large/006tNc79ly1fj11145fcrj30f00qoab6.jpg" target="_blank" rel="noopener">stackoverflow 的这个答案</a> 中找到解决方案:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">WebView mWebView = (WebView) findViewById(R.id.mwebview);</span><br><span class="line">mWebView.setWebViewClient(<span class="keyword">new</span> WebViewClient() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onPageFinished</span><span class="params">(WebView view, String url)</span> </span>{</span><br><span class="line"> ExperimentingActivity.<span class="keyword">this</span>.setTitle(view.getTitle());</span><br><span class="line"> }</span><br><span class="line">});</span><br></pre></td></tr></table></figure><p>或者</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">webview.setWebChromeClient(<span class="keyword">new</span> WebChromeClient() {</span><br><span class="line"> <span class="meta">@Override</span> </span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onReceivedTitle</span><span class="params">(WebView view, String title)</span> </span>{ </span><br><span class="line"> <span class="keyword">super</span>.onReceivedTitle(view, title); </span><br><span class="line"> } </span><br><span class="line">});</span><br></pre></td></tr></table></figure><h3 id="iOS"><a href="#iOS" class="headerlink" title="iOS:"></a>iOS:</h3><p>同样, 我们可以很容易的找到 iOS 的<a href="https://stackoverflow.com/a/20059707" target="_blank" rel="noopener">解决方案</a>:</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">void</span>)webViewDidFinishLoad:(<span class="built_in">UIWebView</span> *)webView{</span><br><span class="line"> <span class="built_in">NSString</span> *theTitle=[webView stringByEvaluatingJavaScriptFromString:<span class="string">@"document.title"</span>];</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>有了上面代码后, 我们可以很容易的实现获取标题的代码, 比较麻烦的是还需要修改 WebView 的 c++ 部分, 如果最终要在 Lua 中使用的话还需要导出接口.</p><h2 id="进度条"><a href="#进度条" class="headerlink" title="进度条"></a>进度条</h2><h3 id="Android-1"><a href="#Android-1" class="headerlink" title="Android:"></a>Android:</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">webview.setWebChromeClient(<span class="keyword">new</span> WebChromeClient() {</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onProgressChanged</span><span class="params">(WebView view, <span class="keyword">int</span> progress)</span> </span></span><br><span class="line"><span class="function"> </span>{</span><br><span class="line"> }</span><br><span class="line">});</span><br></pre></td></tr></table></figure><h3 id="iOS-1"><a href="#iOS-1" class="headerlink" title="iOS"></a>iOS</h3><p>iOS 我找了很久, 貌似是没有办法获取进度的, 只能做一个假的效果:</p><blockquote><p>在开始加载网页的时候启动一个定时器, 让进度条以一个缓慢的速度前进, 收到加载完成的消息时让进度条瞬间移动到终点.</p></blockquote><p>所以, 我们可以分平台实现, 也可以统一代码, 都使用假的进度条.</p><hr><p>经过与 PM 的一番交流后, 一致同意放弃这两个功能. 因为觉得目前的效果还可以, 加上不改动 c++ 就可以热更新上去.</p><p>所以很可惜, 这次没有办法搞彻底了, 下次有机会我们接着搞.</p>]]></content>
<summary type="html">
<p>天美新游戏《乱世王者》出了之后, 我玩了一阵子, 这个游戏的精美程度刷新了我对 slg 游戏的认识, 比它的模型产品《列王的纷争》给我带来的震撼还要大. 不仅仅是在美术方面, 更多的是在产品和对细节的处理上. </p>
<p><img src="https://ws1.sinaimg.cn/large/006tNc79ly1fj3pcpg3k8j31kw0xw4qp.jpg" alt=""></p>
</summary>
<category term="Quick-Cocos2d-x" scheme="http://blog.justbilt.com/tags/Quick-Cocos2d-x/"/>
<category term="WebView" scheme="http://blog.justbilt.com/tags/WebView/"/>
</entry>
<entry>
<title>聊聊 Google 支付的那些事</title>
<link href="http://blog.justbilt.com/2017/07/23/integrate-google-iab/"/>
<id>http://blog.justbilt.com/2017/07/23/integrate-google-iab/</id>
<published>2017-07-23T02:20:07.000Z</published>
<updated>2017-08-24T16:39:46.000Z</updated>
<content type="html"><![CDATA[<p>在之前的文章中, 有很多篇都是很 Apple IAP 相关的, 而 Google IAB 的却一篇都没有, 这对于占了半壁江山的 Android 很不公平.</p><p>就作为一个开发者而言, IAB 的体验是比 IAP 要好的. 它体现在这么几个方面:</p><ol><li>测试账号就是 Goolge 账号, 不像苹果那样是沙盒账号. 这样不同账号下的应用可以设置同一个测试账号, 避免来回切换账号.</li><li>支持退款查询. 在苹果上, 只能吃了这个哑巴亏.</li></ol><p>但是 IAB 也有一些不太便利的地方, 后面我们会讨论它.</p><a id="more"></a><h1 id="一-集成"><a href="#一-集成" class="headerlink" title="一. 集成"></a>一. 集成</h1><p>IAB 本身提供了一些列的 <a href="https://developer.android.com/google/play/billing/billing_reference.html" target="_blank" rel="noopener">API</a> 去实现购买, 一次典型的购买流程是这个样子的:</p><p><img src="https://ws1.sinaimg.cn/large/006tNc79gy1fhtste5cqlj30fn0fkta9.jpg" alt=""></p><p>这里面有一些细节我们其实是可以不用知道的, 所以我们使用了一个二次封装的库<a href="https://github.com/anjlab/android-inapp-billing-v3" target="_blank" rel="noopener">android-inapp-billing-v3</a>去实现. 使用了这个库后, 实现内购变得很简单:</p><h2 id="1-添加监听"><a href="#1-添加监听" class="headerlink" title="1. 添加监听"></a>1. 添加监听</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">onCreate</span><span class="params">(Bundle savedInstanceState)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>.onCreate(savedInstanceState);</span><br><span class="line"></span><br><span class="line"> bp = <span class="keyword">new</span> BillingProcessor(context, <span class="string">"IAB_LICENSE_KEY"</span>, <span class="keyword">new</span> BillingProcessor.IBillingHandler() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onProductPurchased</span><span class="params">(String productId, TransactionDetails details)</span> </span>{</span><br><span class="line"> <span class="comment">// 购买成功</span></span><br><span class="line"> Log.d(<span class="string">"message:"</span>, details.purchaseInfo.responseData);</span><br><span class="line"> Log.d(<span class="string">"signature:"</span>, details.purchaseInfo.signature);</span><br><span class="line"> Log.d(<span class="string">"productid:"</span>, productId);</span><br><span class="line"> }</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onBillingError</span><span class="params">(<span class="keyword">int</span> errorCode, Throwable error)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (errorCode == Constants.BILLING_RESPONSE_RESULT_USER_CANCELED){</span><br><span class="line"> <span class="comment">// 购买取消</span></span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> String reason = <span class="string">"UNKNOWN_ERROR"</span>;</span><br><span class="line"> <span class="keyword">if</span> (mErrorCodeReason.containsKey(errorCode)) {</span><br><span class="line"> reason = mErrorCodeReason.get(errorCode);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 购买失败</span></span><br><span class="line"> Log.d(<span class="string">"buy failure: "</span>, reason);</span><br><span class="line"> }</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onBillingInitialized</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="comment">// 初始化成功</span></span><br><span class="line"> }</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onPurchaseHistoryRestored</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="comment">// 恢复内购</span></span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="2-处理购买结果"><a href="#2-处理购买结果" class="headerlink" title="2. 处理购买结果"></a>2. 处理购买结果</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">onActivityResult</span><span class="params">(<span class="keyword">int</span> requestCode, <span class="keyword">int</span> resultCode, Intent data)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>.onActivityResult(requestCode, resultCode, data);</span><br><span class="line"> <span class="keyword">return</span> bp.handleActivityResult(requestCode, resultCode, data);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="3-购买商品"><a href="#3-购买商品" class="headerlink" title="3. 购买商品"></a>3. 购买商品</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">bp.purchase(<span class="keyword">this</span>, <span class="string">"android.test.purchased"</span>);</span><br></pre></td></tr></table></figure><h2 id="4-消耗商品"><a href="#4-消耗商品" class="headerlink" title="4. 消耗商品"></a>4. 消耗商品</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">bp.consumePurchase(productId);</span><br></pre></td></tr></table></figure><h1 id="二-测试"><a href="#二-测试" class="headerlink" title="二. 测试"></a>二. 测试</h1><h2 id="1-使用静态响应进行测试"><a href="#1-使用静态响应进行测试" class="headerlink" title="1. 使用静态响应进行测试"></a>1. 使用静态响应进行测试</h2><p>IAB 默认提供了一系列的商品作为测试商品, 这些商品和应用及商品无关, 我们可以使用这些商品来测试充值功能是否集成成功和服务器签名验证是否正确.</p><p>详情可以参考 <a href="https://developer.android.com/google/play/billing/billing_testing.html#billing-testing-static" target="_blank" rel="noopener">这篇 Google 官方文档</a>.</p><h2 id="2-使用真实交易进行测试"><a href="#2-使用真实交易进行测试" class="headerlink" title="2. 使用真实交易进行测试"></a>2. 使用真实交易进行测试</h2><p>在项目上线前, 我们需要测试下应用内的商品处理是否正常, 所以要使用真实的环境来测试. 要达到这个目的, 我们需要进行一些列的操作才可以达到.</p><ul><li>正确创建商品并与项目关联.</li><li>Google Play 后台已经上传并发布了一个 Bata/Alpha 版本.</li><li>使用和 Google Play 后台已经发布的应用相同的签名和版本打包出来的应用.</li><li>在 Google Play 后台添加<a href="https://developer.android.com/google/play/billing/billing_admin.html#billing-testing-setup" target="_blank" rel="noopener">测试账号</a>.</li><li>确认应用的发布地区有这个测试账号的所在地区.</li><li>确保测试用手机安装了谷歌服务框架, 开启翻墙并登陆了上面添加的测试账号.</li></ul><p>这只是其中一部分需求, 还有一些原因会导致你无法正确测试充值.</p><h1 id="三-常见错误及解决方案"><a href="#三-常见错误及解决方案" class="headerlink" title="三. 常见错误及解决方案"></a>三. 常见错误及解决方案</h1><h2 id="1-需要验证身份-您需要登录自己-Google-账号"><a href="#1-需要验证身份-您需要登录自己-Google-账号" class="headerlink" title="1. 需要验证身份. 您需要登录自己 Google 账号"></a>1. 需要验证身份. 您需要登录自己 Google 账号</h2><p><img src="https://ws1.sinaimg.cn/large/006tKfTcgy1fhzbhgvtu7j30dw079gmc.jpg" alt=""></p><p>解决方案:</p><blockquote><p>商品id不正确, 请检查下购买时传入的商品 id 和 Google Play 后台是否一致</p></blockquote><h2 id="2-此版本的应用未配置为通过-Google-Play-结算"><a href="#2-此版本的应用未配置为通过-Google-Play-结算" class="headerlink" title="2. 此版本的应用未配置为通过 Google Play 结算."></a>2. 此版本的应用未配置为通过 Google Play 结算.</h2><p><img src="https://ws2.sinaimg.cn/large/006tKfTcly1fhzbtvd43jj30dw079gmd.jpg" alt=""></p><p>解决方案:</p><blockquote><p>可能在测试账号所在的区没有包含在应用的发售地区中, 请检查配置, 修改后需要耐心等待生效.</p></blockquote><h2 id="3-无法购买您要买的商品"><a href="#3-无法购买您要买的商品" class="headerlink" title="3. 无法购买您要买的商品."></a>3. 无法购买您要买的商品.</h2><p><img src="https://ws4.sinaimg.cn/large/006tKfTcly1fhyskeq0czj30du08caae.jpg" alt=""></p><p>解决方案:</p><blockquote><ol><li>检查版本号是否与发布应用的版本号一致</li></ol></blockquote><p>Updata: </p><blockquote><p>这个是很早之前的一个错误, 貌似 Google 目前已经不再检查版本号这个属性了.</p><ol><li>确认有没有发布 Beta/Alpha</li><li>如果是封闭测试需要点击链接成为测试人员, 连接形式: <code>https://play.google.com/apps/testing/xxx</code></li></ol></blockquote><p>Update 2017年08月15日:</p><blockquote><p>今天又遇到了这个问题, 什么都没有改, 过了一会突然又好了, 猜想是不是发布之后还需要一段时间才能生效.</p></blockquote><h2 id="4-从服务器检索信息出错-DF-DIC-02"><a href="#4-从服务器检索信息出错-DF-DIC-02" class="headerlink" title="4. 从服务器检索信息出错.[DF-DIC-02]"></a>4. 从服务器检索信息出错.[DF-DIC-02]</h2><p><img src="https://ws3.sinaimg.cn/large/006tNc79gy1fhtuo2rcywj30ds08fq3a.jpg" alt=""></p><blockquote><p>可能是商品 id 不正确或者支付逻辑错误导致传入了错误的商品 id, 详细原因可以移步我知乎上的<a href="https://www.zhihu.com/question/26935519/answer/182780390" target="_blank" rel="noopener">这个回答</a></p></blockquote><h2 id="5-添加测试账号"><a href="#5-添加测试账号" class="headerlink" title="5. 添加测试账号"></a>5. 添加测试账号</h2><p>测试账号分为两种:</p><ol><li>下载测试账号列表<ol><li>如果应用处于未发布的状态, 只有这个列表中的用户可以充值</li></ol></li><li>充值测试账号列表<ol><li>这个列表中的用户可以免费充值</li></ol></li></ol><p>测试充值这两个缺一不可.</p><h1 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h1><p>IAB 最麻烦的地方就是它的测试充值, 原因在于它的错误提示不是 “人话”, 这个只能靠我们的丰富经验来解决了.</p><p>参考资料:</p><ol><li><a href="http://blog.csdn.net/n5/article/details/50705745" target="_blank" rel="noopener">http://blog.csdn.net/n5/article/details/50705745</a></li><li><a href="https://developer.android.com/google/play/billing/index.html" target="_blank" rel="noopener">https://developer.android.com/google/play/billing/index.html</a></li></ol>]]></content>
<summary type="html">
<p>在之前的文章中, 有很多篇都是很 Apple IAP 相关的, 而 Google IAB 的却一篇都没有, 这对于占了半壁江山的 Android 很不公平.</p>
<p>就作为一个开发者而言, IAB 的体验是比 IAP 要好的. 它体现在这么几个方面:</p>
<ol>
<li>测试账号就是 Goolge 账号, 不像苹果那样是沙盒账号. 这样不同账号下的应用可以设置同一个测试账号, 避免来回切换账号.</li>
<li>支持退款查询. 在苹果上, 只能吃了这个哑巴亏.</li>
</ol>
<p>但是 IAB 也有一些不太便利的地方, 后面我们会讨论它.</p>
</summary>
<category term="Android" scheme="http://blog.justbilt.com/tags/Android/"/>
<category term="IAB" scheme="http://blog.justbilt.com/tags/IAB/"/>
</entry>
<entry>
<title>最近搞 iOS 版遇到的一些问题和技巧 (五)</title>
<link href="http://blog.justbilt.com/2017/07/16/ios-dev-tips-5/"/>
<id>http://blog.justbilt.com/2017/07/16/ios-dev-tips-5/</id>
<published>2017-07-16T03:36:20.000Z</published>
<updated>2017-07-16T09:30:53.000Z</updated>
<content type="html"><![CDATA[<p>这段时间又积攒下了不少的技巧和解决方案, 感觉 XCode 越来越牛逼了, 尤其是在应用的账号设置方面, 之前遇到的一些低级问题可能已经不复存在了.</p><a id="more"></a><h1 id="此应用在未来的-iOS-版本下将无法使用"><a href="#此应用在未来的-iOS-版本下将无法使用" class="headerlink" title="此应用在未来的 iOS 版本下将无法使用"></a>此应用在未来的 iOS 版本下将无法使用</h1><p><img src="https://ws1.sinaimg.cn/large/006tKfTcgy1fhlkn3of4wj316y0p4aku.jpg" alt=""></p><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案:"></a>解决方案:</h2><p>发生这个问题的原因就是应用没有支持 arm64 CPU 架构, 不同的CPU架构对应的设备如下:</p><blockquote><p>arm64:iPhone6s | iphone6s plus|iPhone6| iPhone6 plus|iPhone5S | iPad Air| iPad mini2(iPad mini with Retina Display)<br>armv7s:iPhone5|iPhone5C|iPad4(iPad with Retina Display)<br>armv7:iPhone4|iPhone4S|iPad|iPad2|iPad3(The New iPad)|iPad mini|iPod Touch 3G|iPod Touch4</p></blockquote><p>一般出现这个问题, 很有可能是 <strong>XCode Architectures 设置或导出包的方式有问题</strong>:</p><p><img src="https://ws2.sinaimg.cn/large/006tKfTcgy1fhlkyqgvv0j30jy02odg2.jpg" alt=""></p><p>这里面有两个地方需要关注一下: <code>Build Avtive Architecture Only</code> 和 <code>Vaild Architectures</code>, 前者是为了加速真机运行的速度, 一般 debug 会设置为 YES, release 版会设置为 NO; 后者是应用所支持的 CPU 架构.</p><p>所以我们必须得保证 Vaild Architectures 中有 arm64, 并且打包出 release 版才可以, 我们遇到这个问题的原因是打包同学使用了 debug 版的 app 做成 ipa 安装包的原因.</p><hr><h1 id="clang-error-no-such-file-or-directory-‘XXX’"><a href="#clang-error-no-such-file-or-directory-‘XXX’" class="headerlink" title="clang: error: no such file or directory: ‘XXX’"></a>clang: error: no such file or directory: ‘XXX’</h1><p>造成这个错误的原因有好多, 这次的报错极其诡异, 连系统库都会报找不到.</p><h2 id="解决方案-1"><a href="#解决方案-1" class="headerlink" title="解决方案:"></a>解决方案:</h2><p>经过排查, 发现是 <code>Other Link Flag</code> 中有一个单行的 <code>-force_laod</code>, 估计是删除的时候少删了一行.</p><hr><h1 id="XCode-真机调试安装失败-提示证书过期"><a href="#XCode-真机调试安装失败-提示证书过期" class="headerlink" title="XCode 真机调试安装失败, 提示证书过期"></a>XCode 真机调试安装失败, 提示证书过期</h1><p><img src="https://ws4.sinaimg.cn/large/006tKfTcgy1fhllu9g2m4j30ad03r3yo.jpg" alt=""></p><h2 id="解决方案-2"><a href="#解决方案-2" class="headerlink" title="解决方案:"></a>解决方案:</h2><p>真机调试的时候一般使用的是开发证书, 开发证书可以有很多个, 在钥匙串中找到过期的证书删掉就可以啦, XCode 会自动匹配一个没有过期的证书.</p><hr><h1 id="导出-dis-包出错-IDEDistributionErrorDomain-error-3"><a href="#导出-dis-包出错-IDEDistributionErrorDomain-error-3" class="headerlink" title="导出 dis 包出错, IDEDistributionErrorDomain error 3"></a>导出 dis 包出错, IDEDistributionErrorDomain error 3</h1><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">error: exportArchive: The operation couldn’t be completed. (IDEDistributionErrorDomain error 3.)</span><br><span class="line">Error Domain=IDEDistributionErrorDomain Code=3 <span class="string">"(null)"</span> UserInfo={IDEDistributionErrorSigningIdentityToItemToUnderlyingErrorKey={}</span><br></pre></td></tr></table></figure><h2 id="解决方案-3"><a href="#解决方案-3" class="headerlink" title="解决方案:"></a>解决方案:</h2><p>其实就是证书和 Provisioning Profiles 没有匹配上, 很有可能是其他小伙伴 revoke 了你的 dis 证书.</p><p><img src="https://ws3.sinaimg.cn/large/006tKfTcgy1fhlmmvhb27j30ud0jajth.jpg" alt=""></p><p>去 <a href="https://developer.apple.com/account/ios/profile/" target="_blank" rel="noopener">苹果开发者中心</a> 查看对应的 Profiles 的 status 是否正常, 修改完后下载运行一下, 然后重新导出就好了.</p><hr><h1 id="沙盒测试-此时没有权限在Sandbox购买此InApp"><a href="#沙盒测试-此时没有权限在Sandbox购买此InApp" class="headerlink" title="沙盒测试: 此时没有权限在Sandbox购买此InApp"></a>沙盒测试: 此时没有权限在Sandbox购买此InApp</h1><p>其实描述的很清晰, 这个沙盒账号不是你这个应用的, 请仔细检查一下.</p><h2 id="解决方案-4"><a href="#解决方案-4" class="headerlink" title="解决方案:"></a>解决方案:</h2><p>更换正确的沙盒测试账号.</p><hr><h1 id="QQ-一键加群跳转过去后群不正确"><a href="#QQ-一键加群跳转过去后群不正确" class="headerlink" title="QQ: 一键加群跳转过去后群不正确"></a>QQ: 一键加群跳转过去后群不正确</h1><p>检查 LSApplicationQueriesSchemes 中是否有 <code>mqqapi</code>:</p><p><img src="https://ws1.sinaimg.cn/large/006tKfTcgy1fhlmr1lvfkj30y00bcgno.jpg" alt=""></p><h2 id="解决方案-5"><a href="#解决方案-5" class="headerlink" title="解决方案:"></a>解决方案:</h2><p>如果没有的话添加一个就好啦.</p><hr><h1 id="导出-dev-包出错-IDEDistributionErrorDomain-Code-14"><a href="#导出-dev-包出错-IDEDistributionErrorDomain-Code-14" class="headerlink" title="导出 dev 包出错, IDEDistributionErrorDomain Code=14"></a>导出 dev 包出错, IDEDistributionErrorDomain Code=14</h1><p>查看终端日志:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">/Users/justbilt/.rvm/bin/rvm: line 66: shell_session_update: command not found</span><br><span class="line">+ xcodebuild -exportArchive -exportOptionsPlist /Users/justbilt/Documents/work/easy_slg_client/frameworks/runtime-src/proj.ios_mac/ios_export_dev.plist -archivePath /Users/justbilt/Documents/work/easy_slg_client/.build/yile.guopan-1.4.0.f592c19533-0607170734.xcarchive -exportPath /Users/justbilt/Documents/work/easy_slg_client/.build/0607192643399129</span><br><span class="line">2017-06-07 19:26:43.906 xcodebuild[4724:25032817] [MT] PluginLoading: Required plug-in compatibility UUID E0A62D1F-3C18-4D74-BFE5-A4167D643966 for plug-in at path '~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/XAlign.xcplugin' not present in DVTPlugInCompatibilityUUIDs</span><br><span class="line">2017-06-07 19:26:44.068 xcodebuild[4724:25032817] [MT] IDEDistribution: -[IDEDistributionLogging _createLoggingBundleAtPath:]: Created bundle at path '/var/folders/ff/wpytbgmd3rl1pn9dd02nbjc80000gn/T/guopan_2017-06-07_19-26-44.068.xcdistributionlogs'.</span><br><span class="line">2017-06-07 19:26:58.844 xcodebuild[4724:25032817] [MT] IDEDistribution: Step failed: <IDEDistributionThinningStep: 0x7fb80e6cb860>: Error Domain=IDEDistributionErrorDomain Code=14 "No applicable devices found." UserInfo={NSLocalizedDescription=No applicable devices found.}</span><br><span class="line">error: exportArchive: No applicable devices found.</span><br></pre></td></tr></table></figure><p>找到其中的 xcdistributionlogs 日志文件路径:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/var/folders/ff/wpytbgmd3rl1pn9dd02nbjc80000gn/T/guopan_2017-06-07_19-26-44.068.xcdistributionlogs</span><br></pre></td></tr></table></figure><p>在 IDEDistribution.standard.log 文件中可以找到真正的错误原因:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">guopan.app/guopan arm64 -></span><br><span class="line">error: Info.plist of “guopan.app/heepayImage.bundle” specifies a non-existent file for the CFBundleExecutable key</span><br><span class="line">error: Info.plist of “guopan.app/walletResources.bundle/Contents” specifies a non-existent file for the CFBundleExecutable key</span><br><span class="line">2017-06-07 11:08:24 +0000 Validating IPA structure...</span><br><span class="line">2017-06-07 11:08:24 +0000 [MT] /Applications/Xcode.app/Contents/Developer/usr/bin/ipatool exited with 1</span><br><span class="line">2017-06-07 11:08:24 +0000 [MT] ipatool JSON: {</span><br><span class="line"> alerts = (</span><br><span class="line"> {</span><br><span class="line"> code = 0;</span><br><span class="line"> description = "Info.plist of \U201cguopan.app/heepayImage.bundle\U201d specifies a non-existent file for the CFBundleExecutable key";</span><br><span class="line"> info = {</span><br><span class="line"> };</span><br><span class="line"> level = ERROR;</span><br><span class="line"> type = "malformed-payload";</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> code = 0;</span><br><span class="line"> description = "Info.plist of \U201cguopan.app/walletResources.bundle/Contents\U201d specifies a non-existent file for the CFBundleExecutable key";</span><br><span class="line"> info = {</span><br><span class="line"> };</span><br><span class="line"> level = ERROR;</span><br><span class="line"> type = "malformed-payload";</span><br><span class="line"> }</span><br><span class="line"> );</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="解决方案-6"><a href="#解决方案-6" class="headerlink" title="解决方案:"></a>解决方案:</h2><p>可以看到这里有两个 Error, 意思就是这两个 .bundle 中的 <code>Info.plist</code> 中的 key <code>CFBundleExecutable</code> 字段指定了一个不存在的文件, 这个指定其实没有啥用, 我们删掉它就可以了. </p><p>用 Sublime 打开这个 plist, 删除下面这两行:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><key>CFBundleExecutable</key></span><br><span class="line"><string>xxx</string></span><br></pre></td></tr></table></figure><p>如果你打开的文件是一团乱码, 那么这个文件应 2进制 形式的 plist, 我们可以用一个命令行工具把它转化为 xml 形式的:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">plutil -convert xml1 xxx/heepayImage.bundle/Info.plist</span><br></pre></td></tr></table></figure><p>然后删除掉.</p>]]></content>
<summary type="html">
万一遇上了呢?
</summary>
<category term="iOS" scheme="http://blog.justbilt.com/tags/iOS/"/>
<category term="iOS-Dev-Tips" scheme="http://blog.justbilt.com/tags/iOS-Dev-Tips/"/>
</entry>
<entry>
<title>使用 Electron 重写 convert2fnt</title>
<link href="http://blog.justbilt.com/2017/07/09/convert2fnt-electron-rewrite/"/>
<id>http://blog.justbilt.com/2017/07/09/convert2fnt-electron-rewrite/</id>
<published>2017-07-09T04:54:42.000Z</published>
<updated>2017-07-16T01:14:38.000Z</updated>
<content type="html"><![CDATA[<p>大概是 14 年 2 月份的时候, 我使用刚学会 Python 写了一个小工具: <a href="https://github.com/justbilt/convert2fnt" target="_blank" rel="noopener">convert2fnt</a>, 为此还写了一篇文章 <a href="/2014/02/01/images_to_bmfont/">将一堆图片转化为BMFont工具</a> 介绍这个工具. 它的主要应用场景是这个样子的:</p><blockquote><p>美术妹子出了一堆图片字, 但是在程序中使用 BMFont 是更加方便的, 这个时候你可以强硬的要求美术妹子重新用 Glyphdesigner 制作一份字体. 但是也可以很温柔的告诉她: “你先去忙吧, 剩下的交给我了.”, 然后在妹子崇拜的目光下, 转身离去, 深藏功与名. </p></blockquote><a id="more"></a><p>恩, 最初版的工具确实能够达到这个目的, 只是过程可能略微麻烦一下. </p><p>在第一版的工具中, 因为用到了 ImageMagick 来拼接图片, 所以你需要先安装这个工具, 然后下载脚本, 安装依赖库, 开始使用. 当妹子满心期待看着你放大招的时候, 你TM还在配环境? </p><p><img src="/face/yilianmengbi3.jpg" alt=""></p><p>第二版中重点优化了配置环境复杂的问题. 使用 Pillow 替换了 ImageMagick, 还使用 PyInstaller 打包了可执行程序. 如果顺利的话, 下载一个可执行程序, 把图片按照规则命名好, 执行下就好了, 很大的进步有木有?</p><p>但是这个工具的使用范围还是局限在了技术人员手, 你不会指望美术策划同学来搭建一个 <code>Python + pip</code> 的环境吧? 我们确实很愿意帮助妹子, 但是策划同学来找你怎么办? </p><p><img src="/face/ruhua.jpg" alt=""></p><p>一个带界面的工具在这时候看起来确实会是一个更好的选择, 这也是我一直所努力的方向. 我尝试过 Python 的各种 GUI 方案: Tkinter, PyWx, PyQt 等等, 这些方案良莠不济, 有的看起来只是一个 Demo, 有的光搭建环境就会让你吐.</p><p>为此我各种尝试,徘徊在各种方案间挣扎了好几年, 直到 <code>Electron</code> 横空出世. 它似乎是一个含着金钥匙出生的项目, 有着 Github 这个全球最大同性交友网站的加成, 一出生便备受瞩目. 当然它也不负众望, 干翻它的前身 Node-Webkit, 使得越来越多的 App 选择使用它来制作:</p><p><img src="https://ww1.sinaimg.cn/large/006tNc79ly1fhdnp3klegj30ln0vcjza.jpg" alt=""></p><p>这里含金量最高的便是: Atom, VSCode 以及 Cocos 的最新产品: Cocos Creater. 其中 Creater 是真正让我下定决定使用 Electron 的项目, 前两个都只是个编辑器, 而 Creater 则是一个解决方案, 一个 2D 游戏的制作工具, 而且从目前的发展来看, 十分的健康. </p><p>以目前 Electron 的火爆, 网上可以找到一大堆的教程, 相信大家可以很轻易的入门这个框架. 这里我和大家分享下自己的心得和踩过的一些坑.</p><h2 id="1-Electron-多个组件的作用"><a href="#1-Electron-多个组件的作用" class="headerlink" title="1. Electron 多个组件的作用"></a>1. Electron 多个组件的作用</h2><h3 id="electron-electron-prebuilt"><a href="#electron-electron-prebuilt" class="headerlink" title="electron/electron-prebuilt"></a>electron/electron-prebuilt</h3><p>根据官方的解释:</p><blockquote><p>Note As of version 1.3.1, this package is published to npm under two names: electron and electron-prebuilt. You can currently use either name, but electron is recommended, as the electron-prebuilt name is deprecated, and will only be published until the end of 2016.</p></blockquote><p>意思就是 <code>electron-prebuilt</code> 已经被废弃了, 建议直接使用:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install electron --save-dev</span><br></pre></td></tr></table></figure><h3 id="electron-package-electron-builder"><a href="#electron-package-electron-builder" class="headerlink" title="electron-package/electron-builder"></a>electron-package/electron-builder</h3><p>这两个都是 Electron 的打包工具, </p><p><a href="https://github.com/electron-userland/electron-packager" target="_blank" rel="noopener">electron-package</a> 只能打出可执行文件(Win:exe, Mac:app):</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">"scripts": {</span><br><span class="line"> "package": "electron-packager . --platform=win32 --arch=ia32 --electron-version=1.4.15 --overwrite --ignore=node_modules --ignore=.gitignore"</span><br><span class="line">},</span><br></pre></td></tr></table></figure><p><a href="https://github.com/electron-userland/electron-builder" target="_blank" rel="noopener">electron-builder</a> 是一个更为先进, 简单的打包工具, 如果不想折腾的话可以直接选择它了.</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">"scripts": {</span><br><span class="line"> "pack": "electron-builder --dir",</span><br><span class="line"> "dist": "electron-builder"</span><br><span class="line">},</span><br></pre></td></tr></table></figure><h2 id="2-一定要准备一个强有力的梯子…或者脑子"><a href="#2-一定要准备一个强有力的梯子…或者脑子" class="headerlink" title="2. 一定要准备一个强有力的梯子…或者脑子"></a>2. 一定要准备一个强有力的梯子…或者脑子</h2><h3 id="npm-install"><a href="#npm-install" class="headerlink" title="npm install"></a>npm install</h3><p>安装 <code>Electron</code> 的途径之一就是通过 <code>npm</code>, 以国内的这个网络环境, 通过 npm 安装一些小的库还勉强可以, 对于 Electron 这种几十兆的库就显得捉襟见肘了, 时间长不说还很容易中断. 这时候就可以选择使用 <a href="https://npm.taobao.org/" target="_blank" rel="noopener">cnpm</a> 来做这些事情, </p><p><img src="https://zos.alipayobjects.com/rmsportal/UQvFKvLLWPPmxTM.png" alt=""></p><h3 id="npm-run-dist"><a href="#npm-run-dist" class="headerlink" title="npm run dist"></a>npm run dist</h3><p>electron-builder 第一次打包时会去下载 electron 的预编译文件, 这个文件很大, 它会默认去 github 上下载, 这时候如果没有翻墙工具就会很惨了.</p><p>我们可以使用 淘宝 提供的<a href="https://npm.taobao.org/mirrors/electron/" target="_blank" rel="noopener">镜像</a>来下载, 使用方法很简单, 在打包前先运行这个命令:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> ELECTRON_MIRROR=<span class="string">"https://npm.taobao.org/mirrors/electron/"</span></span><br></pre></td></tr></table></figure><p>原理可以看文章末尾的链接, 使用后真的是立竿见影, 大家可以对比下下载速度:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">MacBook-Air:convert2fnt bilt$ npm run dist</span><br><span class="line">Downloading tmp-6511-0-electron-v1.6.11-darwin-x64.zip</span><br><span class="line">[==> ] 8.8% of 46.08 MB (65.41 kB/s)</span><br><span class="line">MacBook-Air:convert2fnt bilt$ <span class="built_in">export</span> ELECTRON_MIRROR=<span class="string">"https://npm.taobao.org/mirrors/electron/"</span></span><br><span class="line">MacBook-Air:convert2fnt bilt$ npm run dist</span><br><span class="line">Downloading tmp-6579-0-electron-v1.6.11-darwin-x64.zip</span><br><span class="line">[============================> ] 66.1% of 46.08 MB (951.77 kB/s)</span><br></pre></td></tr></table></figure><h1 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h1><p>最后上一下新版 convert2fnt 截图, 现在还有一些收尾的工作再做, 很快你就会见到它:</p><p><img src="https://ws2.sinaimg.cn/large/006tNc79gy1fhkaqhm6dnj30n80ixq5f.jpg" alt=""></p><p>当策划再有类似的需求时, 可以直接扔给对方一个下载地址. 至于妹子, 当然是选择帮助她啦.</p><hr><p>参考资料:</p><ul><li><a href="http://www.jianshu.com/p/1c2ad78df208" target="_blank" rel="noopener">常用Electron App打包工具</a></li><li><a href="http://blog.tomyail.com/install-electron-slow-in-china/" target="_blank" rel="noopener">加速electron在国内的下载速度</a></li></ul>]]></content>
<summary type="html">
<p>大概是 14 年 2 月份的时候, 我使用刚学会 Python 写了一个小工具: <a href="https://github.com/justbilt/convert2fnt" target="_blank" rel="noopener">convert2fnt</a>, 为此还写了一篇文章 <a href="/2014/02/01/images_to_bmfont/">将一堆图片转化为BMFont工具</a> 介绍这个工具. 它的主要应用场景是这个样子的:</p>
<blockquote>
<p>美术妹子出了一堆图片字, 但是在程序中使用 BMFont 是更加方便的, 这个时候你可以强硬的要求美术妹子重新用 Glyphdesigner 制作一份字体. 但是也可以很温柔的告诉她: “你先去忙吧, 剩下的交给我了.”, 然后在妹子崇拜的目光下, 转身离去, 深藏功与名. </p>
</blockquote>
</summary>
<category term="convert2fnt" scheme="http://blog.justbilt.com/tags/convert2fnt/"/>
<category term="Electron" scheme="http://blog.justbilt.com/tags/Electron/"/>
</entry>
<entry>
<title>优化游戏在 iOS 上的内购</title>
<link href="http://blog.justbilt.com/2017/06/18/refine-game-ios-iap/"/>
<id>http://blog.justbilt.com/2017/06/18/refine-game-ios-iap/</id>
<published>2017-06-18T10:58:03.000Z</published>
<updated>2017-07-09T03:09:19.000Z</updated>
<content type="html"><![CDATA[<p>首先我们看下 iOS 内购的流程, 让我们看下官方的的流程图:</p><p><img src="https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Art/remote_store_fetch_2x.png" alt=""></p><a id="more"></a><p>很简单, 是不是 ? 作为一款游戏来说, 实际情况要复杂的多.首先要思考这么几个问题:</p><ol><li>返回 Game 层时因为内存不足游戏重启了怎么办 ?</li><li>有玩家反应充值不到账怎么办 ?</li><li>如何防 IAP 破解 ?</li></ol><p>作为一款可能在 “国内” 发售的游戏, 如果你们没有考虑过这些问题, 那可能会遇到很多的 “惊喜”. 有一点需要记住, 千万不要低估玩家的 “创造力”. </p><p>首先, 请允许用十分简单的几行代码带大家回顾下 IAP 的实现, 因为后面我们要讨论的内容全部是基于这个前提的. </p><h1 id="IAP-的代码实现"><a href="#IAP-的代码实现" class="headerlink" title="IAP 的代码实现"></a>IAP 的代码实现</h1><p>要开启内购很简单, 两个对象三个步骤就搞定了. </p><p>我们先来说说这两个对象, 分别是 <code>SKPaymentQueue</code> 和 <code>SKPaymentTransaction</code> , 前者是充值订单, 后者是充值队列.</p><h2 id="SKPaymentQueue"><a href="#SKPaymentQueue" class="headerlink" title="SKPaymentQueue"></a>SKPaymentQueue</h2><p>它是整个充值的核心, 负责串联充值的各个流程. 可以用这个对象实现发起购买, 恢复已经购买的物品, 结束购买, 监听充值事件等功能.</p><h2 id="SKPaymentTransaction"><a href="#SKPaymentTransaction" class="headerlink" title="SKPaymentTransaction"></a>SKPaymentTransaction</h2><p>前面说过它是充值的订单对象, 每一次充值的发起都会产生一个唯一的订单对象, 它记录有这个订单的状态, id, 时间, 收据的信息.</p><p>其中收据(<code>transactionReceipt</code>)是最重要的数据, 它是这次充值是否真正扣款成功的唯一条件. 我们要把它发送给我们的游戏服务器, 再向苹果的服务器进行验证.</p><p>下面来说说三个步骤:</p><h2 id="1-添加充值监听"><a href="#1-添加充值监听" class="headerlink" title="1. 添加充值监听"></a>1. 添加充值监听</h2><p>添加监听的逻辑很简单, 就下面这三行:</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> ([<span class="built_in">SKPaymentQueue</span> defaultQueue]) {</span><br><span class="line"> [[<span class="built_in">SKPaymentQueue</span> defaultQueue] addTransactionObserver:<span class="keyword">self</span>];</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>但是这个 <code>self</code> 必须是一个实现了 <code>SKPaymentTransactionObserver</code> 的类的实例, 当有充值的事件时, 类的 <code>updatedTransactions</code> 函数会被调用:</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//.h</span></span><br><span class="line"><span class="class"><span class="keyword">@interface</span> <span class="title">IAPHelper</span> : <span class="title">NSObject</span> <<span class="title">SKPaymentTransactionObserver</span>></span></span><br><span class="line"></span><br><span class="line"><span class="comment">//.m</span></span><br><span class="line"><span class="class"><span class="keyword">@implementation</span> <span class="title">IAPHelper</span></span></span><br><span class="line">- (<span class="keyword">void</span>)paymentQueue:(<span class="built_in">SKPaymentQueue</span> *)queue updatedTransactions:(<span class="built_in">NSArray</span> *)transactions</span><br><span class="line">{</span><br><span class="line"> <span class="keyword">for</span> (<span class="built_in">SKPaymentTransaction</span> *transaction <span class="keyword">in</span> transactions)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">switch</span> (transaction.transactionState)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">case</span> <span class="built_in">SKPaymentTransactionStatePurchased</span>: </span><br><span class="line"> <span class="comment">// 处理支付成功</span></span><br><span class="line"> [[<span class="built_in">SKPaymentQueue</span> defaultQueue] finishTransaction: transaction];</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="built_in">SKPaymentTransactionStateFailed</span>: <span class="comment">// 支付失败</span></span><br><span class="line"> <span class="comment">// 处理支付失败</span></span><br><span class="line"> [[<span class="built_in">SKPaymentQueue</span> defaultQueue] finishTransaction: transaction];</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="built_in">SKPaymentTransactionStateRestored</span>: <span class="comment">// 恢复内购</span></span><br><span class="line"> <span class="comment">// 处理恢复内购</span></span><br><span class="line"> [[<span class="built_in">SKPaymentQueue</span> defaultQueue] finishTransaction: transaction];</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">default</span>:</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure><h2 id="2-请求商品数据"><a href="#2-请求商品数据" class="headerlink" title="2. 请求商品数据"></a>2. 请求商品数据</h2><p>在开始购买之前, 我们需要知道商品的数据, 比如价格, 描述之类的, 这就需要我们先请请求商品的数据:</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">SKProductsRequest</span>* request = [[<span class="built_in">SKProductsRequest</span> alloc] initWithProductIdentifiers:@[<span class="string">@"com.xxx.xxx.01"</span>, <span class="string">@"com.xxx.xxx.02"</span>]];</span><br><span class="line">request.delegate = <span class="keyword">self</span>;</span><br><span class="line">[request start];</span><br></pre></td></tr></table></figure><p>上面就是请求商品的代码, 商品的 id 列表是需要我们自己准备的. 请求的结果会通过一个函数来通知我们, 同样我们需要实现一个代理 <code>SKProductsRequestDelegate</code>:</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//.h</span></span><br><span class="line"><span class="class"><span class="keyword">@interface</span> <span class="title">IAPHelper</span> : <span class="title">NSObject</span> <<span class="title">SKProductsRequestDelegate</span>></span></span><br><span class="line"></span><br><span class="line"><span class="comment">//.mm</span></span><br><span class="line"><span class="class"><span class="keyword">@implementation</span> <span class="title">IAPHelper</span></span></span><br><span class="line">- (<span class="keyword">void</span>)productsRequest:(<span class="built_in">SKProductsRequest</span> *)request didReceiveResponse:(<span class="built_in">SKProductsResponse</span> *)response {</span><br><span class="line"> <span class="built_in">NSArray</span>* products = response.products;</span><br><span class="line"> <span class="comment">// products 就是商品的列表</span></span><br><span class="line">}</span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure><h2 id="3-购买"><a href="#3-购买" class="headerlink" title="3. 购买"></a>3. 购买</h2><p>发起一次购买的逻辑:</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">SKPayment</span> *payment = [<span class="built_in">SKPayment</span> paymentWithProduct:<span class="string">@"com.xxx.xxx.01"</span>];</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> ([<span class="built_in">SKPaymentQueue</span> defaultQueue]) {</span><br><span class="line"> [[<span class="built_in">SKPaymentQueue</span> defaultQueue] addPayment:payment];</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>下面就会进入到我们之前添加的那个充值状态的监听逻辑中去.</p><h1 id="充值流程的优化"><a href="#充值流程的优化" class="headerlink" title="充值流程的优化"></a>充值流程的优化</h1><p>流程方面, 我们大概经过了这么下面几个阶段.</p><h2 id="野生蛮长的第一阶段"><a href="#野生蛮长的第一阶段" class="headerlink" title="野生蛮长的第一阶段"></a>野生蛮长的第一阶段</h2><p>这一阶段的特征是: <code>先完成交易, 后处理订单</code>, 如果仔细观察上面 <code>updatedTransactions</code> 的逻辑的话, 可以看到这样的一行代码:</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[[<span class="built_in">SKPaymentQueue</span> defaultQueue] finishTransaction: transaction];</span><br></pre></td></tr></table></figure><p>这行代码的意义就是标记这个订单已经完成了, 苹果从此以后不会再去通知你这个订单的任何消息了. 在调用这行代码之前, 我们会去处理一些充值结果的逻辑, 其中充值成功我们是要去苹果验证收据的.</p><p>但是验证的这个过程是异步的, 所以可能发生的问题就是:</p><blockquote><p>在验证结果回来之前就关闭了订单.</p></blockquote><p>因此若是验证的过程不够顺利, 比如网络状况不佳, 应用意外退出, 就会导致玩家的这次充值没有到账.</p><h2 id="始料不及的第二阶段"><a href="#始料不及的第二阶段" class="headerlink" title="始料不及的第二阶段"></a>始料不及的第二阶段</h2><p>有了上面那个问题, 我们不禁会想 ? 是不是可以等收到验证结果再去完成订单. 答案是肯定的, 我们确实可以这样做, 苹果也很仁义, 没有完成的订单会在下次请求商品时再次通知我们充值成功.</p><p>简单验证之后, 我们修改了充值的逻辑, 信心满满的上线了. 上线之初确实很顺利, 反馈充值不到账的玩家数量降低了很多, 我们都陷入了巨大的喜悦之中, 再也不会有玩家反应充值不到账了, oh yeah ! </p><p>然后, 后面发生的事情却是我们始料不及的. 我们陆续收到了用户声泪俱下的控诉, 自己是多么多么的热爱这款游戏, 但是却无法享受充值带来的乐趣. 起初还只是可能不到账, 现在则是某一个充值项完全无法购买.</p><p>在诸多玩家的控诉中, 我们逐渐还原了问题的成因. 某一次充值成功后游戏重启, 没有收到钻石, 再次购买该商品则会提示:</p><p><img src="http://ww1.sinaimg.cn/large/7f870d23ly1fgw9pxzo6ej206q04jt96.jpg" alt=""></p><blockquote><p>您已购买此 App 内购买项目。此项目将免费恢复。</p></blockquote><p>根据这个反馈可以很轻易的推断出是因为我们没有 <code>finishTransaction</code> 某一笔订单的原因, 但是却很难重现玩家所遇到的问题. 我们也复现过这个问题, 但是都能够在重启后自动解决. 我们也加了很多埋点去统计玩家的日志, 发现玩家似乎进入了一个卡单的状态, 程序无法获取到某些未完成的订单.</p><p>这个问题困扰了我们好久, 而且反馈的玩家越来越多, 有的玩家甚至已经没有可以能够购买的商品了.</p><h2 id="键入佳境的第三阶段"><a href="#键入佳境的第三阶段" class="headerlink" title="键入佳境的第三阶段"></a>键入佳境的第三阶段</h2><p>虽然上面的问题我们可以将锅甩给苹果, 但是实在受到损失的是我们, 因此还是需要解决这个问题. 经过讨论之后, 我们将目光又转回了第一阶段的方案, 看能否改进哪个方案. </p><blockquote><p>我们可以先将订单数据保存在本地, 然后发起验证请求, 关闭订单, 等验证成功后删除本地保存的订单.</p></blockquote><p>下次启动游戏或者充值时, 我们可以先检查下本地是否有缓存的订单, 有的话就先尝试验证这个订单. 虽然还有一些瑕疵, 比如玩家删除掉应用后就再也找不回之前的订单了, 但是已经是一个很不错的方案了.</p><p>下面我们来实现一下:</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="built_in">NSString</span>*)getRecordTransaction</span><br><span class="line">{</span><br><span class="line"><span class="meta">#if USE_ICLOUD_STORAGE</span></span><br><span class="line"> <span class="built_in">NSUbiquitousKeyValueStore</span> *storage = [<span class="built_in">NSUbiquitousKeyValueStore</span> defaultStore];</span><br><span class="line"><span class="meta">#else</span></span><br><span class="line"> <span class="built_in">NSUserDefaults</span> *storage = [<span class="built_in">NSUserDefaults</span> standardUserDefaults];</span><br><span class="line"><span class="meta">#endif</span></span><br><span class="line"> </span><br><span class="line"> <span class="built_in">NSArray</span> *saved_transactions = [storage arrayForKey:<span class="string">@"transactions"</span>];</span><br><span class="line"> <span class="keyword">if</span> (!saved_transactions or [saved_transactions count] <= <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">return</span> nullptr;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> [saved_transactions objectAtIndex:<span class="number">0</span>];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)recordTransaction:(<span class="built_in">NSString</span> *)transaction</span><br><span class="line">{</span><br><span class="line"><span class="meta">#if USE_ICLOUD_STORAGE</span></span><br><span class="line"> <span class="built_in">NSUbiquitousKeyValueStore</span> *storage = [<span class="built_in">NSUbiquitousKeyValueStore</span> defaultStore];</span><br><span class="line"><span class="meta">#else</span></span><br><span class="line"> <span class="built_in">NSUserDefaults</span> *storage = [<span class="built_in">NSUserDefaults</span> standardUserDefaults];</span><br><span class="line"><span class="meta">#endif</span></span><br><span class="line"> </span><br><span class="line"> <span class="built_in">NSArray</span> *saved_transactions = [storage arrayForKey:<span class="string">@"transactions"</span>];</span><br><span class="line"> <span class="keyword">if</span> (!saved_transactions) {</span><br><span class="line"> <span class="comment">// Storing the first receipt</span></span><br><span class="line"> [storage setObject:@[transaction] forKey:<span class="string">@"transactions"</span>];</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// Adding another receipt</span></span><br><span class="line"> <span class="built_in">NSArray</span> *updated_transactions = [saved_transactions arrayByAddingObject:transaction];</span><br><span class="line"> [storage setObject:updated_transactions forKey:<span class="string">@"transactions"</span>];</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> [storage synchronize];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)removeRecordTransaction:(<span class="built_in">NSString</span> *)purchase_id transactionId:(<span class="built_in">NSString</span>*) transaction_id</span><br><span class="line">{</span><br><span class="line"><span class="meta">#if USE_ICLOUD_STORAGE</span></span><br><span class="line"> <span class="built_in">NSUbiquitousKeyValueStore</span> *storage = [<span class="built_in">NSUbiquitousKeyValueStore</span> defaultStore];</span><br><span class="line"><span class="meta">#else</span></span><br><span class="line"> <span class="built_in">NSUserDefaults</span> *storage = [<span class="built_in">NSUserDefaults</span> standardUserDefaults];</span><br><span class="line"><span class="meta">#endif</span></span><br><span class="line"> </span><br><span class="line"> <span class="built_in">NSMutableArray</span> *saved_transactions = [<span class="built_in">NSMutableArray</span> arrayWithArray:[storage arrayForKey:<span class="string">@"transactions"</span>]];</span><br><span class="line"> <span class="keyword">if</span> (!saved_transactions or [saved_transactions count] <= <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">int</span> index = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">for</span> (<span class="built_in">NSString</span>* transaction <span class="keyword">in</span> saved_transactions) {</span><br><span class="line"> <span class="keyword">if</span> ([transaction rangeOfString:purchase_id].location != <span class="built_in">NSNotFound</span> and (not transaction_id or ([transaction rangeOfString:transaction_id].location != <span class="built_in">NSNotFound</span>))) {</span><br><span class="line"> [saved_transactions removeObjectAtIndex:index];</span><br><span class="line"> [storage setObject:saved_transactions forKey:<span class="string">@"transactions"</span>];</span><br><span class="line"> [storage synchronize];</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> index = index + <span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里我们实现了存储, 获取, 删除的逻辑, 只是参数是 <code>NSString</code> 类型而不是 <code>SKPaymentTransaction</code>. 它其实是 Transaction 的一些有用的数据和我们自己的一些信息放的一个到了一个 <code>NSDictionary</code> 中, 然后将这个 dict 转化为 json 的字符串, 这样做是为了方便存储.</p><h2 id="锦上添花的第四阶段"><a href="#锦上添花的第四阶段" class="headerlink" title="锦上添花的第四阶段"></a>锦上添花的第四阶段</h2><p>其实第三阶段的逻辑已经很严谨了, 我们为了更好的了解玩家的充值行为, 加了很多的日志在这里:</p><ol><li>我们存下了玩家的所有充值结果, 成功的, 失败的, 取消的, 异常的.</li><li>我们存下了玩家在充值界面的各种操作, 比如点击按钮, 界面跳转等.</li></ol><p>在合适的时候, 将这些日志发送到服务器. 这些日志将会成为后面分析玩家行为, 解决纠纷的重要依据.</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>我用 ProcessOn 制作了一个全新的流程图, 地址在<a href="https://www.processon.com/view/link/594e7af4e4b0ad619ac50bd4" target="_blank" rel="noopener">这里</a>:</p><p><img src="http://ww1.sinaimg.cn/large/7f870d23ly1fgwqn64m5sj20ng09laai.jpg" alt=""></p><p>现在在回答下上面提出的几个问题:</p><h2 id="1-返回-Game-层时因为内存不足游戏重启了怎么办"><a href="#1-返回-Game-层时因为内存不足游戏重启了怎么办" class="headerlink" title="1. 返回 Game 层时因为内存不足游戏重启了怎么办 ?"></a>1. 返回 Game 层时因为内存不足游戏重启了怎么办 ?</h2><p>答: 我们在本地存储了充值订单, 游戏会重启, 但这个本地存储不会自动删除.</p><h2 id="2-有玩家反应充值不到账怎么办"><a href="#2-有玩家反应充值不到账怎么办" class="headerlink" title="2. 有玩家反应充值不到账怎么办 ?"></a>2. 有玩家反应充值不到账怎么办 ?</h2><p>答: 这里我们要解决两个问题:</p><ol><li>是否真的没有到账 ?</li></ol><p>可能是实际已经到账了, 只是玩家没有发现, 这个需要后端查询玩家的钻石变化后向玩家解释清楚.</p><ol start="2"><li>是否真的充值成功 ?</li></ol><p>这个首先要玩家提供苹果充值收据邮件截图, 但是这个是可以伪造的, 但是可以阻挡一部分心怀不轨的玩家. 再根据我们前面收集的日志, 加上这个账号历史行为作出一个判断, 是否要给这个玩家补单.</p><h2 id="3-如何防-IAP-破解"><a href="#3-如何防-IAP-破解" class="headerlink" title="3. 如何防 IAP 破解 ?"></a>3. 如何防 IAP 破解 ?</h2><p>这个对于网游来说, 很简单, 就像在服务器端向苹果充值服务器发起验证. 需要注意的是: 服务器端在向苹果验证收据时, 一定要先检查<code>订单的唯一性</code>, <code>充值时间</code>, <code>商品id</code> 是否正常.</p>]]></content>
<summary type="html">
<p>首先我们看下 iOS 内购的流程, 让我们看下官方的的流程图:</p>
<p><img src="https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Art/remote_store_fetch_2x.png" alt=""></p>
</summary>
<category term="Quick-Cocos2d-x" scheme="http://blog.justbilt.com/tags/Quick-Cocos2d-x/"/>
<category term="iOS" scheme="http://blog.justbilt.com/tags/iOS/"/>
<category term="IAP" scheme="http://blog.justbilt.com/tags/IAP/"/>
</entry>
<entry>
<title>Quick-cocos2d-x 中的多语言</title>
<link href="http://blog.justbilt.com/2017/06/03/quick-x-i18n/"/>
<id>http://blog.justbilt.com/2017/06/03/quick-x-i18n/</id>
<published>2017-06-03T12:30:35.000Z</published>
<updated>2017-07-09T03:09:19.000Z</updated>
<content type="html"><![CDATA[<p>其实本来我的标题是 “Quick-cocos2d-x 中的国际化与本地化”, 多语言虽然是其中的主要内容, 但还有很多额外的工作.</p><p>比如: 在韩国上线的游戏必须在游戏第一次启动时弹出一个内容十分长的用户协议, 用户同意后方可继续游戏; 比如很多赌博性质的活动(抽奖, 拉霸, 转盘)都需要修改为其他表现形式. 还有一些技术方面的要求比如游戏用户的数据不能存在 cache 目录下等等.</p><p>每个国家和地区的要求都不尽相同, 有的是硬性的法律法规要求, 有的则是照顾到当地风俗习惯以提高用户体验. 当然, 这些并不在我们这次讨论范畴之内, 等我们的经历足够丰富之后可以再次和大家分享一下.</p><a id="more"></a><p>今天, 主要和大家说说多语言.</p><h1 id="一-策略"><a href="#一-策略" class="headerlink" title="一. 策略"></a>一. 策略</h1><h2 id="1-语言代码"><a href="#1-语言代码" class="headerlink" title="1. 语言代码"></a>1. 语言代码</h2><p>就是不同语言我们需要一个 id 与之对应, 这个有很多种选择, 我们选择了<a href="https://msdn.microsoft.com/en-us/library/hh456380.aspx" target="_blank" rel="noopener">微软翻译的代码</a>:</p><table><thead><tr><th>Language Code</th><th>English Name</th></tr></thead><tbody><tr><td>zh-CHS</td><td>Chinese Simplified</td></tr><tr><td>zh-CHT</td><td>Chinese Traditional</td></tr><tr><td>en</td><td>English</td></tr></tbody></table><h2 id="2-多语言文本-id"><a href="#2-多语言文本-id" class="headerlink" title="2. 多语言文本 id"></a>2. 多语言文本 id</h2><p>我们的策略很简单, 每一个多语言文本都有一个唯一 id, 每一个语言都是由多个 <code>id: text</code> 组成的 json 文件, 如下所示:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"1493084502"</span>: <span class="string">"积分兑换超级大奖"</span>,</span><br><span class="line"> <span class="attr">"1493084508"</span>: <span class="string">"[day]天[hour]小时后结束"</span>,</span><br><span class="line"> <span class="attr">"1493172258"</span>: <span class="string">"活动积分"</span>,</span><br><span class="line"> <span class="attr">"1493723018"</span>: <span class="string">"7日活动积分"</span>,</span><br><span class="line"> <span class="attr">"1493731515"</span>: <span class="string">"该功能暂未开启, 请耐心等待."</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在游戏初始化的时候选择不同语言的 json 加载, 然后有一个函数 <code>tr</code> 接受 id 返回 text, 这就是我们全部的策略. 就是这么简单的策略, 我们也踩了不少坑.</p><p>最开始, 我们是打算弄一个自增的 id, 我们规定了一个从 <code>100000</code> 开始, 每次自增 10 这样的一个 id 生成策略, 之所以自增 10 是考虑到插入 id 的需求. 我们自以为这个策略很鲁棒, 却还是栽了跟头:</p><blockquote><p>合并时会冲突</p></blockquote><p>我们不同的功能是在不同的分支上做的, 完成之后会合并到主分支上. 大家在不同的分支上开发不同的功能时, 没有考虑到多语言后期合并冲突的问题, 而且这个冲突解决起来很麻烦, 得为冲突中的一方分配新的 id, 还得将代码中的 id 都替换掉, 这个过程是十分容易出错的. 怎么办 ?</p><p>我们也想过规定不同的 id 区间, 不同的模块有着不同的 id 起始值, 这样虽然一定程度上解决了模块间冲突的问题, 就算忽略规定这个 id 起始值所带来的额外工作, 多人协作的同一模块怎么办 ? 小伙伴们是不是还得提心吊胆, 小心翼翼的工作 ? 这可不是我们的风格.</p><p>就在我一筹莫展的时候, 我偶然间发现了一个东西: <code>时间戳</code>, 我们可以用这个做 id 呀 ! 虽然理论上还是有可能冲突, 但是两个多语言 id 在同一秒内生成的概率又能有多高呢 ? </p><p>这里还要说说为什么我们没用使用 <strong>英文意义</strong> 作为 id 呢 ? 诚然英文 id 有更高的可读性, 有两个原因导致我没有选择它:</p><ol><li>小伙伴们的英文水平参差不齐, 如果使用英文 id 的话, 很有可能会出现词不达意的情况, 反而降低可读性.</li><li>一个 id 所代表意思可能会发生变化, 如果变了, 是否要修改所有的引用呢 ?</li></ol><h2 id="3-占位符与格式化"><a href="#3-占位符与格式化" class="headerlink" title="3. 占位符与格式化"></a>3. 占位符与格式化</h2><p>我们一开始也是使用 <code>%d</code> <code>%s</code> 之类的东西做占位符, 但是这些东西是严格以来占位符及参数的顺序来替换的, 而同一个占位符在不同语言中的的位置可能是不同的. 比如:</p><blockquote><p>中文: “军官统御等级每增加 %d(1),增加带兵量 %d(10)”<br>英文: “The size of the troop increase %d(10) by officer’s Command Level increase %d(1)”</p></blockquote><p>大家可以看到, 这两种语言下两个占位符的顺序是完全相反的. 如果我们使用这种方式来的话, 就会对玩家造成误解. 那么我们应该怎么做呢?</p><blockquote><p>中文: “军官统御等级每增加 [lv](1),增加带兵量 [amount](10)“<br>英文: “The size of the troop increase [amount](10) by officer’s Command Level increase [lv](1)“</p></blockquote><p>我们使用<code>明确意义的占位符</code>来占位.</p><h2 id="4-图片的多语言"><a href="#4-图片的多语言" class="headerlink" title="4. 图片的多语言"></a>4. 图片的多语言</h2><p>我在一篇文章中看到说要尽可能的避免使用带有文字的图片, 但是这种需求是无法避免的. 如果实现不同语言下用不同的图片呢 ? 我们的做法是给这个图片的命名中加入标记, 标记这是一个多语言图片, 在通过一个函数来获得真正的图片路径.</p><p>例如: 有一个图片的路径是 <code>images/logo.png</code>, 我们需要修改为 <code>images/logo[zh-CHS].png</code>, 标记这是一个中文下的图片, 同理会有一个 <code>images/logo[en].png</code>. 真正加载图片的时候, 会通过函数替换使用当前语言替换掉里面的占位符.</p><h1 id="二-代码支持"><a href="#二-代码支持" class="headerlink" title="二. 代码支持"></a>二. 代码支持</h1><h2 id="1-当前语言的确定"><a href="#1-当前语言的确定" class="headerlink" title="1. 当前语言的确定"></a>1. 当前语言的确定</h2><p>一开始我们是直接使用的 <code>device.language</code> 来确定当前语言的, 但实际情况要复杂的多. </p><h3 id="1-语言残缺"><a href="#1-语言残缺" class="headerlink" title="1). 语言残缺"></a>1). 语言残缺</h3><p>cocos 默认只支持<strong>cn:中文, fr:法语, it:意大利语, gr:德语, sp:西班牙语, ru:俄语, jp:日语, en:英语</strong> 这几种语言, 如果我们要支持一个这里面没有的语言怎么办 ?</p><p>我们需要分别从 Android 和 iOS 哪里获取到设备的当前语言 <code>language code</code>, 然后在 lua 中进行判断.</p><p>Android:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> String <span class="title">getLanguageCode</span><span class="params">()</span> </span>{ <span class="keyword">return</span> Locale.getDefault().toString();}</span><br></pre></td></tr></table></figure><p>iOS:</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">NSString</span>* language_code = [[<span class="built_in">NSLocale</span> preferredLanguages] objectAtIndex:<span class="number">0</span>];</span><br></pre></td></tr></table></figure><h3 id="2-简体-繁体的确定"><a href="#2-简体-繁体的确定" class="headerlink" title="2). 简体/繁体的确定"></a>2). 简体/繁体的确定</h3><p>这个确实值得拿出来一说, 这个 language_code 其实是有一套标准的, 但这个标准有好几个版本, Android 和 iOS 返回的可能不是一个标准, iOS 不同版本可能返回的不是同一个标准, cocos 原生的那个写法实际上是有漏洞的, 就中文来说会有这么几个写法:</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">zh-CHS, zh-Hans, zh-CHT, zh-Hant, zh-cn, zh-tw, , zh-mo, zh-sg, zh-hk</span><br></pre></td></tr></table></figure><p>最终我们需要把这些可能转化为两种 <code>简体/繁体</code>, 我们是这么做的:</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">local</span> language = language_code</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">string</span>.startswith(language, <span class="string">"zh"</span>) <span class="keyword">then</span></span><br><span class="line"> <span class="keyword">if</span> <span class="built_in">string</span>.<span class="built_in">find</span>(language,<span class="string">"Hans"</span>) <span class="keyword">then</span></span><br><span class="line"> language = <span class="string">"zh-CHS"</span></span><br><span class="line"> <span class="keyword">elseif</span> <span class="built_in">string</span>.<span class="built_in">find</span>(language,<span class="string">"Hant"</span>) <span class="keyword">then</span></span><br><span class="line"> language = <span class="string">"zh-CHT"</span></span><br><span class="line"> <span class="keyword">elseif</span> <span class="built_in">string</span>.<span class="built_in">find</span>(language,<span class="string">"TW"</span>) <span class="keyword">then</span></span><br><span class="line"> language = <span class="string">"zh-CHT"</span></span><br><span class="line"> <span class="keyword">elseif</span> language == <span class="string">"zh-cn"</span> <span class="keyword">or</span> language == <span class="string">"zh-mo"</span> <span class="keyword">or</span> language == <span class="string">"zh-sg"</span> <span class="keyword">then</span></span><br><span class="line"> language = <span class="string">"zh-CHS"</span></span><br><span class="line"> <span class="keyword">elseif</span> language == <span class="string">"zh-hk"</span> <span class="keyword">or</span> language == <span class="string">"zh-tw"</span> <span class="keyword">then</span></span><br><span class="line"> language = <span class="string">"zh-CHT"</span> </span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> language = <span class="string">"zh-CHS"</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><p>同理, 不光中文是这样的, 英文也同样有很多方言. 所以我们不能用 <code>==</code> 来判断某个语言, 要用 <code>startswith</code> .</p><h3 id="3-考虑支持的语言列表和玩家存档"><a href="#3-考虑支持的语言列表和玩家存档" class="headerlink" title="3). 考虑支持的语言列表和玩家存档"></a>3). 考虑支持的语言列表和玩家存档</h3><p>要考虑这么两个问题, 通过上一步获取到一个你不支持的语言怎么办? 我们的做法是声明一个默认语言<em>(英语)</em>, 某个语言不支持就用这个语言.</p><p>同时如果游戏内有选择语言功能的话, 我们要优先使用玩家选择的语言.</p><p>声明支持的语言:</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">platform.language_support = </span><br><span class="line">{</span><br><span class="line"> default = flavor.language.en,</span><br><span class="line"> support_list = {</span><br><span class="line"> flavor.language.en,</span><br><span class="line"> flavor.language.zh_chs,</span><br><span class="line"> flavor.language.zh_cht,</span><br><span class="line"> flavor.language.ar,</span><br><span class="line"> flavor.language.ko,</span><br><span class="line"> flavor.language.th,</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>判断语言:</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> #(platform.language_support.support_list) >= <span class="number">2</span> <span class="keyword">then</span></span><br><span class="line"> platform.language = Record.getLanguage()</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> <span class="built_in">table</span>.contain(platform.language_support.support_list, platform.language) <span class="keyword">then</span></span><br><span class="line"> platform.language = platform.language_support.default</span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"><span class="keyword">else</span></span><br><span class="line"> platform.language = platform.language_support.default</span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><h2 id="2-tr"><a href="#2-tr" class="headerlink" title="2. tr"></a>2. tr</h2><p>tr 就是根据 id 返回真正文本的函数:</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">local</span> text = json.decode(<span class="built_in">io</span>.readfile(cc.FileUtils:getInstance():fullPathForFilename((<span class="string">"native/"</span>..platform.language..<span class="string">".json"</span>))))</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">tr</span><span class="params">(_key)</span></span></span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> _key <span class="keyword">then</span></span><br><span class="line"> <span class="keyword">return</span> <span class="string">"???"</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"> <span class="keyword">return</span> text[<span class="built_in">tostring</span>(_key)] <span class="keyword">or</span> <span class="string">"404:"</span>..<span class="built_in">tostring</span>(_key)</span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><p>很简单, 还有一些可以拓展的空间, 比如最近发现的同一个中文如何对应不同 case 英文的问题. 举个栗子: 道具的英文是 <code>item</code>, 我们可能很多地方都会用到这个单词, 但是不同的地方可能会有一些小的区别:</p><ol><li>主界面入口上需要显示为全大写: ITEM</li><li>行首的拼接需要首字母大写: Item</li><li>行中的拼接需要全小写: item</li></ol><p>这个难道要多用好几个 id 来实现吗 ? 我们可以通过 <code>tr</code> 的第二个参数来指定格式. </p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">tr(<span class="number">10000</span>, <span class="string">"upper"</span>) <span class="comment">-- 大写</span></span><br><span class="line">tr(<span class="number">10000</span>, <span class="string">"title"</span>) <span class="comment">-- 首字母大写</span></span><br><span class="line">tr(<span class="number">10000</span>) <span class="comment">-- 小写</span></span><br></pre></td></tr></table></figure><p>内部再处理下这个参数就可以实现同一个中文对应不同的英文了.</p><h2 id="3-formatex"><a href="#3-formatex" class="headerlink" title="3. formatex"></a>3. formatex</h2><p>上面说过有一个函数替换占位符, 其实现如下:</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">string.formatex</span><span class="params">(format, map)</span></span></span><br><span class="line"> <span class="built_in">format</span> = <span class="built_in">string</span>.<span class="built_in">gsub</span>(<span class="built_in">format</span>, <span class="string">"%[(.-)]"</span>, map)</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">format</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- example</span></span><br><span class="line"><span class="built_in">string</span>.formatex(<span class="string">"[attacker]砍了[defender]一刀, 造成了[damage]伤害"</span>, {</span><br><span class="line"> attacker = <span class="string">"张三"</span>,</span><br><span class="line"> defender = <span class="string">"李四"</span>,</span><br><span class="line"> damage = <span class="number">10</span>,</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p>很方便的有木有 ?</p><h1 id="三-UI-的适配"><a href="#三-UI-的适配" class="headerlink" title="三. UI 的适配"></a>三. UI 的适配</h1><h2 id="1-多个横向排版-Label"><a href="#1-多个横向排版-Label" class="headerlink" title="1. 多个横向排版 Label"></a>1. 多个横向排版 Label</h2><p>我们先来看两张图:</p><p><img src="http://ww1.sinaimg.cn/large/7f870d23ly1fgo6l8umlkj20u007p759.jpg" alt=""></p><p><img src="http://ww1.sinaimg.cn/large/7f870d23ly1fgo6o3ya7xj20u007pab6.jpg" alt=""></p><p>上图中的左侧的 Label 在不同语言下的宽度是不一样的, 如果我们不想在代码中手动调整的话有这么几个办法:</p><ol><li>使用一个容器存放多个 Label, 容器会自动排版多个元素的位置</li><li>在编辑 UI 时就预留好可一定的空间</li><li>如果只有两个 Lable 的话, 左侧的右对齐, 右侧的左对齐</li></ol><h2 id="2-Label-Overflow"><a href="#2-Label-Overflow" class="headerlink" title="2. Label Overflow"></a>2. Label Overflow</h2><p>这个概念是从 Cocos Creater 哪里找到的, 如果一个 Label 的实际尺寸超出了其在 UI 编辑时设定的最大范围的话如何处理呢 ?</p><ol><li>缩小 Lable 的字体尺寸</li><li>多余的子使用 … 代替</li><li>增大 Label 的高度</li></ol><p>我在 Quick-x 搞了一个很简陋的实现, 原理就是弄一个死循环, 判断尺寸超了就缩小一个单位, 但是效率不高, 就不贴实现了. </p><h2 id="3-阿拉伯语的适配"><a href="#3-阿拉伯语的适配" class="headerlink" title="3. 阿拉伯语的适配"></a>3. 阿拉伯语的适配</h2><p>大家都知道阿拉伯语的阅读顺序是从右往左的, 因此我们的 UI 最好也能是从右往左的. 但是除非从立项一开始就料想到了这一点, 否则更改全部 UI 是不现实的. </p><p>但是我们可以修改部分 UI, 如上面我们说的 <strong>横向排列的多个UI</strong> , 如果我们采用一个容器来实现的话, 那么很容易的实现容器内的元素顺序逆转.</p><p>我们按照这个思路实现了一个 BoxLayout , 在阿语下 layout 时会从元素的最后一个开始, 反向排版.</p><p>另外一个特殊处理就是 RichLabel, 我们自己实现了一个按字符遍历的富文本. 但是阿语下这个实现几乎变得不可用, 于是在阿语下我们使用普通文本来替换了富文本.</p><h1 id="三-外部工具"><a href="#三-外部工具" class="headerlink" title="三. 外部工具"></a>三. 外部工具</h1><h2 id="1-多语言转换工具"><a href="#1-多语言转换工具" class="headerlink" title="1. 多语言转换工具"></a>1. 多语言转换工具</h2><p>json 作为程序读取的格式是没有什么问题的, 但是用来给翻译人员来翻译就很不方便了, excel 则是一个很不错的选择. 因此我们实现了一套 <code>json > excel</code> 和 <code>excel > json</code> 工具用来做这个转换.</p><p>同时, 为了能更高效的处理各种需求, 我们还有 diff, format, deduplicate 工具.</p><h2 id="2-多语言提取工具"><a href="#2-多语言提取工具" class="headerlink" title="2. 多语言提取工具"></a>2. 多语言提取工具</h2><p>我们的多语言有很多是配置在 excel 中的, 这些 excel 最终会转换为游戏的静态配置, 我们希望最终在游戏中读取的一个多语言的 id, 而不是一串文本. 这样做能够降低静态配置的文件大小, 因为我们的文本有很多是重复的.</p><p>因此我们实现了一个抽取工具, 能够为 excel 中的文本打上标记, 这样在转换的过程中就可以使用 id, 而不是文本了. 效果如下图, # 前是文本, 后面是 id:</p><p><img src="http://ww1.sinaimg.cn/large/7f870d23ly1fgp3n582xwj205902hgld.jpg" alt=""></p><p>我们用的编辑器 CocosBuilder , 很古老的一个 UI 编辑器, 因此也没有多语言的功能, 所以我们做了一个从 ccb 中提取多语言的工具. ccb 本身是一个 plist 的文件结构, 很多语言都有对应的解析库, 写起来很容易.</p><hr><p>参考资料:</p><ol><li><a href="https://zh.wikipedia.org/wiki/ISO_639" target="_blank" rel="noopener">https://zh.wikipedia.org/wiki/ISO_639</a></li><li><a href="https://www.zhihu.com/question/20797118" target="_blank" rel="noopener">https://www.zhihu.com/question/20797118</a></li><li><a href="http://www.gameres.com/thread_480715_1_1.html" target="_blank" rel="noopener">http://www.gameres.com/thread_480715_1_1.html</a> (前辈的踩坑指南, 推荐阅读)</li></ol>]]></content>
<summary type="html">
<p>其实本来我的标题是 “Quick-cocos2d-x 中的国际化与本地化”, 多语言虽然是其中的主要内容, 但还有很多额外的工作.</p>
<p>比如: 在韩国上线的游戏必须在游戏第一次启动时弹出一个内容十分长的用户协议, 用户同意后方可继续游戏; 比如很多赌博性质的活动(抽奖, 拉霸, 转盘)都需要修改为其他表现形式. 还有一些技术方面的要求比如游戏用户的数据不能存在 cache 目录下等等.</p>
<p>每个国家和地区的要求都不尽相同, 有的是硬性的法律法规要求, 有的则是照顾到当地风俗习惯以提高用户体验. 当然, 这些并不在我们这次讨论范畴之内, 等我们的经历足够丰富之后可以再次和大家分享一下.</p>
</summary>
<category term="Quick-Cocos2d-x" scheme="http://blog.justbilt.com/tags/Quick-Cocos2d-x/"/>
<category term="i18n" scheme="http://blog.justbilt.com/tags/i18n/"/>
</entry>
</feed>