2021新的沉迷

这次是Gunpla,我在2021年终于被疫情逼成了一个胶佬。

老婆问过我为什么把拼模型的人称为胶佬,不是塑料模型吗?我也说不上来,大概以前都叫塑胶模型吧。话说回来,小时候玩得模型还得用胶粘,现在都是榫卯结构,不需要任何胶水了。所以干嘛还叫胶佬呢,是模型人,模型人才对嘛。

Anyway,今儿我并不是想说这个,忙着拼胶的我只是想记录下自己收集的某系列钢普拉的演变史,也就是SD版的高达《BB战士》系列和《SDEX》系列之间的关系。

先说说SDEX这个系列。因为我从机战游戏入坑,并不反感SD比例的高达,而SDEX去年刚出了牛高达和沙扎比,还特别便宜,我就一口气都入了。从16年到现在,它包含了17个机体,收集的时候没遇到什么困难,价格基本都带折扣。

说到这儿就比较奇怪了,疫情开始之后各个钢普拉系列都涨价,最高级的PGU一机难求。为什么SDEX还能有折扣呢?当然,也并不是每台SDEX的机体都在折扣,系列中的10号和14号,巴巴托斯的第四形态和天狼座形态,我这儿就只有高价转卖屋的货。

起初我以为是巴巴托斯人气高,毕竟上一部还算有口碑的高达动画,也就是《铁血孤儿》了,主角机体自然人气高。但后来研究发现不对,价格高仅仅是因为缺货,缺货说明万代懒得生产。接下来我在dalong.net看机体截图的时候,才发现大龙的评测里写到(感谢谷歌翻译):SDEX的10号机就是BB战士401,而14号机是BB战士402。

根据这个信息我去查贩售网站,立刻就发现BB战士这俩完全不缺货,也是打折状态。于是代替SDEX我入了BB战士的这两款,也算是间接的避开转卖屋把这个系列收集齐全了。

SDEX的官方定位是价格低廉,可动性很好的SD般高达。做的时候发现SDEX的板件很简陋,而网上也有很多骂声。我个人感觉除了分色是硬伤,细节刻画和可动性确实不错,非常适合拿来练习涂装。涂装先按下不表,因为收全了这个系列,我就好奇了起来:SDEX真的像网上所说的那样吗?于是我决定考证以下两个问题:

  • SDEX的分色比BB战士差很多吗?
  • SDEX的可动性好于BB战士吗?

TLDR,先说我的考证结论:两个答案都是否定的。SDEX系列应该算BB战士系列的延申。它按照更现代的审美重新设计机体比例,可动性没啥提高(都用PC-303连接件),分色水平也没降低(600日元的分色本来就是这么差)。BB战士的高达系列机体快6年没更新了,哪怕算上三国那些改编作,也近4年没发新货了。SDEX在21年下半年还在出新作品,可以说是BB战士秽土转生了。

硬要说区别的话,BB战士包含高价机体,有的产品能卖到1000日元甚至2000日元以上,而SDEX全部都在600日元的档次。同价位的BB战士和SDEX并没有任何区别,贴纸/连接件/分色都是差不多的水平。具体分析待我慢慢道来……


首先是时间上的关系。

BB战士系列的构成一直比较混乱,最后一款机体编号已经到了412号,比HG系列还要多。不仅是因为很多机体出过2次,还因为它包含了各种正统高达动画和高达衍生动画的机体。我估计万代最初是想诱导玩家搞收集,给各种SD比例的产品都加了个BB战士编号。一直出到18年2月的412号《三国传》貂蝉之后,新的SD三国动画《三国创杰传》终于和BB战士彻底割裂,这个古老的系列自此断更了(至少到2021年再没更新)。毕竟现在的小朋友恐怕都不知道BB战士这沙雕名字的来历,BB弹这种危险的玩具,也早就不让小朋友玩了。

只看正统的高达作品,BB战士保持着一定的更新频率,每年总要出一些。最后的两款机体是在16年动画《铁血孤儿》放映时同步推出的高达巴巴托斯,也正是和SDEX重复的那两个产品。出完这俩之后,BB战士的高达机体就停更了,距今快6年了。

再看SDEX,它是面向海外先展开的系列,其他国家15年就开始卖了。但是日本在16年才开始贩售这个系列,属于出口转内销。第一款在日本贩售的SDEX就是《巴巴托斯第四形态》。可以说从日本的角度来看,SDEX是BB战士的延续:16年以后就BB战士没有了,接替它的SDEX开始负责发布SD比例的高达。

