« January 2010 | Main | March 2010 »

February 28, 2010

为什么一定要有密码?

以前写过一篇 可不可以只有密码没有用户名? 。里面提过一个让用户使用 email 做用户名,不设置密码的方案。今天想细化一下。

不设置密码有时候比设置密码,给用户的安全方面的感觉更好。因为,有些服务用户并不在乎帐户数据的私密性,也不在乎被人冒充。(比如在我的 blog 的留言,并不需要注册)要求输入一个密码,对用户反而是件很纠结的事情。用自己常用密码吧,若被服务商恶意记录的话,自己别的位置的帐户变得不太安全。随便选个新密码吧,很可能忘记。输入个简单的统一密码吧,基本行同虚设,跟没有密码区别不大。反而提供了虚假的安全感。这种安全感比没有更差。

而服务提供商在乎的只是用帐户名去标识一个用户,制作用户注册的流程,即费时,又费力,还提高了新用户进入的门槛。

我个人认为这样会好一些。

  1. 用户使用仅仅需要输入一个他使用的 email 地址(鉴于中国国情,另兼容输入 QQ 号也可以)

  2. 当发现用户第一次使用这个服务,触发注册确认的流程。提示用户激活,从 email 中收取激活链接。在这步,如果用户是误输入帐号(比如他曾经注册过,就会发现自己输入错误),可以取消。这样不会对他人造成骚扰。ps. 这个步骤也可以酌情取消或可选(并不阻止用户进行下面的服务体验),毕竟让用户通过 email 确认也有一定门槛。

  3. 用户登陆后,可以选择给帐号加上密码,但不是必须。可以根据用户的使用情况,采集用户在帐户里使用的服务种类、数量和时间,向用户推荐加上密码保护。或者建议用户不要加密码。

  4. 在用户不采用密码保护期间,收集用户登陆的资料,例如 ip 地址和登陆时间。对异常登陆做一些猜测。并结合用户帐号的安全性需求,定期发 email 通知用户。或是定期向用户 email 发送帐号登陆记录(以不骚扰用户的频率),比如长期没有使用服务,某天登陆了,就发一封 email 通知用户。

February 24, 2010

在 C++ 中引入 gc 后的对象初始化

这几天白天都在安排面试,其实还是有点累的。晚上就随便写点程序,好久没摸 C++ ,有点生疏。也算是娱乐一下吧。

主要工作其实是在 C 库的基础上做一个 C++ 的中间层。跟在 C 库的基础上做 lua 中间层差不太多。前几天加入了 gc 后,发现了一些有趣的用法。

比如对于构造对象。 C 的 api 中,如果创建一个对象失败,就会返回空指针。但是对于 C++ 就不一样了,new 是不应返回空指针的。书本上的推荐做法是在构造函数里抛异常。但是我又不太想进一步的引入异常机智,怎么办呢?

简单的方法是让 C++ 的封装类的构造函数什么都不干。btw, 我听 google 的 Mike Burrows 也讲过 popular C++ constructs can be bad for abstraction 。

然后加一个 init 函数来初始化这个对象,成功则返回 this 指针,失败则返回 NULL。

如果没有 gc 的时候,我需要这样做:

A *a = new A;
if (!a->init()) {
  delete a;
  a = NULL;
}

有了 gc 后很方便,只需要

A *a = (new A)->init();

嘿嘿,有 gc 还是方便很多呀。只是 new 的操作优先级让我有点不爽。

ps. 我知道即使没有 gc ,我也可以用很现代的 template 技术做到同样的事情。而且代码看起来更有品味。不过我感觉自己越来越老土了,写 C++ 也越来越 C Style 了。完全没有 10 多年前对 C++ 的火热激情。

February 23, 2010

C++ 中的接口继承与实现继承

为这篇 blog 打腹稿的时候,觉得自己很贱,居然玩弄 C++ 起来了。还用了 template 这种很现代、很有品味的东西。写完后一定要检讨。

起因是昨天写的那篇关于 gc 的框架。里面用了虚继承和虚的析构函数。这会导致 ABI 不统一,就是这个原因,COM 就不用这些。

