pygame編程入門之八:Making Games With Pygame2
4. 游戲?qū)ο箢?/h3>
一旦您加載了模塊,并編寫了資源處理函數(shù),您就需要繼續(xù)編寫一些游戲?qū)ο罅恕_@樣做的方式相當(dāng)簡單,盡管一開始看起來很復(fù)雜。你為游戲中的每一種對象編寫一個類,然后為對象創(chuàng)建這些類的實例。然后,您可以使用這些類的方法來操作對象,給對象一些動作和交互功能。所以你的游戲在偽代碼中,會是這樣的:
#!/usr/bin/python
# [load modules here]
# [resource handling functions here]
class Ball:
# [ball functions (methods) here]
# [e.g. a function to calculate new position]
# [and a function to check if it hits the side]
def main:
# [initiate game environment here]
# [create new object as instance of ball class]
ball = Ball()
while 1:
# [check for user input]
# [call ball's update function]
ball.update()
當(dāng)然,這是一個非常簡單的例子,您需要輸入所有的代碼,而不是那些帶括號的注釋。但是你應(yīng)該有基本想法。把一個類放在一個類中,你把所有的函數(shù)都放在一個球上,包括init,它會創(chuàng)造出所有的球的屬性,然后更新,它會把球移動到它的新位置,然后在這個位置上移動blitting到屏幕上。
然后您可以為所有其他的游戲?qū)ο髣?chuàng)建更多的類,然后創(chuàng)建它們的實例,這樣您就可以在主函數(shù)和主程序循環(huán)中輕松地處理它們。與此形成對比的是,在主函數(shù)中啟動球,然后有許多無類的函數(shù)來操作一個集合球?qū)ο?,你將會看到為什么使用類是一個優(yōu)勢:它允許你把每個對象的所有代碼放在一個地方;它使用對象更容易;它添加新對象和操作它們變得更加靈活。
您可以簡單地為每個新球?qū)ο髣?chuàng)建球類的新實例,而不是為每個新球?qū)ο筇砑痈嗟拇a。魔法!
4.1. 一個簡單的球類
這里有一個簡單的類,它具有創(chuàng)建球?qū)ο笏匦璧墓δ?,如果在主程序中調(diào)用update函數(shù),那么就可以在屏幕上移動:
class Ball(pygame.sprite.Sprite):
"""A ball that will move across the screen
Returns: ball object
Functions: update, calcnewpos
Attributes: area, vector"""
def __init__(self, vector):
pygame.sprite.Sprite.__init__(self)
self.image, self.rect = load_png('ball.png')
screen = pygame.display.get_surface()
self.area = screen.get_rect()
self.vector = vector
def update(self):
newpos = self.calcnewpos(self.rect,self.vector)
self.rect = newpos
def calcnewpos(self,rect,vector):
(angle,z) = vector
(dx,dy) = (z*math.cos(angle),z*math.sin(angle))
return rect.move(dx,dy)
這里我們有球類,init球函數(shù)集,更新函數(shù),改變了球的矩形在新的位置,和calcnewpos函數(shù)計算出球的新位置根據(jù)其當(dāng)前位置,移動和向量。我馬上就會解釋物理。
另一件需要注意的事情是文檔字符串,這段時間稍微長一點,并解釋了類的基礎(chǔ)知識。這些字符串不僅對您自己和其他程序員來說很方便,而且還可以用于解析代碼并記錄代碼的工具。它們不會對程序產(chǎn)生很大的影響,但是對于大的程序來說它們是無價的,所以這是一個很好的習(xí)慣。
4.1.1. Diversion 1: Sprites
為每個對象創(chuàng)建類的另一個原因是精靈。你在游戲中渲染的每一個圖像都是一個精靈對象,因此,首先,每個對象的類都應(yīng)該繼承精靈類。這是Python類繼承的一個很好的特性。現(xiàn)在,球類擁有所有與Sprite類一起的功能,并且球類的任何對象實例都將被Pygame注冊為精靈。而對于文本和背景,它們不移動,可以把對象放在背景上,Pygame以不同的方式處理精靈對象,當(dāng)我們查看整個程序的代碼時,你會看到它。
基本上,你為那個球創(chuàng)建一個球?qū)ο蠛鸵粋€精靈對象,然后你在sprite對象上調(diào)用球的更新函數(shù),從而更新精靈。精靈還提供了復(fù)雜的方法來確定兩個物體是否相撞。通常情況下,您可能只是在主循環(huán)中檢查它們的矩形是否重疊,但這將涉及到大量的代碼,這將是一種浪費(fèi),因為Sprite類提供了兩個功能(spritecollide and groupcollide)來為您完成這項工作。
4.1.2. Diversion 2: Vector physics
除了球類的結(jié)構(gòu)外,這段代碼值得注意的是矢量物理,用來計算球的運(yùn)動。任何涉及到角運(yùn)動的游戲,除非你熟悉三角學(xué),否則你不會走太遠(yuǎn),所以我將介紹一些你需要知道的基礎(chǔ)知識來理解calcnewpos函數(shù)。
首先,你會注意到球有一個屬性向量,它是由角和z組成的,這個角是用弧度來表示的,它會告訴你球運(yùn)動的方向。Z是球運(yùn)動的速度。所以通過這個向量,我們可以確定球的方向和速度,以及它在x軸和y軸上的移動程度:

上面的圖表說明了向量背后的基本數(shù)學(xué)。
在左手圖中,你可以看到球的投影運(yùn)動是由藍(lán)線表示的。這條線的長度(z)表示它的速度,角度是它移動的方向。球運(yùn)動的角度總是從右邊的x軸上取下,從這條線順時針方向測量,如圖所示。
從球的角度和速度,我們可以算出它沿x軸和y軸移動了多少。因為Pygame不支持向量本身,我們只能通過沿著兩個軸移動它的矩形來移動球。所以我們需要在x軸(dx)和y軸(dy)上解決這個角度和速度。這是一個簡單的三角學(xué)問題,可以用圖中所示的公式來完成。
如果你以前學(xué)過基本的三角學(xué)知識,這對你來說都不應(yīng)該是新聞。但是,為了防止健忘,這里有一些有用的公式可以記住,這將幫助你對角度進(jìn)行視覺化(用度來表示角度比弧度更直觀)。

5. User-controllable objects
到目前為止,你可以創(chuàng)建一個Pygame窗口,并渲染一個可以在屏幕上運(yùn)行的球。
下一步是制造一些用戶可以控制的球拍。這可能比球簡單得多,因為它不需要物理(除非你的用戶控制的對象會以比上下更復(fù)雜的方式移動,比如像馬里奧這樣的平臺角色,在這種情況下你需要更多的物理知識)。用戶控制的對象很容易創(chuàng)建,這要歸功于Pygame的事件隊列系統(tǒng),正如您將看到的。
5.1. 一個簡單的球拍類
球拍類的原理與球類相似。你需要一個init函數(shù)來初始化這個球(這樣你就可以為每只球拍創(chuàng)建一個對象實例),一個更新函數(shù),在它被擊到屏幕之前,在球棒上執(zhí)行每幀的變化,以及定義這個類實際要做什么的功能。下面是一些示例代碼:
class Bat(pygame.sprite.Sprite):
"""Movable tennis 'bat' with which one hits the ball
Returns: bat object
Functions: reinit, update, moveup, movedown
Attributes: which, speed"""
def __init__(self, side):
pygame.sprite.Sprite.__init__(self)
self.image, self.rect = load_png('bat.png')
screen = pygame.display.get_surface()
self.area = screen.get_rect()
self.side = side
self.speed = 10
self.state = "still"
self.reinit()
def reinit(self):
self.state = "still"
self.movepos = [0,0]
if self.side == "left":
self.rect.midleft = self.area.midleft
elif self.side == "right":
self.rect.midright = self.area.midright
def update(self):
newpos = self.rect.move(self.movepos)
if self.area.contains(newpos):
self.rect = newpos
pygame.event.pump()
def moveup(self):
self.movepos[1] = self.movepos[1] - (self.speed)
self.state = "moveup"
def movedown(self):
self.movepos[1] = self.movepos[1] + (self.speed)
self.state = "movedown"
正如你所看到的,這個類與它的結(jié)構(gòu)中的球類非常相似。
但是每個函數(shù)的作用是不同的。首先,有一個reinit函數(shù),它在回合結(jié)束時使用,而bat需要被設(shè)置回它的起始位置,任何屬性都被設(shè)置回它們的必要值。
接下來,球拍移動的方式比球要復(fù)雜一些,因為它的運(yùn)動很簡單(向上/向下),但它依賴于使用者告訴它移動,不像球在每一幀中不斷移動。為了理解球的運(yùn)動方式,看一個快速的圖來顯示事件的順序是很有幫助的:

