AI 编程时代,想象力成了最被奖励的能力

技术文章 杂谈

同时,它也在惩罚厌恶失控的人。 在2017年谈论自动驾驶? 2017年夏。 我的一位大四学长马上要毕业了。他外号叫"大帅",是个非常自信的人,每天都带着他招牌的微笑。临别之际,我送了他一本我看完的《腾讯传》。 在空荡的宿舍里,他跟我聊起电动汽车,聊起自动驾驶。 他问了我一个问题,以后的车自己就会开了,怎么样? 我仔细想了有30秒,然后回答:“那这辆车一定热得半死,这得消耗多少算力去算啊。” 现在我的脑海里还记得这件事,是因为学长当时从默认的微笑中突然闪过了一瞬特殊的表情。这个表情我很难用言语形容,它似乎是疑惑、一点点的惊讶、一点点的无奈、感叹等复杂感情的结合体。 后来说了什么,我已经忘了。但是最近几个月发生的事情,让我确认了一件事: 我没有足够的想象力。或者准确地说,我不敢去幻想我认知之外的事。 在那次聊天8年后,我拿到了腾讯的蓝色工牌。9年后,我在腾讯正好赶上了AI大模型的这一场工业革命。腾讯在2026年上半年为所有员工提供了几乎用不完的token,我有幸体验到了当世顶级的大模型。 AI已经不只是"帮我补全代码的工具"了 到了2026年,事情变得完全不一样了。过去AI在项目里编造函数的情况基本消失,它开始能够读懂整个项目、提出方案、完成设计、编写实现,也开始真正参与我的判断过程。个人用户也只需要充值一点钱,就能让聚合了这个世界几乎所有编程知识的顶级AI大模型,亲临指点自己的编程项目。 变化已经大到,我无法再把它当作偶尔补全代码的工具了。 所以,我们还缺什么呢? 想象力。 还有,对失控感的容忍。 失控感:我为什么迟迟不接受AI 四个月前,即使我已经确认AI大模型的能力远超我的想象,我也还是无法放心地把工作交给它。 去年下半年,我接入Claude Sonnet 3.5修网页前端问题,来来回回修不明白,一下午花了我30多块人民币。当时我的第一印象是,AI不过如此。 不过,那个结论的下面,其实藏着我对未来的恐惧。这导致很长一段时间里,我对AI带着评估的视角,评判它吐出的观点是不是正确。另外,由于我是功能的负责人,我希望拥有对功能的掌控感。所以我只是把AI当作一个"帮我写代码的工具",并不想让它参与设计和决策。 我也尝试过让AI分析需求,但当它说出一些错误结论的时候,我会有一种"无能狂怒":“我等了你半天了(实际也就5分钟左右),你就跟我说这个?” 当时,我既期待AI能真的让我成为甩手掌柜,让我不用那么努力地思考方案,最好是把设计和实现全包了;同时我又担心AI写的东西不对,到头来发现问题了,查BUG的还是我。这挺矛盾的。就像我面对很多事情喜欢非黑即白一样,在这件事上我也是非黑即白的:要么AI全部做完,要么由我来思考,AI只负责执行。 显然,当时的AI无法全部做完一整个需求。我只能自己思考,然后让AI执行。 我没有激发出当时最强的Opus 4.6的所有能力。我不喜欢那种"我打了这么多字告诉你我要干嘛,你却想得不对、做得不对"的感觉。 有趣的是,四个月前,当AI犯错时,我会很生气。因为如果是我自己思考,大概率不会犯这个错误。这是一个自我保护的想法:我担心自己的工作会被替代,AI犯错时,佐证了"我不会被替代",给予我一定的安全感。 AI针对同一个问题,每一次输出的答案都会略有不同,每一次写的代码也会略有不同。这和之前我们对编程的理解是不一样的。之前的编程下,函数、脚本都接受它们要求的输入,然后输出固定格式的输出。而到了大模型时代,这个"约定"被打破了,每个人都可以用自然语言去指挥AI做事情。我无法掌控AI做什么,这让我感到失控。更糟糕的是,AI越来越强,带给我的不安全感也在增强。过去的自己在心底是一个自卑的人,知识面有一些短板,而AI把我仔细、认真的优势覆盖了,我感觉到的,是价值感的缺失。 现在回头看,我才明白:我不是输给了AI,我是输给了自己的失控感。 一场进行中的编程"工业革命" 在没有AI的年代,我们的古法编程流程是,接到新需求 -> 分析已有代码和新需求的关系 -> 确定方案 -> 开始写代码 -> 发现问题 -> 修改方案 -> 再写。不管是什么需求,我们总可以找到一个"看起来能用"的方案,即使我们没有足够的时间和能力去评估方案是否最佳,也可以先动手,做出一版看似可以用的功能。 但是有了AI,还用这一套流程,就会限制AI的能力。必须要更新流程了。我目前摸索的流程是:接到新需求 -> 先把需求和项目里的相关代码告诉AI,让AI理解需求(与此同时,我自己也在看代码)-> AI通过自己的理解输出几个可行的方案,让我决定(与此同时,我和策划基于需求的边界和目的进行深入探讨)-> 选定方案后,脑暴这个方案可能的问题 -> 经过多轮脑暴之后,方案基本没有太大的缺陷 -> 实施。 大学时学习的软件工程、系统分析与设计方法等课程,讨论各种中间设计文档、各种开发模型,本质上都在约束一件事:动手前先思考。AI并没有发明这件事。它改变的是这件事的经济性。 以前不是不知道应该先设计,而是在很多项目里,“充分设计"的成本高得不现实:看清楚整个项目、检索相似实现、枚举边界情况、生成候选方案,每一项都要花掉大量人力。于是这一步总被工期牺牲掉。而AI把分析、检索、枚举、生成候选方案的成本突然压低了,那些过去经常被牺牲掉的设计步骤,重新变得值得做。 区别在于,“发现问题"被前置了。在大型项目中,AI拥有看到整个项目代码的能力,它能识别已有代码的最佳实践,帮助我们分析需求。 但它也可能是错的。之前的一些功能因为设计上的妥协、因为疏忽,可能给AI指向了错误的方向。AI可能说了一个错误结论。那么,就把反对它结论的证据告诉它,让它继续思考就行了。 为什么我们要把"发现问题"前置?因为现在AI编写代码的速度太快了。AI降低了重写一段代码的成本,却提高了在错误方向上一口气跑很远的风险:它一个小时可以沿着错误的方向生成十几个文件、测试和文档,而以前的人类一天可能才写几百行。所以方向校验反而比过去更重要。 方案之争:AI、我、我的同事 在大型项目中,没有人能对整个项目所有细节了如指掌。它更像是盲人摸象,大家用自己的工程经验和知识面,用尽各种技巧和编程范式保证自己的功能健壮。 但每个人和AI的观点是不同的。 AI,洞悉了项目代码和文档,基于这世上的知识,给出它认为最合理的答案; 我,知道一些项目里的隐性规约,知道策划需求的边界和没表达出来的部分,还有一些自己的思维习惯; 我的同事,他可能更熟悉我这次要修改的模块,能够给出很多建议和未经仔细思考斟酌的方案。 极端情况下,会出现三者各自有自己的方案的情况。 我曾经非常惧怕这个情况。在古法编程时代没有这个问题:如果我自己思考,我肯定按我的思考做;如果同事给了意见和方案,我们讨论后就可以采纳。但是AI来了,就算我和同事讨论过后,由于我们对代码的了解总有缺陷,我必须再把讨论的结果告诉AI,让AI再理解一次。一旦AI说的"看起来有道理”,我又得和同事开启一轮讨论。 我之前是不具备这种讨论能力的。我总觉得一个功能,我有思路了就去做,遇到什么问题再改就行了。这个思路在古法编程时代看不出问题,因为大家都是盲人摸象。而现在,AI像一个不知疲倦、读得非常快的第二审阅者,它能快速扫描大量代码,找到相似的实现,枚举风险,提出反例,帮你攻击一个方案。但它依然可能把图看错。所以"动手"的时机被延后了,而这让我感觉非常不适应。 “动手"的时机真的延后了吗? 当我发现自己写不出很好的提示词,我开始查阅提示词相关的文档。官方文档里反复强调,要把AI想象成一位才华横溢但刚入职的新员工,你对它解释得越精确,结果就越好。...