说起 COM ,我脑子里就浮现出各种条条框框。对用 COM 搭建起来的 Windows 这种巨无霸,那可真是高山仰止。套 dingdang 的 popo 签名:虽不能至,心向往之。

好吧,我琢磨了一下如何解决下面的问题,又不把虚继承啦,虚析构函数啦之类的暴露在接口中。

简单说,我有几个接口是一层层继承下来的,唤作 iA iB 。iA 是基类,iB 继承至 iA 。

然后,我写了一个 cA 类,实现了 iA 接口;接下来我希望再写一个 cB 类,实现 iB 接口。但是,iB 接口的基类 iA 部分,希望复用已经写好的 cA 类。我想这并不是一个过分的需求。正如当年手写 COM 组件时,我对手写那些 AddRef Release QueryInterface 深恶痛绝。

用虚继承可以简单的满足这个需求:

#define interface interface iA { virtual void foo() = 0; }; interface iB : virtual public iA { virtual void bar() = 0; }; class cA : virtual public iA { virtual void foo(); // etc... }; class cB : virtual cA , virtual public iB { virtual void bar(); // etc... };

每当我创建一个 cB 对象,并返回 iB 接口指针时,cB 对象内部的继承关系,可以用个简单的图表示如下:

iB  +- cB
|      |
+      +
iA  +- cA

但是,我们在公开的 iX 接口定义中,使用了虚继承。这在不同编译器上可能有一些差异。如果组件写好后,动态库想拿给别人用,无法预知别人用的编译器,就会有问题。

嗯,这可能是个伪命题。或许不需要解决。但感觉最近有点犯贱。那么下面就讨论一下:怎么不在接口定义中使用 virtual 继承,而达到同样的目的呢?

我的方法是用 template 做一个中间层,然后手工写一些转发代码:

#define interface struct interface iA { virtual void foo() = 0; }; interface iB : public iA { virtual void bar() = 0; }; class cA : public iA { public: virtual ~cA(); virtual void foo(); // etc... }; template< typename IF > class tA : cA , public IF { virtual void foo() { cA::foo(); } }; class cB : tA { virtual void bar(); // etc... };

这样,模板 tA 解决了多继承后,接口实现的转发问题。(因为手工转发了 foo 的调用)

既然要转发,为什么要用多继承而不用组合呢?因为我需要 ~cA 正确的发挥作用。比如在 cA::foo 中可以正确的 delete this 。

下一个问题:如果还有一个 iC 继承于 iB ,然后在实现 cC 的时候,想复用 cB ,同时不想写太多的 api 转发代码。怎么办?

我初步的想法是,把 tA 模板的实现都改成虚继承,然后做一个 tB 模板。转发 cB 扩展的几个 api 。然后让 cC 从 tA tB 虚继承出来。随手写了一个,但编译有点问题,不想深入研究了。弄出个模板虚拟多继承,绝对是蛋痛啊。

研究 C++ 果然是浪费生命。

February 22, 2010

在 C++ 中实现一个轻量的标记清除 gc 系统

最近想把 engine 做一个简单 C++ 封装,结合 QT 使用。engine 本身是用纯 C 实现的,大部分应用基于 lua 开发。对对象生命期管理也依赖 lua 的 gc 系统。关于这部分的设计,可以参考我以前写的一篇 为 lua 封装 C 对象的生存期管理问题

当我们把中间层搬到 C++ 中时,遇到的问题之一就是,C++ 没有原生的 gc 支持。我也曾经写过一个 gc 库。但在特定应用下还不够简洁。这几天过年休息,仔细考虑了一下相关的需求,尝试实现了一个更简单的 gc 框架。不到 200 行代码吧,我直接列在这篇 blog 里。

这些尚是一些玩具代码,我花了一天时间来写。有许多考虑不周的地方,以及不完整的功能。但可以阐明一些基本思路。

首先我需要一个标记清除的 gc 系统,用来解决引用记数不容易解决的循环引用问题。它的实现不想比引用记数复杂太多,并有相同甚至更高的性能。

我不想使用复杂的 template 技术,利用太多的语法糖让使用看起来简单。如果需要让这些 C++ 代码看起来更现代,更有“品味”,我想也不是很难的事情。

