在日本写代码的日子(四)

吃完饭回到宿舍,那时候没有智能手机,笔记本2万多RMB一台。所以当晚应该是没有和家里通话,但是好像又报了平安,也许是投币电话吧,不太记得了。直接从壁橱拿出褥子被子,自己套上套子,铺在榻榻米上就睡了。

可能是累了,晚上睡得很好。但是起来有点出鼻血,也不知道是什么原因。

第一天的安排每个部门不同。我们VAIO的7个小本是上午先去昨天那个楼里见语言培训老师,然后下午各自到单位报到。按理应该穿西装,但是发现西装被打包到了海运的行李里。所以只好穿着牛仔裤衬衣硬着头皮去报道。同行的其它小伙伴都是西装,包括上海Lady,西装短裙,挺好看。

培训老师是一个带金丝眼镜的老太婆,头上收拾得清清楚楚,头发拢得干干净净,衣着得体,看上去就是受过很好教育的那种。她笑眯眯地给大家做了个简单测试,摸了一下大家的底。她对我的评价是可以像日本人那样直接用日语思考,因此说话比较自然迅速,但是基础还不太牢。

中午再次吃完盒饭之后,去工作单位报到。那时的VAIO在品川站西边御殿山Hills所谓的2号馆,也就是索尼最老的一栋楼里。54年还是56年造的。因为日本地震频繁,所以这么老的楼做了很多加固措施。所谓的加固措施就是在房子的外墙和过道里安装很多2、30厘米宽的角铁,形成三角形架子。这些铁架子就这么暴露在外面,所以整个楼其实看起来如同停车库的铁架子那样,很难看。而且里面的空间也很狭小封闭,窗户不多,很像防空洞。

我们VAIO的7个小本加上2号馆旁边研修会馆里面上班的几个研究生首先被集中在一间会议室里进行入职前人事的最后说明。过一会儿门口就来了很多30后半40前半的女性。后来知道这些是各个部门的庶务,也就是国内公司所谓的行政人员。日本女性的就业率是呈M字形,在20多岁一个高峰,40前后另一个高峰。中间就是育儿休职的时间。休职期间有一定的收入,但是复出时基本大部分已经跟不上原来职位的需要,所以只能做庶务。这些当然都是后来慢慢了解到的。

当时很快眼光就停留在一个看起来比较年轻的女性身上。倒不是说有多么好看(虽然在在场的几个庶务里面是属于比较好看的),主要是她的举止看起来特别优雅的样子。说话很小声且面带微笑,手指并拢指尖向上挡住嘴,然后微微点头哈腰。感觉很有修养的样子。反正当时我就觉得这肯定就是传说当中的女校毕业生。

人事说明完成之后,门口的庶务就进来领人了,感觉就好像等在幼儿园门口的家长进来接孩子一样。这时我发现刚才一直在看的那个姐姐径直向我走了过来,原来她就是我所在部门的庶务。难怪我看她顺眼,哈哈😄

我跟着她到了自己所在的部门,看到了面试我的部长。然后部长带我去见了课长,课长又带我去见了系长(就是组长),组长叫来了一个30多岁看起来挺老实的日本人,跟我说这是你的Tutor(就是带新人的师傅)。

寒暄之后,知道Tutor姓福田。但是日本叫福田的人也不少,所以就把他的名字“岳士”当中的第一个音节拿出来和姓拼在一起。本来福田的日文发音是fukuta,现在变成了fukutake,这样就好区分了。

然后,组里面已经有了两个中国人,就是01/02过去的。一个姓沈,一个姓马。姓沈的是男的,姓马的是女的。因为“沈”在日文当中的发音与“陈”相同,所以课长就问我有没有nickname。我说那就取个简单的,叫Tim好了。于是乎在社内这个名字就一直用到现在。

Tutor和我说,傍晚在顶楼食堂安排了一个欢迎会,整个VAIO的人参加,来看我们这7个新人。届时每个人要说几句话,所以他就和我练习了一下,帮我修正了一些语法和习惯表达。

很快到了傍晚,大概7点左右吧,我再次跟着庶务,到了顶楼食堂。食堂里乌压压坐满了好几百号人(也许更多?),然后我们7个人上台列成一排,每个人说几句话,基本就是叫啥名字,在啥部门,4649(yoroshiku,请多多关照)。我可能说得稍微多些。比如什么很高兴能和你们这些才华横溢的工程师一起工作,学习先进技术之类的。

接下来是每个人的Tutor代表公司赠送一台VAIO给我们当做见面礼。这个机器貌似是送给我们的,但其实也就是我们后面日常工作使用的“生活机”。VAIO里面的传统是每个人至少有两台机器,一般一台笔记本一台台式机。笔记本主要用来跑outlook、office,也就是日常工作当中的收发邮件、写文档写会议纪要,以及访问公司内的各种IT系统,比如考勤系统、薪资系统、报销系统等使用。这台机器被称为“生活机”。日语原文是叫“生活Machine”。而台式机一般作为开发机,即“开发Machine”。

所以,后来我离开VAIO的时候,是把这台机器归还給公司的。虽然貌似是送给我了的。

还记得我前面说的没带西装的事情了么?台上7个人,就我穿着牛仔裤衬衣。当时感觉有点囧,没想到下台之后Tutor和我说就我最理解VAIO的文化,穿得休闲。我开始还以为他是给我解围,后来发现的确其它人都穿得比较随意。于是乎对于这家公司又多了几分好感。

当然,后来我甚至穿着大裤衩光着脚在办公室里跑来跑去,被课长拦下来友好地提醒了一下。所以也并不是没有规矩的。只不过,每天在品川站上下班黑色的人群洪流当中,如果你看到一些穿着像学生背着双肩包但是年龄显然不是学生的人,那么很大概率是索尼的人。这足以说明这家公司在日本的“outstanding”

在日本写代码的日子(三)

0月15日很快就到了。因为不想看到有人哭,我没有让任何人送我。

北京的20多人因为回去准备行李,是从北京坐CA的航班。上海则是MU的。时间上被安排的基本上差不多时刻到东京成田,北京的大概早30分钟的样子。

然而在国内机场,还是有不少离别哀愁上演:不是我的,而是那些研究生和他们的女朋友。这时我心里一种莫名的优越感升起,因为我没女朋友。∠( ᐛ 」∠)_

两个女生当中有一位是有男朋友的,并且因为种种原因,在这两天匆匆领了证。后来证明这似乎是正确的,因为其它的,基本好像都黄了。当然,这也可能是女生和男生的区别,因为其它的都是男生。

其实这是我人生第一次坐飞机。但是我努力装作很老道的样子。飞机上好像没有任何特别的事情,3个小时的航程也很快结束了。

