python制作galgame引擎(四)

  这一篇主要讨论的内容是帧的切换以及按钮的处理。

  这个帧,并不是普遍意义上的帧数什么的,事实上,这货是我自己定义的一个概念。如果你不明白帧是什么,请务必再看看第一篇的内容,这个术语在那里我定义过了,这里不累述。

  前面三篇所讨论的东西,合起来做出来的效果也只是一个静态的无趣玩意,完全不能被称作galgame。但是,使用第三篇所封装的类,实现一个真正的galgame其实很容易。事实上,我们可以把一帧和一个NodeItem类等同起来,每一帧其实就是一个NodeItem的实例调用update方法而已。这样的话,切换帧的过程也就可以等于对NodeItem值进行更新的过程。而需要的值,都已经包括在一个待解析的字符串列表里面了。只需要对每一项进行解析,再对NodeItem赋值,更新过程就完成了。OK,现在,所有的材料貌似都已经准备完成,也许应该开始写代码了?

  且慢,为了更好地实现“动起来”这一目标,需要引入一个玩意,或者说概念:Frame Index。嘛,虽然这货也是我随意取的名字……但是,正如分析的,我们不需要这个值,也可以进行切换不是么?是,没有这个值的话,切换也是可以的,并且也是容易的-----之所以引入这个值,是为了为按钮的实现做做铺垫-----要知道,按钮的引入,就破坏了顺序读取元素这个大前提,为了支持随机读取,index是必要的。同时,之后如果需要支持save和load的话,index也是必要的。

  很明显,index是每一帧都需要的,为了使书写脚本时候不那么蛋疼,可以很简单的使用这种语法:

   

0

 

  正如你所见的,一个单独的数字就可以了,这样的话,正则式也十分容易书写。

    def __InitReParserIndex(self):

        pat = r'^\d+?$'

        ##TELL re to match any line

        return re.compile(pat,re.M)

  容易得没话说,不是么?:-)

  说真的,如果需要支持随意读取,我推荐把列表转为字典。首先,字典的随机读取速度很快,其次,使用index作为key的话,取值也比较自然……我反正推荐这样做。也很简单,代码如下:

LNode = parser.split('script.sanae')
dirNode = {}
for i in LNode:
    dirNode[parser.searchIndex(i)] = i

  searchIndex方法就是一个对index的正则匹配,这个字典以一个数字(index)为键,待解析字符串为值。

  扯了这么多,接下来就是重头戏,选择按钮的实现。

  考虑script语法。每一帧,极有可能是多个按钮,每一个按钮需要一个label;同时,每一个按钮的作用是返回一个将要读取帧的index的值。因为选择支出现频率不算高,所以只要语法辨识度高,不会混淆就好。

  我采用的语法是这样

[choices]

くるみ(kurumi)->20

ひいらぎ(hiiragi)->200

[/choices]

  事实上,那个[choices]不是必要的,正则匹配的内容只有->,但是[choices]的话,会使得阅读script时更加清晰。

 

## 初始化正则式
self.RPChoice = self.__InitReParserChoice()

def __InitReParserChoice(self):
    pat = r'(.+?)->(\d+)'
    return re.compile(pat)
    
## 匹配
## findall的话,如果没有会返回None,否则
## 会返回一个tuple,第一项为按钮的label,
## 第二项为下一帧的index
self.ChoiceBranch = self.RPChoice.findall(target)

 

  然后来写一个按钮类。这个也很容易。

   

