« November 2012 | Main | January 2013 »

December 29, 2012

房租分配问题

今天读到策划同学的周报中提到的一个关于合租房子的分摊房租问题。

引用周报中的一节如下:


上周在搬家,和喵、刘阳一起租房子住,遇到一个问题,就是分摊房租。中式的解决方法一般都是商量一下,但具体怎么商量,没有手段,总之就是大家估摸一下,觉得大略上说的过去就OK了。很少有拉下面子认真谈价格的,即使心里其实觉得并不认可。

在这方面,美国人还真能想一些办法,这是一个旅美的留学生在博客上写的,他和老美同学的商议方式: 两个人A,B合租一个二居的房子,比如每个月是1500美元,因为主卧和次卧有大有小,价格肯定是不均的,那么两个人分别写两个价格,也就是对主卧和次卧的心理价格。可以很极端,比如1400:100,但总额必须是1500,因为这是A,B必须接受的大条件,然后公开,除掉开价完全相当的情况,两间卧室必然各有一个出价最高的人,价高者入住,而月租则是A,B对这个卧室开价的均值。例如A出价是900:600,B出价是1000:500,那么A住次卧,价格为550,B住主卧,价格为950。两个人都得到了自己认可的房子,而价格还低于自己的预期。

这一方案还有一个优势,就是双方都无法通过恶意的叫价来损害对方,获得利益。相信很多同学会提出一个更直接的解决方案:一个人提价格方案,另一个人选择。但是这一方案也有点问题,提价格的人相对是吃亏的,对吧?

遗憾的是,这种做法,似乎无法推广到三个人的情况。


我觉得这个问题很有趣,晚上想了一下,其实是可以推广到三人甚至更多人的。

先说这个原始策略为什么让双方都感觉合理:

从经济学角度看,每个人都希望别人多分摊一些房租。你对一个房间了价值有一个评估,如果别人高于这个价格租下这个房间,对你就是有利的。原始方案正是如此,让双方都觉得最终方案对自己有利。我们基于这个基础来推广这个方案就可以了。

首先, 两个人对两间房间的分配策略,原始策略已经做的很好了。我们要做的仅仅是把三个人分三间房划简到两个人分两间房的问题。(其中不考虑竞价相同的问题。原始方案也没给出解决方案,可以重新来一次,也可以投骰子等。)

对三人策略,我的方案是: 三个人分别写下自己想要哪一间房间,并想对这个房间出多少价。

结果有三种 :

  1. 三个人对同一个房间竞价。这种情况比较好解决,我们直接让出价最少的人出局。然后出价最高的人得到这间房间,其价格是前两个价位的平均值。 接下来的问题就回到了两个人选两个房间的问题了。

  2. 有两个人对同一个房间竞价,另一个人选了别的房间。这种情况也好解决,直接让选相同房间的两人中价高者得,价格是两者平均值。

  3. 三个人各选了不同的房间。这种情况不太好处理,下面仔细分析。

假设 甲 选了 1 号房间,给出了价格 A ;乙选了 2 号房间,给出了价格 B ; 丙选了 3 号房间,给出了价格 C 。

我们先去掉 A B C 中绝对值最小值, 比如是 C 。 保留 1 号房间的 A 报价和 2 号房间的 B 报价。

其实可以这样看:甲和乙一起倾向于用 A+B 的价格租 1 2 号房间。那么他们集体对 3 号房间的估价是总价 - A - B 。

有很少的情况, 这个估价会变成负数。如果碰到了, 我认为可以简单的认为是甲和乙对 3 号房间的估价为 0 。

之后,就可以按均价原则定出 3 号房间的价格为 C 与 (总价 - A -B) 的平均值,并以这个价格租给 C 。

剩下的又变成了两个人分配两间房的问题了。 :)


补充:有同学认为,如果 A+B+C 低于总价的时候,case 3 会使得 C 最终价格超过他的预期,不太合理。

我认为这是合理的。因为大家可能都想用高价得到好的房间;也可能由于经济原因,希望尽量少分摊房租而倾向住不那么好的房间。

当 A+B+C 低于总价的情况发生时,往往是后一种情形。(否则,多数人会出高于均价的价格)

当所有人都出低价的时,出最低价的人应该承担略高的价位,这是你在出很低价格的时候要考虑的后果。也就是恶意出不合理的价格,伤害的是自己而不是别人。

December 25, 2012

模糊逻辑在 AI 中的应用

今天收到人民邮电出版的杨海玲同学寄来的几本书,首先感谢一下。看来短期内是没有那么多精力全部去读了,所以先随便翻翻感兴趣的章节。

在《游戏人工智能编程案例精粹 》 和 《 Windows 游戏编程大师技巧 》 中都分别有一章谈及模糊逻辑。记得前几年我的同事 Soloist 同学曾经研究过一小段时间,给我做过简单介绍,我便仔细把这两章书读了一遍。感觉都是点到为止,所以又翻了一下 Wikipedia 的 Fuzzy Logic 的介绍。午饭时跟做 AI 的同事交流了一下,觉得可以做一点笔记记录理解的部分。