到了成田机场第一个感觉是飞机飞回上海了?因为机场里的感觉太相似了。而且日文里有很多汉字,就更相像了。

完成各种手续,提取行李之后,出了机场,看到了等待在门口先到的北京来的同学们。他们好像等得有点不耐烦了,各个脸色都不太好看。

这时,地面好像摇晃了一下。刚在想是否是晕机的时候,地面又摇了一下,而且很明显。没错,是地震。日本的地震果然频繁,刚下飞机就来给我们打招呼了。

不过也不知道是大家比较疲惫了还是已经做好了充分的心理准备,好像并没有人惊慌。大家有序登上开过来的大巴车,前往未知的土地和生活。

大巴车开了大约1个多小时,把我们所有人拉到了一个椭圆形的建筑物当中。这楼到底是哪栋楼我已经记不太清楚了,好像是索尼位于大琦的那个楼,也就是前几年卖掉的那个。

在这个会议室里面准备了一些盒饭,也就是日本所谓的便当。这饭今天看来应该还是可以的,但是当时还没有习惯吃冷饭,所以感觉不是特别好。特别是北方的同学,好像很不高兴。

吃完饭来了个人事部的领导,和我们说明因为国内最新政策的变化,所以要停止我们在国内的挂靠,也就是停止缴纳4金,并要求我们签一份同意书。北京的同学毕竟见多识广,政治觉悟高,刚才又没吃爽,于是闹将起来了。质问为什么不在国内说,而是拉到这里来说,这不是胁迫么?于是拒绝签字。上海的小本科年纪小,觉得停了就停了吧,所以开始也附和了一下,后来看公司态度坚决,也就签了。于是乎又被北京的同学投射了一次憎恶的目光。

后来好像又闹了好几天,最后解决了。细节不是很清楚,反正就知道那时候北京的同学们还商量着去大使馆告状。

接下来就是办理宿舍的入住了。宿舍在新川崎,一个其实比较靠近横滨的地方。大家上班的地方其实不是很一样,上海的几个小本本主要是VAIO招的,在品川上班。路其实不是很近,但是有快车直达。

宿舍其实是索尼的单身宿舍,本来供30岁以下未婚人士申请居住的。格局是3DK,也就是三室一餐厅一厨房的格局。我们人比较多,没有那么多房间,所以3个人住一套。整个楼是L字形的,因此三个房间要么是2间朝南一间朝北,要么就是一间朝西两间朝东。而且朝北朝西那间比较小。

公司大概是担心大家不愿意住小间,就要求党员出列住小间。这也是我第一次知道,原来党员的先锋模范作用也是可以域外适用的。

女孩子只有两个,她们就比较舒服了。而且是5楼顶头的房间,采光很好。我住在一楼,朝西,而且窗外就是楼梯,因此房间基本上是不见阳光的。

而且,我们托运的行李是走的海运,因此还需要一段时间才能到达。所以此时的我们都是只有随身行李。不过,房间是榻榻米的和式,入住了才发现其实这种房间压根不需要家具。

不过小房间也有好处,就是房租便宜。一个月1万还是1万2,包括1次被罩和床单的更换洗涤费用。大的贵2千。女生的房间甚至还有烘干机。有公共的洗衣房,投币的那种。

虽然是成年人的宿舍,但是男生楼层和女生楼层是隔开的,楼梯上有门,晚上会上锁。门房间24小时有人看守。看门的老大爷每天都不厌其烦地教我们日本的礼貌,要是出门不打招呼他会追出来把你抓回去。。。

分好房简单刷洗了一下就到了晚饭的时间了。公司并没有给我们安排晚饭。于是一群人商量着怎么办。因为那时只有我日语还可以,所以结果就是几十个人跟着我去吃吉野家,因为那是最近的一家饮食店。然而那家店很小,一次最多也就能坐10来个,于是乎大家排队轮换着吃。。。

在日本写代码的日子(二)

拿到offer之后的日子平淡无奇,时不时去开个会安排一下接下来的手续。由于大多数入职者都不会日语,因此在暑假安排了日语的培训,就如同我前面提到的第一届师兄师姐那样。只不过我们这次的地点变成了上海张江科技部下属的一个招待所。后来听说好像是学校在得知日本的物价之后,大大提高了代培的费用导致。

给我们上课的日语老师来自熊本县,一个矮矮胖胖的日本女老师,好像是联合国还是日本非盈利机构支教过来的,也就是应该很便宜。宿舍是两人一间,同屋的大哥姓严,没啥学日语的天赋和兴趣,整天用他的IBM ThinkPad远程登陆他托管在电信的一台服务器编译linux内核。那个时候没有什么云服务,服务器在我心里是无比高档的存在。也不懂linux,感觉编译内核的人好高端。

培训只有3个月不到的时间,封闭式,只有周末可以回家。课程很满,但是我是基础最好的一个,所以很轻松。北京招的20几个人后来也过来一起培训了一段时间。50来号人里面,只有两个女生。

按照当时政府的要求,因为缺少这样的案例,没有相关的管理方法,需要我们在国内挂靠一个单位,否则就变成失踪人口(后来知道比我们早的01、02届就是当了1-2年失踪人口)。所以培训期间就挂靠在索尼(中国)之下,月薪800,因为这是当时缴纳四金的最低限。有几个人也不知道什么缘由,想起来去银行申请信用卡,结果批倒是批下来了,额度貌似只有100元,也是很搞笑。

熊本的老师课上得如何已经不太记得了,就记得她说她家在大海边,经常涨潮就被大海给吞掉。我们觉得好可怜,说了些安慰她的话。没想到她说这样很好,因为每次被冲掉就可以拿到政府的补偿款,好几百万日元。所以每次冲掉之后他们就搭一个便宜的,等它再被冲掉。。。我那时觉得这人怎么那么坏,现在想想估计也就是穷,否则怎么会来中国支教。

还有就是她告诉我们到了日本绝不要给NHK开门。后来证明这个提醒真是太重要了。同行的一个小伙子(也就是办信用卡的那位)估计上课没认真听,到了日本不仅给别人开了门,还给了别人银行账号。于是乎每月被准时扣除视听费,而且无法取消。

最后出发的时间是10月15日。课程应该是9月底就结束了,留出2个星期给大家准备行李。上海的同学都准备了很多很多行李,因为听人事说会给每个人分配一个10吨的集装箱,就是万吨轮上那种。老实说,把我们全家的家具放进去都装不满。(平日大家搬家的那个箱子一般就是1~2吨)

这些日子妈妈有些伤心,因为儿子要离开她生活了,而且去那么远的地方。但是妈妈又不愿意给儿子阻力,所以只是偷偷地流泪。泪水之后,将所有的思念转化为了行囊。妈妈专门去家具店定做了一套家具,包括床,床头柜,大立柜,书桌等。后来知道,所有人里面只有我有实木家具,其他人最多也是宜家。