八月 16, 2026 · JonathanLin

游戏服务器开发经验(五)应对复杂需求

技术文章 游戏服务器开发经验

什么是复杂需求? 在本文中,“复杂需求”指的是, 需求涉及到非常多的模块; 每个模块都有多层抽象重载; 状态变更逻辑复杂,边界情况众多,策划无法给出统一处理的方案; 要在已有的功能代码上进行开发,避免造轮子。需要在满足新需求的同时,保证之前的功能正常。 或者更简单的判断方式是,你看着代码思考的时候,发现脑容量上下文无法处理所有可预料的情况时,就是复杂需求。 为什么我们往往分不清复杂需求? 由于需求本身足够复杂,策划也是人,AI也有上下文限制,大家都无法将需求一次性分析完整 因此策划说的话 和 写的内容,可能是寥寥数语。因为很多细节策划在自己没有玩游戏,并且自己没看代码的情况下,确实无法思考具体的需求细节;策划那边也会每天思考,脑暴,过了一段时间又有新的想法。 程序这边收到的需求,可能只是完整需求的冰山一角。 直到需求慢慢地补全,在主玩法周围慢慢补全其他的需求的时候,才会发现这个需求的规模。 也就是说,当需求大到“脑容量上下文无法处理所有可预料的情况时”,我们就要有警报在脑袋里开始轰鸣,是复杂需求来了。 为什么复杂需求难以开发? 横跨多个模块,一个人是无法同时了解多个模块的业务逻辑的,一个模块之前的业务逻辑不一定适配当前的新业务,需要仔细评估是复用还是造轮子; 策划的需求可能只有一个预期的方向,但是这个方向上的边界情况可能非常复杂,需要程序来整理可能的边界,并且要做出取舍; 策划提出的需求可能是一个很特殊的方案,但是在大型项目中可能有不同的实现方式。如果贸然按照策划的思路实现,可能会带来副作用。因此需要耐心研读已有代码,领会之前的代码结构设计。 实现需求并不是终点,实现需求并提供一种可复用的机制,才是开发的正确标准。一旦过早“为了实现而实现”,会导致代码非常僵硬,并且与框架层代码脱节而难以处理各种情况。 如何应对复杂需求 在需求没有完全出来的时候,可以写一些“为了展示效果而临时开发”的代码,帮助策划在游戏内体验,寻找设计方向;但是这些代码自己心里一定要有数,只是临时实现。后续要争取时间进行优化; 需求慢慢完备之后,需要花时间站在更高的角度上分析需求涉及到的模块,分析需求中的主线脉络,对应于代码里的设计模式。而不是头痛医头,脚痛医脚,把大需求拆成小需求后,针对各个小需求自己实现自己的部分,到最后无法统一管理; 如果能够站在更高的角度上切分需求和涉及到的模块,就能针对各个模块去了解用到的部分的代码。我们并不需要完整领悟别的模块的代码,但是自己需求里用到的部分一定要心里有数 如果我们无法正确切分需求,则需要寻找需求涉及到的模块的负责人,询问他们相关需求的实现是什么样的。这里要记得,一定要说出完整的需求链路,而非单纯询问某个API。一定要让其他模块的负责人知道合适的上下文,让他们的判断来帮助我们更好的正确实现需求。 如果发现有一块代码怎么写都非常别扭,需要处理各种边界的时候,说明代码里有一部分逻辑是可以整理的。 如果需求真的很复杂,如果已经走了弯路,如果已经到了身心俱疲的情况,请务必向上反馈,争取时间。精神burnout带来的伤害是客观存在的,要好好保护自己的身心健康。 打好每一行日志。我们无法理解所有已有模块,但是我们可以靠日志了解运行的时序。 记得自测。一定要自测。哪怕是用GM指令自己构建一个环境也要自测。在复杂需求中,只有日志和游戏里的自测能够让我们确认自己的思路是否正确。 在策划自己也拿不准情况的时候,很多边界,如果不会带来严重后果,不用着急去问策划,而是可以自己先记下来。当功能开发差不多了,可以在游戏里体验时,再告诉策划,让策划边体验边思考这些边界应该如何处理。 如何使用AI帮忙 让AI帮忙分析你的需求。请记住,这里一定要把客观的需求告诉AI,而非把自己的方案告诉AI。在过去的很长一段时间里,AI无法完整领会需求,AI写的代码无法编译通过等。但是现在,opus4.6已经能够客观冷静分析需求。AI能翻阅整个项目的代码,去搜索其中它感兴趣的代码,它可以在代码仓库的角落里找到很多有用的功能,也会分析已有项目的设计模式。大多数情况下,它做的比人类更好。 让AI与你进行头脑风暴。AI不知道的是你的需求具体要实现什么。即使已将需求文档提交给它,也会产生误解。另外,它偶尔会选择“看起来最合适的方案”。这个时候就要与它进行多轮的头脑风暴,告诉它我们具体的需求和期望。 让AI帮你整理你不懂的代码模块。很多设计模式可能会将代码写的很松散,继承好几层,导致代码很难整体阅读。让AI整理你感兴趣的代码和已有的需求是如何实现的,可以加速我们对需求的理解。 应对复杂需求时的心态 如果需求已经复杂到你无法一次性想通所有场景和边界时,说明这个需求一定是多个模块互相组合的。在这种情况下,最大的敌人是完美主义,是希望“一次全做对”的想法。 不要害怕返工,不要觉得自己写的东西被删了是浪费时间。相反,正是因为有了这些歪路,才让我们找到了正确的方向。真正需要恪守的规则是,我们最后提交的方案一定要是我们能做到的最好的方案。 考虑不到那些需求的边界是很正常的事。不要因为没考虑到而自责,而是把它当做一件习以为常的事,就像家里的猫咪又去扒拉沙发,不小心把饭碗打翻一样,无法避免。 不要害怕bug很多的情况。当然,前提是你已经了解需求的主要脉络,在代码里做好了兜底处理,尽可能保证不要内存泄漏,要是真的崩了只崩自己的这个需求。bug多并不直接意味着人不行,也有可能是需求确实复杂,涉及模块确实众多,很多边界来不及考虑。 接受“人脑无法处理所有情况”的现实。如果你要思考需求,请你打开代码,对着已有代码进行思考。不要在没有代码的时候思考需求如何实现。 未雨绸缪 每一个正在服役的项目框架,一定有自己特有的兜底机制。在实现需求的时候,也许他们要的很急,你也应该多想想能否用得上那些兜底机制。 框架里一定有一些非常常用的模块,那些模块一定要多了解,多看代码。特别注意:放下评价代码的标尺,切换为“它这么写是为了解决什么?”“我能用这块逻辑吗?”的思考方式。验证方式是下一次看到某个已看过的模块的时候,在脑海里能回忆起粗略的细节,而非“它写的还可以”“我看不懂,好累啊” 在需求刚宣讲的那天,一般会有一小段时间,你既知道策划想要什么,又知道大概的模块有哪些,而且此时还没有定具体的实现细节。这个时候就是最佳的分析已有模块的时机。千万不要错过这个窗口期。 你的工作不等于你的价值 复杂需求让人最难过的一点是,随着需求会一点一点的明确,之前写的代码会不停地被删掉,仿佛他们之前不应该出现,与此同时,随着需求的清晰,需求的边界也慢慢清晰,我们会慢慢发现之前的思考都是错误的。 谁喜欢犯错呢?谁都不喜欢犯错。但是复杂需求就是这样,有一种让所有人不得不犯错的能力。即使你知道项目里各个模块的业务,你也很难一次两次就把功能做完善。更有可能的是,策划体验完之后,过来说,“我们对刷新/每次/减半/随机的理解好像不太一样” 如果你之前写了太多简单的增删改查、抽奖发奖,做的技能都是具体的某段逻辑,不需要考虑那些边界的话,可能很难想象一件事不断地挂在胸口,仔细一思考全是边界,看着需求截止日不断来临,而代码里还有很多临时实现。那一种绝望的感觉。 但是,这种绝望并非我们程序一人所致。要慢慢接受这种“胸口挂着事情”的感觉,接受“需求复杂到无法光靠大脑想明白”,而是要打开代码和AI大模型进行分析;在需求截止日之前提前预警,如实描述当前遇到的问题,需要什么样的帮助。最后,工作只是工作。 你的工作不等于你的价值。 ——《伯恩斯新情绪疗法》 我最近总有一种感觉,我坐在电脑前,看着显示器里的代码。我感觉我的眼睛和显示器之间,似乎有一堵无形的空气墙,或者是透明保护罩。我开始看很多项目里常用的底层框架和模块,静下心来思考当时是为了解决什么问题。 承认自己的渺小与无知 我曾以为大厂的程序员一定上知天文下通地理。但最让我惊艳的是他们的思考方式。我总是过早的希望得到一个结论,然后去实施。但是在大厂程序员和AI大模型眼里,需求是需要一遍遍确认的;已有代码是需要认真研读的;边界是肯定想不全的,但是要提前想;已有的代码是要复用的。他们很多决策都是依赖已有代码,他们是看着代码进行分析的。他们也许记不住某个具体函数名,一次性无法完全在心里想全所有业务边界,但却可以巧妙的通过合理的代码设计来管理热点内存数据,保证内存不泄露,正确申请、使用和释放。 我们本来就不可能理解所有模块的代码,就算是AI大模型也有上下文的限制。但是我们可以通过其他模块的配合,和自己的模块的干净维护,来保证需求的健壮,在不同的边界上执行已有的处理方式。(至少不会crash) 渺小和无知是我们人类的常态,渺小和无知并不可耻,可耻的是知道自己无知而放弃了学习,可耻的是不愿意选择正确的方式继续推进开发。