接口和实现希望尽量分离,对用的人少暴露细节。但不拘泥于教条,强求做成类似 COM 那样的通用 ABI 。还是尽量利用 C++ 语言本身提供的机制,不滥用。

使用尽量简单,不要让使用人员有太大负担。

功能满足最低需求即可。代码容易阅读,使用人员可以很快理解原理,不至于误用。也方便日后扩展以适应新的需求。

代码如下:(可打包下载

/* * filename: i_gcobject.h * Copyright (c) 2010 , * Cloud Wu . All rights reserved. * * http://www.codingnow.com * * Use, modification and distribution are subject to the "New BSD License" * as listed at . */ #ifndef interfacce_gcobject_h #define interfacce_gcobject_h #define interface struct interface i_gcobject { virtual ~i_gcobject() {} virtual void touch() {} virtual void mark() = 0 ; virtual void grab() = 0 ; virtual void release() = 0 ; static void collect(); }; #endif

所有支持 gc 管理的接口都继承至 i_gcobject ,提供三个方法,

  1. mark 可以把这个对象打上标记,被标记的对象将不会被 collect 回收。

  2. grab 将对象挂接到一个被称呼为 root 的特殊 gcobject 上。

  3. release 将对象从 root 上取掉。

另提供 touch 的模板方法供 mark 回调,用来标记同一对象中的不同部分。

mark 方法一般在 touch 方法中使用,另外,collect 方法将主动调用 root 的 mark 。


/* * filename: i_gcholder.h * Copyright (c) 2010 , * Cloud Wu . All rights reserved. * * http://www.codingnow.com * * Use, modification and distribution are subject to the "New BSD License" * as listed at . */ #ifndef interfacce_gcholder_h #define interfacce_gcholder_h #include "i_gcobject.h" interface i_gcholder : virtual i_gcobject { virtual void hold(i_gcobject *) = 0; virtual void unhold(i_gcobject *) = 0; static i_gcholder * create(); }; #endif

i_gcholder 为 root 的接口,提供 hold 和 unhold 方法来挂接需要持久保留的 gcobject 。


/* * filename: gcobject.h * Copyright (c) 2010 , * Cloud Wu . All rights reserved. * * http://www.codingnow.com * * Use, modification and distribution are subject to the "New BSD License" * as listed at . */ #ifndef gc_object_h #define gc_object_h #include "i_gcobject.h" class gcobject : virtual i_gcobject { bool marked; public: gcobject(); virtual void mark(); virtual void grab(); virtual void release(); struct f_unmarked; }; #endif /* * filename: gcobject.cpp * Copyright (c) 2010 , * Cloud Wu . All rights reserved. * * http://www.codingnow.com * * Use, modification and distribution are subject to the "New BSD License" * as listed at . */ #include "gcobject.h" #include "i_gcholder.h" #include #include static bool gc_trigger; static std::vector gc_pool; static i_gcholder * gc_root = i_gcholder::create(); struct gcobject::f_unmarked { bool operator() (gcobject * value) { bool unmarked = value->marked != gc_trigger; if (unmarked) { delete value; } return unmarked; } }; gcobject::gcobject() : marked(!gc_trigger) { gc_pool.push_back(this); } void gcobject::mark() { if (marked != gc_trigger) { marked = gc_trigger; touch(); } } void gcobject::grab() { gc_root->hold(this); } void gcobject::release() { gc_root->unhold(this); } void i_gcobject::collect() { gc_root->mark(); gc_pool.erase(remove_if(gc_pool.begin(), gc_pool.end() , gcobject::f_unmarked()), gc_pool.end()); gc_trigger = !gc_trigger; }

gcobject 为具体的 gc 实现,实现了 mark 、grab、release 和 collect 方法。

  1. mark 采用的直接向一 bool 变量设置标记。这个标记利用了 trigger 这个乒乓开关,每次 collect 都会切换状态。

  2. grab 和 release 可以把对象挂接到 root 上,或从上取掉。

  3. collect 会主动从 root 开始 mark ,并释放那些没有 mark 的对象。


/* * Copyright (c) 2010 , * Cloud Wu . All rights reserved. * * http://www.codingnow.com * * Use, modification and distribution are subject to the "New BSD License" * as listed at . */ #include "i_gcholder.h" #include "gcobject.h" #include #include #include class gcholder : public virtual i_gcholder, virtual gcobject { std::vector hold_set; std::vector unhold_set; bool set_changed; bool hold_set_sorted; bool unhold_set_sorted; void combine_set(); virtual void touch(); virtual void hold(i_gcobject *obj) { hold_set.push_back(obj); hold_set_sorted = false; set_changed = true; } virtual void unhold(i_gcobject *obj) { unhold_set.push_back(obj); unhold_set_sorted = false; set_changed = true; } struct f_mark { void operator() (i_gcobject *obj) { obj->mark(); } }; public: gcholder() : set_changed(false), hold_set_sorted(true) , unhold_set_sorted(true) {} }; void gcholder::combine_set() { if (!hold_set_sorted) { std::sort(hold_set.begin(),hold_set.end()); hold_set_sorted = true; } if (!unhold_set_sorted) { std::sort(unhold_set.begin(),unhold_set.end()); unhold_set_sorted = true; } if (!unhold_set.empty()) { std::vector::iterator iter1 = hold_set.begin(); std::vector::iterator iter2 = unhold_set.begin(); while (iter1 != hold_set.end() && iter2 != unhold_set.end()) { if (*iter1 == *iter2) { *iter1 = NULL; ++iter1; ++iter2; } else { assert(*iter1 < *iter2); ++iter1; } } i_gcobject * null = NULL; hold_set.erase(std::remove(hold_set.begin(),hold_set.end(),null) , hold_set.end()); unhold_set.clear(); } } void gcholder::touch() { if (set_changed) { combine_set(); set_changed = false; } std::for_each(hold_set.begin(), hold_set.end(), f_mark()); } i_gcholder * i_gcholder::create() { return new gcholder; }

gcholder 理论上可以有多个实例,并相互挂接。(否则不需要继承至 i_gcobject )这个设计可以用来模拟多级的堆栈。但实际上并不需要这么复杂。因为在大部分应用里,如果你的程序有一个周期性的主循环,就可以不在 gc 系统里模拟出一个多级的堆栈。我们只用在循环之外做 collect 即可。再堆栈调用的较深层次触发 collect 反而效果不佳,会导致许多临时 gc 对象无法回收。


最后来看一个玩具代码,用 stl 里的 mutliset 实现了一个简单的树接口。可能没有什么使用价值,但它演示了一个较复杂的对象相互引用的关系。并可以展示 gc 如何正确工作。

/* * filename: test.cpp * Copyright (c) 2010 , * Cloud Wu . All rights reserved. * * http://www.codingnow.com * * Use, modification and distribution are subject to the "New BSD License" * as listed at . */ #include "gcobject.h" #include #include #include interface i_tree : virtual i_gcobject { virtual void link(i_tree *p) = 0; static i_tree * create(); }; class tree : public virtual i_tree , virtual gcobject { tree *parent; std::multiset children; struct f_mark { void operator() (tree *node) { node->mark(); } }; virtual void touch() { if (parent) parent->mark(); std::for_each(children.begin(), children.end(), f_mark()); } void unlink(); virtual void link(i_tree *parent); public: tree() : parent(NULL) { printf("create node %p\n",this); } ~tree() { printf("release node %p\n",this); } }; void tree::unlink() { if (parent) { parent->children.erase(this); parent = NULL; } } void tree::link(i_tree *p) { unlink(); if (p) { tree * cp = dynamic_cast(p); cp->children.insert(this); parent = cp; } } i_tree * i_tree::create() { return new tree; } int main() { i_tree *root = i_tree::create(); root->grab(); i_tree *node; node = i_tree::create(); node->link(root); node = i_tree::create(); node->link(root); i_gcobject::collect(); printf("collected\n"); node->link(NULL); i_gcobject::collect(); printf("finalize\n"); root->release(); i_gcobject::collect(); return 0; }

我们在实现一个基于 gc 的对象时,可以先定义出需要的接口,让接口从 i_gcobject 继承。例如上例中的 i_tree

然后在实现这个接口时,可以虚继承 gcobject 。例如上例中的 tree

如果有需要,就重载 touch 方法,在 touch 方法中 mark 相关的 gcobject 。对于 tree 这个例子,就是调用父亲和孩子节点的 mark 。

对象依然可以写析构函数,相当于对象的 finalize 。在析构函数中,不要再释放和它相关的 gcobject ,那些留给 gc 系统去完成。(例如在 tree 里,就不要在 ~tree 中 delete children 容器中的变量,也不需要把自己从父亲节点上摘掉)


如果仅仅只是使用那些接口,则不需要再包含 gcobject.h ,因为 gcobject 的细节只供实现 i_gcobject 时使用。

February 20, 2010

搬家

这个 blog 最初架设在我工作中管理的一台服务器上。感谢公司允许我这么干。(据说很多公司是不允许的)

前段时间,我管理的另一台服务器硬盘出了些硬件故障。据了解,这批机器已经超长服役,早该退休了。反正迟早要挪数据的,我就顺便自己租了个 VPS ,把 blog 搬了过去。

跟其它独立 blog 比,我已经很幸运了。因为机器在公司自己的机房里。机房管理人员都是同事,关系也不错。甚至曾经有段时间,他们中有几人还直接挂在我的部门名下,名义上是我的下属。

早几年刚刚要备案时,是同事转告我一定要去办理那些手续,因为有人查到我的主页挂的 ip 来至于公司的机房。我也很快办理了手续。后来几年,除了接过一个电话,核对我的备案信息,一直相安无事。

网上最近风传查得严了,朋友也劝我在国内自己租个 VPS ,我想想也不错。

大部分人都向我推荐 linode 。我自己测试了一下速度,的确不坏。我下载可以达到 200KB/s ,ping 值也在 300ms 以下。

这个带宽够我翻墙看 youtube 了,非常不错,我已经很满意。

我选的是 linode 在 NJ 的 IDC 。听说 CA 的 IDC 要稍微更好一些。只是那天申请的时候没有空位。(第 2 天又有了)貌似现在换 IDC 也很方便。以后增加 IP 很容易,万一被墙了,应该可以换个 IP 的。

linode 的管理界面非常友好,我很喜欢。装系统也就是 3 秒钟的事情。各种设置都很贴心。一个月 20$ 的价格我觉得还是可以接受的。如果推荐朋友使用,可以拿到 20$ 的回扣。所以,如果还有朋友也有租 VPS 的念头,可以点这个链接。或者填我的 referral code : 538bab39bc1265a2ce54115d1f86e2bc81e4d133

这只是一个推荐 :) 如果有人填我的 referral code 我会很高兴。支付很方便,我用我的招行信用卡很顺利的就付了款。从申请到开通,装好系统,没超过五分钟。


