写这个项目的直接原因是最近推galgame推得有点过头,gal推过头的直接结果就是YY能力上涨,抱着“我也想写好玩的剧本”的轻率念头,也就开始了这个项目。不过从直接感觉来说,galgame毕竟也是开发成本(个人)以及技术要求最低的游戏类别之一,这当然也算是原因。
于是到了现在,一个半成品式的框架就搭好了。实话实说,gal引擎开发,技术难度不算大。但是,需要考虑的方面却相当多,许多看起来很简单的东西开发起来却很麻烦,看上去很麻烦的东西只要换个思路,就会大为简化。
这篇文章涉及的只是一个Galgame引擎,剧情和素材方面,直接使用guochaoer君的Sakia。后者是一个自制的galgame,在google code上托管,开发时间先于我一年。guochaoer君的代码,给了我不少思路和代码方面的启迪,在此给予深深感谢。有兴趣的话,请务必拜会guochaoer君的项目,地址 Sakia
不多说了,现在考虑引擎的编写。
我们需要几个初期的目标,我的初期目标为:
1.一个能稳定运行的,可扩展的框架
2.代码逻辑和剧情内容彻底分开
第一个目标是显然的,关键是第二个。之所以提出这个目标也正是因为guochaoer君的代码,他的代码实际上写得不错,但是最大的弊病是代码和剧情文本混在一起,整体上来看,就显得比较凌乱,耦合度太高。这样做还导致一个问题:代码扩展性不太好。如果后面需要增添功能的话,很有可能大改,在效率上不太划算,也容易失误。
所以,我做了这样的设计:代码的执行部分放在run.py这个程序中,而剧情,图片,音乐,放在一个script文本中,代码和文本通过一个Parser解析器来联系。
文本的解析,就我掌握的部分---也是通常的手段,就是正则式。
为了解析方便,规定特定的语法是必要的。
在定义语法之前,先明确哪些语法是必须的,这涉及到一个问题是:一个gal,最基本的元素是什么。也就是说,舍弃掉哪些内容,gal才是gal,而不会被认为是别的什么?
我总是倾向于先搭建出一个最简单,最基本的的框架,复杂部分在整个框架搭建好后,慢慢添加。好处就是,能尽快体验到竣工时的成就感,而随后添加复杂内容的时候,也能检验代码是否足够解耦而且易扩展,并且有能对质量差的代码进行重构的机会。这是以前崔老师教给我的,真是无往不利。当然坏处也很明显,容易挖坑不填……恩。
就我个人来看,一个gal,最简单的当然是只有背景图片,背景音乐,剧情文本三种元素的游戏。人物对话并不是必要的,一个好例子就是,很多时候,深夜推gal懒得戴耳机,直接把psp声音关掉也不影响剧情体验……按钮的话,也有缘之空那种整个游戏只有三个选择支的gal,三个,基本等于没有……人物的图片,单纯是我找不到合适的png……
还考虑,鼠标点击之前和之后实际上元素发生了切换。但是最简化模型,可以把切换都省了,直接在画面上显示一幅图片,几行文本,播放音乐----这相当简单。
ok,背景图片,背景音乐,文本,三种元素。为这三种元素定义各自的语法并不困难,比如我自己,就定义成这样-----
[background = 'xxx'] ---- 解析这个获得背景图片的名字,也就是xxx部分
[BGM = 'xxx'] ----------- 解析背景音乐的名字
这似乎是自然而然的。
但是文本有点不同。考虑到文本是gal中最频繁使用的元素,每次使用[Text = ]这样的语法,就太麻烦了,但是如果直接使用.*匹配,又太过粗放。于是我使用了<>,一对尖括号进行区分,这样写正则式也相当简单。
综上所述,我们就有了三个很好的正则式:
def __InitReParserBackground(self): pat = r'''^\[background\s*?=\s*?'(.+?)'\]$''' return re.compile(pat,re.M) ## I think paser a gammer like [BGM=xxx] is a good way def __InitReParserBGM(self): pat = r'''^\[BGM\s*?=\s*?'(.+?)'\]$''' return re.compile(pat,re.M) ##I'd to use THE spcial way to define TEXT,like this: def __InitReParserText(self): pat = r'\<(.+?)\>' ##TELL re to match \n return re.compile(pat,re.DOTALL)
我用了三个简单函数包裹各自的正则式。
考虑到事实上它们都是用来解析的,可以打包成一个类-----理所应当吧?还额外有一个好处:正则式的编译部分相当花时间,包裹成类的话,直接在初始化的时候完成编译是相当合算的。
还没完,虽然现在的简单框架不涉及画面的切换,但是我们还是得为切换做准备,直白地说,我们需要一种方式区分两帧---这里的帧是我自己定义的一个术语,大致就是指任意时刻游戏中包含的可感受的内容,对gal来说,就是某一刻的背景图片,音乐,文本的总和。
简单的做法是用空行进行区分,事实上,这种方式相当有效。
那么,使用定义的语法,如果写script的话,就这样。
[background = 'B1.png']
[BGM = '1-16.ogg']
<同人社团“5年目の放课后”实际的成员只有Kantoku一人,Kantoku,日文写作カントク,意思是“监督”,他出生于1985年3月,活跃在ACG多领域的人气画师(风笳补注:需要注意,SAVE功能并未添加)>
< Kantoku 并非自幼就开始接触绘画或者购买ACG商品,小学时候他还仅仅只是个沉迷于四驱车的小男孩,升入初中后,名作《魔卡少女樱》红遍全日本的时候,Kantoku也无可避免的成为了这部漫画的fan,>
上面的内容,直接截取自我这里,也就是guochaoer君的剧本……嘛,很容易读和解释吧?于是Parser类的代码如下:
import re class Parser(): def __init__(self): self.NodeIndex = 0 ##Def any frame to a Node,and to point the SEQ,it is need a index self.Background =None self.BGM = None self.Name = '' ##The argv about the speaker's name,maybe who is NONE self.Text = '' self.RPBackground = self.__InitReParserBackground() self.RPBGM = self.__InitReParserBGM() self.RPText = self.__InitReParserText() ##There of above are REGULAR EXPRESSION,to paser the Gammer which I define ##only ONE compile can cut some time,MAYBE..... def __InitReParserBackground(self): pat = r'''^\[background\s*?=\s*?'(.+?)'\]$''' return re.compile(pat,re.M) ## I think paser a gammer like [BGM=xxx] is a good way def __InitReParserBGM(self): pat = r'''^\[BGM\s*?=\s*?'(.+?)'\]$''' return re.compile(pat,re.M) ##I'd to use THE spcial way to define TEXT,like this: def __InitReParserText(self): pat = r'\<(.+?)\>' ##TELL re to match \n return re.compile(pat,re.DOTALL) ##split the script by each empty line def split(self,target): script = open(target) LNode = [] Node = '' for line in script: if line != '\n': Node += line else: LNode.append(Node) Node = '' LNode.append(Node) return LNode def parser(self,target): if self.RPBackground.search(target): self.Background = self.RPBackground.search(target).group(1) if self.RPBGM.search(target): self.BGM = self.RPBGM.search(target).group(1) if self.RPText.search(target): t = self.RPText.search(target).group(1)
上面的代码截取自最终项目代码的Parser类,当然只是一部分,但是也能看出一些东西。首先,类进行初始化的时候,对正则式进行编译。定义若干self属性,来保存解析后的内容。之所以用self属性,是我希望若非指名,下一帧的属性值直接使用上一帧的属性值。带来的好处就是,[background= 'xxx']或者[BGM = 'xxx'],只需要某一帧定义一次,接下来,如果不是显式改变这个值,都保留着,直到结束或者更改为止。
split方法用来分割帧,返回一个待解析的字符串列表,parser方法用来解析。
本来打算直接结束Parser类的解说的,貌似涉及后面一些设计,还是就这样吧……
ps.还有什么剧情好的gal么?汉化的最好,psp版首选;日语(生肉)的话,pc版的就好,psp就不用了,psp上没有翻译的说……