class Button(object):
    ##As a Button,these properties are necessary:
    ##A RECT:Contains the pos and size
    ##THE label:A micro text
    ##THE Image:decide the LOOK of the button
    ## 这里按钮的话,我期望给与更多的选择---我是指
    ## 按钮的外观。支持图片为背景,同时也支持纯色
    ## 背景。话说,有人会用纯色么?
    ## 为了支持纯色的写法,初始化很臃肿,万幸的是
    ## 基本都有默认值,所以事实上也不麻烦
    ## pos是按钮的位置,我这里处理为中心点的位置,
    ## 而非左上角,请务必注意
    ## 按钮类为了方便,代码与nodeitem有重复……
    ## 如果实在看不过去的话,请改掉就好 Orz
    ## 初始化也挺长的……
    def __init__(self,pos,size,image,font,label = '',bgcolor = None,fontSize = 24):
        self.pos = pos
        self.size = size
        self.surface = pygame.Surface(size,SRCALPHA)
        self.label = label.decode(DECODE)
        self.fontColor = (0xFF,0xFF,0xFF)

        ##To make sure the function is'n too long
        ##I split the code to three functions
        self.image = self.__LoadImage(image,bgcolor)
        self.font = self.__LoadFont(font,fontSize) 
        self.__Combination()


    def __LoadImage(self,image,bgcolor):
        ## Use the pure color
        if bgcolor != None:
            try:
                self.surface.fill(bgcolor)
            except pygame.error:
                print 'Cannot use the color'
                raise SystemExit
            return self.surface
        ## Use a image
        else:
            try:
                Image = pygame.image.load(image).convert_alpha()
            except pygame.error:
                print 'Could not load the image'
                raise SystemExit
            Image = pygame.transform.scale(Image,self.size)
            return Image

    ##Maybe I should consider reuse the code
    ##Now It is stupid
    def __LoadFont(self,font,fontSize):
        try:
            font = pygame.font.Font(font,fontSize)
        except pygame.error,message:
            print 'Cannot load font:',name
            raise SystemExit,message
        return font

    def __Combination(self):
        Image = self.image
        labelSurface = self.font.render(self.label,True,self.fontColor)
        xPos = (Image.get_width() - labelSurface.get_width())/2
        yPos = (Image.get_height() - labelSurface.get_height())/2
        Image.blit(labelSurface,(xPos,yPos))
        self.surface.blit(Image,(0,0))

    ## 调用这个方法在指定的位置绘制自己
    ## 由于需要绑定,surface的传入是必要的
    def render(self,surface):
        x,y = self.pos
        w,h = self.surface.get_size()
        x -= w/2
        y -= h/2
        surface.blit(self.surface,(x,y))

    ## check one point is in THIS BUTTON
    ## Orz,repeat codes....I'm lazy...
    ## 判断鼠标点击是否在这个按钮上,我这里
    ## 直接用了collidepoint,值得注意的是
    ## 直接用surface.get_rect()方法返回
    ## 的rect不能用……具体可以试试
    def is_over(self,point):
        x,y = self.pos
        w,h = self.surface.get_size()
        x -= w/2
        y -= h/2
        rect = pygame.Rect((x,y),self.size)
        return rect.collidepoint(point)

   

  上面就是一个按钮类,只有绘制自己和判断点击两个功能,相当纯粹了……

  然后就是使用,这个也不难:

 

CHOICEBUTTONFROMTOP = 50
CHOICEBUTTONSIZE = (200,40)
def __updateChoice(self,choice_branch):
    dir_button = {}
    num_choice = len(choice_branch)
    button_distance = (SCREENHEIGHT-2*CHOICEBUTTONFROMTOP)/(1+num_choice)
    for (count,i) in enumerate(choice_branch):
        dir_button[i[0]] = (i[1],Button(\
        (SCREENWIDTH/2,CHOICEBUTTONFROMTOP+button_distance*(1+count)),\
        CHOICEBUTTONSIZE,'button.png',os.path.join('FONT','hksn.ttf'),\
        i[0]))
    
    self.ChoiceButtons = dir_button

 

  初始化了一系列按钮并绘制。(SCREENHEIGHT-2*CHOICEBUTTONFROMTOP)/(1+num_choice)是我自己推出来的一个小公式……貌似工作得挺好的……大致就是在离屏幕顶和屏幕底50像素这个范围内,对按钮进行等距排列。就我试验的结果来看,8个都是可以的,9个以上没试过,估计很挤很难看?对了,按钮一定要进行清空处理,不要忘了,我们的引擎,当不传值给某个属性的时候,该属性会直接沿用上一帧的值。对于其他的属性是求之不得的,按钮就不行了,毕竟只能出现一帧。

  最后看看完整的update方法,这个第一篇的时候出现过了,但是不完整,现在完整了。

   

