python制作galgame引擎(一)

房东的猫 提交于 2019-11-28 21:21:02

  写这个项目的直接原因是最近推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上没有翻译的说……

 

 

   

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!