這里發(fā)生的是控制球棒的人按下按鈕,將球棒向上移動。主游戲循環(huán)的每個迭代(每一幀),關(guān)鍵是是否進(jìn)行,球拍的狀態(tài)屬性對象被設(shè)置為“移動”,moveup函數(shù)將調(diào)用,導(dǎo)致球的y位置降低速度屬性的值(在本例中,10)。換句話說,只要鍵盤被壓住,球拍就會以每幀10個像素的速度向上移動屏幕。state屬性還沒有使用,但是在處理自旋還是想要一些有用的調(diào)試輸出,也是很有用的。
一旦玩家過去,第二組框被調(diào)用,球拍的狀態(tài)屬性對象將回到“靜止”狀態(tài),和movepos屬性將回到(0,0),這意味著當(dāng)更新函數(shù)被調(diào)用時,它不會把球拍移動。所以當(dāng)玩家松開按鍵時,球拍就會停止移動。簡單!
5.1.1. Diversion 3: Pygame events
那么我們怎么知道玩家什么時候把按鍵按下,然后釋放呢
有了Pygame事件隊列系統(tǒng),年青人!這是一個非常容易使用和理解的系統(tǒng),所以這不會花很長時間:)您已經(jīng)在基本的Pygame程序中看到了事件隊列,它用于檢查用戶是否退出了應(yīng)用程序。移動球拍的代碼就這么簡單:
for event in pygame.event.get():
if event.type == QUIT:
return
elif event.type == KEYDOWN:
if event.key == K_UP:
player.moveup()
if event.key == K_DOWN:
player.movedown()
elif event.type == KEYUP:
if event.key == K_UP or event.key == K_DOWN:
player.movepos = [0,0]
player.state = "still"
這里假設(shè)您已經(jīng)創(chuàng)建了一個bat的實例,并調(diào)用了object player。
您可以看到熟悉結(jié)構(gòu)布局,它遍歷Pygame事件隊列中每個事件,并用event.get()函數(shù)檢索。當(dāng)用戶點擊按鍵,按下鼠標(biāo)按鈕并移動操縱桿時,這些動作會被注入到Pygame事件隊列中,然后直到處理。
所以在主游戲循環(huán)的每次迭代中,你都要經(jīng)歷這些事件,檢查它們是否是你想要處理的,然后適當(dāng)?shù)靥幚硭鼈?。在球拍身上的事件pump()函數(shù)。在每次迭代中調(diào)用update函數(shù)保持隊列流。
首先,我們檢查用戶是否退出了程序,如果他們退出了,就退出。然后我們檢查是否有任何鍵被按下,如果是,我們檢查它們是否是移動球拍的指定鍵。如果是,然后調(diào)用對應(yīng)移動功能,并設(shè)置適當(dāng)?shù)腷at狀態(tài)(盡管 moveup movedown改變了moveup()和movedown()函數(shù),這使得簡潔的代碼,并且不破壞封裝,這意味著您將屬性分配給對象本身,沒有引用該對象的實例的名稱)。
注意這里我們有三個狀態(tài): still, moveup, and movedown。同樣,如果您想要調(diào)試或計算旋轉(zhuǎn),這些都是很方便的。我們還會檢查是否有任何鍵被“松開”(即不再被按住),如果是,我們就會阻止球拍移動。
6. 把它們放在一起
到目前為止,您已經(jīng)學(xué)習(xí)了構(gòu)建簡單游戲所需的所有基礎(chǔ)知識。您應(yīng)該了解如何創(chuàng)建Pygame對象,Pygame如何顯示對象,如何處理事件,以及如何使用物理將一些動作引入到您的游戲中。
現(xiàn)在,我將展示如何將所有這些代碼塊放到游戲中。首先要做的是讓球觸到屏幕的兩側(cè),讓球棒能夠擊球,否則就不會有太多的比賽了。我們用Pygame的碰撞方法來做這個。
6.1. 讓球擊中兩邊
讓它在兩側(cè)彈跳的基本原理很容易理解。你利用球的四個角坐標(biāo),檢查它們是否與屏幕邊緣的x或y坐標(biāo)相對應(yīng)。如果右上角和左上角都有y坐標(biāo)為0,你就知道這個球現(xiàn)在在屏幕的最上面。在我們計算出了球的新位置之后,我們在更新函數(shù)中做了所有這些。
if not self.area.contains(newpos):
tl = not self.area.collidepoint(newpos.topleft)
tr = not self.area.collidepoint(newpos.topright)
bl = not self.area.collidepoint(newpos.bottomleft)
br = not self.area.collidepoint(newpos.bottomright)
if tr and tl or (br and bl):
angle = -angle
if tl and bl:
self.offcourt(player=2)
if tr and br:
self.offcourt(player=1)
self.vector = (angle,z)
檢查這個區(qū)域是否包含了球的新位置(它總是應(yīng)該的,我們不需要有else子句,盡管在其他情況下你可能想要考慮它。)
然后檢查四個角的坐標(biāo)是否與該區(qū)域的邊發(fā)生碰撞,并為每個結(jié)果創(chuàng)建對象。如果是的話,對象的值是1,或者是真值。如果不,那么價值將是零,或者是假的。
然后我們看它是否擊中了頂部或底部,如果是,它改變了球的方向。使用弧度,我們可以簡單地改變它的正/負(fù)的值來做到這一點。還會檢查球是否從側(cè)面消失了,如果它有的話,我們會調(diào)用offcourt函數(shù)。在游戲中,重新設(shè)置球,在調(diào)用該函數(shù)時指定的玩家的分?jǐn)?shù)增加1點,并顯示新分?jǐn)?shù)。
最后,根據(jù)新的角度重新編譯向量。就這樣。球?qū)g快地從墻上彈回來,并以優(yōu)雅的姿態(tài)離開墻面。
6.2. 讓球碰到球拍
把球打到球拍身上很類似,它會撞到屏幕的兩側(cè)。仍然使用碰撞法,但是這次要檢查球的矩形和球拍是否碰撞。在這段代碼中,還添加了一些額外的代碼來避免各種故障。您會發(fā)現(xiàn),為了避免出現(xiàn)小故障和bug,您必須添加各種額外的代碼,因此習(xí)慣了它是一件好事。
else:
# Deflate the rectangles so you can't catch a ball behind the bat
player1.rect.inflate(-3, -3)
player2.rect.inflate(-3, -3)
# Do ball and bat collide?
# Note I put in an odd rule that sets self.hit to 1 when they collide, and unsets it in the next
# iteration. this is to stop odd ball behaviour where it finds a collision *inside* the
# bat, the ball reverses, and is still inside the bat, so bounces around inside.
# This way, the ball can always escape and bounce away cleanly
if self.rect.colliderect(player1.rect) == 1 and not self.hit:
angle = math.pi - angle
self.hit = not self.hit
elif self.rect.colliderect(player2.rect) == 1 and not self.hit:
angle = math.pi - angle
self.hit = not self.hit
elif self.hit:
self.hit = not self.hit
self.vector = (angle,z)
用另一段語句開始這部分,因為這是前面的代碼塊中執(zhí)行的,以檢查球是否碰到了邊。
如果它沒有擊中兩邊,它可能會擊中一個球棒,所以繼續(xù)進(jìn)行條件。第一個故障修復(fù)是在這兩個維度縮小球員矩形3像素,停止背后的球拍抓球(如果你想象你只是把球拍這球跟蹤,矩形重疊,所以通常球?qū)⒈弧按驌簟?。
接下來檢查這些矩形是否會發(fā)生碰撞,還有一個小故障。請注意,我已經(jīng)對這些奇怪的代碼進(jìn)行了注釋——對于那些查看代碼的人來說,解釋一些不尋常的代碼總是好的,因此當(dāng)看到它的時候,您就會理解它。如果沒有修復(fù),球可能會擊中球棒的一角,改變方向,一幀后仍然會發(fā)現(xiàn)自己在球拍內(nèi)。然后它會認(rèn)為它再次被擊中了,并改變了它的方向。這種情況可能會發(fā)生幾次,使得球的運(yùn)動完全不真實。
所以我們有一個變量,self.click,當(dāng)它被擊中時,我們將它設(shè)置為True,然后在后面加上一個False。當(dāng)我們檢查這些矩形是否發(fā)生碰撞時,我們也檢查是否self命中是true/false,以阻止內(nèi)部的反彈。
這里的代碼很容易理解。所有矩形都有一個碰撞函數(shù),你可以在其中輸入另一個物體的矩形,如果這些矩形是重疊的,如果不是,它就會返回True。我們可以通過從pi中減去當(dāng)前的角度來改變方向(同樣,你可以用弧度來做一個簡單轉(zhuǎn)變,它會把角度調(diào)整90度,然后把它往正確的方向發(fā)送;你可能會發(fā)現(xiàn),在這一點上,對弧度的徹底理解是有道理的?。榱送瓿晒收蠙z查,我們換了self.hit。如果們被擊中后的框架,那就返回False。
然后重新編譯這個向量。當(dāng)然,您希望刪除前一段代碼中的同一行,這樣您只需要在if-else條件語句之后才做一次。這是它!合并后的代碼將允許球擊中兩側(cè)和球拍。
6.3. 成品
最終的產(chǎn)品,加上所有的代碼塊,以及其他一些代碼將它們整合在一起,看起來就像這樣:
#
# Tom's Pong
# A simple pong game with realistic physics and AI
# http://www.tomchance.uklinux.net/projects/pong.shtml
#
# Released under the GNU General Public License
VERSION = "0.4"
try:
import sys
import random
import math
import os
import getopt
import pygame
from socket import *
from pygame.locals import *
except ImportError, err:
print "couldn't load module. %s" % (err)
sys.exit(2)
def load_png(name):
""" Load image and return image object"""
fullname = os.path.join('data', name)
try:
image = pygame.image.load(fullname)
if image.get_alpha is None:
image = image.convert()
else:
image = image.convert_alpha()
except pygame.error, message:
print 'Cannot load image:', fullname
raise SystemExit, message
return image, image.get_rect()
class Ball(pygame.sprite.Sprite):
"""A ball that will move across the screen
Returns: ball object
Functions: update, calcnewpos
Attributes: area, vector"""
def __init__(self, (xy), vector):
pygame.sprite.Sprite.__init__(self)
self.image, self.rect = load_png('ball.png')
screen = pygame.display.get_surface()
self.area = screen.get_rect()
self.vector = vector
self.hit = 0
def update(self):
newpos = self.calcnewpos(self.rect,self.vector)
self.rect = newpos
(angle,z) = self.vector
if not self.area.contains(newpos):
tl = not self.area.collidepoint(newpos.topleft)
tr = not self.area.collidepoint(newpos.topright)
bl = not self.area.collidepoint(newpos.bottomleft)
br = not self.area.collidepoint(newpos.bottomright)
if tr and tl or (br and bl):
angle = -angle
if tl and bl:
#self.offcourt()
angle = math.pi - angle
if tr and br:
angle = math.pi - angle
#self.offcourt()
else:
# Deflate the rectangles so you can't catch a ball behind the bat
player1.rect.inflate(-3, -3)
player2.rect.inflate(-3, -3)
# Do ball and bat collide?
# Note I put in an odd rule that sets self.hit to 1 when they collide, and unsets it in the next
# iteration. this is to stop odd ball behaviour where it finds a collision *inside* the
# bat, the ball reverses, and is still inside the bat, so bounces around inside.
# This way, the ball can always escape and bounce away cleanly
if self.rect.colliderect(player1.rect) == 1 and not self.hit:
angle = math.pi - angle
self.hit = not self.hit
elif self.rect.colliderect(player2.rect) == 1 and not self.hit:
angle = math.pi - angle
self.hit = not self.hit
elif self.hit:
self.hit = not self.hit
self.vector = (angle,z)
def calcnewpos(self,rect,vector):
(angle,z) = vector
(dx,dy) = (z*math.cos(angle),z*math.sin(angle))
return rect.move(dx,dy)
class Bat(pygame.sprite.Sprite):
"""Movable tennis 'bat' with which one hits the ball
Returns: bat object
Functions: reinit, update, moveup, movedown
Attributes: which, speed"""
def __init__(self, side):
pygame.sprite.Sprite.__init__(self)
self.image, self.rect = load_png('bat.png')
screen = pygame.display.get_surface()
self.area = screen.get_rect()
self.side = side
self.speed = 10
self.state = "still"
self.reinit()
def reinit(self):
self.state = "still"
self.movepos = [0,0]
if self.side == "left":
self.rect.midleft = self.area.midleft
elif self.side == "right":
self.rect.midright = self.area.midright
def update(self):
newpos = self.rect.move(self.movepos)
if self.area.contains(newpos):
self.rect = newpos
pygame.event.pump()
def moveup(self):
self.movepos[1] = self.movepos[1] - (self.speed)
self.state = "moveup"
def movedown(self):
self.movepos[1] = self.movepos[1] + (self.speed)
self.state = "movedown"
def main():
# Initialise screen
pygame.init()
screen = pygame.display.set_mode((640, 480))
pygame.display.set_caption('Basic Pong')
# Fill background
background = pygame.Surface(screen.get_size())
background = background.convert()
background.fill((0, 0, 0))
# Initialise players
global player1
global player2
player1 = Bat("left")
player2 = Bat("right")
# Initialise ball
speed = 13
rand = ((0.1 * (random.randint(5,8))))
ball = Ball((0,0),(0.47,speed))
# Initialise sprites
playersprites = pygame.sprite.RenderPlain((player1, player2))
ballsprite = pygame.sprite.RenderPlain(ball)
# Blit everything to the screen
screen.blit(background, (0, 0))
pygame.display.flip()
# Initialise clock
clock = pygame.time.Clock()
# Event loop
while 1:
# Make sure game doesn't run at more than 60 frames per second
clock.tick(60)
for event in pygame.event.get():
if event.type == QUIT:
return
elif event.type == KEYDOWN:
if event.key == K_a:
player1.moveup()
if event.key == K_z:
player1.movedown()
if event.key == K_UP:
player2.moveup()
if event.key == K_DOWN:
player2.movedown()
elif event.type == KEYUP:
if event.key == K_a or event.key == K_z:
player1.movepos = [0,0]
player1.state = "still"
if event.key == K_UP or event.key == K_DOWN:
player2.movepos = [0,0]
player2.state = "still"
screen.blit(background, ball.rect, ball.rect)
screen.blit(background, player1.rect, player1.rect)
screen.blit(background, player2.rect, player2.rect)
ballsprite.update()
playersprites.update()
ballsprite.draw(screen)
playersprites.draw(screen)
pygame.display.flip()
if __name__ == '__main__': main()
除了展示最終產(chǎn)品,我還會把你們帶回到TomPong上,所有這些都是基于此的。
下載,看看源代碼,你會看到一個全面實施pong使用的所有代碼。在本教程中,您看到的以及很多其他的代碼我已經(jīng)添加各種版本,比如一些額外的物理旋轉(zhuǎn),和其他各種錯誤和故障修復(fù)。