保险起见,我把我的域名也从万网转移到了国外。我选的是 name.com ,支付同样方便快捷。

域名转移过程比网上朋友抱怨过的那些要稍微顺利一点。万网的同学还是比较积极的,服务我还算满意。在网上在线留言就解决了我所有问题。最后打过电话确认后,又用 email 沟通了两次,就搞定了。

不过中间快递过两份材料才把转移认证码发给我,这个流程让我有点不爽。转移搞定后,我登陆 name.com ,发现人家都是把 Auth Code 直接列出来的,不需要这么繁杂的手续去索取。

February 17, 2010

虚杯以待

这次韩寒和刘谦各写了一篇 blog 而之后又隐掉的事情,我是在 greader 从和菜头那看来的

我喜欢韩寒的直爽,如果有机会认识,我会想去交这样一个朋友。

这次看了刘谦写得这篇 blog ,我发现他也是一个很不错的人呐。

有人问起,“先把杯子里的水倒掉才行” 这个禅宗典故是从哪里来的。我也懒得自己敲了,转一段

南隐是日本明治时代著名的禅师,他的一杯茶的故事常常为人所津津乐道并予以启发。有一天,一位大学教授特地来向南隐问禅,南隐以茶水招待,他将茶水注入这个访客的杯中,杯满之后他还继续注入,这位教授眼睁睁地看着茶水不停地溢出杯外,直到再也不能沉默下去了,终于说道:“已经满出来了,不要倒了。” "你的心就像这只杯子一样,时面装满了你自己的看法和主张,你不先把自己的杯子倒空,叫我如何对你说禅?"南隐意味深长地说。