def update(self,parser):
    self.__updateNodeIndex(parser.getNodeIndex())
    self.__updateBGM(parser.getBGM())
    self.__updateBackground(parser.getBackground())
    self.__updateText(parser.getName(),parser.getText())

    self.Surface.blit(self.Background,(0,0))
    self.Surface.blit(self.TextBox,self.TextBoxPos)
    
    if parser.getChoice() != []:
        self.__updateChoice(parser.getChoice())
        for i in self.ChoiceButtons.keys():
            self.ChoiceButtons[i][1].render(self.Surface)
    else:
        self.ChoiceButtons = {}

   

  大多数方法都介绍过了,虽然不完全。有兴趣的请直接看整个项目工程的源文件。github

  其实还没完呢……还有个run.py文件。这个文件负责运行,还有切换帧什么的……

# -*- coding: utf-8 -*-
import pygame
from pygame.locals import *
from sys import exit
import NodeItems
import Parser

## 提供一个找不到index的处理,这时候直接导向
## 一个index为65535的帧,有兴趣的同学可以
## 试试,个人觉得很好玩……miku的声音很萌
ERRORINDEX = 65535

pygame.init()
screen = pygame.display.set_mode((800,600),0,32)
clock = pygame.time.Clock()

## 把文本转化成一个字典,前面说过了
parser = Parser.Parser()
LNode = parser.split('script.sanae')
dirNode = {}
for i in LNode:
    dirNode[parser.searchIndex(i)] = i

## Create an instance of the NodeItem
## which contains the musics,images,and
## text,buttons etc.
## In face,One NodeItem represents a
## frame which you are watching

## 脚本的运行必须从index=0开始,这个值
## 可以做启动界面来着……我当时是这么想的,
## 虽然我懒过去了……
nodeItem = NodeItems.NodeItem(screen)
parser.parser(dirNode[0])
nodeItem.update(parser)

while True:
    for event in pygame.event.get():
        if event.type == QUIT:
            exit()
        if event.type == MOUSEBUTTONDOWN:
            ## check where the click point on
            ## 这里判断鼠标点击的位置。咦,英文注释太多,
            ## 看不清?其实是我懒得删了……好累的
            ## 可以围观一下我忧伤的英文水平,话说
            if nodeItem.getChoiceButtons():
                click_point = event.dict['pos']
                dict_buttons = nodeItem.getChoiceButtons()
                ##Through over which button been clicked
                ##Surily if the point out of any button's
                ##area,we make it freeze :-)
                for key in dict_buttons.keys():
                    ## each dict_button is a tuple,
                    ## like this (index,button)
                    ## index is the next Node index
                    ## to change the control stream.
                    ## button is a instance of Button
                    index = int(dict_buttons[key][0])
                    button = dict_buttons[key][1]
                    if button.is_over(click_point):
                        ## 给定下一个index的值
                        nodeItem.setNextIndex(index)
                        break
            
            else:
                ## setNextIndex()方法的默认处理
                ## 是self.index+1
                ## self.index就是当前帧的index
                nodeItem.setNextIndex()

            
            ##The following codes update screen
            ## 这就是我说的那个错误处理……卖萌用的。具体的在script最后
            ## 有定义。为了实现那个,用了一个不太优雅的异常处理
            ## 我只是蛋疼了。
            NextIndex = nodeItem.getNextIndex() ##get key of one Node
            try:
                Node = dirNode[NextIndex] ##get a Node which is a string
                parser.parser(Node)
                nodeItem.update(parser)
                
            except KeyError:
                print 'Cannot search the index:',NextIndex
                Node = dirNode[ERRORINDEX] ## freeze the inscreasing of index
                parser.parser(Node)
                nodeItem.update(parser)
        
    clock.tick(5)

    
    pygame.display.update()

   

  好了,完毕完毕,多谢各位的捧场。整个项目的基本思路和实现就到此为止了。嘛,其实下一篇还有一个附录样的东西,讨论跨平台和文本对齐。@solu,今天和他讨论了好久。具体的请围观solu

  最后,读到这里您辛苦了!お疲れ様です。おつかれさまです!

   

转载于:https://my.oschina.net/AproSanae/blog/105862

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值