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并且遍歷它的子結點,將所有子結點加入到當前容器中。遍歷完畢后這棵樹就創建完畢了。(未完待續)