在游戏人工智能编程中举了个实际的例子来说明这个问题:在一个 FPS 游戏中,NPC 有多种武器可供选择,这些武器的威力、射程等有所差异;策划决定根据 NPC 和玩家的距离以及 NPC 武器弹药的余量,两个因素来考虑 NPC 当下应该选择哪一种武器对抗玩家。这个例子不错,不过我有另一个自己的例子:在 MMO 中,NPC 在追击玩家时可能会考虑几个因素:离开他的出生点的距离,以及玩家的实力,或是自己的 HP 量等等。下面我用自己的例子来说明问题。

先甩开模糊逻辑是什么不谈,我们在做 AI 定制的时候会遇到怎样的很难决策的问题呢?

策划往往会定义规则:当 NPC 距离出生点很远时,他们停止攻击玩家并返回出生点。

这里有一个问题,即使是最新手的策划也看得出来,这个规则描述是不严谨的。到底什么叫“很远”。所以通常,需要加一个定义,追击半径,变成“当 NPC 距离出生点超过追击半径时,他们停止攻击玩家并返回出生点”。然后,在策划表格里估计就会多出一个表项:追击半径 40 米 之内的东西。

顺便吐槽:把设计这个规则(追击条件),和填写这个数字( 40 米追击半径)的工作分开,分别称做系统策划和数值策划,我怎么看都是件极不靠谱的事情。

程序不难实现,规则也很清晰。可如果规则多于一条,就不那么简单了。

假设我们加了一条规则:当 NPC 的 HP 很少(比如以 20% 为界限),他们不再追击玩家。

NPC 做决策就需要考虑两个因素,这两个因素导出的决策结果可能截然相反,这时怎么办更合理?比如,当 NPC 的 HP 充足时,它们应该追击玩家;可 NPC 可能离开出生点已经太远了,以距离原则来讲,它又应该放弃追击。

在黑白分明的逻辑判断角度讲,我们可以简单的定义出严格的规则。比如两条规则去交集,只有都满足才追击,否则放弃。但你能发现其中不合理之处么?

如果我们以 20% 的 HP ,或距离 40 米为分界线。某一个条件上,19% 和 20% 的细微差别,或是 39 到 40 米的变动,都足以改变决策结果。但是又有些看起来巨大的差异,比如 1% 到 19% ,或是 1 米到 39 米却对结果无影响。

一个更生动(更接近人)的决策行为可能是这样的:当其中一个条件处于边界左右时,我们更多的考虑另一个条件的影响。比如,当 NPC 的 HP 在 20% 左右时,无论是 19% 还是 21% 都对决策结果影响不大,这时,距离成了更重要的参考依据。反之亦然,无论 NPC 离开出生点 39 米还是 41 米,影响决策结果更多的是他的 HP 值。

要做到这点,不是简单的 if else 就可以达到效果的。无论用什么方案,都会增加 AI 的计算量。决定在何种环境下为哪些 NPC 附加这种 AI 是另一个问题。我们先不谈成本,仅说说怎样做到。

我的第一反应和中午饭局上许多同学的反应一样,为每个决定因素加上一定的概率因子。不再以 40 米为分界非 0 即 1 ,而是让追击概率成为和距离有关的一个函数。把几个影响决策结果的因素的概率全部算出来,然后用随机数去模拟决策过程。

但,模糊逻辑并不依赖随机的模拟。简单说,利用模糊逻辑决策,所有的输入都确定的情况下,一定会得到确定的结果。所谓的模糊指那些边界条件是模糊的。

一般我们会给每个变量定义三档强度标准,比如对于距离远来说,我们可以定义 10 米算近,20 米算有一定距离,40 米算远。那么低于 10 米都算很近了,而超过 40 米都算很远了。但介于 10 米到 20 米之间的,即可以算很近也可以算有一定距离;而 20 米到 40 米之间则可以算有一定距离,称之为远亦尚可。画在坐标图上,是左右两个半梯形,和中间一个三角形的形状。事实上,用曲线来表现也可以,但不利于计算,一般不会采用。

从数值上分析,可以看作,低于 10 米就 100% 算近,而远于 40 米则 100% 算远。处于 20 米的地方则 100% 算保持一定距离。中间状态则是平滑过渡的。

然后,对结果的期待程度也可以分成三类。就追击而言,可以分为,完全不想追击、希望追击、强烈希望。用同样的方式定义出来。

最后,我们可以排列出所有的规则,比如:

当离开出生点很远,且 HP 很少时,完全不想追击。 当离开出生点一定距离,且 HP 很多时,强烈希望追击。 当离开出生点一定距离,且 HP 适中时,希望追击。 等等。

由于我们有两个条件,每个条件有三档状态,所以完整的规则会产生 9 条。定义这些规则需要策划的专业知识。但略微有偏差的规则不会让结果突变,缺少一些条件和规则,计算也能进行下去。