之前,我也写了一篇关于刘谦那个魔术的 blog 。其实是没有任何立场的。

只是喜欢这个魔术,并且好奇,所以猜了一下怎么做到的。这种猜测,应该也是观赏魔术表演的乐趣之一。

如此而已。

February 15, 2010

缘分天注定

昨晚睡不着,随便找了本老片子看。 Serendipity

情节很假,但是假得让人喜欢。

结尾的时候那段讣告,我震惊了。看来做字幕的大哥对这部片子相当有爱啊。

  庄纳顿.卓加…… 
  有线体育台优秀监制 
  既失红颜,复破婚盟 
  肝肠寸断,遂于昨宵撒手尘寰 
  享年三十有五 
  庄君文质彬彬,公而忘私 
  平素言行,绝无痴态 
  孰料,阳寿将尽之际…… 
  流露鲜为人知的浪漫 
  刹那惊艳,化作萦回梦魂 
  遍觅芳踪,历尽一波三折 
  本性情怀,浮现无遗 
  呜呼,踏破铁鞋…… 
  方觉一番心事悉付东流 
  语云:匹夫可以夺其爱…… 
  不可夺其志 
  庄君依然坚信…… 
  人生际遇绝非偶然 
  冥冥中,上苍早已…… 
  安排周详,丝毫不爽 
  其友文坛巨擘…… 
  时报主笔甸恩谓…… 
  庄君晚年脱胎换骨 
  有所顿悟 
  一言以蔽之…… 
  欲臻天人合一…… 
  须坚信一字 
  此字古已有之 
  于今亦然,唯“缘”而已