六月 3, 2026 · JonathanLin

Linux家用服务器维护指南

技术文章 DIY折腾

2025年,对绝大部分普通人来说,真的没必要折腾家用服务器。除了安装好的那一刻感到的快乐之外,剩下的就只有吃灰和浪费电而已。 但是这却是一次难得的和Linux亲密接触的机会。如果你感兴趣,那么玩玩也不错。 标题中“家用服务器”指的是什么 本文中探讨的家用服务器,指的是购买的小型主机设备(Intel NUC系列等),自己安装操作系统进行使用的服务器。不包括群晖等成品NAS设备。 用户购买小型服务器,在上面安装Linux系统,然后自己安装各类服务,自己管理端口,内存,磁盘,自己进行运维。 一般人的家用服务器无非用来做两件事: 照片存储 下载4K超高清无删减电影 程序员可能会多做几件事: 部署自己的git私有仓库 定时任务执行自定义脚本 在线电子书(有些技术类电子书真的只有影印版pdf,临时想看还要下载,很麻烦) 极客玩家可能会多做几件事: 部署游戏服务器私服(幻兽帕鲁等) 养宠物的朋友可能会多做几件事: 将家用摄像头的录像上传到自己的服务器永久保存 以下是各种建议 建议一:一定要用Docker进行部署 既然选择了自己维护,那么你大概率下载到的都是开源的软件。而这些软件的开发人员的想法是不一样的。 有的程序员喜欢通过全局变量的方式控制软件,有的程序员只建议用特定的方式启动软件等等。 而且安装的方式也不一定是规范的。比如有的软件允许你用root安装,在没有经验的时候你可能会这么做,而有的软件在文档里强烈建议你为它的软件建立一个用户组和用户。你可能这个软件跟着文档安装在了/usr/local里,但是下一个软件可能放在/home/用户 下。而这会让未来的你一头雾水,毕竟这台服务器一般不会连上去使用。 而docker提供了一种更优雅的方案,每个软件都运行在自己的容器中,容器里有自己独立的变量和目录。然后你可以在docker-compose.yml中指定容器内端口对应的服务器端口,容器内目录对应的服务器目录等。另外,如果你运行的软件都需要用数据库,那么你可以用容器安装一个数据库,比如MySQL8,然后定义一个数据库的容器network。其他软件的容器可以访问这个network。 比如,我的gitea的docker-compose.yml内容如下: services: gitea: image: docker.gitea.com/gitea:1.24.0 container_name: gitea restart: always ports: - "13300:3000" - "13022:22" volumes: - /data/gitea/data:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro environment: - USER_UID=1000 - USER_GID=1000 - GITEA__database__DB_TYPE=mysql - GITEA__database__HOST=mysql8 - GITEA__database__PASSWD=gitea123 - GITEA__database__NAME=gitea - GITEA__database__USER=gitea networks: - mysql8_database_network networks: mysql8_database_network: external: true 而我的MySQL8容器的docker-compose.yml内容如下: services: mysql8: image: mysql:8....