考证到这里就很有趣了,16年出的SDEX巴巴托斯,说明书上印着SDEX-10。万代为了和动画放映同步,在海外跳过09号产品,先出了10号产品。在日本就更乱来了,居然第一款就是10号产品。所以日本市场上最早开始贩售SD比例铁血孤儿机体,是SDEX-10号巴巴托斯第四形态。

明明是10号,却率先发售了

在2个月之后,万代推出了《BB战士401巴巴托斯DX版》,和SDEX-10区别在于附送了1~6形态全套外甲。估计那会儿已经购买了SDEX-10的消费者都很崩溃,这也是该产品没有再版难以买到的原因之一吧……

DX就是deluxe的缩写,在日本一般表示豪华版,也就是多几套皮

同样的玩法在《铁血孤儿》第二季播出的时候又来了一遍:为了和动画播放进度同步,万代把中间的09/11~13全都跳过了,直接发售了SDEX-14号。过几个月推出《BB战士402巴巴托斯天狼座DX》,依旧和SDEX-14一样,只是附带了可有可无的机动工兵小车等。

这就是为什么SDEX-10和SDEX-14现如今买不到:它们推出得最早,而万代后来只再版了BB战士401/402,毕竟这俩价格更贵更赚钱。600日元的SDEX是BB战士缩水版,肯定不好卖,也就懒得出了。

接下来说说板件上的关系。

前面我说了BB战士的401/402号,和SDEX的10/14号板件一模一样,那是字面意思上的一模一样,连板件的标签都是一样的,重复的板件都写着SDガンダムバルバトス。这个标题明显是BB战士系列的用法,并且DX字样只在多出来的板件上才有标记,说明一开始就是这么设计板件的。我猜测开发这俩系列的部门本来就是同一波人,当SDEX准备在日本发售的时间正好赶上《铁血孤儿》开播,BB战士又要上新,于是就让两个系列直接共享了板件。

大概BB战士的大头形象已经过时了,新的SD比例更纤细,符合现代审美。而且新技术下的组件细节会更多,刻线丰富。所以我猜BB战士本来就要进化,进化的结果就是以SDEX的招牌重新包装。而401/402可能就是最后的BB战士了……(希望未来的更新打我脸)

玩家嘲讽SDEX分色差,官方吹嘘SDEX可动性高,看板件就知道这俩说法都是错的。BB战士系列一直存在600日元的廉价产品,随着通货膨胀的加剧,600日元能承受的板件数量越来越少,这价位的分色早就变差了。而BB战士包含更高价格的版本,导致一些玩家脑海中只记得高价产品的优良分色。如果放到同一个价位,近几年的产品分色水平很相近——所以要怪就只能怪日元贬值了。

BB战士398号,一样得靠贴纸补色,600日元要什么自行车!

再说可动性,早在2011年,BB战士推出366号飞翼高达EW的时候,开始使用全新的编号为PC-303的连接件,用于活动的关节部分。后续几年所有的BB战士高达产品,除了OO高达7剑/独角兽2号机/新吉翁等这种老机换皮产品,全都在用PC-303。而SDEX系列,所有机型也在使用PC-303,二者可动性上没什么显著的变化。

SDEX-01元祖高达继承了PC-303连接件,倒是偷胶更值得吐槽……

BB战士系列中,上一套被大量复用的连接件的编号为PC-300,最后一次露面是在365号新安州身上。可以说从这台机体之后,SD高达就全部都换PC-303了,大概比较省钱吧。直到18年万代又新开坑了SDCS这个系列,一台机体N套骨架用于骗钱,SD才有了可动性的提高,不过这就是其他话题了。再看更老一点的机体,360号的独角兽的可动连接件叫做PC-7,更加简陋。但是独角兽家族的重点在爆甲,可动性差一点也无妨。

BB战士在366号之后,万代陆续发布了很多使用PC-303的高价位产品。这类高价产品的可动性不会比SDEX的600元产品差,官方大概只是拿SDEX和同机体但是设计时间更老的BB战士做比较,例如329号元组高达等,才能得出可动性更强的结论。SDEX去年出了牛高达和沙扎比,BB战士系列中这俩机体用的也是PC-303,可动性(我猜)没太大区别,只是分色/价格有区别而已。