这些家具最后拆开打包成了十几个包,加上其它物件大概一共25个包裹。上海Lady有30多个,好像大部分是鞋子。我是第二名。

好玩的是北京的同事们好像得到了人事错误的说明,不知道可以带家具。所以大部分人都只是一两个拉杆箱到了日本,然后看到上海同事堆积如山的行李彻底傻眼。不过大多数来自北京的人从心底也没打算久呆,一来是北京的官气太重,所有人都想混体制;二来是北京的普遍学历较高,因此有不同的视野。后来的情况也印证了这一点。不到3年,北京来的基本都回国了。那个陈姓博士,1年多便回去,好像是做了教授,成了第一个回国的人。

在日本写代码的日子(一)

午休时间,接着写。

在听到对面的“哦”之后,其实自己反而有些不踏实了,心里暗默着“叫你丫装B,一会儿听不懂估计要暴死了”。

不过我又马上开始心里建设了。“那又怎么样?我是个中国人,日语不好有啥稀奇。他们会中文么?我开口说日语已经是对他们的莫大恩惠了,没理由来挑剔我。”

面试官很多的时候,看起来很吓人,其实挺好的。因为他们经常会犹豫谦让,不知道该谁来提问。

“那你先自我介绍一下吧”

旁边的中国男人开口救场了。也许并不是救场,只是套路。

之前我在网上填写的材料里是有提到我有日语能力的。所以据此我迅速判断了在场的人可能根本没看过我的资料,或者只是在刚才我进来之前简单瞄了一眼。所以我决定比较详细地自我介绍一下,哪怕其实我材料里都写了。

除了基本的个人信息及兴趣爱好(其实就是码代码_(:з」∠)_),我不怀好意地着重介绍了一下我家里有的索尼的产品。面试官显然兴奋了不少,也问了一些问题,比如产品的使用反馈,索尼品牌在中国的形象等。我也仔细询问了桌上那些大大小小的电脑,得知来的面试官都是VAIO各部门的部长,电脑是各部门的代表产品。其中,一个高个子部长面前的是一个超小的笔记本,他的大手飞快地敲着那个超小的键盘,看起来十分好笑。我记得我还小小“嘲笑”了一下他,把满屋子的人斗逗乐了。至于当时说的是啥,现在也不记得了。

接下来被要求讲项目经验,讲了大二暑期在某创业公司做ERP系统的事情。那个时候ERP很新,而且完全是不同的领域,可以看出在座的面试官基本不怎么了解。我很高兴,因为这样我可以胡诌了(笑)

开个玩笑。其实面试官们并不是想要考察这些具体的内容,毕竟只是一个本科生。所以虽然他们会时时发问,其实主要是考察我的逻辑是否能自洽,也就是是否能说圆了。要说圆最省力的方法就是说真话,不要瞎吹。学生时代的我们,任凭怎么瞎吹都是难以惊动到这些四五十的老狐狸的。幸好当时的我已经懂得这样的道理。

面试内容其实差不多就是这样。事实上,当时我的日语水平只能勉强应付自我介绍和简单的对话,项目部分其实是日英中混杂着在谈,所以特别消耗时间。

一面结束之后被通知留下,而其他人基本都回去了。心里不停揣测着原因,一直等到当晚7点多所有面试结束。因此也有了机会看到一个清华的陈姓博士的面试刚开始不久,一个面试官便冲出了会议室让其它两个房间都暂停面试,来看这位博士的项目。后来知道这位博士的导师是当时H.264标准的委员会委员,该博士参与了H.264标准的起草工作,而那个时候索尼正在进行H.264编码器相关的实现,所以如获至宝。

终于到了晚上,人事小姐姐~( ̄▽ ̄~)~又来领我进了房间。这次房间里只有两个人,是一面时第一排里面的。后来才知道一位是VAIO产品软件部门的负责人,一位是VAIO研发部门的部长。两个人都相中了我,相执不下,所以留下了我想进一步了解我的专长。

后来又听说因为面试官们记不住那么多中国人的名字,况且中国人的姓氏区分度又很低,他们其实私下给每个来参加面试的人都起了绰号。比如我的绰号叫“传教士”,原因是人又高又瘦,比较能吹(我认为我肯定不是最能吹那个,关键是他们能听懂我吹)。而我的另外一个同班女同学被称为“上海Lady”,大概是有品位的意思。

两位部长在简要介绍各自的部门业务内容之后,让我发表意向。我说我自己是学自动化的,所以想搞软硬结合的方向,于是乎淘汰了软件部门,选择了研发部门。

二面就是研发部的部长带领着他下面的课长来看我,决定把我放到哪个课里。当然他们没和我这么说,都是我自己瞎琢磨的。

三面是东京国际人事部的领导了。主要和我确认是否愿意较长时间(5到10年)呆在日本,并且和我交代确认到日本生活的各种细节。

最终,次年1月拿到了offer。同期上海的有20多人,北京的20多,基本上是清北复交这四所学校,后来听说相对于报名人数录取率不到1%。后来有机会和招我的部长喝酒,问了他录取我的理由。“嘛,懂日语能交流,底子不错”,部长如是回答。虽然不知道是否确实如此,不过我本人是接受的。因为我在大学的成绩其实并不好,大部分刚及格,还有4门红灯(理论、思想那种)。但是C语言我是免修的,最终考了98分。

Offer上写着年薪500万日元,安家费50万,无奖金,无加班费,去日本和回国搬家由集团下属物流公司承担。那一年海尔给我校本科生的待遇是800,住12人一间宿舍;而华为则是3000左右。

在日本写代码的日子(序)

大家好,我是《从零开始手敲次世代引擎》系列文章的作者。一直在想是否要在技术文章之外,另外写一点儿生活方面的东西。一来是分享,二来也是写给自己的晚年,日子久了很多都被时间冲刷地模模糊糊了。今天准备开个头。

这个系列文章准备分享一些杂谈,主要是关于一些自己的职业道路,以及日本的风土人情等。可能也会涉及到一些语言学习方面的东西。

2001年,我读大二的时候,索尼开启了第一次在中国的国际人才直接招聘。就是从国内的大学直接招聘人才去日本工作。关于这个事情的背景和各种经纬,其实有一本书,是当时索尼人事部部长所写的。里面有一些他和当时中国高层面谈时的故事,也有中韩印三国(当时招聘的对象国)学生特质的比较,懂日语的读者感兴趣的话可以一读。日本亚马逊就应该就可以买到。

第一届入职员工的语言培训学校选在了我所在的大学。那时候学校正在鼓励二专,我选了日语。原因有二:一是小时候玩红白机为了能搞懂一些菜单选项或者通关密码自己瞎琢磨过一段时间,二就是当时得知了索尼有这么一个国际招聘项目。那时倒也不是很清楚具体的待遇,更多的只是年轻时对外面世界的好奇。