十二月 6, 2025 · JonathanLin

游戏服务器开发经验(四)避免写Bug的习惯、技巧和心态

技术文章 游戏服务器开发经验

编写此文的目的,并不是教大家如何写出没有Bug的代码。 没有Bug是不可能的。 话虽如此,但是我们完全可以在开发阶段,避免一些低级的Bug。笔者在游戏上线后维护服务器的时间较长,我发现线上所谓的严重问题,真正出问题的代码就是那么几行(一般不会超过10行)。也就是说,这些容易出问题的部分,并不是系统中复杂的部分。因此,我们一定有办法在事前对其进行规避。 我认为避免Bug需要我们有良好的开发习惯,和冷静耐心的心态。于此之上,再通过经验加以一些技巧,就可以避免很多大小Bug了。其中,我认为最重要的是习惯。好的习惯不会让你多花太多时间,但是能尽早发现问题。 习惯和技巧 自测 自测,即代码编写结束后,在本地或开发环境中运行,然后通过白盒或者黑盒的方式测试自己开发的代码。 在本地开发时,务必拥有一个开发环境,这个开发环境可以测试自己正在开发的、还未合并到主干的代码。有了这个环境,你就可以快速地将自己新开发的内容运行起来。如果可以Debug查看每一行的结果就更好了。 自测的意义在于,你可以成为自己代码最早的测试人员,验证自己的思路是否真的被正确开发。自测能给你一个机会,让你执行自己的代码,并且能让以下容易遗漏和犯错的部分得到快速验证: 仔细检查if语句的逻辑判断是否正确 检查玩家数据状态是否正确改变 注销后再登录,即可检查玩家数据是否正确写入 如果你的环境不允许你打断点,或者一些情况比较复杂(多线程环境或有延时函数等),你可以在本地多打一些debug级别的日志,查看数据的处理过程。 Q&A 我等客户端同学开发完,联调的时候再一起测不行吗? 可以,但是在联调时你的工作重心已经到了其他地方,你会忽略或遗忘当时写这段代码的一些想法。而这些想法很有可能就是容易出错的地方。另外客户端同学一般只会测自己感兴趣或者担心出问题的部分,他们关注的部分不一定和你一致。 要是我都写正确了,自测不就是浪费时间吗? 不。我们人类一定会犯错,即使我们逻辑写对了,也有可能会把诸如道具产销日志等一些不影响功能,但是运营、策划需要的部分遗漏,也有可能会忘记打warning日志或者error日志,或者是发现一些变量名不合适,或者是重新回看的时候发现代码结构冗余臃肿。这些都是值得修改优化的点。如果我们不自测,那么可能未来就没有时间拿来安排自测了。 服务器开发逻辑的时候,没有现成可用的客户端包进行测试,自测很麻烦。 是的,很麻烦,但是并非毫无办法,接下来会介绍一些自测的技巧。 自测的技巧 对于简单的客户端和服务器交互的接口,可以想办法在客户端手动输入协议号及协议结构,发送指定的协议数据,然后在服务器打Debug断点进行检查。 对于玩家数据经常变化的新功能,可以在数据变化推送的时候打印debug日志,通过日志来确认玩家数据正确性。 对于情况比较复杂的功能,可以先写一些私下用的一次性gm,先模拟复杂的情况,然后再进行目标功能的测试。、 总之,只要你想测到你的功能,在环境允许的情况下,一定有办法测得到。 变量及函数命名技巧 表示某事的次数不要用Times,而是用Count。原因是Times容易和Time混淆,无法一眼区分到底是次数还是时间 表示时间长度不要用Time,而是用Duration。因为“时间”这个概念本身就有歧义,可以表示某个时刻,也可以表示某一段时间。建议用Time甚至Timestamp,TimeMillis,TimeSeconds来具体指代某个时刻的时间戳,用duration来指代时间段。 如果针对某个对象要设置某个属性的值,并且设置时还要对其有特殊逻辑,并且项目中已经装了lombok依赖,则最好不要再命名setXXX(),因为这样会重写lombok的@Setter省略的函数,并且容易让阅读者忽略这个函数内有特殊逻辑。 不要使用setXXX(),但是你可以针对这个业务逻辑,定义一个更贴合业务的逻辑,比如设置玩家积分时,同时更新排行榜上的玩家数据,则setScore()会让人误以为这个函数只是给对象里的score属性赋值,而如果是叫updateScore()或updateScoreAndRank(),则可以直观的知道,这个函数要更新积分,并且可能会更新排行榜。 代码分段 代码一定要分段写。印象中有一些书籍或者公司里的开发规范中约定,每个函数不要超过多少多少行。我想也许是担心大家把函数写得太长,可读性变差而设的规则。但是,我认为真正的问题在于可读性。在某些特定情况下,一些函数可能来不及优化,不得不写得很长,但我们至少可以通过分段和注释,对代码进行梳理。 一个好的代码分段结构,大概如下: //单行注释A:介绍接下来一小段要执行什么内容 aaaaa; bbbbb; if (c) { ddd; ..... } //单行注释B:介绍接下来一小段要执行什么内容 aaaaa; bbbbb; if (c) { ddd; ..... } //... 注意,在两段代码之间,有一个空行。这个空行很重要。他代表着上面这段代码的含义是最上面单行注释A,而下一段代码的含义是单行注释B。如此,阅读者可以很轻松地理解这两段代码到底分别执行了什么逻辑。 分段的技巧 在理清楚需求的业务逻辑后,你可以先写一个空函数,然后再函数内只写注释,不写逻辑。 public void reward() { //判断功能开启 //判断活动开启 //判断玩家传参 //判断玩家领取状态 //扣除玩家消耗 //修改状态 //发放奖励 //推送奖励弹窗 //返回成功给客户端 } 当你把这个函数需要做的事情梳理清除,简单写出注释,分段也是水到渠成的事情了。...