扯了这么多,还发在Blog里,是琢磨着万一有缘人搜到我这个贴,看完后可以放心的买SDEX。这系列很便宜,而且也没人买,八折以内就能搞定。因为是新货,刻线和细节都很足,手涂喷涂都很适合出效果。再说了,到手价四五百日元的东西,涂坏了不心疼,再怎么涂也比素组漂亮,极大的提升了我的涂装自信。

至于巴巴托斯,BB战士的402不如401好玩。但如果只是为了涂装,能买SDEX就买,像我一样买不到SDEX的话,用BB战士凑一套也挺好,抵制转卖屋从我做起。

之后如果还能想起来更新,就慢慢发一些自己涂装的SDEX上来,也算留个足迹吧。

(感谢dalong.net的丰富资料,有标记的图片都来自dalong.net

2020新的十年

乍一看缺了2019年的内容,其实因为大部分记录都留在了推上。

而因为大环境变得越来越难,所以并不想在公开场合讨论什么了,都是旧的牢骚,没有任何事情变得更好。

今年就不写游戏记录了,得空专门整一篇。

还是记录下年初debug的收获,抓取了pixi.js的的WebGL调用记录,发现做简单的2d游戏用不了几个函数,这里也就47个而已。

它们是:

gl.activeTexture, gl.attachShader, gl.bindBuffer, gl.bindFramebuffer, gl.bindTexture, gl.blendFunc, gl.bufferData, gl.bufferSubData, gl.clear, gl.clearColor, gl.compileShader, gl.createBuffer, gl.createFramebuffer, gl.createProgram, gl.createShader, gl.createTexture, gl.deleteShader, gl.disable, gl.disableVertexAttribArray, gl.drawElements, gl.enable, gl.enableVertexAttribArray, gl.framebufferTexture2D, gl.frontFace, gl.getActiveAttrib, gl.getActiveUniform, gl.getAttribLocation, gl.getExtension, gl.getParameter, gl.getProgramParameter, gl.getShaderParameter, gl.getUniformLocation, gl.isContextLost, gl.linkProgram, gl.pixelStorei, gl.scissor, gl.shaderSource, gl.texImage2D, gl.texParameteri, gl.uniform1f, gl.uniform1i, gl.uniform1iv, gl.uniform3fv, gl.uniformMatrix3fv, gl.useProgram, gl.vertexAttribPointer, gl.viewport

分分类的话,大概是这么几种。

查询设备状态或扩展接口

gl.getExtension
gl.getParameter
gl.isContextLost

操作和设置属性

gl.disable
gl.enable
gl.blendFunc
gl.frontFace
gl.scissor
gl.viewport

清屏和绘制

gl.clearColor
gl.clear
gl.drawElements

操作vertex属性数组

gl.vertexAttribPointer
gl.enableVertexAttribArray
gl.disableVertexAttribArray

操作各种texture对象

gl.activeTexture
gl.createTexture
gl.bindTexture
gl.pixelStorei
gl.texImage2D
gl.texParameteri

操作framebuffer对象

gl.bindFramebuffer
gl.createFramebuffer
gl.framebufferTexture2D

操作各种buffer对象

gl.createBuffer
gl.bindBuffer
gl.bufferData
gl.bufferSubData

关于shader的接口

gl.createShader
gl.shaderSource
gl.compileShader
gl.getShaderParameter
gl.createProgram
gl.attachShader
gl.linkProgram
gl.getProgramParameter
gl.deleteShader
gl.useProgram
gl.getUniformLocation
gl.getAttribLocation
gl.uniformMatrix3fv
gl.uniform3fv
gl.uniform1f
gl.getActiveAttrib
gl.getActiveUniform
gl.uniform1i
gl.uniform1iv

除了shader部分比较繁琐以外,其他的都很简单,而shader相关的也可以很容易的封装起来。
但实际上由于opengl自带状态,从api调用的录制结果脑补当前场景,非常困难。
比如到底操作的是哪个buffer?究竟哪个texture是活动的?framebuffer的数据都是设置正确了吗?
再加上最好能自动合并渲染批次,所以pixi.js这样的库还是很有必要的。

说到多线程的Bug

刚才提到了多线程,想起来以前遇到的bug,记录下免得以后忘记了。

起因是一个小哥试图优化多线程的loading,在没看完其他代码的情况下,觉得应该在idle的时候把线程的priority降低,给其他更重要的革命工作让出CPU。

然后他没想到的是,loading会调用::Sleep(),结果就是idle之后常睡不醒,优先级太低了其他革命工作都忙得不行,哪有机会让loading出来说话。

他更没想到的是,其他革命工作是依赖于loading的结果的,毕竟革命也要吃饭,没有loading的数据啥也干不了。

所以结果就是loading被负优化,反而变得更慢了,整个游戏也变得更慢。老婆听我讲了这个bug之后做了个特别棒的比喻:

导游带一堆客人去景点,然后让客人自己排队入园。客人问导游你不进去么?导游说我去很多次了你们排着我在外面歇着。没想到门票都在导游手里,一堆人在门口嚷嚷半天谁也进不去,于是都等着了。

所以说优化要先做benchmark,脑补的优化基本都是负优化,没有benchmakr数据支持,你优化个毛线?这事儿我没少干过,就不多吐槽了。

但是必须强调下,文中的优化小哥可不是我,我是修bug的那个。俗话说的好,前人栽树后人乘凉,前人挖坑后人吃瘪。革命工作嘛,总是会有这样那样的问题,别抱怨了好好搬砖吧。

程序员的执念

昨天在看一个吉里吉里(krkr)Timer问题,开启Timer后,游戏会随机崩溃,报错说Timer已经被删除。

题外话,我个人是很讨厌Timer这种东西的。我见过很多糟糕的Timer设计,而且它容易被滥用。使用者会产生一种“我在做并行逻辑”的错觉,而且大部分设计者在设计有关Timer的逻辑时,并没有完全想明白。随口设计出诸如“延迟x秒发生这个,再延迟y秒发生那个”,当这些逻辑交织在一起的时候,无论是设计还是实现都会非常困难,edge case很多,最终导致产生不可预期的结果。

但Timer本身不复杂,对于krkr这样针对电子小说的游戏引擎更是如此。我随口说了句总不至于多线程吧?毕竟krkr内部的解释器是单线程的,所以Timer触发的逻辑肯定得在主线程里执行。但这样一来解释不了Bug的成因,为何会出现删除Timer后OnTimer函数仍然被调用,并且在OnTimer里面操作Timer的时候又说Timer已经被删除?我脑补了两个原因:

1、删除Timer不会终止它的OnTimer函数,这种设计是引擎缺陷。

2、OnTimer触发后就无法被取消了,这种设计也是有问题的。

脑补当然没用,看代码就好了。于是我翻出krkr的源码,找到Timer的实现,还真是多线程……然后就脑补了20种多线程的常见问题。等读完源码,发现逻辑虽然饶了几圈,但bug是没有的,大致上是这样的过程:

krkr首先启用一个单独的线程来处理Timer触发的逻辑,它通过::Sleep来控制Timer的等待(很不精确,但省CPU)。被触发的Timer通过PostMessage传递给隐藏Win32 HWND,然后在独立的UtilWndProc执行逻辑。因为UtilWndProc是由主线程创建的WindowProc callback,所以和解释器处在同一个线程。在这个消息处理函数中,Timer逻辑被丢给了自带的Event模块,Event模块执行实际的OnTimer。krkr保证了如果一个Timer被删除,它在Event中注册的所有OnTimer事件同时也会被删除。已经在等待队列的Timer,检查到自己被删了就直接return,什么都不会发生。

我个人觉得这个Timer实现的太复杂了,可能20多年前CPU宝贵,krkr没有一个持续tick的函数,只能通过多线程来实现吧。而windows里这样传递消息也是krkr固有的模式,其他人提到过:http://bbs.niratea.org/test/read.cgi/krkr/1323994344/389

总之我假设的两个问题都是不存在的。这样一来就是上层逻辑的问题了,于是仔细看了看,发现了真正的原因(WTF,为什么一开始不看?):

1、删除一个krkr对象,并不会自动把对象引用置为void。这个操作非常c++,但对于自带gc的脚本引擎来说还是挺猎奇的。这个错误的假设造成了一些逻辑问题。

2、Timer被多次创建,且保存在parent的成员变量上。这样新创建的Timer会导致旧的Timer引用丢失,而gc并不会第一时间删除这些timer。一旦新的Timer被删除,旧的OnTimer函数被触发的时候,旧的OnTimer函数里通过成员变量访问的“自身”已经是新的Timer,所以会出现问题。

修复很简单,每次创建新Timer前记得删除旧的即可。

这个问题其实可以通过引擎的设计来避免,毕竟用户可能会犯错。OnTimer如果能传入自己,用户就没有必要通过额外的方式保存Timer的引用,这样OnTimer可以保证操作的timer对象肯定是有效的。用户即使重复创建Timer,造成的结果也无非是Timer被重复触发,而不是抛出异常。另外带gc的脚本引擎提供删除功能真的好么?20年前内存大概也非常宝贵吧。

当然,正确的使用Timer才是解决问题的根本。不过我之前就说过,Timer是个容易被误用的模块。尤其是游戏,大部分基于Timer的设计逻辑都是经不起推敲的(个人看法),能少用就少用吧。

Unity的美好与绝望

先说好的吧。可能目前最令我满意的就是IMGUI这部分API的定义,写Editor Plugin和In-game Debug Menu爽到飞起,很快就能出效果。

UI一向是很麻烦的东西,要保存状态,还要异步,还要同步View和Model的数据。很多框架动不动就是MVC,各种模式,用起来都痛苦的一比。这个IMGUI非常简单粗暴,反正也不能用来开发更复杂的玩意儿,设计和使用契合度非常高。

目前用到的其他特色的设计,比如Component Based,和别的商业引擎比起来大同小异。当然从价格和可用性的角度,Unity更胜一筹,以后有机会再多扯点。

主要还是吐槽,就说点看似美好其实比较坑的东西吧。

官方文档

全还是比较全的,这个我主要吐槽两点。

第一是组织方式,有些很重要的东西(比如对象的生命周期),或者说Unity和其他引擎比很不一样的东西(例如特殊目录,Editor/Resources/),隐藏在犄角旮旯等着你来发现。基本上都是在别处找到更直接的教程,然后拿到关键字,再到官方来看细节。

第二是官方真喜欢视频教程啊,不能搜还看得慢,动不动20分钟说点屁事。不能写点图文教程吗?比如IMGUI的各种用法,第三方写的比官方好用多了。

资源加载

三种方式,内在逻辑截然不同。

以Editor内操作为主,基于Reference的这套可能比较适合小游戏,或者非程序员开发的游戏。只要一切资源都是经过Editor操作和绑定的,那么后续过程也都是自动的。官方强力推荐这种,美术和策划友好的东西。

所有不能在Editor上绑定的数据,都需要Load-on-demand。一但牵扯到Load-on-demand,需要动态加载数据,事情就来了。

其实加载资源(Loading)是非常简单粗暴的操作。储存方面就是打个包,把数据和标识存起来。读取方面,只要给定一个标识(比如路径或者id),然后决定是同步加载还是异步即可。同步立刻返回结果,异步就给个handle或者允许callback。这样的底层设计几乎可以满足所有基本需求,其他花哨的玩意儿再上面继续包装即可。

但Unity的坑无处不在。首先是打包,Unity有两套截然不同的资源管理方式,分别对应Resources目录下的文件和Asset Bundle,姑且称为R和A。

R的打包是自动的,所有此目录下的文件都会在运行时打包,方便的很。这么好的东西,官方给出的Best Practice居然是“别用”。写文档的朋友,你以为自己很幽默吗?不能用的东西做出来吃屎呢……

A的打包是手动的,只不过没有现成UI可用,需要在Editor里调用API。但官方文档写的稀烂,打包和加载的过程同DLC的资料搅合在一起,还要牵扯到Cache和Server。乍一看仿佛要启动什么web service做host才能使用,打开还得从WWW下载。其实只要StreamingAssets目录一放,自己写脚本打包就好。

别忙,坑还没完。

R打包出来的资源是不带扩展名的,因为自动打包的时候就是根据扩展名来的,打好就全给删了。加载的时候遇到重名,请提供type。再出现重复我就不知道要怎么玩了……R的路径是相对于Resources的,这个倒还能理解。

A打包出来的资源,标识是写入的时候给定的数据,如果写的时候带了扩展,那就是有扩展名,目测不能重复。

所有“测试的时候想用R,发布的时候想用A”的同学,请自己写wrapper吧……真的好痛苦好痛苦。实际上更好的做法是,测试的时候直接读取Editor里面的AssetDatabase,并不需要使用Resources。官方说的“别用”就是字面意思,但您能提示下还有别的选择吗?直接介绍大家用AssetsBundle真的很不友好啊。

序列化和热更新 Hot-swapping

Unity一大特色就是各种自动,写个public直接就能在Editor上改了。而且改改c#,一回头自动把新的dll热更新上去,数据都还在。

听着很美好,实际情况是,改完代码出现大量Null Reference的异常……

https://gist.github.com/cobbpg/a74c8a5359554eb3daa5

这个小哥大概记录了一下,我踩了半天坑再补充一点:

1、首先是ISerializationCallbackReceiver,调用时机很诡异,Editor下如果你选中了带有这个接口的对象,Inspector会疯狂触发OnBeforeSerialize()。存起来很慢的东西就悲剧了。而OnAfterDeserialize()在Swap的时候会触发多次,目测Editor有创建多个实例,有些数据是没准备好的,还得自助检查。

2、在Editor里创建的各种引用,例如Component都会存下来。但如果动态AddComponent出来的玩意儿就悲剧了,在OnBeforeSerialize()之前就会被干掉,请自己重新动态创建。后来证明只要是Unity的Component,引用都会正确的保存下来,核心的混乱点还是Editor里有多个实例,初始化状态各不相同。

3、虽说需要动态创建,但特么的所有AddComponent这类方法,甚至GetTime都不允许在callback种调用。大概恢复数据的时候是多线程环境?所以只能自己搞个boolean做dirty check,然后在Update()里面实际恢复数据。实际上参考官方的例子,可以在OnEnable里面做恢复处理,比Update要省。

4、虽然官方号称大部分数据类型都可以自动保存,但“大部分”这种说法就意味着你需要被干很多次才能修复“少部分”的问题。比如标记为Serializable的数据,运行时可以为null,但Swap的时候会变成non-null状态。例如字符串变成了””,结构体变成了初始状态,最坑的是Array,里面的null元素会被填充。如果有逻辑依赖于null-check就悲剧了。

以后遇到新的再补充。

补充个新坑:打包的时候如果用BuildAssetBundleOptions.None这个option,看似人畜无害的默认选项居然是用LZMA压缩整个包,第一次开启会做解压缩操作,慢的一笔。最好的方式是使用ChunkBasedCompression,这个选项开包很快。港真我不知道None这个选项除了给网络下载几件衣服的DLC用以外还有什么应用场景,官方文档用这个名字(None是压缩的,咱还提供UncompressedAssetBundle选项哦~),并且用在代码范例真的好么?

2018第一篇总不能是忏悔吧

唉,还是得忏悔。

最近半年玩了600小时游戏的我实在太那啥了……当然代码也写了不少就是了,只能说肝功能严重受损吧。

命运2虽不是那么好玩,三个多月每天肝也搞了160小时。多人游戏果然有毒,凑起伙儿来就停不住。(为将来的MHW埋下伏笔)

后来120小时白了DQ11,感动得不行。DQ可是咱入坑游戏的系列,我要是有空真得写个千八百字的来诉诉衷肠。

华纳的中土世界不对胃口,强行玩了16个小时还是放弃了,还不如AC(这算夸奖么)

巫师3,Dishonored和地平线资料片都随便玩了一两个小时,权当填充空白。

值得一提的是,暗黑3的资料片终于让我给打穿了。前后花了将近20个小时,一路上各种捡暗金装备还是挺爽的。但剧情太弱了,关卡也很弱,有种自动生成的感觉(此处命运2的资料片可以一战)。打完发现我没选赛季人物,直接GG。

好了,最后要说的就是那个了。对的,就是那个。(BGM响起)

《怪物猎人世界》在1月26日发售了!

接下来的一切都发生得如此迅速,等我回过神的时候,已经是3月14日了。在这一天,我成为了苍蓝星,整个大陆的猎人都向我发来祝贺。

\(T__T)/

以后的日子大概需要加速填坑吧——如果岚龙崩霸煌黑没有DLC的话。

BTW,倒是真的弃用国内SNS了,不想打个字还得斟酌半天,累人。

日后尽量写点Unity的使用笔记,还有Javascript的踩坑记录,看时间和心情吧。

2017年底赶稿

一般来说这种事情不会发生,那就是我整整两年都没更新blog……

其实16年研究了不少新东西,17年也是。然而没写blog就导致没有记录,心得体会也就难以保存下来。脑子是个好东西,但时间的力量更加强大,上了年纪的我不能再靠记忆力啦。

这都怪weibo和wx这些碎片化的SNS黑洞(好像之前谁说要弃用SNS来着?)

总之,16年到17年的变化还是蛮大的,物理上移动了很长的距离,生活节奏也变得完全不同。16年玩得最多的居然是掌机,通关了火纹if和皇骑前传。主机方面,跟着太阳战士毫无意外的白了黑魂3之后就放下了,直到2017年PS4的爆发。仁王、地平线、FF12以及命运2吃掉了我大量的业余时间,都是值得一玩的作品,让我重燃了对AAA的信心。

悲剧的是因为办公室和家里各有侧重的缘故,PC平台被基本搁置了,勉强有命运2撑场面。好像随着生活方式的变化,主机+电视变得更加合适,至少可以一个人玩一个人看。

好多年不怎么动弹的我,因为这一圈跑动,主动见到了不少很久没见的朋友。也因为新居住地是旅游胜地,我被动的见了不少朋友。作为社交懒惰症患者,这也挺难得~

还能说啥呢?对2018(的怪物猎人世界)充满期待吧……

2016迟来的忏悔

根本没有弃用任何SNS,有没有!!整一年,什么都没写。

2015超级超级忙,项目的事情就不多说了,都是辛酸泪。但Steam上总算搞出点花样,感觉不错。年底各种折腾装机,Linux,跨平台编译,树莓派等等,玩得杂,忘得快。

当然,这些内容我慢慢补。历年的传统,先谈谈玩了什么游戏。

去年爹妈来过年,帮我稍了块3DS烧录卡,于是乎终于可以玩跨区和汉化的游戏了。通关《勇气默示录》和《逆转裁判》之后,本以为《怪物猎人4》是中文版,有希望继续玩下去,但马赛克太重还是放弃了。妄我买了两份卡带和两份主机。

后来忙得只有碎片时间,就主要攻略炉石传说了。AAA当然不会错过,巫师3让我换了GTX960,然而并没有通关。尝试了Bioshock Infinite,也没有通关。但同样是突突突的Call of Duty-MW3就很快通关,可见5个小时才是最合适的游戏长度。

接下来在华丽丽的九月,各路大作隆重登场,然后我基本上就沉迷了。

首先一口气通关了合金装备3、4、5,还看了各种历代回顾,写了万字长文。为此购买的豪华版游戏和主机简直让我大出血!但PS4绝对值回票价,因为后来玩了上百小时的Bloodborne,最近还白金了。人生第一白,献给这么重口的游戏,我觉得我的性格已经被改变了。

天涯明月刀有很多老同事参与,我也玩了玩,画面不错,其他不想多说。PC上的怪猎OL移植版终于公测,风评不好,不敢试,遂作罢。

去年没怎么多玩Indie。Her Story是个非常妙的作品,深得我心,5小时通关。BattleBlock Theater也不错,玩着挺逗的,然而并不能让我不停的玩下去。Transistor让人眼前一亮,可惜去年AAA攻势凶猛,都统统搁置了。PS4会员给了好多Indie,但只有个潜行的动作游戏我稍微玩了玩,关卡太难弃了。

今年会继续推Dark Souls,并不准备完美通关,太累人了。这种hardcore游戏,体验体验就好,不能耽搁填坑大业。

是的,今年一定要填坑,说什么也不能拖了。

心宽体胖

上午空着肚子研究了半天css和php,把blog的模板重新规划了一下:

  • 页面自适应各种分辨率,这样才对得起我的2k显示器。
  • Archives显示最近50年的文章链接,估计是够用了。
  • 时间显示精确到分钟,大幅缓解了我的强迫症。

最终感想:好饿。

Calling Convention

之前并没有很在乎这方面的事情,直到最近需要在某个bcb编译的程序中集成steam sdk。

故事是这样的,steam sdk提供的接口是c++代码,官方号称支持visual studio和gcc两种编译器。但我不想先做一层包装转换成c的接口再使用,所以打算强行在bcb中链接这些lib。

现在问题来了,由于c++的abi很混乱,微软又喜欢玩花活,一旦接口是c++的格式(class, virtual table)就很微妙。这就牵扯到了calling convention的事情,也就是函数调用约定。这件事情本来很简单,在c的时代只要解决两个问题就好:1、参数个数是不是固定的。2、参数是如何被放到堆栈中的。第二个问题无关痛痒,所以c规定参数一律是从右到左压入栈中,这样用的时候就可以从左到右依次弹出。

所以calling convention只和第一个问题有关,这样就诞生了两个关键字:

__cdecl:支持不固定个数的参数,调用方caller负责清理堆栈。好处是printf之类的东西可以实现,坏处是函数内部不能对堆栈玩花活,因为管理权在外面。

__stdcall:参数个数固定,被调用方callee负责清理堆栈。好处就是函数结尾直接用ret xx解决所有问题,坏处是参数个数固定,因为编译期就得决定ret之后的字节数。顺道说,COM中所有调用都是__stdcall。

目前为止一切还是有序的,直到link的时候,出现了name mangling的问题。M$和Borland使用了截然相反的方式来修饰函数名,具体可以参考《Using Visual C++ DLLs with C++Builder》中的Table A。还好双方都提供了解决方法,任选一种就可以生成合适的符号名,可以愉快的link。

即使link没有问题,问题也还远未解决,一旦开始使用c++的接口,run-time的问题就浮现出来。这就要提到M$的发明:

__thiscall:用于non-static member function,会把this放入ecx,再调用函数。好处就是this指针非常容易找到,调试方便。坏处就是……这是M$的发明,而非标准。还好时至今日很多编译器都支持了__thiscall,巨硬不再孤家寡人。

实际上bcb对this的解决方法非常简单,就是把它当作第一个函数的参数,照样传递进去即可。调试时候也不见得有多麻烦,而且如今编译器对代码的优化非常风骚,ecx在入口可能还有用,进入函数体之后几乎就完全无用了。

__fastcall:时代的眼泪,从名字看很拉风,但定义特别复杂。具体就是说把前几个较小的参数从寄存器里传入,而不是压入堆栈中。这样调用双方省去了很多进出栈的操作,故名fast。时至今日,这些开销似乎没什么人在乎了。

其他的调用约定我没怎么看就不说了,手头的情况只和后两种有关。这个bcb的程序诞生于近20年前,作者大概觉得函数调用开销是一件很重要的事情,所以默认全部函数都采用__fastcall。而steam sdk的c++接口全部用默认的__thiscall,运行的时候caller把参数往寄存器一丢了事,而callee还苦苦在ecx和堆栈中寻找,代码就跑飞了。

解决方案非常悲剧,对于每个class的每个non-static member function,都提供一个静态函数作为wrapper。内部帮他们把参数位置转换一下,然后再调用sdk中的实际代码。

用macro封装的效果如下所示(1个参数)

#define STEAM_API_CALL_WRAPPER1(ret, type, interface, index, var1)	\
ret STEAM_API_CALL type##_##interface(type * thisptr, var1) {	   	\
	asm { 						\
		MOV eax, [ebp + 4 * 3];	\
		push eax;				\
		MOV ecx, thisptr;		\
		MOV edx, [ecx];			\
		CALL [edx + 4 * index];	\
	} 							\
}
//