我们把参数依次代入所有的规则,可以得到决策结果分别处于“完全不想,希望,和强烈希望”三个类别中的置信度。这里,一条规则中引用两个条件时,我们取教小的值。假设,当 NPC 距离出生点为 30 米时,对于“很远”的置信度为 0.5 ;而 HP 为 30% 时,对于“HP 很少”的置信度为 0.4 ;那么,对于完全不想追击的置信度为 0.4 和 0.5 中较小值 0.4 。

计算完所有的规则,取那些重复的决策结果中较大的一个。即若超过一条规则的推导结果是强烈希望追击的话,就取最大的一个数。最终,我们会得到“完全不想、希望、强烈希望”三档分别的置信度。

到底最终决策的期望度有多高,这是一个被称为去模糊化的过程。简单的说,它是对前面得到的三挡置信度形成的图形取质心的过程。我不想详述。

采用模糊逻辑,可以让 AI 在同一时刻能做出的所有反应都计算得到一个期望实施的程度值。如果我们可能需要 NPC 去判断,是应该追击玩家,还是原地防御,或是逃跑做一个选择;又或者要根据自己的 HP MP 量,对手的状态等,在自己可以释放的若干技能中做一个合理选择;那么,只需要单独为每个决策都按配置好的模糊规则做一个计算,选择最强烈的倾向即可。

可以看出,采用这种方法,决策结果和决策条件是严格对应的。对于决策条件较少的情况,我们完全可以用配表的形式,以及设计调整合理的公式的方式达到同样的效果。但是随着决策条件的增加,复杂度会发生组合爆炸,人力不可控制。


当决策条件太多时,一个明显的问题是规则的组合爆炸。我们需要描述每个条件处于每种状态时,决策结果应该是怎样的。而 Combs method 可以帮我们应对这种情况。通俗说,我们可以把“当离开出生点很远,且 HP 很少时,完全不想追击。”这条规则分解成两条独立规则“当离开出生点很远,完全不想追击”以及“当 HP 很少时,完全不想追击。”

用这个方法简化规则,有时会产生矛盾或是违反直觉。但实践表明,用这种一维的规则列表,会得到与前面那种完整的规则组合非常近似的结果。


ps. 我没想过通过短短一小篇文字就把这个问题讲清楚,因为那是不可能的。学习从来不是件轻松的事情。

December 14, 2012

Luacc

因为内存限制问题, 我们暂时放弃了 luajit 。这两天,我想另辟蹊径找到别的方法去加速 lua 程序的运行。

所以我这两天做了这么一个玩具,试一下是否可行。

luacc 是类似 cython 的东西,它允许你在 Lua 代码中直接写 C 代码。由于是用 tcc 运行时编译运行的,所以你可以获得和 C 一样的效率。(同样,C 语言引入的问题也同样要考虑)

之所以我称之为玩具,是因为它现在还不支持复杂的数据结构。你只能把单层的,以 string 为 key 的 lua table 映射到 C 代码中(表现为一个 user type ,其实是一个 C struct )。目前还不能用数组做数据交互。

它可以利用一个内建类型 object 来持有传递 lua 的对象,但不能操作它。

有兴趣做进一步完善的同学,可以去 github 自取

December 12, 2012

一个 Lua 内存泄露检查工具

昨天我们发现每日构建的服务器突然在一个晚上内存暴增了 8 G ,显然是发生了内存泄露。

之前,我们在 skynet 里留下了许多调试协议,使我们很快的确定了发生泄露的服务:在一张地图的 lua State 中。可以确定是地图的 lua 实现中,有些 lua 对象在不断的生成。生成速度不快,但确实没有人解开引用,导致内存持续增长。