后来实际去读了二专,听老师说索尼给的待遇很好,于是更加坚定了决心。

记得二专是一周9节课,主课教材是《新编日本语》,据说与日语系正规军相同。只不过最终只学到第3册开始的地方,总共好像是有4册。

另外一个与一般语言培训所不同的是,有一些日本历史经济文化的辅助课。后来发现这些课的内容对于和日本人聊天很起作用。毕竟语言只是工具,文化才是交流的核心。日本人是比较小格局封闭的文化,单单会语言是不太容易赢得他们的认同感的。日本人经常说以心传心,或者要读懂空气。这种能力其实就是来自于对日本文化和心理的了解,不是光靠语言就可以解决的问题。

二专毕业的要求是过N2,就是日语能力考2级。我很轻松地过了。自己觉得主要原因有两个:一是一个语感的长期积累。其实虽然系统地学习是在报了二专之后,但是其实从小学开始就有一些日语方面的接触,比如抄写过五十音图。后来也自学过一两本《标准日本语》,看过一些日漫。第二个原因就是二专期间因为目的明确,所以学习也有动力,比较认真。

不过就如很多日语系同学所说的,大学里所学的语言更多是作为语言学研究对象的语言,在实际的生活当中很难用起来。所幸我当时自己买了一套名为《自学生活日语》的教材,这是写给日本在其他国家的战争遗孤看的,所以内容十分生活化,每一课都解决一个具体的生活问题,比如如何理发,如何买菜,如何坐公共汽车等。后来到了日本发现,这些知识都非常有用。

但是比较搞笑的是,到了2002年秋,就是一线企业开始2003年秋招的时候,我却完全没在状态。每天浑浑噩噩地在宿舍码代码,丝毫没有考虑出路的问题。甚至于,差点错过了心心念念的索尼的国际招聘活动。

记得那天睡了个懒觉,起床下楼去打开水,发现宿舍楼里人都不见了。觉得奇怪的同时,蛋定打完水回宿舍码代码(应该是在写一个类似于3DS的三维编辑软件)。不一会儿楼下传来一阵喧闹,大家都回来了。

“你们都去哪儿了?”

我头也不回地问刚进门的舍友。

“去听索尼的宣讲了。”

“哦”,我故作镇静地应到,其实心里千万匹草泥马略过。

“说了些啥?”

“到日本东京总部去工作,按照当地水平发工资,提供住宿。”

“怎么报名?”

“给了个网址,让填写。”

“好,给我看看。”

于是乎,飞快地打开网页进行填写。内容好像挺多的,但是肯定没有要求填写在校成绩。

过了些日子,收到了笔试通知。考场在哪儿忘记了,反正不是本校。人挺多的。题目分软件和硬件,自己选做。但是软件里也不是一点硬件都没有,包括一些电路图方面的东西。(也好像是我软硬件都做了)最前面还有智商题,找出明显不同的那个图案之类的,可能和考公务员的那个差不多。好像还有英文题,但是巨简单。

现在只记得软件部分有一道考递归的题目,先写一个递归方法,然后阐述递归的优缺点。我应该是答了诸如简单但容易栈溢出什么的。

最终笔试成绩没有公开,只是得到了一面通知。本校参加笔试的貌似基本都得到了一面通知,所以估摸着笔试淘汰率不高。

因为人数众多,一面好像总共进行了3天,共3个房间每人45分钟的样子,在陆家嘴某栋高楼里。时值秋冬交接,出了2号线高楼边的寒风呼呼啦啦的。人生第一次西装皮鞋,全身不大对劲。

好像是早到了1个多小时,在休息室和别的人闲聊。虽然有不少清华北大的,倒也没觉得很大压力,毕竟自己也准备了许多年了。

来带我进面试房间的人事姐姐很漂亮。后来和同届进入公司的人聊,大家都对她印象颇好。到了门口她和我说“人有点多不要紧张”,我“嗯”了一声就跟着她进去了。其实心里想的是“老子做了N年学生会主席几百人阵仗都见过紧张个毛”。面试房间里两张会议桌拼在一起,后面乌压压坐了一堆人。人事姐姐在最左侧坐下,旁边有个戴眼镜的中国男人,后来知道叫张泉,是国际人事部的。

不过我首先倒是没有怎么关注人,而是看到第一排每个人面前放着的笔记本电脑,大大小小各不相同。感觉最大的有15或者17寸(也许是19寸),最小的可能只有10寸甚至不到。

戴眼镜的中国男人首先开场,说这些面试官都是日本来的,我可以讲英文或者中文,他可以帮助翻译。

“可以讲日语吗?”

我问到。“哦!”桌子对面乌压压的人群不约而同吐了口气,如释重负。我感觉场面似乎有些反转,好像我才是面试官ԅ(¯﹃¯ԅ)

(上班了,先写到这里吧)

从零开始手敲次世代游戏引擎(卌)

终于四十了。
前面两篇我们使用bullet库进行了一些简单的弹性碰撞的物理场景仿真演示。也很粗略的说了一下碰撞检测的一些相关概念。但是这显然是不够的。
为了真正理解计算机是怎么进行这些物理计算的,特别是在游戏这么一个软实时系统当中,是如何“又快又省”地做到这些的,我觉得非常有必要再造一下轮子。
在前面的文章当中,我们提到过,碰撞检测的基本数学原理就是高中所学的解析几何。简单复习一下:
3维空间的两个几何体(或者2维空间的几何形状)如果发生碰撞,则说明它们之间有了肢体接触。用数学的语言来说的话,就是至少存在一个点,满足这个点既在A上又在B上。或者,如果用集合的概念来说的话,就是
Acap Bne Ø
所谓解析几何,就是通过建立某种坐标系,将几何体转化为代数方程,然后用代数的方法去研究几何问题。在解析几何当中,A与B的交点就是代表A与B的方程所组成的方程组的所有解。(如果只考虑边界与边界的交点的话)
因此,很显然的,几何上A与B是否相交的问题,就可以转化为在某个坐标系下代表A的方程与代表B的方程是否有共通解,也就是方程组是否有解的问题。
然而,在游戏当中,仅仅依靠这种方法是不行的。因为:
  1. 游戏当中绝大多数的几何体都是无法解析表达的。就是说你写不出它的方程。比如我们一个足球游戏,要检测脚与球的碰撞。球还好说,脚就是一个很难用解析的方式(方程)去表示的几何体;
  2. 当代游戏当中往往有成百上千个几何体,各种形状。如果它们均可动,那么任何两个几何体之间都可能会发生碰撞。为了检测这样的碰撞,我们不得不检查所有可能的组合。这是一个很大的量;
  3. 方程组是否有解的问题本身就是一个复杂的问题。就算有判定公式的些特殊情况,判定公式的计算,对于游戏引擎那可怜的ms计算的周期来讲,也往往是十分昂贵的;
  4. 游戏当中往往需要知道的只是碰上了还是没碰上,并不需要知道到底是A的什么地方碰到了B的什么地方,也就是说不需要精确解(定量判断),只需要定性就好。
