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吧……真的好痛苦好痛苦。

序列化和热更新 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()之前就会被干掉,请自己重新动态创建。

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

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

以后遇到新的再补充。

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接口的形式提供最具有兼容性。

弃用各种SNS

鉴于没有多少时间可以被杀掉,我决定弃用各种SNS。
前段时间因为搬家折腾了五六周,现在终于安顿下来,又要准备开始过年。

先把自己的工作环境整理好,接下来就是工作时间。
坑已经挖了很多,必须赶紧填起来……

现在问题来了:哪个坑的优先级比较高呢?

2015华丽开场

本来想1号贴一篇的,可惜新年搬了一次家,错过了……

老样子,先写去年的关键字:HoS,Titanfall,Torchlight2,FF14,Call of Juarez Gunslinger……当然还有:炉石传说,Risk of Rain,以及我用150+小时几乎完美通关的《Dragon Age Inquisition》。

DAI不枉我等了好多年,B社这次十分给力。下一个期待点是《Witcher 3》,看这跳票的节奏应该不会是个雷。

14年后半年逐渐退出了所有的投机活动,感觉浪费自己的时间,还会扭曲价值观(严肃脸)。没想到年底遇上倒霉的事情,浪费了不少钱,真是瞎折腾的一年。

不过14年算是混到了一个credit,可惜上头不给力,把口碑弄坏掉了。接下来又是自研项目,先看看会搞成这么样子吧。反正已经对AAA厌倦了,一切都是浮云,搬砖不分高低贵贱……

恩,就这样吧。

Photoshop中的Multiply Blend Mode

Multiply是PS中常用的一种混合方式,中文版称为“正片叠底”模式。
说起来非常简单,就是颜色分量相乘。但实际上简单的相乘并没有考虑到alpha channel。
如果两个图层都是RGBA四个通道,Multiply的算法还得加入alpha。

为了搞清楚这个问题,我琢磨了好几个小时。最后从颜色含义入手,配合一些测试结果,终于推出了PS的混合公式:

OutAlpha = A1 + A2 - A1*A2
OutColor = (1.0 - (1.0 - C1)*A1) * (1.0 - (1.0 - C2)*A2)

可以看出这个公式对于A1和A2是对称的,所以被混合的两个图层没有前后之分。

推理过程如下:Multiply混合可以看作是两张幻灯片叠在一起,因为变得更厚所以透过的颜色变少,从而达到减色效果。在这里颜色分量Color是光可以透过的程度,1.0即表示该颜色分量全部可以透过。1.0-Color表示被吸收掉的部分。在PS的混合算法中,Alpha用来调节吸收程度。因为Alpha经常用于表示透明度,1.0表示完全不透明,即吸收程度为100%。

结合以上两个定义:

(1.0 - Color) * Alpha // 表示被吸收的颜色
1.0 - (1.0 - Color) * Alpha // 表示最终透过的颜色

把两个最终透过的颜色相乘,即得到我们需要的最终减色结果。

在忽略Alpha只有RGB混合的情况下,Alpha都取1.0,则公式可以化简为:

OutAlpha = 1.0
OutColor = C1 * C2

结果为颜色分量直接相乘,也就是到处都可以找到的对Multiply的算法的描述。

以下是c#版的实现……

static byte Clamp(int result)
{
    if (result < 0)
        return (byte)0;
    
    return result > 255 ? (byte)255 : (byte)result;
}

static byte MultiplyColor(int lhs, int rhs, int lhsAlpha, int rhsAlpha)
{
    if (lhsAlpha == 0)
        return Clamp(rhs);
    else if (rhsAlpha == 0)
        return Clamp(lhs);

    int lhsMultiply = (255 - (255 - lhs) * lhsAlpha / 255);
    int rhsMultiply = (255 - (255 - rhs) * rhsAlpha / 255);
    int result = rhsMultiply * lhsMultiply / 255;
    return Clamp(result);
}

// same as Photoshop multiply blend mode
static public void Multiply(Bitmap lhs, Bitmap rhs, Rectangle roi)
{
    BitmapData lhsData = SetImageToProcess(lhs, roi);
    BitmapData rhsData = SetImageToProcess(rhs, roi);

    int width = lhsData.Width;
    int height = lhsData.Height;
    int offset = lhsData.Stride;

    unsafe
    {
        byte* lhsPtr = (byte*)lhsData.Scan0;
        byte* rhsPtr = (byte*)rhsData.Scan0;

        for (int y = 0; y < height; ++y)
        {
            for (int x = 0; x < width * 4; x+=4)
            {
                int lhsAlpha = lhsPtr[x + 3];
                int rhsAlpha = rhsPtr[x + 3];

                // multiply color with alpha factor
                lhsPtr[x + 0] = MultiplyColor(lhsPtr[x + 0], rhsPtr[x + 0], lhsAlpha, rhsAlpha);
                lhsPtr[x + 1] = MultiplyColor(lhsPtr[x + 1], rhsPtr[x + 1], lhsAlpha, rhsAlpha);
                lhsPtr[x + 2] = MultiplyColor(lhsPtr[x + 2], rhsPtr[x + 2], lhsAlpha, rhsAlpha);

                // also blend the alpha channel
                int retAlpha = (lhsAlpha + rhsAlpha - lhsAlpha * rhsAlpha / 255);
                lhsPtr[x + 3] = Clamp(retAlpha);
            }
            lhsPtr += offset;
            rhsPtr += offset;
        }
    }
    lhs.UnlockBits(lhsData);
    rhs.UnlockBits(rhsData);
}

2014驾到

这里确实已经变成年签了,好久不登录连密码都忘记了。

2013很丰富,通了很多游戏,更新了战斗装备,所以关键字一大堆。

有ROSE,树莓派,DQ,DAO,巫师,星际2,炉石传说……

还有悲剧的丢包,棒极了的圣诞游,难忘的歌剧,以及超萌的小猫……

年中开始做AAA,却很快产生了厌倦,无论去留都不完美,十分鸡肋。

作为互联网难民,翻墙的代价还真不小啊……

对未来就不期待什么突破了,2014能完坑我就很满意啦。