Pygame游戏开发之三
初出茅庐
Pygame中除了Sprite,还有一个DirtySprite,它是由Sprite派生出来的绘制效率更高的精灵,较之Sprite多了以下几个属性:
dirty = 1
如果设为1,则进行重绘,并且重置为0
如果设为2,则一直重绘,并且永远不设为0
如果设为0表示不需要绘制
blendmode = 0
混合模式,参见blit函数的最后一个参数
source_rect = None
原图的裁剪矩形,等同于我们之前定义的(self.offset, self.size)
相对于self.image的左上角坐标永远是(0,0)
visible = 1
是否需要被绘制
我们可以发现,source_rect和我们之前定义的(self.offset, self.size)的表示的意义是一样的,于是可以将原来的RenderObject类加以改进,如下:
class RenderObject(pygame.sprite.DirtySprite) :
framewait = 50
images = []
def __init__(self, selfdata) :
pygame.sprite.DirtySprite.__init__(self)
self.dirty = 2
self.image = self.images[ int(selfdata[0]) ]
self.rect = self.image.get_rect()
self.source_rect = self.image.get_rect()
self.blendmode = 0
self.visible = 1
self.speed = [0.0,0.0]
self.frame = 0
def move(self, xgo, ygo) :
if xgo or ygo :
self.rect.move_ip(self.speed[0] * xgo, self.speed[1] * ygo)
def update(self) :
pygame.sprite.DirtySprite.update(self)
self.frame += 1
并且让RenderObject继承pygame.sprite.DirtySprite,移除self.offset和self.size,而改用self.source_rect来代替,而且position变量也改用Sprite的成员变量rect来代替,这样一来有个好处就是绘制函数不用我们去操心了,只要提供image(源Surface),source_rect(源Surface中需要绘制的区域),rect(目标绘制在屏幕的矩形区域)以及一些辅助变量,绘制工作就由DirtySprite去做了。
这里有必要重新解释一下Sprite的工作原理(如何被绘制以及如何进行更新),Sprite有一个容器类Group,pygame.sprite.Group是承载了很多Sprite的容器,它有以下一些方法供调用:
Group.sprites()
返回所有该容器包含的Sprite的列表
Group.copy()
返回一个包含当前Group内相同的Sprite的Group的新的实例。
Group.add(*sprites)
添加任意数量的Sprite到这个Group内。
Group.remove(*sprites)
移除任意数量的Sprite从这个Group内。
Group.has(*sprites)
如果给定的sprites都存在那么返回True,否则返回Flase,这个和in操作类似(“if sprite in group: …”)。
Group.update()
调用所有在当前Group内的Sprite的update()函数,注意每个Sprite都有一个update()函数,如果某个类继承了Sprite,可以覆盖这个函数进行相应的更新操作。
Group.draw(Surface)
将当前Group内的所有的Sprite绘制到Surface上来,注意这里需要用到Sprite.image来确定源Surface和Sprite.rect来确定绘制的位置(当然,如果是DirtySprite还需要知道source_rect的值)。
Group.clear(Surface_dest, bgd)
将bgd绘制到Surface_dest上来。一般用于原窗口的背景绘制。
Group.empty()
将所有的Sprite从这个Group中移除。
如果有了Group,当我们需要更新操作的时候,只需要将所有的Sprite都加到这个Group中,然后调用Group.update()就可以将所有在这个Group中的Sprite全部更新了,而不需要一个一个去调用Sprite.update(),这个操作大大便利了我们编程。
我们可以在RenderObject中添加一个update函数来覆盖DirtySprite的update函数,并且做一些我们需要做的工作,可以设定一个帧数self.frame,每次调用update函数,帧数自增1,方便日后动画的播放。
举个最简单的例子,我们通过pygame.sprite.Group()来创建一个Sprite的容器类,然后往里面添加我们的Sprite对象,并且不断更新Sprite,将其绘制到屏幕上:
myGroup = pygame.sprite.Group()
myGroup.add( RenderObject([0]) )
myGroup.add( RenderObject([1]) )
while True :
myGroup.update()
myGroup.draw(screen)
pygame.display.update()
当然Group的性质是递归的,每个Group可以有多个子Group,形成一棵树或者是一个森林,当调用根Group的函数时,它会将所有它的子孙的函数全部访问到,这样一来只要我们原先将所有的游戏元素的关系用一棵树的形式建立起来,这样每次更新或者渲染都只需要对根结点进行操作了。
继续举例说明:
Root = pygame.sprite.Group()
Son1 = pygame.sprite.Group()
Son2 = pygame.sprite.Group()
Son1.add(RenderObject([0]))
Son1.add(RenderObject([1]))
Son2.add(Player([13]))
Son2.add(Animation([2,12]))
Root.add(Son1)
Root.add(Son2)
while True :
Root.update()
Root.draw(screen)
pygame.display.update()
这段代码将所有的游戏元素(两个普通物体和两个Player)组织成一棵树,并且通过Root这个Group来管理所有的游戏元素的更新以及绘制(为了描述方便,主循环中将事件处理这一部分暂时去掉了)。
这样就又带来了一个问题,游戏中的元素往往是很多的,比如有100只野怪,那么上面的代码必然会出现至少100个类似XXX.add(Monster([num]))的代码,一来看起来很别扭,二来不好维护,一旦需要变更一些关系就要大幅度修改代码,这个是不可取的。
于是我们还是学习上一节讲到的将数据写到文件中,因为这里的数据是以树状结构呈现的,所以数据的组织我借用了XML的文件格式(语法稍微有些不同,这样写是为了省去一些不必要的操作),以下是其中一段元素的组织形式:
<Group name=gameMgr selfdata=(-1,-1) pos=(0,0)>
<Group name=loginWnd selfdata=(-1,-1) pos=(0,0)>
<Animation name=cloud selfdata=(3,12) pos=(0,0)>
</Animation>
<Picture name=title selfdata=(0,0) pos=(20,80)>
</Picture>
<Menu name=menu selfdata=(1,1) pos=(50,120)>
</Menu>
</Group>
</Group>
每一对<>之间表示一个游戏元素,第一个字段是当前元素的类名,便于创建的时候根据类名来声明类的实例;第二个字段name是当前实例的唯一标识;第三个字段imageidx记录了当前元素的Surface在RenderObject.images[]中的始末索引;第四个字段pos表示在创建这个类的实例的时候该image在屏幕的左上角坐标。每一对<></>之间的元素是当前元素的儿子,这样一来,最外层的表示的就是Root根结点,层层之间建立父子关系就成了一棵树。
接下来就是需要写一个Python模块用于对当前的文件进行解析,将所有的游戏元素从文件中读取并且保存到一个递归的列表中。
所谓递归的列表其实就是树形列表,我们需要自定义一个这样的类,每个类的实例表示的是树形列表的一个结点,那么每个结点保存的信息有当前结点的数据域data,当前结点的孩子结点的集合sonlist,它又是一个列表,并且保存当前结点的父亲结点的指针以便回溯。并且分别给它们一个默认值,每个结点的父亲结点默认为None,也就是C语言中的NULL,表示的是空指针。类的实现如下:
class myTree :
def __init__(self, data) :
self.data = data
self.sonlist = []
self.parent = None
def add(self, mytree) :
mytree.parent = self
self.sonlist.append(mytree)
def output(self) :
print self.data
for xp in self.sonlist :
xp.output()
添加两个成员函数add和output,add是为了添加儿子结点,output是用来测试用的。
有了以上的树形列表,我们就可以写自己的文件读取器了,我把它取名为sXMLReader(similar with XML),原因是和XML还是不尽相同的。
class sXMLReader :
def __init__(self, filename) :
do_init()
def parse(self, string) :
do_parse()
def next(self, nowline) :
next_line()
do_init()主要做三件事情,读取文件并把所有的行保存到self.filelist中,然后对每一行进行一个预处理(主要是去掉串前后的空格和Tab还有回车等没用的字符),最后进行递归的访问next函数进行读行并且建立关系树。
self.root = myTree([])
self.nowroot = self.root
self.next(0)
首先建立一个空的根结点root,然后将当前结点指针nowroot指向它,self.next(0)表示当前访问第0行,next函数的操作比较简单,如下:
def next(self, nowline) :
if nowline == len(self.filelist) :
return
process_this_line()
if matchPre :
self.nowroot = self.nowroot.parent
self.next(nowline+1)
else :
son = myTree(nownode)
self.nowroot.add(son)
self.nowroot = son
self.next(nowline+1)
首先判断当前行是不是越界,如果越界说明无需访问。否则处理这一行的文字内容,这是由process_this_line()来完成的,要根据你自己设定的语法来处理,就不再累述了。最后一部分才是关键,matchPre用于判断当前这一行是不是和前面某一行进行匹配,如果匹配成功(也就是说当前行是类似</XXX>的形式),那么nowroot的指针指向它的parent,然后继续访问下一行;否则,生成一个新的结点,并且把这个结点添加到当前的nowroot结点中,然后更新nowroot为这个新加入的结点,继续访问下一行。其实整个过程就是一个栈,遇到<>好比入栈,遇到</>就好比出栈。
文件读取写完后,我们只需要调用:
gameItem = sXMLReader('ctrl_config.sxml')
gameItem.root 就保存了游戏元素树的根结点,通过递归访问就可以得到所有的游戏属性,利用Group和Sprite将所有游戏元素构造出来即可,具体参见以下代码:
def CreateGameItem(node) :
group = None
if node.classname() == 'Group':
group = pygame.sprite.Group()
else :
classname = node.classname()
selflist = node.find('selfdata')
render = renderobj.CreateClassByName(classname, selflist)
render.setpos(int(node.find('pos')[0]), int(node.find('pos')[1]))
return render
for son in node.sonlist :
group.add( CreateGameItem(son) )
return group
根据node.classname()得到当前结点是容器类还是普通的Sprite类,如果不是容器那么设置相关属性,然后返回当前结点信息;如果是容器类则创建一个Group并且遍历它的子结点,将所有子结点加入到当前容器中。遍历完毕后这棵树就创建完毕了。(未完待续)