所以,一般来说游戏当中并不会直接对场景物体求交,而是通过给场景物体包裹一个基本几何体(被称为是碰撞盒,也叫包围盒,等等),将复杂的场景物体求交近似为简单几何体求交来进行的,也就是我们前一篇所提到过的那些基本形状。
这些基本形状十分有用。不仅仅在物理引擎当中会用到它们,它们对于场景的快速建模(关卡设计)、场景渲染的优化、调试等都非常有意义。所以我们有必要在引擎当中实现它们。
实现基本几何体
让我们在Framework目录下面新建一个子目录,取名为Geometries。然后在下面新建一个Geometry类,定义基本的几何体共通的属性:
我们首先定义了几何体形状类型,用于在运行时判断实例所代表的几何体形状。然后我们定义了我们希望所有几何体都支持的几个方法:
  1. 获取球形包围盒(GetBoundingSphere)
  2. 获取与坐标轴平行的长方体(立方体)包围盒(GetAabb)
  3. 获取物体在以当前线速度和角速度保持运动一小段时间(time Step)之后的AABB包围盒
球形包围盒
球形包围盒直接的碰撞检测比较单纯,如下图。当两球心距离 d>r_{1}+r_{2} 的时候,碰撞未发生。反之,碰撞发生。
但是,虽然公式很简单,计算量却不是最小的。这是因为空间任意两点之间的距离为原点到这两点的向量的差、所得到的向量的长度。向量的长度计算涉及到乘方与开方,是比较慢的计算。
图片来自网络:http://jccc-mpg.wikidot.com/intersections-and-collision-detection
AABB包围盒
AABB包围盒是长方体包围盒的一个特例,它的长宽高平行于坐标轴。也就是说,没有旋转。AABB包围盒不见得是(或者说很多时候不是)最小的长方体包围盒。但是因为它没有旋转,所以有以下这样非常好的性质:
  1. 当且仅当两个AABB包围盒在各个坐标轴上所对应的(投影的)区间都发生重叠的时候,两个AABB包围盒才相交。
这就是说,对于AABB包围盒的碰撞检测,我们可以检测其在每个坐标轴上的投影。只要有一个坐标轴上的投影不重叠,那么两个AABB包围盒就不发生碰撞。
虽然对于最坏的情况(发生了碰撞),我们需要在x,y,z三个轴上各自进行一次区间是否重合的检测,但是每次检测实际上只是分别比较两个投影区间的最小值/最大值,这对于CPU/GPU来说是非常简单的操作,计算量很低。
图片来自网络:http://myselph.de/gamePhysics/collisionDetection.html
好了。有了这个基本的几何体类,我们就可以从它派生生成各种具体的几何形状的类了。比如球体:
长方体:
等等。我们可以这样不断添加我们所需的几何体来丰富我们引擎的功能。
下一篇我们将尝试将这些几何体可视化,并且实现一个我们自己的碰撞检测模块。
关于Windows版的连续集成
之前我们使用Circle CI实现了Linux/MacOS/Android三种平台的连续集成。但是因为Circle CI不支持Windows的连续集成,所以这个问题一直放置到现在。
好在另外有一个名为AppVeyor的云服务商为所有GitHub上的开源项目提供了免费的Windows的连续集成环境,在本篇的代码当中我们利用了这个环境。
使用方法非常简单,在AppVeyor网站完成项目注册之后,同样是通过一个YAML文件来描述所需要做的工作。对于我们这个项目,这个文件如下:
只不过目前这个文件好像只能放在项目根目录下,与Circle CI的目录结构稍微有些不一样。
关于编辑器
如我在系列开篇所述,可能是由于历史原因,我更喜欢使用命令行。相应的,作为命令行当中的优秀文本编辑器-VIM,是我用来写本系列代码的主要工具。
但是这并不是说我推荐使用VIM,或者反对其它的编辑器。其实编辑器的选择只有一个标准,那就是自己用得舒服即可。
其实最近我也在用VS Code,很好用。强烈推荐。

从零开始手敲次世代游戏引擎(三十九)

从零开始手敲次世代游戏引擎(三十八)当中我们使用bullet实现了一个基本的场景物理仿真,但是没有对游戏当中所使用的物理仿真进行一些最基本的解说。本篇我们就来简要看一下是怎么做到的。
如前一篇所说,游戏当中用到的物理仿真,一般包括以下两个主要的环节:
  1. 碰撞检测:主要用来检测当前帧当中的几何体是否发生了碰撞;
  2. 运动学(力学):主要用来推算下一帧当中几何体的位置。
碰撞检测的理论基础是我们高中所学的解析几何。由于复杂几何体的解析表达十分复杂甚至不存在,对于游戏这种软实时系统,一般是采用基本几何体替代场景当中的复杂几何体来进行碰撞检测的。这些基本几何体被称为碰撞模型,也称为碰撞包围盒。
被作为碰撞模型使用的基本模型一般有如下几个:
  1. 平面。平面在其延展方向是无限大的。比如方程 z = 1z = 1 就描述了一个平行于x,y轴的面积为∞的平面;
  2. 球体。球体可以通过球心坐标和半径方便地描述;
  3. 长方体(立方体)。可以通过其中心坐标,以及长、宽、高进行描述;
  4. 圆柱体。圆柱体可以通过其中心坐标,底面半径及高描述;
  5. 圆锥体。与圆柱类似;
  6. 胶囊体。是一个圆柱和两个半球体的复合。一般用来作为游戏当中人形角色的包围盒,相对圆柱形的平底,半球形的底面对于地面的坡度或者较小的起伏有较好的适应性。
  7. 三角形。虽然是平面图形,但是我们在之前已经知道了,游戏当中的几何体一般都是用一连串的三角形描述的。所以对于不能使用上面这些基本形的情况,我们依然是使用一个三角形组成的网格模型,只不过一般顶点数远小于渲染用模型的低模,来进行碰撞检测。
这些基本形状的碰撞检测的原理就是解析几何当中的求交点计算。当然这是有相当计算量的,所以在游戏当中会采用很多手段来减少计算量,比如使用AABB算法来避免不必要的求交计算。
而运动学则主要是根据牛顿三定律计算物体的运动和位置的变化。
比如下面这么一个场景:
那么我们可以为每个台球绑定一个球形的碰撞模型,而为球台绑定一个长方形的碰撞模型。至于球杆,我们可以绑定一个圆柱体。当我们按下键盘上某个按键的时候,我们给白色的母球加一个100牛顿的力,那么就得到如下这么一个仿真的结果:
 
 
视频封面