曾经有很多人做过 Lua 的内存分析工具,但是我懒的去搜了,花了半天时间自己写了一个。(已经开源在 github 上

原理是这样的:

这个叫作 snapshot 的库,只提供一个函数,它可以对当前的 Lua State 做一个完整的快照。但由于我并不像做 Lua State 的序列化工作(虽然做法很像),为了减少分析数据,只记录了复杂对象的引用关系。

即,记录下所有 table thread userdata function 间的引用。

我们可以在不同时间,对 Lua State 拍两个快照,相比较后,就很容易知道新增加的内存处于何处了。

一开始我想用 lua 来编写这个工具,其实也的确做的到。但如果有 Lua 写就得相当小心,因为 Lua 代码的运行过程本身会影响 State ,即,你想对它观察,就可能改变它。

用 C 直接调用 lua API 来遍历 lua State ,影响要小的多。

最终得到的对象引用关系数据其实很丰富,但因为我希望结果对 State 的影响小一些,就把一些信息合并成字符串储存到结果表内了。 snapshot 的返回结果只是一张简单的 table ,每个 Lua 对象,都以指针(lightuserdata) 为 key 储存在表中;对应的 value 是一个 string 足够详细的描述了这个对象的引用关系。

比如例子中的 dump.lua 这段程序,运行后就会得到这样的结果:

userdata: 0052E760      table
00521810 : tmp : dump.lua:7

userdata: 0052EB98      table
00521810 : S1 : dump.lua:7

这表示,两次 snapshot 间,增加了两个 table 。它们是被运行在源代码 dump.lua:7 处的函数中 tmp 和 S1 两个变量引用住了。


snapshot 生成的报告不太利于人阅读,但你可以写代码做进一步分析。比如可以逐级建立 table 的引用关系,用更人性的方式展现出来。由于只是一个临时工具,它也已经快速的帮我们定位了昨天的内存泄露问题所在,暂时我就不花精力去完善它了。

December 07, 2012

登陆认证系统

最近在忙代理项目 狂刃 的事情。

因为这个项目,我们提前建立了平台开发团队。但许多东西开始的都很仓促,比如需要对接用户登陆认证系统。

虽然已经有很多成熟的认证协议,比如最有名的 kerberos 。但这次时间紧迫,我就临时设计了一个简单协议。

因为不是 web 应用接入,所以我不想直接使用 https 来提交用户名和密码,而基于 http 协议,在不安全信道上建立了一个自定义协议来应付一下。这种临时设计的协议当然不会很缜密,但也基本够用。

这样,合作方的客户端可以较容易实现相应模块。

在整个业务中,有三个实体:

C 玩家客户端 G 游戏服务器 E 我们的认证平台

登陆流程是这样的:

  1. G 发现有 C 企图登陆时,生成一个一次性的 salt (如果直接用时间的话,可选对称加密),然后发送给 C

2, C 收到 salt ,利用自己的用户密码,对 salt 加密并签名。加密可以使用标准的 DES 算法,签名可以用标准的 MD5 算法。具体方法是:讲用户密码先 md5 hash 一次,得到一个串。取串的前一半做 DES 加密算法的 key 加密 salt ;然后把结果连接上密码 hash 串的后半部分,一起做一次 MD5 。密文和签名连接在一起,最终会得到一个加过签名的密文 secret 。

  1. C 将第 2 步生成的 secret 连同自己的登陆名,以及需要登陆的游戏名发送给 E 。

  2. E 收到 secret 后,从登陆名查询到用户信息,用保存在 E 上的密码做反向操作,验证签名是否正确。若正确则解出 salt 。失败则发送错误信息给 C ,登陆流程结束。

  3. E 利用解开的 salt ,附加上用户 id 。利用游戏名查询到事先和 G 约定的游戏服务器密码,做同样流程的加密签名处理,发送回 C 。

  4. C 转发认证密文回给 G 。G 用约定要的服务器密码,校验签名并解密,核对 salt 是否是在步骤 1 里发送出去的 salt ,确认用户的合法性。

整个流程,G 和 E 不必保持通讯,只需要事先约定要密码。这可视做一个简化版的 kerberos 协议,其中有一些安全隐患这里不一一指出。目前暂且可用。日后再来完善。

G 和 E 不保持通讯有一个好处:G 不需要对 C 保持连接状态,而只需要最终检查一下 C 发过来的认证包就可以知道认证是否通过。


设计完毕后,我用 C 实现了基本的校验认证函数。老马同学整合到他用的 web server 中,完成了认证服务器。并做了压力测试,结果还不错。

可惜合作方是用 Windows 做开发的,不方便联调。他们在开发期不希望依赖我们的用户系统,而我们的平台系统即使给他们,在 windows 下短期也较难配置起来。

我一拍脑袋,决定帮合作方现写一个认证用 web server 在 windows 下跑。对于我来说,最称手的某过于 lua 了。利用现成的 lua socket 模块,整个认证服务器只需要不到 100 行代码 。把用户名密码直接配置在 lua 源代码中,放到一个 table 中即可,数据库都不需要。反正是调试嘛。

一开始图简单,直接 bind/listen 了 web 端口,然后没 accept 一个认证请求,就处理一个。用阻塞方式工作。我觉得也就是调试一下流程,调通了后就可以用老马做好的正式认证服务器了。

可写完之后,手又痒了。感觉不支持并发很看不过眼,赶紧重写了。


幸亏这次重写,让事后扛住了一次压力。

昨天,合作方花前请了一个公会 600 人帮忙做了一次压力测试。这次测试他们认为是自己的事情,就没有通知我们。结果因为要用他们自己的用户系统(直接对公会放号),就没有联系老马,而是用了我那个拍脑袋临时写的测试用认证服务器。

还好做了并发处理,没在这个问题上翻船。不过昨天还是出了点小事故,在晚饭间接了好几个电话才搞定。

由于一开始考虑到是测试用,我把服务器 bind 地址默认配置成了 127.0.0.1 。结果合作方直接改成了服务器的配置 ip 。然后许多玩家认证不过,客户端编写的时候又没有足够的提示信息。搞了老半天才清楚,服务器配有电信和网通双线,所以是有双 IP 的。最后配置改为 0.0.0.0 搞定了这个问题。


12 月 12 日补充:

关于中间人攻击的问题:

我们需要防范用中间人代理玩家的所有通讯,在玩家完成认证后,取代玩家做一些事情。

为了防止玩家和游戏服务器间的通讯包被篡改,我们需要对游戏服务器和玩家客户端间的通讯协议加密。加密的首要步骤,就是在握手时,交换一个秘密,之后用这个密码对数据加密。

为了安全的交换密码, SSL 这种协议使用的是非对称加密,以及 CA 信任链完成的。但我们这个应用环境下,却有一个额外的优势:玩家和服务器间已经有了一个秘密(用户密码),不需要用复杂的方式交换。

但直接用用户密码做通讯加密是不可行的。因为游戏服务器并不知道这个密码,只是认证服务器知道。

那么,可以这样:在认证环节,认证通过后,认证服务器产生一个随机数,并同时用玩家密码和服务器密码加密两份,返回给玩家。

如果这个环节存在中间人,中间人不可能知道随机数,也无法伪造出两份密文蕴含同样的随机数。

玩家解出属于他的通讯密码(随机数),并把另一份转交给服务器。这样,游戏服务器和玩家间就安全的交换的通讯密码了。

December 06, 2012

网络游戏中商人系统的一点想法

在网络游戏中倒卖货物是一大乐趣(如果经济系统做的不坏的话) 。我见到许多 wow 玩家以此为乐,在国产游戏中,以梦幻西游为首,开店摆摊也让人乐此不疲。我最近对此有一些想法,和我们公司的策划交流以后,发现几句话很难说清楚。

大部分人第一反应都是增强版的拍卖场,比如增加求购系统,或是更方便的拍卖寄售系统,各种其它的拍卖场增强。

对于稀有品,我认为现在 wow 的拍卖场已经可以解决大部分问题,是不需要替换的。但一般商品,我认为应以增加流通,方便需求类玩家为主。最简单的方案是让系统根据商品流通速度来自动调节价格,系统统一销售和收购。但我们也知道光靠这种计划经济是很难得到一个合理的市场环境的,也少了许多玩家参与的乐趣。

所以我才有了下面这些想法:

玩家可以学习一个技能开始向系统盘店. 玩家可以决定这个店卖些什么. 可以卖的商品种类是有限的,比如 5 种. (并可以提升商业等级增加) 同时向系统申请一个商店货仓,以存放商品。

所有玩家的店其实对于卖家来说是在一起的. 如同 wow 的拍卖场, 卖家可以看到所有商品, 并不需要知道从谁的店里卖到的. (除非对买家有商品分类的需要,才考虑提供实体摆摊这样的设计)

商家可以决定他的店里卖的商品的收购数量(必须达到设定的数量),最高收购价格, 最低卖价. 统一提交给系统. 玩家把战利品卖给收垃圾的 NPC 时,其实是和这整个系统关联的. 会有一个关联收购价.

系统定期收集整个世界所有店的收购总数和收购价, 经过系统抽税后, 卖给整个收购市场. 然后所有收购到这批货物的商家和平摊这笔货款, 也就是说,无论你收购预定价是高还是低, 都不会比同期的商家收购价有所不同. 但是你的预期收购价过低,有可能导致补不全货.

系统补货完毕后,会根据集体的出售价经过抽税后, 决定一个商品在这个时期的销售价. 然后玩家在购买这类商品的时候, 会在一段时间内以一个固定价格买到. 玩家买到货物, 并不直接打款给商家, 而是等待到一个周转期后, 按平均规则打款.


资本需求和使用需求在整个游戏系统中是严格分离的. 从表现上简单说就是:

即使你从事大豆买卖, 你承接了系统的大豆收购和销售任务, 并从中获利(或承担损失风险) , 当你需要大豆自己吃的时候, 也不能从你收购的大豆里拿一部分出来自己用. 你一样要从系统那里买, 也就是间接从自己仓库里买的. 从事大豆业务的时候, 大豆本身是不会进入你的私人银行仓库的. 商人货仓作为一个玩法的设计是独立存在的.

如果真要类比的话,就是现实里你可以炒大豆期货, 但你要自己磨豆浆还是要去超市买大豆的. 你炒大豆的单可以获利,也可以影响大豆的市场价格. 不同的是,我们并没有提供期货类似的东西, 没有人可以炒单.

对于低等级装备这种在 wow 里几乎不可能存在于拍卖场的东西. 装备等级相同的东西可能有属性不同以适应不同的职业. 对于商人来说,是统一收购的. 比如我要收购 40 级装备 10 件, 那么不具体指定收购什么. 你收购到什么也是不用知道的. 如果有多个人同时收购 40 级装备. 那么系统再卖掉后, 不会对应到具体某个人仓库里有什么而把货款打到特定人的账上. 换句话说, 不会因为你运气后, 收到特定有人需要的东西而赚到钱. 而是在一个订货周期后,系统把入账平均分给所有经营这种货物的人. 你可以看成每种货物只有系统一个卖家. 商人拥有的这种商品的经营股份而已. 商人因为是股东而有定价权.

每个想做商人的人在系统里都可以查到这类商品的现金流, 压货数量(可能是因为没有人买, 也可能是因为价格过高, 这需要商人自己判断). 在游戏规则内没有垄断. 如果某件商品压了 100 件货而没有卖掉. 你如果判断是因为价格过高, 可以申请 20 件销售, 并把价格设低. 但总价格不会直接降低到你设置的价格上(你甚至可以直接设为 0) , 而是用你设置的 20 件价格和原价做一个运算, 得到一个更低的价位出来. 如果不出现新的商人介入, 系统就会充当这个角色以自动降低收购价和卖出价. (这个时候压的 100 件的商人如果死不降价, 多出的货就会自动在系统间以低价流通, 交易还是会顺利在有需求的玩家间进行的)

这里的关键在于隔离使用需求者. 让做生意的人专心做生意, 并承担风险. 使用需求者更容易买到自己的东西, 并有一个相对公允的价位.


设计这个系统想做到:

所有玩家的战利品都会进入玩家市场流通, 而不会被系统直接回收变成货币投放. (在少数情况还是会保留这种设计)

商品价格一定程度上受供求关系影响, 但又不是特别敏感, 有一定的稳定性. 直接影响价格的是少数开商铺的商人玩家. 这是一个小群体, 但不是系统(避免计划经济) , 也很难垄断。商人起的最重要作用是调节商品价格在一个合理位置。并因为这个玩法中风险和收益并存而显得有趣。

系统可以向收购和销售两方面抽税, 并在商品无人需求或暂时被人垄断时, 做一些系统补贴。根据商品产出地的地图不同, 可以税率不一样. 然后我们可以结合运镖玩法(物流因素), 自动调节税率.

December 04, 2012

程序员的职业素养

今天收到了《程序员》杂志 2012 年 12 期的样刊。上面有我应邀写的一篇稿子,在这里为自己存个档。


程序员是可以当作一生的职业。但首先,你需要热爱编程,而不是把它作为完成其他人生目标的工具。

既然计划投入数十年的人生,那么一定会不断的反思自己在哪些方面的努力是更有效率的。换句话说,现在的你,和十年前的自己到底有哪些不同,十年后又怎样超越现在的自己?

我的职业生涯还远远没有过半,深深感觉沉淀不足。总会有新的理解迭代到旧有的想法上。这次应邀来用简短的文字探讨这个深刻的话题,只能尽力来表达一些现阶段的浅薄之见。

我认为,一个程序员,无论他在哪个子领域工作,都需要在三个方面提高自己。

首先,保持对未知领域的好奇心,尽力开阔视野。

如果你只精通一门编程语言,那么就赶快去学习另一门,最好和之前的那门语言亲缘关系越远越好。这可以让你从不同的视角去看待过去的问题。

如果你只专注于一个领域,那么深入研究一下其它领域会有很大的帮助。大多数人都喜欢在自己熟悉的知识结构下解决问题,因为全新的东西总有学习门槛,你需要去了解很多基础知识才能开始实际的工作。在大脑里把相关信息组织起来轻松调配,和借助外部资料是很不一样的。后者要经历一个相当痛苦的过程。但是,一旦你习惯经常学习,可以逐步掌握一套自己的方法减轻这种痛苦。大多数人实际会遇到的领域有限,看似没有价值的知识,学习起来更为困难。要做到这点,需要保持单纯的好奇心。

其次,把握各个层次上的细节。

尽可能向人解释清楚系统每个层面的运行。硬件如何调度机器指令;数据在硬盘、内存、缓存、CPU 间的流向;代码如何被编译链接,代码经历了何种过程被加载到内存,JIT 怎样加速字节码的运行;操作系统怎样管理线程、处理 IO ;软件用到的第三方模块和工具如何在处理数据;在网络环境中,数据流的通讯协议;你的代码中每个模块逐个层次中的相互关系……

对细节掌握的越多,思路会越清晰。在每个层次上,你会看到不同层次的设备对上一层业务逻辑的抽象方式,直到最终你直接面对的业务。对业务的抽象能力,不仅仅来自于你的业务的熟悉程度。这种能力是随同细节把握能力同时俱备的。了解的越多,就越能知道你经手工作的合理性。

第三,对代码的直觉。

优秀的程序员能很快的发现性能热点、找到系统崩溃的原因、找出不合理的代码…… 准确的估算能力非常重要,快速心算出每个模块的开销和输入的数据量之间的关系;在写下每行代码时能够判断其对性能的影响、以简洁去挑战各个层次模块间的耦合复杂度。

培养这种能力,以我个人浅见,除了不停不断的编写代码,别无良方。在写代码的同时,时刻保持着思考,对坏味道的地方零容忍。只要在最早的时刻动手,任何推倒重来的代价都不会太大;而放任它们在那里只会让局面演变到不可收拾。

保持自己总有代码可写,不断的去发掘自己新的兴趣点,拓展新领域。单纯一些,编程本身就是一件有趣的脑力活动,而不必仅仅为了解决一些问题而写程序。

云风,2012.11.6

December 03, 2012

闲扯几句图形界面的设计

前段时间在微薄上对 windows 以及 vc 发牢骚。对于已经熟悉的软件,我很难忍受图形界面。

但图形界面对于大多数用户还是必不可少的,学习门槛低是最大的原因。但仅仅为降低学习门槛为原则去设计界面却绝对不是好的设计。

图形界面和命令行界面最大的区别是用户了解软件的角度不同。对于命令行界面的软件,用户的第一直觉大约是,我想做一件事情,所以我使用(或编写)这个软件。那么,这个软件应该如何达到我的目的。先有需求,然后寻找答案。通常,软件会有使用说明、可能和软件一起分发放在一个文本文件里,更多的是用命令行参数,传入一个 -h 之类的命令列出来。也可以在网上搜索。总之,需要用户去挖掘。当然,有的软件有所有默认配置,不需要参数就可以启动。如果更改了需求,可以去修改配置文件。修改配置文件的过程,往往可以通过观察默认配置的写法来学习。

图形界面的软件则不是这样。

当用户打开软件,图形界面直接展现出来。即使他是带着疑问:我该怎样完成我的需求?但看到界面后,第一反应却会是:那些第一眼直观可见的操作控件是做什么用的?

这个次序就变成了:我看见了一些功能,我想了解这些功能能达到什么目的,这些能做的事情里是否有我需要的。

如果按钮无害,用户会点点试试看发生了什么,现代软件通常还会在他鼠标移动过去时浮动一个提示框来做详细告之。受过往操作经验的影响,他可能还会按一下右键看一下右键菜单;拖动一下看起来可以拖动的控件;点一下可以点的下拉菜单;进一步学习软件能做什么。整个学习过程也就自然完成了。当软件的界面排布完全符合他以前用过的类似品时,这个过程可能省略到看一眼这么简短,然后直奔主题去点击他需要的那个按钮。

有时为了让用户可以直达目的,而不是先让用户自行尝试了解所有功能,再去匹配自己的需求(后者可能相当低效,以至于让用户中途放弃),软件需要制作一个简短的新手引导。这在最近的手机软件或网络游戏中相当常见。


在我看来,图形界面设计之所以被称为设计。必然需要经过思考,先将软件要解决的问题抽象分类,理清数据组织结构,帮用户把一些从用户角度看起来不太相关的事务提取出共性,归纳到类似的物件上。

比如 windows 把文件,文件夹等都图形实体化,然后用拖动来代表对其的操作,并把类似删除这种操作也实物化为垃圾站这种东西可以作为一个操作对象。

然后经过合理的布局和分类,排布到不同层级的界面展示。同时需要做一些创造,用恰当的形式来占现。我所说的形式,并不指更漂亮的图标,线框,字体;而是指类似弹出菜单,工具条这样的东西。在 windows 软件的年代,开发者似乎并不太愿意去考虑新形式,许多人觉得现有的控件已经很丰富了,我们只需要去选择。反正用户都已经接受了这些东西,他们熟悉,可以不用担心不一样的东西带来的风险。当然,最重要的是,可以减少开发成本。而最近几年,这种想法已经少的多了。在手机 app 领域,我们时常能看到耳目一新的东西。虽然弄巧成拙的也有不少,但,原来还可以这样方便的操作啊,这种感慨也挺多的。

这些才是设计师存在的价值。只是重新排布一下按钮的摆放、计算一下留白空间、选择好看的字体、画几个精美的图标,从视觉上讨好用户,只是界面设计中需要考虑一个方面而已,我觉得,甚至谈不上是重要的一个。


好的界面设计,我认为要考虑以下几个方面。

一是展示功能。主界面是否能展示软件解决的关键问题。用户都是从这里开始摸索软件,然后把他们看到的功能一一记下来,然后和自己心中的疑问去匹配,判断能否完成自己的需要。

一个软件,或是软件中一个模块。往往只需要解决一两个关键需求。用户心中的疑问也只有一两个。过多的功能堆砌都是不恰当的。因为接受用户的指令只是达成目的的一个开始环节,界面还需要承担信息反馈这个任务。大多数软件都需要让这个占据最大的屏幕空间。

从易于了解功能的角度讲,界面控件的可辨识程度,就相当重要。简洁的控件设计,恰当的留白以突出主要功能都是为了让用户视觉上可以直击我们需要引导让他看到东西。

二是操作效率。界面控件应该有合适的大小和间距易于定位,相关操作应有尽量短的移动距离。用图标区分相似元素的可辨识度是很重要的。合适的二级菜单可以避免主界面上信息量过大的问题,又可以方便的引导用户做多步操作,但是形式却很重要。这方面,我觉得 tweetbot 就做的不错。

三是高级操作带来的快捷操作。chrome 以前,我一直在用 opera 。最大的喜爱原因就是鼠标手势。虽然不是所有人都习惯复杂的鼠标手势,但一旦学会,可以更便捷的操作。高级操作形式不应是达成目的的唯一操作方法,但作为一个可选项却很有意义。

最后,我觉得最重要的是,应该充分理解软件是要做什么,而不是一味的模仿。没错,用户总希望新的软件和他用过的软件是一样的,这样就不用再学习。用户习惯什么的,是那些模仿设计最好的借口。直到我们看到新的设计被用户接受后,然后更新我们的界面。当然,也有很多新设计从始至终都没被人接受。我觉得那大多数不是因为扭曲了用户习惯,而是本身就设计的不够好。

比如,我认为 QQ 的好友分组展现就是一个糟糕的设计。当然,作为山寨软件之王,那也是从 MSN 里抄来的。

最糟糕之处在于,为什么一个好友就只能存在于一个组里?(或许是我不知道如何把一个好友分到多个组里)我的同学最近在和我一起打魔兽,因为这个原因,我就需要把他从同学分组里拖去魔兽组?

每个分组的组名都占据有限的界面空间也是一个浪费。更何况,这个组里有多少人,且其中多少人在线根本不是主要需要展现的信息,也侵占了屏幕。打开主界面,用户的主要目的是快速找到联系人,而不是独自欣赏:我有好多朋友啊。

假设允许同一个人在不同分组里也有许多问题。

首先,分组这个操作就变得复杂了。到底是复制一个好友去一个分组,还是移动一个好友到一个分组就会困扰用户。删除好友和从一个分组里删除好友也变得不同。网易泡泡就曾经面临这个问题。其结果就是,让各种无所谓的操作充斥着右键菜单。另外,当一个人同时处于多个组内,也很可能在主界面上重复出现同一个人的信息。

这个问题的本质在哪里?或许分组本身就是错误的。分组的本意或许是想借鉴 Windows 的文件夹的图形抽象方式,但一开始就错了。大多数分组行为是为了给人打上标签方便找到人,而不是让人归属到文件夹下。抽象角度出了问题,怎么改良都对不了。

现代社交网站乎都是用给人打标签来解决这个问题的。一个人可以没有,或有多个标签。大多数操作都是围绕人而不是标签的。标签只是检索人的一个手段而已。其实不光是社交网站,我们看到,在 gmail 这样的 email 软件、douban 这样的管理自己读书电影评论网站中,也是如此。

假设用标签系统来取代好友分组系统,我会这样去做。

在界面的顶端保留一条标签栏,横向罗列现在关注的标签。我可以展开这个标签栏,展示我创建的所有标签,并可以通过鼠标点击选择或屏蔽掉我的关注点。然后,在界面下方列出所有拥有至少一个关注标签的好友。我可以通过直接点击标签栏中某个特定标签把相关人排在最前面,而不用因为人员名单过长而去拉动滚动条。甚至,滚动条都应该是隐藏的,就如 gtalk 那样,必须用户强制开启。

给好友打标签或去掉标签的操作都放在好友下方,可以点击好友名字展开(类似 tweetbot) 。

我不保证这样的设计一定会被用户接受,因为用户习惯往往是可怕的固执。但这样的设计可以节省许多界面空间以展示更多用户需要的信息。对组的操作也被简化了。


我能找到的另一个例子是计算器。我有点计算器 app 控,在 ios 上购买或免费下载了许多计算器 app 了。计算器的功能其实都差不多,但是却少有让人使用感觉方便的。

我觉得这个坏头是 windows 自带计算机开始的。他区分了基本计算器和科学计算等。结果大多数计算器 app 也沿袭了这个设计(当然,几乎所有 app 也想到,可以利用重力感应横置设备来快速切换,而不用再让用户选菜单了)。且几乎所有的计算机 app 都模仿了传统实物计算器的布局。

区分不同类型的计算器版面的初衷或许是好的。如果界面按钮过多,或降低用户的操作效率。因为按钮越少越密集,定位的难度就越高。当用户没有那么多功能需求时,应该让界面更简洁。但这真是矛盾不可解决的吗?

许多计算机 app 还自带单位转换器,可惜多数却做成一个额外的模式。可为什么我需要切换模式去转换单位?不应该是计算完毕后,我需要数字结果的另一种表达吗?

同样的问题还出在进制转换等等需求上。

现代图形界面还带来了比传统计算器液晶显示屏更丰富的展示能力,cpu 的处理能力也远非那些机器可比。怎么用好它却是一个难题。

我不想在这里花太多文字来解释怎么把这些问题解决好。我想说,如果你用过 Calculator∞ 自然会和我有一些新的想法。我可以负责任的说,作为一个计算器 app 控,我用了它的免费版第一天就毫不犹豫的付费了。而在昨天我在微薄上夸了它两句,它居然就限免了。有兴趣的同学赶紧装一个试试。