被感动了。

英文原文写得也很好:

Johnathan Trager, prominent television producer for ESPN, died last night from complications of losing his soul mate and his fiancee.

He was 35 years old. Soft-spoken and obsessive, Trager never looked the part of a hopeless romantic.

But, in the final days of his life, he revealed an unknown side of his psyche. This hidden quasi-Jungian persona surfaced during the Agatha Christie-like pursuit of his long-reputed soul mate, a woman whom he only spent a few precious hours with.

Sadly, the protracted search ended late Saturday night in complete and utter failure. Yet even in certain defeat, the courageous Trager secretly clung to the belief that life is not merely a series of meaningless accidents or coincidences.

Uh-uh. But rather, its a tapestry of events that culminate in an exquisite, sublime plan. Asked about the loss of his dear friend, Dean Kansky, the Pulitzer Prize-winning author and executive editor of the New York Times, described Jonathan as a changed man in the last days of his life.

“Things were clearer for him,” Kansky noted. Ultimately Johnathan concluded that if we are to live life in harmony with the universe, we must all possess a powerful faith in what the ancients used to call “fatum”, what we currently refer to as destiny.   

February 14, 2010

关于那个手穿玻璃

今天很俗的看了春晚。抱着本连着 wifi 上着推看的。

刘谦的魔术不错,最后擦玻璃的时候预感要表演手穿玻璃了。但是看到了还是觉得很神奇。

仔细想了一下,玻璃上应该有洞的。肯定这一点,就对一开始为啥弄个圆桌子坐一圈恍然大悟了。

玻璃应该是双层的,可以梭动。这样才能一开始把洞藏起来。这样一大圈,就方便藏那个洞了。只要玻璃干净,转得时候就很难发现了。借助表演者的胳膊掩饰,就可以把洞从旁边梭过来。

一直都听人说,春晚赵本山的小品可以看看。这些年都没看,今年难得坐在电视机前看了。唉,都什么品位啊。


最后,转个同事推的段子:

除夕已过,圣战日已到,诸君,不管你是魔法使, 高阶魔道士,步兵还是退役的骑兵, 为了团部, 勇敢的战斗吧,去死去死团万岁...

大家,节日快乐 :D

Live long and prosper ! \V/,

February 08, 2010

