我是从写游戏脚本入的编程这行

今天翻看老照片,翻到了下面这张:

故事要从 2012 年说起,那时候我大二,梦想是去迪信通卖手机,业余爱好是捣鼓手机,就是刷系统,美化主题,逛论坛啊这些,也加了一些奇奇怪怪的 QQ 群。某天下午,看到群里有人说:“有会写自动脚本的吗?求合作,一起搞 DNF!”

DNF 我知道,游戏脚本基本没听说过。当时也不知道哪根筋搭错了,反手就回了句:“我会!”想想那时候仅有的编程经验应该还是初中微机课上用 VB 写过一个加减乘除计算器。。。

不一会,一个现在看起来略显非主流的头像闪烁了起来,是的,它来了!已记不起当时点开小窗后我们聊了啥,只是现在想想人生真的很奇妙啊,一系列看似无关的事情,无关的人将你引向一个当时看起来是自己选择出来的道路,只有若干年后再一回忆,觉得就像是一场电影,只是进度条到那了。

之后就是没日没夜的加班,边学边做。可能是天生就注定要干这行吧,过程中没有枯燥和痛苦,不会的问题就去贴吧,QQ 群去“求大神”,没人理我就自己研究到深夜。而且说来惭愧,直到现在其实我也没正经看过几本编程的书,也看不下去,一直以来学习新语言,新框架的套路就是:

先拿来用,遇到不会的再查。听起来好像这样的方式是浅尝辄止,不过我有些完美主义强迫症,我养成了对每一个一知半解问题刨根问底的习惯,敷衍不了自己,不然就会有坐立不安的感觉…于是每次爬完一个坑,就会挖出新的坑,就再爬;同时,如果我看到了某个新语法、新特性,我就一定会去联想它的使用场景,想尽办法在之后的代码里用到它,甚至要回过头去重构之前的,这也是我个人的另一个学习方法。
于是日积月累之后自然就显得「经验老道」了吧?

废话有点多了,回正题。最终经过 3 个月的奋战,有了如下成果:

主程序一共 17,958 行代码,之所以记的这么清楚,是因为强迫症还会保留着他写过的所有代码


看!这神奇的编辑器,这神奇的语法,相信会有老同行能够认出来这是当时风靡一时的按键精灵(关注点不要在代码上了,要脸..)。按键精灵支持的语言是 vbs,仅有的 vb 计算器的编程经验竟然还用上了==,你说神奇不神奇!

现在可以说说开篇时候的那幅图了。它是当时合作的 DNF 工作室一角,看到那光膀子纹身大哥了没?涩会银!就现场看着你改 bug,改不好当场就砍死的那种!哈哈,开玩笑~当时看着满屋子的机器,全部都运行着自己的脚本,所有屏幕上的小人就像勤劳的小蜜蜂那样在一刻不停地干活,那感觉甭提有多兴奋了!

不过如此庞大的脚本对于按键精灵来说真的是不堪重负了,平均每分钟都有机器出现脚本故障,一言不合就程序崩溃,游戏卡死。当时没有更好的技术手段来解决这个事情,只能再写一个脚本来监控主脚本,发现主脚本或游戏崩溃了就重启它们。然而坑爹的是监控脚本也会崩溃!于是又用 VB 写了个监控监控脚本的程序。。。

哎,说多了都是泪啊!谁让咱那时还不知道外面的世界已经有了这么多好的语言,好的平台,偏偏选了按键精灵啊!

越说越跑题了,长话短说吧!后来没过多久剑灵出了,然后基本上所有的工作室都开始转换阵地,跟我们的合作也就凉凉了。还有件值得一提的事情是那时候比特币刚刚流行,当时哥们撺掇我一块挖矿,不仅被我严词拒绝了,我甚至还说服了他别做这个!直到现在我都没脸再联系。。。唉,人生啊!

正文

目前在大大小小的公司从事 Web 前端工作已接近 4 年了,JavaScript 早已成了本命语言。也是某个无聊的午后,打完流放之路的我格外的空虚寂寞且累!于是灵光一闪,既然这么累,不如写个自动脚本放松一下?按键精灵肯定是不会再按键精灵的了,这辈子都不可能再打开它了,毕竟我现在已经掌握了世界上最好的语言。那何不借助 Node.js,让 JavaScript 和自动脚本一起搞搞事情?说干就干!

先是在 Github 上搜索了一番相关类库,脑壳疼,果然没有如意的。想想自动脚本需要的一堆 API:图色识别、键鼠操作、系统指令等等等等,要自己全部实现一套真的是一个天量工程了啊。于是我盯着桌面上的大漠插件发呆。。。

等等!大漠插件是啥?为什么要盯着它发呆?

如果你干过小学语文课代表的话你就肯定知道:当文章中出现了一把枪,哪怕只是一笔带过,那它肯定会射击的,否则作者根本就没必要提它。嗯,所以这只是我娴熟的运用了一下文学手段,不着痕迹地就引出了下文:

什么是大漠插件?

老同行肯定知道,这插件本身的名号甚至盖过了当时它依托的各类自动脚本平台,如:按键精灵,易语言,VB(它倒不是脚本平台),总之就是大名鼎鼎!它实现了一整套自动脚本相关的 API,这么跟你说吧,你能想到的它都支持!所以开始想办法能让 node 去调用它吧!

首先,它是用 C 语言写的 dll 插件,规范是 COM IDispatch。好的,关键字很全了:node com dll。于是谷歌一下,我就知道了可以使用 winax 来调用!真棒,天作之合!

然后就有了它:dm.dll!也没啥技术含量,就是封装了一下。更有意义的反而是把它俩结合在一起这事情本身,所以放出来觉得也是一件利国利民的好事情啊!哈哈哈哈,先去翻翻文档吧~顺便给个星?

怎么写游戏脚本?(干货篇)

现成的插件都有了,代码咋写肯定不用我来多嘴了。这里主要分享一些老脚本工作者的心得。

游戏脚本不比内存挂,更不比封包挂,基本处于该产业的鄙视链底端。但优点是:安全!因为它基本只能做到和真实玩家一样的事情,只是不怕苦不怕累,能一直干活而已。由于没有飞天遁地那些特技,所以一般不会被封号。而且上手也简单啊,毕竟我这种半路出家的都。。。

然后不谈业务能力,它本身的原理就自带了缺点,即基于图色识别和键鼠操作会带来两个问题:

  • 得到的信息量一般不会比人多
  • 操作具有不可控性

首先说第一点,比如说当前游戏的角色,我想知道它装备了哪些物品,如果人需要打开装备栏才能看到,那脚本也一定需要(这里不讨论通过内存 / 本地缓存读取),甚至打开了由于装备栏过于花里胡哨无规律可循,基于图色文本的识别方式会过于复杂导致放弃。

然后第二点的不可控性是由于大量的键鼠操作会受到网络波动,客户端卡顿等外界因素的影响而导致操作失败或无效。就比如上面说的打开装备栏,有的游戏支持快捷键,你调用了点击键盘的命令,然而这个时候可能突然谁给你发了个窗口抖动使游戏窗口处于未激活状态,或者就是客户端卡了一下,都会导致键盘命令不会正确触发装备栏打开;就算不用快捷键,比如点击某处的图标也可以打开,那有可能当你在点击之前突然游戏发了一条公告,窗口内部把那个图标给挡住了,你永远点不到,甚至点到广告。。。

所以,除非你的脚本代码能覆盖到所有场景,否则不可能一直奔放,基本在运行了一段时间后会鬼畜当场,或者直接崩溃卡死。

那明白了上述两点,其实就可以对症下药了,做不到完美那可以做到相对完美。咱们还是一点一点说:

信息获取