上传视频封面

 
 
球之所以会从球台边界跑出去,是因为目前我们给球台绑定的还是一个非常基本的长方形形状,所以球台边界和球台里面是一样高的。就导致了这个现象。
我们可以通过绑定更为复杂的形状,或者在球台边界绑定额外的长方形,来解决这个问题。
关于游戏逻辑
我在前面的文章当中也提到了,游戏逻辑本身并不应该是游戏引擎的一部分。游戏引擎提供接口给游戏内容开发者编写游戏逻辑。
因此在本篇的例子当中,我对我们的项目代码结构进行了重组,新加了一个游戏逻辑基类:GameLogic,并且改变之前将引擎代码直接编译成为可执行文件的做法,取而代之的是将其编译成为库文件,最终与Game目录下的从GameLogic继承而来的具体游戏逻辑进行链接,得到最终的可执行文件。
当然,仅仅这样其实还是远远不够的。为了封装成为一个可以复用的游戏引擎,我们接下来至少还需要做如下的工作:
  1. 将头文件分离出Private和Public版本。Private是编译引擎的时候参照的,具备完整的信息,而Public版本只暴露二次开发时需要知道的接口。这样做可以进一步分离游戏引擎和游戏本身,方便游戏引擎的版本升级;
  2. 统一不同RHI的Shader编写方式,导入一个中间方式(如:Nvidia CG),并且编写相关的转换工具;
  3. 对Asset目录进行区分,将引擎所用资源和每个游戏所用资源分开;
另外一方面,我们的游戏引擎现在虽然有了基本的图形渲染、输入、物理仿真模块,但是从功能上还缺少诸如AI、动画、视频播放、音频音效等部分。
另外从开发调试以及优化的观点,我们还缺少驱动模块(用来调整不同模块的执行频率以及CPU多核心调度)、Profiling及调试模块等。
最后我们还需要有编辑器,方便对导入的资源进行简单的修改,以及场景结构的安排等。

从零开始手敲次世代游戏引擎(三十八)

从零开始手敲次世代游戏引擎(三十七),本篇我们来进行物理引擎的一些基本探讨。
最后执行的效果如下:
视频封面

上传视频封面

场景的创建
首先我们使用Blender来创建一个测试用的场景。这个场景很简单,由一个平面和两个球体组成。布置好之后,可以切换到摄像机视角看一下位置:
视频封面

上传视频封面

接下来我们用GIMP创建地板的贴图。创建一个空白的512×512的图像,然后用滤镜加入彩色的噪点,再用一个马赛克滤镜将其转化成为彩色蜂窝状的马赛克拼接就可以了。将其保存为TGA图片,放到贴图目录当中。
视频封面

上传视频封面

然后我们再回到Blender当中,为平面指定这个图片作为贴图。注意如果贴图出现错位现象,需要修一下UV。
视频封面

上传视频封面

好了,将场景导出为OGEX文件,然后用文本编辑器修改一下OGEX当中的贴图路径。(因为导出的路径为绝对路径,而我们的引擎需要相对路径)
导入物理仿真库
在游戏当中有两类常用的物理计算:
  1. 碰撞
  2. 运动学(力学)
在详细讨论这些之前,我们先导入一个在业界比较有名的,也是比较成熟的库来建立一些感性认识。
参考引用*1是一个名为Bullet的物理仿真库,是由原SIEA(索尼互动娱乐美国)的员工写的并作为开放源码项目在GitHub上面提供。因为它也是采用了CMake的编译系统,所以我们可以很容易地将其导入到我们项目当中。具体细节这里就不赘述了,感兴趣的可以直接看本章对应的源码当中build_bullet脚本。
编写PhysicsManager
为了管理物理仿真,我们在我们的引擎代码的Framework/Common下面新建PhysicsManager的源代码,将Bullet的初始化/销毁以及创建物理仿真用场景的代码进行包装。
在PhysicsManager的Tick事件处理代码当中,我们从之前写的SceneManager那里获取需要渲染的场景结构,对于其中的每个SceneNode进行检测,看看是否需要进行物理仿真:如果需要,我们使用Bullet创建一个刚体模型(含碰撞盒),将其绑定到对应的SceneNode上面,并将其加入到物理仿真场景(DynamicsWorld)当中去。然后我们通过调用DynamicsWorld的stepSimulation,计算下一个1/60秒的仿真结果。
仿真结果的反映
在渲染时,我们同样从SceneManager获取所需要渲染的场景结构,遍历所需要渲染的SceneNode并检测其是否绑定了刚体模型。如果绑定了刚体模型,则我们使用存储在刚体模型当中的最新仿真结果更新SceneNode的Transform(也就是空间位置与姿态)。这样我们仿真的结果就会在渲染的结果当中反映出来了。
其它
因为Blender当中也集成有Bullet,本来我是想直接在Blender当中绑定物理模型并直接导出的,但是目前最新的OGEX(2.0)似乎还不支持物理模型的导出。所以我就自己利用OGEX的Extension自定义结构对OGEX进行了一点儿简单的扩展,使其可以支持物理模型。
不过我目前还没有对Blender的OGEX导出脚本进行修改,因为我还不了解Blender的Python接口。所以写这篇文章的时候我是通过文本编辑器直接编辑OGEX文件(Asset/Scene/physics_1.ogex)加入了这些扩展属性。
如之前文章下面有读者评论的,业界其实用得比较多的场景导出格式是COLLADA。之后等我们支持了COLLADA之后,这些问题也自然解决了。所以目前我不准备在这方面花太多精力。
另外,因为这个场景很简单,整个仿真很快就结束了,不太容易观察,所以我修改了前一篇所写的InputManager,加入了“按下R键重置场景”的功能。也就是说,按下键盘上的R键就可以重新载入场景进行仿真。
最后完成的代码在:
图标
参考引用
图标
图标
图标
图标
图标
图标

从零开始手敲次世代游戏引擎(三十七)

