《LifeLine》这个文字冒险游戏确实挺新颖的,当然,是对我们这一群习惯了 GUI 和 CG 的人来说。以前也有过类似形式的游戏,比如,逗 B 编程小王子就说,他们公司以前写过一个 “短信游戏”,你发短信,告诉主人公下一步该怎么走怎么走……一条短信一块钱。
一块钱买了这个游戏,三天通关。在玩通关了之后,心血来潮:这东西的原理一定很简单!游戏肯定是以脚本方式工作的!脚本肯定是存在一个 TXT 里面或者数据库中!为啥?直觉。要相信一个程序猿的直觉。
那好,自己写一个出来玩玩。
拿到游戏脚本
由于下载不到 ipa 文件(iOS 专用),只好转战 Android 平台,下载了 apk。解压缩,在 assets
文件夹中找到了 StoryData_en.txt
等文件,打开看了以下,恩,确实是我们需要的游戏脚本。(点击查看 非官方中文版游戏脚本)
iOS 版有官方简体中文版,Android 只有德文、英文、法语、日语、俄语,并没有中文。当然,国内汉化组很多……比对了国内汉化组和官方中文版,惊叹有些时候山寨的水平竟然能高于原著。
分析游戏脚本
脚本拿到了,就可以开启 “上帝模式” 了。本来想着是 “只需要跳转跳转再跳转” 就好了,应该很简单吧?结果发现,并没有第一想法那样简单。脚本里面使用了很多自定义语法,可能还得自己写一个脚本解释器才能让它工作。(后来才知道,这东西叫做 “模板引擎”,和各种框架的 V 层异曲同工)
大致看了一下游戏脚本,里面的语法不太难,标签元素不太多,大致分为以下几种:
- 游戏区块定节类:
:: BlockName
,其中BlockName
是区块的名字。整个游戏是一个区块一个区块进行的,区块之间有跳转,每个区块有自己的名字。区块没有结束标志。 跳转类:
[[BlockName]]
,其中BlockName
是需要跳转到的区块的名字。延时跳转类:
[[delay XXX|BlockName]]
,其中XXX
是需要延时的时间,单位为h
(小时)、m
(分钟)、s
(秒)。比如,在出现 “我们 4 小时后见”,后面会有一句[[delay 4h|SomeBlock]]
。使用延时跳转后,会先出现一句 “泰勒繁忙”,然后开始延时,游戏暂停,延时后再进行跳转,以正常速度进行游戏。交互选择类:
<<choice [[foo_1|bar_1]]>> | <<choice [[foo_2|bar_2]]>>
。其中foo
是提示语句,bar
是跳转到的区块的名称。由于游戏中都是 “二选一”,所以都按照二选一来处理就好了~记得中间有一个|
哦~屏幕显示类:
[SomeSentence]
,其中SomeSentence
都是英文,例如显示 “泰勒繁忙” 之类的提示语。这些提示语做了国际化处理,对应的翻译在另外的 xml 文件中。变量赋值类:
<<set $foo = bar>>
,其中foo
是变量,变量前要加上$
,bar
是值。如果值为 string,外面需要加上引号。这点和大多数编程语言都一样。变量输出类:
<<$foo>>
,其中foo
是变量。判断类:
<<if >>
、<<elseif >>
、<<else >>
、<<endif >>
,都能看懂吧?判断条都是$var op value
的形式,例如$medicine gte 3
(药品数量大于 3 片)。op
我发现的有is
(等于)、eq
(等于)、gte
(大于等于)。静默类:
<<silently>> <<endsilently>>
,好象是为了防止输出用的吧?被这一队标签包裹的内容都不会产生输出。注释:
// Comment
。都懂吧……正如游戏 Script 开头所说If you’re reading this, you’re in for spoilers but you probably don’t care! Thanks for playing, and have fun.
你开心就好。
恩就这么多了。
实现
读脚本文件并进行标准化
大致思路如下:
- 去掉注释
- 去掉空行
- 去掉
Choice
__之间__的|
- 将每一个标签元素都自成一段
解析标签
由于每一个标签都是独立的一行,所以解析起来很简单了:大多数情况下只需要判断标签的前面半部分是什么就好了。
对于跳转什么的,可以拿 Map,一下子就实现了,很好用,但是 if、else、set 这些东西呢?我的做法是将它们拿正则表达式或者纯的 strReplace 替换成 Python 的语法,然后使用 eval(SomeStatement)
获取结果。然后发现,if 好像有嵌套……那就拿一个简单的栈来玩吧。
比如,处理 if
:
def __do_if():
# 全局变量表
parameter = self.__parameter
# 标签对我来说并没有用,扔掉
script = script.replace('<<if', '')
script = script.replace('<<elseif', '')
script = script.replace('>>', '')
# 将 is、eq、gte 等东西替换成 Python 的
script = script.replace(' is ', ' == ')
script = script.replace(' eq ', ' == ')
script = script.replace(' gte ', ' >= ')
# 用正则表达式,把模板里面带 $ 的变量替换为全局变量表里面的变量
script = re.sub(r'\$(\S+)', r'parameter["\1"]', script)
# 为了防止缩进,做一下 Strip
script = script.strip()
# eval 一下,拿到判断结果
judgeResult = eval(script)
return judgeResult
延时跳转和本地化我没有做……一是当时没有发现(以为 xml 文件没有用,直接删掉了),二是,我懒……
但是为了获得一些类似的体验,我设置了几个 “延时时间”,最起码能让哪些语句 “一条一条地出现” 而不是 “哗啦一下子全部出现”。延时怎么做?time.sleep(SomeSeconds)
,注意单位是 Seconds。
半成品
半成品我已经放到 Github 上好久了,请打开任意门~。
运行 python3 main.py
即可。一定是 Python3 啊~
啥?GUI?要什么自行车啊!
总结
模板引擎,简单实现嘛,又不用考虑效率神马的。
好像 3 Minute Games 并没有说不允许对它的游戏做逆向工程啊~更何况是 “仅作为学习和研究使用”。
实现这个东西大概花了我两个下午和三个晚上。不过呢,人家可是公司啊!美工、剧本……还要求不能出 BUG……点个暂顺手物质上支持一下也是应该的。
参考资料
- Python 基础教程. [挪]Magnus Lie Hetland 著,司维 等译. 人民邮电出版社. 2014 年 6 月
发表回复