关于招聘

承蒙大家厚爱,到现在,我已经通过 email 收到了 21 份简历。不过两天时间就能收到这么多,而且看得出大家都是很热情,大多数不仅仅附了简历,还专门写了好多文字。还真是受宠若惊啊。

很不好意思,我只回复了两份,别的都还没有动键盘回复。一是周末在打游戏(平时实在没空),二是需要和同事讨论一下才有结论。我们工作室 11 号放假,10 号晚上办公室就没人了。也就这两天会比较所有的简历吧。

嗯,已收到的简历我都看完了(真是花了不少时间呢),明天会发给工作室其他写程序的同事看。我想若安排面试,就是年后的事情了。我们大约在 21 号正式上班。

请原谅我这个周末的懈怠。接下来我会尽力回复 email 的。

FF13 剧情完成

终于盼到周末,把 FF13 熬夜完成了。

游戏时间大约在 50 小时左右,没看任何攻略,没有炼级,没有改武器,基本一路冲到底的。就是几个 boss 稍微难一点,retry 了几次,大体上对于熟练 RPG 玩家没什么难度。

然后接着就是回头做 64 个冥碑任务(现在完成了 20 多个,其中只做了一个 A 级的比较难),开传送点等等。我估计把盘走完,还需要至少五十小时吧。

这次剧情依旧不错,我比较喜欢,但不是最好。(FF 10 和 FF 7 我更喜欢一点)我的日文水平是入门级里的入门,理所当然的,好多地方没看明白。只能连蒙带猜了。生日那天,居然传出官方消息说是五月要出中文版,这算是今年的生日礼物吧 :D 一定会买来再玩一次的。

角色里三个 mm 都不错,任挑一个我都喜欢。不过感觉这次有点恶趣味。虽然没明白做百合向的,我想有几个镜头也是故意做的满邪恶了 :D

战斗系统的进化我很欣赏。敢于放弃传统,把战斗的爽快感做出来而不失简洁和策略。强化 Break 系统真是让人拍案。虽然从里面可以看到 FFX / XI / XII / X-2 / VII CC 等等的影子,但放在一起再加一点点就是个全新的东西。融合的很好。

人物基本的可升级数值设定只留下 HP 物理 魔法 三个值(加上 ATB 槽和职业两个等级),我觉得是要莫大的勇气的。不过想想也够了。即可以做得丰富,又不多余。

我想 ATB 进化到这个样子,我们已经不能单纯把 RPG 游戏分成即时和回合制了。也并非如许多策划念叨的:回合的间隔缩小就成了即时。FF13 每次做出操作选择的时间间隔并不短,但流畅度,紧张感,爽快感却并不见少。

其实 FF13 里吸收的元素都不是新鲜东西,只是很融洽的组合在一起罢了。

February 05, 2010

招聘程序员

今天发个不太正式的招聘信息:

我们这里工作室,需要招聘两名程序员。在未来至少一年的工作是:专职制作维护 3d engine 中的模型编辑器和场景编辑器。(每个一人维护)

要求:

  1. 熟悉 C 语言,或 C++ 。至少有一门动态语言的经验,lua 最好,python 或其它也可以。

  2. 能够全心制作编辑器(至少一年),最好有相关经验(非必须)。

  3. 有 3d 方面编程经验优先,但不是必须。需要有学习 3d 编程的兴趣和动力。

  4. 有任意一款 GUI 框架编程经验者优先。(例如 QT )

待遇方面,不会太高,但也不算低。所以特别牛的同学就免了,可能满足不了你的要求。就算愿意,我也心里不舒服。

完全抱着从头学习的想法的同学也需要考虑一下。我们还是需要有一点点经验的,不可能花半年时间慢慢培养到可以开始动手干活。(经验指:可以独立编写 GUI 程序)

有兴趣的同学可以给我发 email 附上简单的简历。

附:工作地点在杭州。一周五天工作,无强制加班。入职公司是网易(杭州)。

我们是小团队,工作室制。我是团队负责人,全权负责团队一切事务。工作室下属于网易游戏部门(网易互动娱乐)。

团队以研发为主,暂不涉及游戏运营等。