从零开始手敲次世代游戏引擎(三十六)文末所预告的,我们暂且将图形渲染告一个段落,开始一些其他模块的基本编写工作。本篇将探讨用户输入模块的编写。
我们知道,电脑一般是以键盘和鼠标作为主要的输入设备;而手机则是主要依靠触摸屏以及内置的重力加速度传感器/陀螺仪;游戏主机设备则一般是游戏手柄输入。
虽然外形和使用方法差别明显,但是对于电脑系统来说,这些都是外设,而且是低速外设。这些设备多通过串行总线(比如USB)与计算机系统相连。
由于操作系统的一个重要职能就是管理硬件设备,而且设备可能会被多个程序所共享,因此在我们一般不需要直接与这些硬件设备的驱动直接打交道,而是通过操作系统所提供的接口进行访问。这也意味着,在不同的系统上我们会遇到不同的接口,即使是同一个类型/型号的硬件设备。
除了本世纪兴起的体感类游戏之外,传统的游戏一般以开关量输入和模拟量输入为主要控制输入信息。开关量就是指诸如键盘按键、鼠标按键、手柄按键那种,只有按下和非按下(弹起)这两种状态的控制量;而模拟量输入则是指诸如手柄摇杆或者近些年手柄两肩上的板机那样,在一个范围之内可以连续改变其角度从而产生一个连续变化的输入的控制量。
我们首先来使用键盘当中的光标方向键来模拟开关量,控制模型的转动,像下面这样:
视频封面

上传视频封面

