首页 我的文章
想用Pygame做一款Galgame?这里有几个坑!

>>摘要:

使用Pygame开发小体量的Galgame是完全可行的,通过pygame能实现绝大多数的功能。只不过使用pygame有许多的坑要踩,而国内资料并不算多, 一旦踩到了不那么常见的坑,真的会让人崩溃……

>>全文:

    我第一次接触Galgame应该是著名的《ヨスガノソラ》,之后很长一段时间都没再接触过Galgame。几年前,突然接触到了《千恋*万花》。
    除了画面、音乐之外,最让我震惊的是它的故事,它让我知道了Galgame也可以成为一个良好的故事的叙述载体。(幼刀天下第一!)
    最近在某宝乱逛的时候突然发现了一本书,是讲Pygame的技术书。虽然我没有下单购买,但我还是第一时间在网上搜索了有关的教程,并突然有了制作自己的Galgame的打算。
    说干就干,我用了大约三天的时间制作了一个还说得过去的Galgame,复刻了《あえて無視する、キミとの未来》的一个小片段。期间踩坑无数。
    在这里,我将介绍用Pygame实现Galgame的大致思路与容易踩的坑,而不介绍pygame的模块与用法,想要了解这方面知识的朋友请自行搜索。
    

一,台本的存储问题

首先,我在决定要制作一个Galgame之后,我先想到的问题是台本的存储问题。一个良好的“引擎”(我不知称如此简陋的程序为引擎是否合适)应该使游戏控制代码与游戏的流程代码分离。 因此我首先想要借鉴《処女はお姉さまに恋してる》的手机移植版的解决思路,将台词写入一个txt文档。 (我为什么如此印象深刻,是因为当时少女爱上姐姐的移植版有个bug,需要玩家手动修改txt文档中某个错误的符号才不会卡住。) 不过我立刻否定了这个想法,一是无论采取什么特殊符号分割,都有在台词中出现的可能性;二是如果游戏规模变大,纯文本的读取处理速度将成为一个短板,无法立刻找到需要找到的某一句台词。 因此我使用了sqlite数据库来存储台词。 数据库中包含以下两个表 gameScript、gameParm。 gameScript中包含scriptid, bgimg, bgm, spk, cont, charimg, charsound等等。 gameParm中包含游戏各个参数,包括游戏窗口大小、游戏进度(即scriptid的值)、当前分辨率下角色立绘的中心位置、当前分辨率下文本框的位置等等。 我将这些参数写到了一个类中,这些参数都有默认值,如果初始化时读取sqlite中的值出错,就会按照默认值设置。 再说说gameScript表,scriptid存储每句台词的序号,保存、读取功能都基于此序号。bgimg是背景图片,用pygame.image.load()读取,并用pygame.transform.scale()变形为gameparm中的游戏窗口大小。 bgm存储的是游戏的背景音乐,在之后我会详细地说说pygame的播放音乐的问题。 spk是当前讲述人的名字,默认为'none'。 cont是文本框应该显示的内容,如果为'none'则不渲染文本框。由于pygame只支持渲染单行文本,因此多行文本需要自己想办法解决,在下面会有详细的说明。 charimg是人物立绘,在下面我会详细介绍人物立绘的处理。 charsound是人物语音。 按照渲染顺序由底向上,依次渲染背景图片、人物立绘、文本框背景、姓名框、姓名和台词文本和其他UI。
    

二,音乐播放的问题

pygame有两个播放音乐的方法,一是pygame.mixer.music,同时只允许播放一个音乐,支持mp3格式。另一个是pygame.mixer.Sound,不同的channel可以同时播放,支持wav和ogg格式。 但是如果你录下了wav格式的音乐,然后用sound播放,很可能你会得到如下提示:unable to open file xxx.wav。 你从网上下载了.wav的音频,发现有的可以播放没有问题。 如果你去搜索此问题,你可能搜到很多解决方法,如:忘了插音乐播放设备,插个耳机就好了之类的。 你一顿操作,却发现这个问题完全无法解决…… 但其实这是因为pygame不支持32位的wav。如果你用Au将音频的位深度修改为8位就会发现一切问题都迎刃而解。 这个问题困扰了我一天左右,大概有三分之一的开发时间我都在尝试解决这个问题…… 如果是背景音乐,就用pygame.mixer.music播放就好了。而如果是人物语音,则需要一点小小的代码:

    if gameScript.scriptText[scriptId][gameParm.SCRIPTMAP['charsound']]!= 'none':
        if soundChannel.get_busy():
            soundChannel.stop()
        soundurl = gameParm.RSC + 'charsound/' + gameScript.scriptText[scriptId][gameParm.SCRIPTMAP['charsound']]
        sound = pygame.mixer.Sound(('%s'%(soundurl)))
        soundChannel.play(sound)
    
通过channel能够实现“在下一条语音停止当前语音”的效果,如果想要实现“在下一台词停止语音”的效果,则不必判断当前是否有人物语音,直接停止channel即可。
    

三,pygame只能渲染一行文本的问题

解决问题的方法是人为地将台词分割。而具体的分割长度,需要视游戏窗口分辨率与字体大小而定,需提前地测量好后填入gameParm中(即代码中的gameParm.SCRIPTMAXLENGTH)。

    def cut_text(self, text, lenth):
        textArr = re.findall('.{' + str(lenth) + '}', text)
        textArr.append(text[(len(textArr) * lenth):])
        return textArr
    

     fontObj = pygame.font.Font(gameParm.RSC + "songti.ttf", 28)
        fontObj.set_bold(True)
        heightMigration = 0
        for scriptText in gameScript.cut_text(gameScript.scriptText[scriptId][gameParm.SCRIPTMAP['cont']], gameParm.SCRIPTMAXLENGTH):
            scriptSurfaceObj = fontObj.render(scriptText, True, gameParm.WHITE)
            DISPLAYSURF.blit(scriptSurfaceObj, (gameParm.SCRIPTPOS[0], gameParm.SCRIPTPOS[1]+heightMigration))
            heightMigration = heightMigration + scriptSurfaceObj.get_height() + gameParm.SCRIPTVERTIVALSPACING
    
    

四,人物立绘的问题

储存人物立绘不像储存人物语音,同时可能有多个人物在不同的位置。因此我规定人物立绘脚本的规则为: 不同人物的立绘用;(分号)分割,同一人物立绘属性由图片名称、位置、其他控制条件组成。 而如何确定位置我也稍稍的想了下,总体以当前画面共有多少立绘同时出场来定每个立绘的位置。如“2LEFT”则表示共出场两人,当前立绘在靠左侧。“1CENTER”表示出场一人,立绘居中。而每个位置具体的数值则需要提前测试好后记录在数据库的表gameParm中。 如果有时需要一些特殊的控制条件,如下移表示人物的躲藏等等,则可写在其他控制条件中。这部分的特殊命令规则各位可以自己定夺。

总结来说

    想要用Pygame开发一款简单的galgame的话是可行的,但是需要你解决的问题也有很多,加上网上关于pygame的资料并不能说多,因此选择用它来做个小游戏还是需要慎重考虑的。