使用之前必须逐个定义,还得注意虚函数在虚表中的位置。不幸中的万幸是bcb和vs的虚表排列都是一样的。万幸中的不幸是,steam sdk中居然还有头文件和编译好的lib中虚表里函数顺序不一致的情况。大概是某个程序员手贱没事就调整函数的位置,又不肯重新编译sdk吧……

STEAM_API_CALL_WRAPPER1(bool, ISteamUserStats, SetAchievement, 7, const char *pchName);

经过这么多折腾之后,终于成功在bcb中链接运行了steam sdk。接下来发现还有callback的问题,steam要调用从bcb程序里传入的函数指针!解决方法和上面类似,callback函数也需要有一层wrapper……

只要对所有使用到的接口做上述处理,最终一切都和谐美好的运行起来,完全没有使用额外的dll,直接在bcb中使用了steam_api.dll。我研究了一下网上已有的实现,都是采用包装一个wrapper dll的方式。我这边的方法相对安全一些,否则wrapper可以被轻松的替换掉。当然咯,steam有提供额外的drm措施,简单换掉wrapper并不能移除steam绑定就是了。所以如果不是像我这样有强迫症的话,写个wrapper dll是最简单直接的方式。

结论是如果以后需要写什么对外发布的sdk,还是以c接口的形式提供最具有兼容性。