首先我们需要在Framework/Common当中新建一个Runtime Module: InputManager,其中包含平台无关的四个方向键的按下和恢复(弹起)响应函数:
然后我们需要将各个平台的按键消息与其挂钩:
Mac OS
我们需要将Cocoa当中的按键消息(NSEventTypeKeyDown, NSEventTypeKeyUp)与InputManager进行挂钩,大致是下面这么一个样子:
Windows平台
我们需要将Windows消息当中的(WM_KEYDOWN, WM_KEYUP)消息与InputManager的方法进行挂钩,大约是下面这个样子:
Linux
我们需要将XCB Event当中的(XCB_KEY_PRESS, XCB_KEY_RELEASE)与InputManager的方法进行挂钩,大致是下面这个样子:
这里需要注意的是XCB返回的是键盘的扫描码。这个扫描码实际上是指的按键的物理位置(电气位置),在不同类型的键盘上是不一样的。我这里简便起见直接使用了扫描码,但是这其实是不对的(没有通用性)。实际上还应该需要根据系统提供的键盘配置文件将扫描码转换成为按键名称之后才对。
映射完成之后,我们需要在InputManager.cpp当中添加这几个按键响应对于场景的操作逻辑。这部分其实才是日常的游戏开发当中所说的游戏程序(逻辑)的重要一部分。也就是说,我们目前到此为止所写的代码多属于游戏引擎的开发,而游戏内容的开发(使用游戏引擎开发游戏)的编程工作则主要是编写用户输入是如何影响游戏场景(当然也包括AI输入等)。
因此,这部分应该是可以由引擎使用者改写的。在软件行业这叫“支持二次开发”。这又是一个很深的坑,因为我们需要封装我们的引擎接口,选择需要支持的二次开发语言(比如Lua、JavaScript、Python、C#或者是C++),并提供一整套相关的工具。这些我们在后面会通过许多篇文章来进行。这里我们首先采用Hard Coding的方法来快速测试。
首先我们在Framework/Common/GraphicsManager.hpp当中删除按照时间自动旋转模型的代码,并暴露两个新的方法:
第一个方法表示将模型按照X轴进行旋转,第二个方法表示将模型按照Y轴进行旋转。参数radians表示旋转的弧度。我们的引擎采用右手坐标系,所以radians为正表示逆时针旋转(当👀朝着坐标轴-方向看的时候),而为负表示顺时针旋转。
然后我们在InputManager.cpp当中,根据不同的按键调用这两个方法就可以轻松实现如上面视频所示的用按键对模型旋转的控制了。比如下面这样:
关于演示用模型
演示用模型来自参考引用*1,许可证类型为个人使用(非商业用途)。一些贴图为TGA格式,所以我根据参考引用*2在Framework/Parser之下新增了TGA文件格式的解析器。TGA格式很简单,所以就不另外写单独的特别篇来阐述解析器的编写了,有兴趣的请直接看本篇对应的代码。
 
参考引用:
图标

从零开始手敲次世代游戏引擎(Android特别篇)-3

在文章从零开始手敲次世代游戏引擎(Android特别篇)-2当中我们完成了执行环境的部署,并打通了开发环境和执行环境。但是到目前为止我们只是完成了C/C++部分的交叉编译,并没有实现Android应用程序的开发。
事实上,Android是一套基于Linux上Java虚拟机的Java程序集团。也就是说,与之前我们开发macOS版类似,我们无法在C/C++当中完成与系统各项服务:如窗体管理/输入输出管理等的交互。在macOS当中,我们写了Object C++代码来桥接Cocoa和我们的引擎;在Android当中,我们需要通过JNI glue来桥接Java和C/C++代码。(JNI = Java Native Interface,Java本地代码接口)
参考引用*1为我们展示了在Java Code当中调用C/C++代码的方法。然而,采用这种方式的程序其主循环(Main Loop)仍然是在Java代码当中。这样的做法对于一般大多数应用程序来说是可以接受的,但是对于诸如游戏引擎这类对于性能要求比较高的场合,是不太合适的。
如果做过一些Android Java开发的人应该知道,Android的应用开发其实某种程度挺类似于网页开发,GUI开发的基本单位是Activity,每个Activity包括一个界面(可以通过基于XML的resource文件定义)和一组控制这个界面的程序(Java),Activity之间的迁移通过暴露Intent接口来实现。这与Web开发的网页(通过HTML编写)+脚本(JavaScript)+ URL参数的方式是十分类似的。
所以,为了实现高效的本地代码,最好是能够用C/C++直接写Activity,而不是在Java编写的Activity当中通过JNI去调用C/C++写的功能Function。
当然我们并不是第一个吃鸡的人。这类问题在早期的Android版本当中是不好解决的,但是在今天,由于商业引擎的推动,较新的Android版本当中已经有了比较好的解决方式:Native Activity。参考引用*2为我们展示了这个Native Activity的使用方法。
让我们将参考引用*2的代码下载下来,放到我们的代码树的Platform/Android目录(新建)下。然后我们启动我们的docker容器(切换到Android开发环境),在Platform/Android目录下执行:
就可以编译生成这个Sample的APK文件了。这证明了我们的docker环境是正常的。
(这里面有个小细节。Google Samples当中的代码似乎已经有一段时间没更新了。对于Native Activity这个Sample,其依赖的“com.android.support.constraint:constraint-layout:1.0.1”这个组件已经过期。如果编译的时候提示找不到这个包,请将其改为“com.android.support.constraint:constraint-layout:1.0.2”。具体位置是在app/build.gradle当中)
(可以从这里下载编译生成的APK安装包。注意是开发包,没有进行签名,系统会提示安装风险,请自行判断)
执行的效果如下:
 
 

视频封面

上传视频封面

 
 
有了Google官方的Sample,接下来我们需要解决的问题主要就是如何将Android标准的基于Gradle的项目与我们的基于CMake的项目统合起来。
一种首先可能会想到的方法是,我们采用从零开始手敲次世代游戏引擎(Android特别篇)-1当中介绍的方法,先将我们的引擎代码编译称为动态库(libMyGameEngine.so),然后在Android应用当中引用这个库。也就是说,我们需要分开的两个步骤,第一次编译引擎,第二次编译应用。
其实在参考引用*2当中,已经为我们展示了直接在Gradle当中进行CMake的调用,于应用构建的过程当中自动构建本地代码模块的方法。如果我们打开参考引用*2的app/build.gradle文件,可以看到如下的内容:
红框所示部分,就是通过Gradle调用CMake的关键。
不仅如此,Gradle还会自动将CMake生成的生成物保存到Gradle的编译目录,所以我们只需要在我们的AndroidManifest.xml当中直接引用我们的引擎动态库就可以,而不必关心它会被放在什么地方。
上图最后一行的MyGameEngine,就是我们动态库的名字。注意在Linux环境当中,编译出来的实际文件名是libMyGameEngine.so。但是这个前缀“lib”和扩展名“.so”是不需要指定的。
为了生成这个动态库,我们需要在Platform目录下面新建Android目录,并且参照参考引用*2当中的写法,如下书写CMakeLists.txt
其中,AndroidApplication.*,OpenGLESApplication.*是将参考引用*2当中的代码,按照我们的架构和继承关系进行分解之后的产物。而AndroidAssetLoader.*,是从AssetLoader派生出的子类,原因是当我们最终在打包APK(APK是Android上面的安装包)的时候,如果将Asset目录下的资源打包进去,那么这些资源其实并不会在安装之后展开到安装对象的文件系统当中,而是继续以压缩文件的方式存在(APK文件其实就是一个ZIP压缩包。Java编译之后的jar文件也是一个ZIP压缩包)
这就是说,在这种情况下,通过通常的fopen/fclose/fread系列的API我们是无法读取到资源文件到,而是需要通过Android NDK提供的特殊接口,AAssetManager去读写这些文件。所以我们这里单独派生出了一个类来对应这个特别的需求。
将资源文件打包进APK的方法是,在app/build.gradle当中添加sourceSets,然后指定从哪里拷贝资源文件:
好了,在进行完这些改造和代码的重组之后,我们可以通过在Platform/Android目录下执行./gradlew assembleDebug来完成整个项目的编译构建工作。(需要在安装了Android SDK/NDK的环境当中进行)
代码在下面这个链接当中。目前在模拟器上运行是OK的,在我的Huawei P9上面仍然有问题,尚在Debug当中。
图标
本地代码(C/C++)调试方法:
与PC本地开发不同,嵌入式开发由于代码执行环境与开发环境是分离的两个环境,无法直接使用调试器(gdb / lldb)启动并调试可执行文件。这里需要用到的就是调试服务(如gdbserver)
方法是,首先将预先交叉编译后的调试服务程序推送到目标机器。对于Android,在NDK的prebuild目录当中提供了预先编译好的gdbserver。使用adb push命令推送过去就可以了。注意需要根据目标机器的CPU选择正确的版本:
(注意推送之后需要添加“x”属性)
然后,通过adb shell命令启动gdbserver,并指定需要调试的程序,以及gdbserver侦听的TCP端口号(用于接收来自开发环境的调试命令)。因为这个命令不会自动退出,需要在最后指定“&”参数将其置于后台,以便我们继续输入命令:
然后,通过adb forward命令,将运行环境当中gdbserver侦听的端口映射到开发环境当中:(实质上是ssh port forwarding)
然后在开发环境当中启动gdb。NDK当中有提供这个客户端,在下面这个目录当中
$ANDROID_NDK_HOME/prebuilt/linux-x86_64/bin/
启动之后,输入下面这个命令,连接运行环境当中的gdbserver:
之后就如同在本地使用GDB一样,通过b命令设置断点,s命令单步等。不再赘述。
对于被打包在APK当中的C/C++代码,因为该代码由Java启动(即使使用Native Activity,其实程序仍然是从Java层启动的)。也就是说,在运行环境里面并没有可以启动这个应用的可执行文件。对于这种情况,首先需要通过手点击主菜单应用程序的图标,将程序启动起来,或者(对于我们这种使用docker没有window的情况)使用如下命令启动应用:
然后使用
列出执行当中的应用进程,查找我们的应用程序的进程号码(下图星号所示):
然后执行:
来启动gdbserver。其它的相同。
如果程序一启动就崩溃,那么这种方式显然赶不及,那么还需要在“设置 > 开发者选项”当中打开“等待调试器”。不过这一项通常是无效状态,需要在“选择调试应用”当中首先指定被调试应用,这一项就可以选择了。打开之后,应用程序启动会立即中断,等待我们将gdb连通之后,输入“c”命令,就可以继续执行了。
 
Circle CI的自动化测试
采用Docker环境的好处是我们可以直接在Circle CI当中完成大多数不需要人工参与的自动化测试。我们可以将到此为止所写的整个编译以及构建执行环境、打通与开发环境的连接的操作全部写到Circle CI的YAML脚本当中,从而完成程序的自动测试。相关脚本请直接参考GitHub上的源代码当中的.circleci/config.yml文件。
并且,Circle CI支持将编译结果保存提供下载。上文当中提供的APK下载就是采用的这种方式。具体做法同样参考上述配置文件。
 
关于Android环境下的调试输出
我们的引擎目前是直接将调试信息输出到stdout / stderr,但是在以APK形式安装的程序当中,stdout / stderr是被重定向到/dev/null,也就是被丢弃的。因此,为了能够看到我们的Log,我们需要将Log重新定向到NDK所提供的机制当中。具体的做法有三种:
  1. 修改我们的代码,将相关代码替换成Android NDK的接口。但是这显然对于跨平台的代码来说不可接受;
  2. 编写专门的调试模块,并且对于不同的平台采用不同的输出方法。这个在我们后续的文章当中会有介绍;
  3. 利用Unix/Linux的管道和重定向机制,在应用程序里面另外启动一个线程,从我们的主线程接收Log并且将Log输出到NDK的接口当中。这个是本篇采用的方法。具体实现在Platform/Android/AndroidApplication.cpp当中。
另外,查看这个Log输出的方法是使用下面的命令:
但是这个命令会输出所有的log,包括我们的,包括系统和其它应用的。NDK提供的Log接口当中可以指定一个Tag,在我们的例子当中,我们指定了“MyGameEngine”作为我们的Tag,所以我们可以通过下面使用这个命令这样来过滤:
这样我们就可以看到大量我们的程序输出的关于重力加速度传感器的读数:(下面的数据因为是在模拟器当中采集的,所以不变)
参考引用
图标
图标
图标
图标
图标
图标
图标
图标
图标