这一篇主要讨论的内容是帧的切换以及按钮的处理。
这个帧,并不是普遍意义上的帧数什么的,事实上,这货是我自己定义的一个概念。如果你不明白帧是什么,请务必再看看第一篇的内容,这个术语在那里我定义过了,这里不累述。
前面三篇所讨论的东西,合起来做出来的效果也只是一个静态的无趣玩意,完全不能被称作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
最后,读到这里您辛苦了!お疲れ様です。おつかれさまです!