假设你想要获取的信息是:地上的这件装备属于哪个部位?(头饰?腰带?还是戒指?等等)。人当然可以通过物品的样子识别个八九不离十,但脚本不可以。笨办法是收集所有装备的特征信息,比如相对位置的颜色,或者干脆就是截图,来硬识别。不到万不得已尽量别,缺点有三:

  • 你要经常更新装备库
  • 识别慢,效率低
  • 工程量巨大(如果你要对外推脚本的话,可能还要维护多套兼容包

好的办法是先去多研究游戏本身的交互,比如说把物品拖送到公屏,公屏里会显示出物品的描述文字,那你就可以通过文字识别来做;或者说把物品拖到人物属性面板上,面板上人物对应的部位会亮起来,那你可以通过识别对应部位的特征色来做。

还有,假设你现在要做自动打怪了,可是游戏中的怪物都是花花绿绿的 3D 炫酷模型,根本没法用特征识别。那这个时候就上机器学习吧!开玩笑开玩笑。。。其实可以去动动游戏源文件的心思。到游戏的安装目录里多点点看,一般你会找到游戏的贴图文件,想办法解压出来,再稍微懂点 dx 脚本就更好了,去改贴图!把所有怪物给染个颜色,没有特征咱就制造特征!那万一没找到,或者改不好咋办?那再回归到游戏本身,想想有没有可能利用某些职业的特点来降低打怪难度?比如说选那种无脑 AOE 就可以过图的?再不行去读内存啊!(这又是一个新的深坑,慎重!

去年我在写流放之路脚本的时候,有个需求是:希望能够控制人物刷图时拾取物品的类型。但爆出来的物品何其多啊,靠硬识别肯定是不现实的。于是就研究这游戏的设置,发现竟然客户端本身就支持物品过滤器,那类型过滤这份工作就可以转化成动态修改物品过滤器文件,设置里再重新载入它

再说几个自己想到的点:

能不识图就别识图,能不识字就别识字

这些操作都是比较耗时且费人力的。。大部分都可以转化成识别某一点,或者多个点的颜色来替代。举个例子:比如你要判断角色是否死亡,你完全没有必要去识别死亡后的游戏弹窗图片,而只需要去找某个基本没有颜色变化的区域,比如底部的操作栏,它只会当死亡弹窗出现的时候才会由于弹窗阴影而发生颜色变化,那你就直接取操作栏的某一个位置颜色来对比就行了。

能不识别就别识别

这个看起来有点夸张了,咋连识别都不让了?这里的意思其实是好多识别其实是可以通过计算算出来的,不要做不必要的识别操作。比如你要判断人物的包裹是否满了?先说个取巧一些的办法:你可以无脑去拾取某件物品,发现拾取了很多次物品还在地上,那可以间接推测出包裹已满。这里识别物品一直在地上一般要比直接识别包裹本身简单。

不过上面这个办法只是取巧了些,再深入考虑一下:假设所有物品的取放操作都是脚本完成的,那理论上每一次取放操作和包裹的剩余格子数脚本应当做到“心里有数”,所以最极致的做法就是脚本连试都不用试,捡完最后一个能捡的它就知道包裹已经满了!

当然了,这个无疑会增大不少编程复杂度,对程序本身有追求的可以当作进阶尝试。这里自夸一下,当时流放之路那种变态的物品系统,每个物品的大小不一致,包裹还分了多个页面,有些页面还只能放特殊物品,为了节省空间很多物品需要采用拖动摆放这种复杂操作(要有足够的放置空间,你放的位置还要保证尽量少地侵占空间,而且拖放动作对于脚本本身就是一个极不稳定的操作),即使这样我都做到了让脚本“心里有数”,嘻嘻!

不可控性

脚本的所有行为其实都可以抽象成:行动 + 反馈

比如你现在想要打开装备栏,那你的行为是:点击 i反馈是:装备栏出现。ok,那你可以基于此,封装一个行为反馈方法:

1
do(task, until)

每一步操作,都严格确定好行为与反馈,通过 do 方法来调用,task 代表行为,until 代表反馈,那上面的需求就可以这样:

1
2
3
4
// 直到 800, 750 的坐标处出现某个颜色(装备栏出现),都不停地执行点击 i
async function openEquipment() {
await do(() => keyPress('i'), () => isColor(800, 750, '#ffffff'))
}

当然了,你可以给上述方法设置一个通用延时,设置一个最大重试次数等等。总之每个操作如果都严格遵循这个基本规范,组合起来就不怕不稳。

据我所知,脚本圈子里的程序员好多都只是给每个操作后面手动 delay 一个经验值来提高稳定性,甚至会在 delay 后面再复制一行操作,据说这样会得到稳定 buff。。。

上面这部分可以看作是局部的稳定性优化,如前文所言,依然无法覆盖所有场景,你的 do 函数很可能会卡死在某处。。那么再来理一下思路,既然无论如何都 cover 不了所有的场景,那干脆就不管它们了!就直接按照一切都正常的样子去写你的脚本,然后用全局调控的思路去处理所有非正常的场景!

这不是一句废话,覆盖不了不代表调控不了,所有的异常实际上都可以抽象成我的正常操作没有在预期时间内得到正确的反馈。基于此,再来封装一个高层次点的框架吧!它用来调度所有的局部模块,一旦某处长时间卡死,或者主动弃疗(比如说上面的 do 函数你设置了一个最大重试次数,只要超过就视为异常)。那么该主控程序将直接重启游戏,或者将游戏强行拉回到某个可控的场景,再从该场景处重新执行局部的操作逻辑,于是理论上就可以实现稳定奔放了。

ok,再祭出一个神器:wow-state-machine —— 为游戏自动脚本量身定做的状态机,它就是整个脚本的”调度中心”。即使是基于 Node.js 的单线程,你也能够实现”同时”检测角色血条,掉落物品,游戏状态等等各种来触发不同的操作,如下图是本人之前做的流放之路脚本的主框架部分代码:


清晰明了,简洁优雅!👏

除了写游戏脚本还能干啥?

请看下面这段 VCR:

传说中的薅羊毛啊!嘘~

那还能不能干点正事了?

请再看下面这段 VCR:

这是之前做的某业务,需求是:希望能够自动创建微信群,然后共享二维码。
于是就写了这个支持分布式工作的脚本,可以 N 个脚本一起干活,没事就建群玩,生成好二维码随时待命!除非请求量过大,一般场景都能做到像请求个普通数据那样,用户毫无感知,他们是无法想到这二维码的背后蕴藏了多少高阔技的!啊哈哈哈哈~

所以会写自动脚本算是多了一项傍身的技能,也就多了解决问题的新思路与新手段。不过。。

如果某项需求实现起来过于复杂,那往往是需求本身出了问题…

相关工具


 评论