八月 10, 2025 · JonathanLin

游戏服务器开发经验(三)线上维护

技术文章 游戏服务器开发经验

游戏项目公测上线之后,玩家总能找到我们预期之外的方式,做出超乎预料的行为,线上服务器就会有各式各样的错误情况。即使在开发过程中我们已经尽力去编写健壮的代码,完成黑盒白盒测试和代码Review,也无法完全避免线上的各种问题。 不过我们不需要为此悲观。在Java中,底层的虚拟机足够健壮,此外JVM也提供了很多工具给我们进行排查和修复问题。 重载class以修复错误逻辑 在线上服务器中业务逻辑有问题时,我们可以针对出问题的类,在本地修复代码逻辑后,编译出新的.class文件,然后上传到服务器能访问的文件夹目录中,使用Java虚拟机的redefineClasses()方法,将新编译的正确业务逻辑的类class重载到线上的JVM虚拟机中,覆盖之前的class。 如此一来,线上执行的class被替换,后续具体的方法被调用时,执行的是新class的代码业务逻辑。 在线上执行本地编写的代码 使用Groovy脚本,可以在线上服务器中运行代码。在本地编写好groovy脚本代码,通过线上提供的接口(可以是后台接口,或者是GM管理员指令入口)执行groovy脚本内容。 如此一来,当我们对线上服务器的情况有疑惑,需要打印具体的内容;或是希望能修复线上用户的错误数据,就可以使用直接执行脚本代码进行操作。 需要注意的是,Groovy脚本实际上是对使用者要求非常高的一项技术,几乎是线上游戏修改器般、黑魔法般的存在。用得好,可以快速修复玩家数据,能够快速查到服务器异常状态;用得不好,甚至可以直接删库跑路。 关于重载class和执行groovy脚本,代码可参考 https://github.com/jonathanlin768/JavaHotFix 仓库。 策划配表重新加载 有一些在线无感知的游戏热更新是通过策划配表重新加载来实现的。偶尔有一些版本开发过程中,可以先把代码更新到服务器线上,但是不着急马上把新功能开启给玩家,策划可以在功能还未和玩家见面时,修改新功能相关的数值,也可以通过修改活动时间等方式来动态调整功能的开启。 既然策划配表重载可以调整功能的开启,那么必然也可以完成功能的关闭。在开发过程中可以定义一张系统功能表,上面记录各种功能的id,并且有一个强制关闭的标记。开发过程中如果判定功能id关闭,则不允许执行后续逻辑。 如此一来,如果线上已出现异常情况(如新玩法无法正常继续、奖励被刷),则可以快速先关闭具体的功能id,让后来的玩家无法继续造成错误情况。 日志 所有人都知道要打印日志,但是到底打印什么日志最有用的呢?不同的项目有不同的答案。但是打日志是非常有用的,这一点毋庸置疑。 根据我的理解,要打印日志的情况有: 策划配表配错时,打warning。在开发阶段,策划要对自己的表进行测试。他们很有可能填错了数值而不知道。这时候打印出日志可以快速定位到底哪里配错了。 千万不能执行到的错误逻辑,打error。开发过程中如果发现一些情况是异常的,是正常逻辑肯定走不进来的,甚至可能因为异常而导致记录了错误的状态,则一定要打印日志。比如某个玩法一定要组队才能游玩,但是玩家在没组队的情况下请求游玩,说明玩家在之前已有错误,因此需要打印日志以记录情况。 玩家多次反馈出问题的功能逻辑,在最关键处打info。比如某个玩法结束后发奖失败,则需要在发奖的各个阶段打印日志,用以分析哪一个步骤出现问题。 玩家核心玩法行为日志记录,这个可以打info,也可以使用额外的行为日志记录(因为玩家行为可能很多,全部打到log文件会太大,所以可以将其通过消息队列发送到另一个数据仓库中进行汇总分析)。比如MMO游戏的玩家地图跳转,SLG游戏的战斗记录等。 不要打印日志的情况有: 接口的业务逻辑中,已有明确错误提示后返回的情况。如在商店道具购买时道具不足,客户端已能收到有意义的文字错误信息,则再在服务器打印错误日志是意义不大的。 尽量不要在太公用的业务逻辑里打印日志,即使要打印也要加一些条件。这是因为公用逻辑会被各个不同的业务调用,并且一般是主要的玩法。打印日志不加条件限制的话,会造成日志过多而不方便排查。 什么时候用info,warning,error? 这个问题也是不同的项目有不同的答案。 我的理解是:为了观察玩家和服务器状态的用info,不影响玩家继续游玩,但是确实可能有异常的用warning,要是执行到就一定是有错误的用error。 版本更新前的内部服务器日志一定要关注 当版本开发完毕,到测试阶段时,一定不要觉得测试人员没有反馈就完事大吉了。一定要关注内部服务器的日志,尤其是新打印的日志。内网能看到的日志,外网一定能看到;而外网能看到的日志,内网也一定能复现。 最后,不用担心日志打太多的问题。查不到问题的原因才是更坏的情况。 服务器状态监控 版本更新后,新玩法开启的几天,一定要有意识观察线上服务器的CPU和内存使用状态。就算没有搭grafana那样的工具,你也可以连到线上服务器通过Linux命令查看是否异常。如果CPU过高或者内存不够,则需要尽快把问题反馈出来,从而尽快分析并且处理问题。 这绝对是一个好习惯,原因是有一些问题可能玩家遇到了没反馈出来,他们可能觉得无伤大雅,但是那些没反馈的问题,对服务器的状态也许存在一些风险。 线上问题定位 前文所述内容都是为了避免和修复线上问题。而如何定位线上问题,才是服务器开发中最困难的一环。 这件事的困难之处在于, 无法复现玩家所遇到的情景; 玩家反馈的信息是玩家理解的内容,不一定是服务器上的问题所在 情况是耦合发生的,触发条件苛刻 而当我们花了大量时间排查,最后可能发现只要改不超过10行代码就能解决问题。这或许有一点幽默,但是却是经常发生的情况。 但当我在此回看过去,我发现线上问题一定是能查到结果的,只不过是时间成本问题罢了。真正阻碍我们查到线上问题症结的是我们的内心。 要相信问题是一定能被排查到的 计算机科学是一门年轻的学科。它远没有达到物理学中“电子双缝干涉”那样深奥而暂时无解的情况(当然你一定要拿“比特跳变”这种极端罕见而异常的情况抬杠的话,我就认输)。而游戏服务器的环境配置,都是由你我这样的凡人搭建的。游戏服务器的业务逻辑更是由团队里的大家编写的。所以一切的问题是一定能定位得到的。 而在排查过程中,要相信一点: “所有的问题都会解决的” 当你在排查线上问题的时候,不要惧怕那些线上问题,也不要惧怕那些给你施压的人。程序员常有的完美主义让我们认为一切的代码问题都是可控的,因为业务逻辑是我们一行一行写出来的。但是这不代表代码就一定没有问题。恰恰相反,出问题才是常态。出问题不代表我们不好不优秀,而是“我们还有未知的内容需要去学习”。不要惧怕那些给你施压的人,因为与问题正面对抗的人,就是屏幕前的你。你本应该是伟大而强大的。专注于你要排查的问题,像侦探一样耐心地分析各种蛛丝马迹,所有的问题都会解决的。 分析玩家所述内容,排查异常情况 当玩家反馈错误的情况时,根据玩家的描述定位到具体的玩法,这时不要直接钻到玩法的业务逻辑代码之中,那就如大海捞针了。我们应该根据玩家提供的玩法信息,游玩时间,反过来去查找日志,观察其在出问题的前后执行了什么操作、消耗和获得了什么道具、游玩了其他的什么玩法,在此期间是否有可疑的错误日志等等。 这一步同时你需要去理解玩法的具体内容,比如时间,状态等。通过日志和玩法内容,去勾勒玩家当时的情况。 模拟玩家所遇到的环境 有条件的话,尽量能把出问题的玩家的数据导入到自己的服务器上。这样我们就可以在本地进行各种尝试。如果能把玩家出问题前后的玩家数据拿到本地则更佳。即使没有玩家数据也没关系,在内部环境中模拟玩家所遇到的情况,通过玩家提供的视频,和服务器上的日志,可以得到玩家的状态,进行游玩,也一定会有新的理解。如果实在是无法在本地复现,你也可以用脚本到服务器上执行一些代码,打印玩家现在的状态(现在的状态也是有意义的,玩家有自己的方式再次复现)。 仔细甄别“问题情况”和“猜测的原因” 当线上问题发生时,如果是大问题或者难排查的问题,会有很多人的噪音在耳边叽叽喳喳。他们提供的思路不一定是错的,但是可能和当前最重要的问题没有关系。一定要分析出问题的情况在哪里,专注于要修的问题的情况。会有一些对游戏很了解,但没有看代码的人提出很多猜测,要理性地分辨这些猜测,小心仔细地验证各方论点。 如果真的查不出,就打印排查日志 分析玩家行为、导玩家数据到本地、模拟出问题的环境、并且各方讨论也没有结果,那么请打印好排查日志。 排查日志一般是为了以下目的: 检查关键代码逻辑是否被访问到; 当执行代码逻辑到某一处时,检查玩家的状态是否正确。 因此这样的排查日志,需要打印的细节一定要越丰富越好。如果打印的内容很多,最好能用StringBuilder将多行内容拼成一个String,然后直接用一行log打印出来,以避免日志收集器收录到只言片语。 你甚至可以针对某一个玩家游玩时,仅针对他打印各种细节日志,甚至可以包括程序调用的堆栈信息。

七月 13, 2025 · JonathanLin