手机彻底没电是在七月十八日凌晨三点。
林浩按了十三次电源键,屏幕始终漆黑。他把它放在窗台上,对著月光——月光很亮,但没用。钙鈦矿电池需要的是太阳光中的紫外线,月光太弱了。他试了檯灯,试了手电筒,都没用。那0.0%的电量像一道深渊,把所有未来的可能性都吸了进去。
他站在窗前,看著手里这块黑色砖头。2028年的技术结晶,现在成了一块废铁。小艺休眠了,或者说,死了。在电量归零的瞬间,那个温和的女声,那些精確的数据,那些超越时代的洞察,全都沉默了。
他唯一剩下的,是记忆。是之前看过的那些资料,那些架构图,那些算法思路。但记忆会模糊,会出错,会遗漏细节。他不能再问“小艺,这个函数怎么写”,不能再问“这个参数的最佳值是多少”,不能再问“如果遇到这个bug该怎么解”。
他只能靠自己了。
林浩把手机收进抽屉最底层,用几本书盖住。然后他坐回电脑前,打开一个空白的文本文档。
標题:“浩宇1.0引擎重构备忘录”。
他开始写,用最朴实的语言,把自己还记得的东西都记下来。
“1. 高並发战斗引擎核心思路:事件驱动 协程 无锁队列。但2002年没有协程库,用状態机模擬。无锁队列用cas实现,但2002年的c 编译器不支持原子操作,用互斥锁 內存屏障替代。”
“2. 网络同步优化:客户端预测 服务端矫正。关键:状態快照差分压缩。算法思路:將游戏状態编码为位图,只同步变化的部分。压缩用简单的游程编码(rle),2002年cpu能承受。”
“3. 物理引擎简化:2d刚体碰撞,用分离轴定理(sat)检测。但《传奇》是格子移动,不需要连续物理。改为格子碰撞 射线检测,性能更高。”
“4. 技能系统:用脚本驱动,但2002年没有好的脚本引擎。改为配置表 硬编码。每个技能是一个状態机,有前摇、施法、后摇三个阶段。”
“5. ai系统:行为树,但太复杂。改为有限状態机(fsm),五个状態:閒置、追击、攻击、逃跑、死亡。”
他写了三页。停下来时,天已经蒙蒙亮。窗外有鸟叫声,清脆的,一声接一声。
他看了一眼时间:凌晨五点。他睡了两个小时,够了。
阿坤和王磊是早上八点来的。两人都带著黑眼圈,但眼神清醒。阿坤背著一个鼓鼓囊囊的书包,里面是他从学校图书馆借的数学书:《计算几何》《图论导论》《数值分析》。王磊提著一个塑胶袋,里面是二十包泡麵,十根火腿肠,一箱矿泉水。
“这是接下来一周的粮草。”王磊把塑胶袋放在墙角。
“我推演了状態同步的数学模型。”阿坤拿出草稿纸,上面是密密麻麻的公式,“但有个问题:如果网络延迟超过300毫秒,预测纠正会导致明显的画面抖动。2002年,很多玩家还在用56k猫,延迟可能到500毫秒。”
林浩接过草稿纸看。阿坤的推导很严谨,但思路还是传统的那一套:降低延迟,优化算法。这解决不了根本问题。
“我们换一个思路。”林浩说,“不追求零延迟,而是让玩家感受不到延迟。”
“怎么做?”
“客户端不只做预测,还做预渲染。”林浩在白板上画,“服务端同步的不仅是当前状態,还有未来几帧的预测状態。客户端收到后,不是立即纠正,而是平滑过渡到预测状態。这样即使有延迟,画面也是流畅的,只是有轻微的『飘移感』。对《传奇》这类游戏来说,可以接受。”
阿坤盯著白板,手指在空中比划,心算。过了一会儿,他说:“需要服务端做状態预测,计算量会增加30%。”
“但客户端体验会好很多。”林浩说,“玩家不会因为延迟高就骂娘,只会觉得『这游戏有点飘,但能玩』。在2002年,这已经是降维打击了。”
王磊插话:“服务端扛得住吗?我们只有一台二手ibm伺服器。”
“所以需要优化。”林浩说,“阿坤,你来设计预测算法,要准,但不要太复杂。王磊,你来优化服务端架构,用事件驱动,避免线程切换开销。我负责把整个引擎的手工重构出来。”
“手工重构?”王磊皱眉,“什么意思?”
“意思是我要用手抄代码。”林浩说,“把我脑子里的架构,一行行写成2002年能运行的c 代码。没有现成的库,没有参考文档,只有记忆。我会先写核心框架,你们基於框架实现具体模块。”
阿坤和王磊对视了一眼。他们从林浩的语气里听出了什么——一种破釜沉舟的决心,一种不成功便成仁的狠劲。
“从哪开始?”阿坤问。
“从最核心的战斗引擎开始。”林浩说,“今天,我要写出战斗引擎的骨架。阿坤,你继续完善数学模型,今晚我要看到完整的预测算法偽代码。王磊,你搭建测试环境,我要能在一台机器上跑起十个客户端模擬器,模擬不同网络延迟下的表现。”
“十个客户端……”王磊苦笑,“咱们就三台电脑。”
“用虚擬机。2002年有vmware了,虽然慢,但能用。”
“行,我试试。”
分工完毕。三人各自坐下,面对电脑。
林浩新建了一个c 工程。开发环境是visual c 6.0,2002年的主流。界面很古老,但他熟悉。他新建了一个头文件:battleengine.h。
然后他开始写。没有自动补全,没有语法高亮(vc6有,但很基础),没有在线文档。他完全靠记忆,把那些在2028年看来理所当然的设计,翻译成2002年能理解的代码。
第一行:
// 浩宇1.0 高並发战斗引擎
// 设计目標:支持单服5000人同时战斗
// 核心思路:事件驱动 状態同步 预测矫正
// 作者:林浩
// 日期:2002年7月18日
然后是类定义。他先定义了几个核心类:battleunit(战斗单元)、skill(技能)、buff(状態)、battlefield(战场)。每个类只有最简单的属性和方法声明,具体实现后面再填。
写到skill类时,他停住了。技能系统是战斗的核心,但2028年的设计太复杂,有技能前摇、施法时间、弹道、命中判定、伤害计算、效果施加……一套下来,一个技能类可能有几十个属性和方法。在2002年的硬体上,这么重的类,实例化几百个就会卡死。
他必须简化。
他刪掉了原本的设计,重新写。这次,一个技能只有五个属性:id、名称、施法时间、冷却时间、效果类型。效果类型是个枚举:直接伤害、持续伤害、治疗、控制、召唤。伤害计算用一个简单的公式:基础伤害 攻击力係数攻击力-防御力係数防御力。控制效果只有两种:定身、沉默,持续固定时间。
简单,但够用。至少对第一个demo来说,够用了。
写到buff类时,又遇到问题。2028年的buff系统支持多层叠加、持续时间刷新、效果合併、优先级判断。但2002年不能这么搞。他再次简化:buff不能叠加,同类型后到的覆盖先到的。持续时间用帧数计算,每帧检测是否到期。效果只有属性修正(加攻、加防、加减速)和状態附加(定身、沉默)。
他写了一个上午。到中午时,头文件写完了,大概三百行。这只是骨架,但结构清晰,职责分明。
“阿坤,来看一下。”林浩说。
阿坤走过来,站在他身后,看屏幕。他看得很慢,很仔细,有时会停下来,想几秒,然后继续。
“这个battlefield类,”阿坤指著一行代码,“用二维数组存储单元引用,查找效率是o(1),但內存开销大。如果地图大,会爆內存。”
“地图不会大。”林浩说,“第一个demo,战场就100x100格,每个格存一个指针,4位元组,总共40kb,可以接受。”
“那单元移动时的碰撞检测呢?还是遍歷所有单元?”
“用空间分区。把战场分成10x10的区块,每个单元只和同区块及相邻区块的单元检测碰撞。算法你熟。”
阿坤点头:“四叉树或者网格。我推荐网格,简单,2002年够用。”
“行,那你来实现。”
阿坤回到自己电脑前,开始写空间分区算法。林浩继续写源文件。
下午,他遇到了第一个大难题:事件驱动框架。
2028年的游戏引擎,事件系统是核心。玩家操作、技能释放、伤害触发、状態变化,全都是事件。事件队列、事件监听、事件派发,一套完整的发布-订阅模式。但在2002年,c 没有lambda,没有函数对象,没有標准库里的function。要实现事件系统,得用函数指针,或者自己造轮子。
林浩选择了最土但最可靠的办法:用整数类型標识事件,用switch-case分发。每个事件有一个结构体,包含事件类型和一堆union栏位。监听者註册回调函数,事件发生时,遍歷所有监听者,调用对应的函数。
他写了两个小时,写出了事件系统的雏形。测试时,发现性能有问题:每次事件派发都要遍歷所有监听者,如果监听者多,会成为瓶颈。
“用哈希表。”王磊不知什么时候站到了他身后,“事件类型做key,监听者列表做value。查找效率o(1)。”
“但2002年没有std::unordered_map,得自己实现。”
“我有现成的。”王磊说,“以前写外掛时攒的代码库,开源链地址哈希表,经过优化,在2002年的机器上跑得飞快。”
“好,拿来用。”
王磊从他的旧硬碟里翻出代码。確实是优化过的哈希表,用了內存池、缓存行对齐、快速哈希函数。林浩集成进去,事件派发的性能提升了五倍。
晚上八点,战斗引擎的核心框架完成了。能跑,但不完整。林浩写了一个简单的测试:创建两个战斗单元,互相攻击。屏幕上,两个像素小人你一拳我一拳,血条减少,直到一个倒下。
很原始,但基础逻辑是通的。
“接下来是网络同步。”林浩说,“王磊,你的udp可靠传输层怎么样了?”
“搞定了。”王磊说,“基於rudp(可靠udp),加入了前向纠错、选择性重传、流量控制。在模擬的200毫秒延迟 5%丟包环境下,传输可靠率99.9%。”
“好,集成进来。”
集成网络层花了三个小时。到晚上十一点,他们终於能在两台电脑之间跑起战斗demo了。一台做服务端,一台做客户端。客户端控制红色小人移动、攻击,服务端同步状態,另一个客户端能看到。
延迟明显,有卡顿,但能玩。
“还不够。”林浩说,“要加预测和矫正。”
阿坤拿出了他今天的成果:三页纸的预测算法偽代码。林浩看了,思路清晰,但实现复杂。
“我们分步来。”林浩说,“先实现最简单的:客户端预测移动,服务端矫正位置。技能和伤害先不做预测,等同步。”
“行。”
又写了两个小时。凌晨一点,预测移动完成了。测试时,故意加了300毫秒延迟。红色小人在客户端移动得很流畅,但在服务端的视角里,它有轻微的“飘移”,会突然跳到正確位置。这是预测错误的矫正。
“视觉上有点怪,但能接受。”王磊说。
“嗯,先这样。”林浩说,“今天到此为止。明天做技能预测和伤害同步。”
三人瘫在椅子上。累,但兴奋。他们用一天时间,做出了一个可用的战斗引擎骨架。虽然简陋,但架构先进,有扩展性。
“老大,”王磊突然说,“你脑子里的这些东西,到底从哪学的?”
林浩沉默了几秒。窗外是沉沉的夜,没有月亮,只有几颗星星。
“如果我说,我梦见过未来,你信吗?”
王磊笑了:“我信。不然没法解释。你写的这些设计,我从来没见过,但仔细一想,又觉得就该这么设计。像是……站在山顶看山路,知道哪里该拐弯,哪里该直行。而我们还在山脚下摸索。”
阿坤轻声说:“我也有这种感觉。你给的算法思路,不是凭空想的,是经过千锤百炼的最优解。但你又没时间千锤百炼。”
林浩没回答。他关了电脑,站起身。
“睡觉。明天六点起。”
“六点?!”王磊哀嚎。
“嗯,六点。我们只有两周了。”
三人简单洗漱,躺下。工作室里只有两张行军床,林浩睡地铺。硬地板,但累到极致,躺下就著。
第二天,六点,闹钟响。
三人爬起来,用冷水冲脸,泡麵当早饭,然后继续。
第二天,做技能预测。更难,因为技能有前摇、弹道、命中判定,任何一步预测错误,都会导致严重的不同步。林浩设计了“预测-验证-回滚”机制:客户端预测技能命中,如果服务端验证不通过,就回滚到上一个状態,並补偿玩家。
实现这个机制,又花了一天。到晚上时,他们终於能在高延迟下,比较流畅地释放技能了。虽然偶尔会有“技能打中了但没伤害”或者“没打中但跳伤害”的bug,但概率很低。
第三天,做状態同步优化。阿坤实现了状態快照差分压缩,把每次同步的数据量从2kb降到了200位元组。这意味著,在56k的拨號网络下,同步延迟能从500毫秒降到100毫秒。这是质变。
第四天,做ai。简单有限状態机,但林浩加入了一些小技巧:ai会走位,会逃跑,会吃药。虽然还是很蠢,但比《传奇》里那些站桩怪强多了。
第五天,整合。把战斗引擎、网络层、ai、资源管理全部整合到一起。出了无数bug,修到凌晨三点。
第六天,第七天,第八天……
每天睡四小时,吃泡麵,喝矿泉水。工作室里堆满了草稿纸、泡麵盒、空水瓶。三人的鬍子长了,头髮油了,眼睛红了。但代码一行行增加,功能一个个实现。
到第七天晚上,他们有了一个可玩的demo:一个简单的战场,玩家控制一个角色,可以移动,释放三个技能(火球、治疗、衝锋),打五个ai怪物。有血条,有蓝条,有经验值,有升级。虽然美术只有简陋的像素图,虽然音效只有“噗噗噗”的简单音效,虽然內容少得可怜——
但它流畅。
在模擬的200毫秒延迟下,移动流畅,技能释放流畅,战斗流畅。没有卡顿,没有漂移,没有明显的不同步。这在2002年,是奇蹟。
王磊控制角色,在战场上跑了一圈,放了几个技能,打死一个怪。他盯著屏幕,看了很久。
“我操。”他说,声音很轻。
阿坤也盯著屏幕,手指在颤抖。
“我们……做出来了?”他问。
“做出来了。”林浩说,声音沙哑,“浩宇1.0引擎,第一个可玩demo,完成了。”
三人坐在电脑前,看著屏幕。红色的像素小人在简陋的地图上奔跑,释放火球,怪物倒下,经验条增长。
很简单,很粗糙。
但它是完全基於全新架构的,是他们三个人,用两周时间,从零写出来的。
“接下来,”林浩说,“我们要在两周內,基於这个引擎,做出《浩宇传奇》的第一个版本。內容要足够多,能让玩家玩上一天。美术要看得过去,不能太丑。运营后台要简单,能开服,能收钱。”
“两周……”王磊喃喃道。
“嗯,两周。”林浩说,“但我们有引擎了。有了引擎,內容生產会快十倍。阿坤,你负责数值和关卡设计。王磊,你负责运营后台和支付系统。我负责美术资源和剧情文案。”
“美术你也会?”阿坤问。
“不会,但可以学。”林浩说,“2002年的游戏美术,像素图,我能画。剧情文案,我能写。我们没时间找外包了,全部自己来。”
王磊看著林浩,看了很久,然后笑了。
“老大,我服了。”他说,“我这辈子没服过谁,但我服你。你说干,我就干。你说两周,我就两周不睡觉。”
阿坤点头:“我也是。”
林浩看著他们。两个伙伴,眼里的光和他一样,是疲惫的,但燃烧的。
“那就干。”他说。
三人击掌。很轻,但很重。
窗外的天又黑了。星星出来了,很多,很亮。
工作室的灯,又亮了一夜。
浩宇1.0引擎,活了。
而浩宇科技的第一个游戏,就要诞生了。
时间,还剩下两周。
他们,必须贏。