pygame編程入門之八:Making Games With Pygame2
4. 游戲?qū)ο箢?/h3>
一旦您加載了模塊,并編寫了資源處理函數(shù)依啰,您就需要繼續(xù)編寫一些游戲?qū)ο罅说暝妗_@樣做的方式相當簡單,盡管一開始看起來很復雜鸯两。你為游戲中的每一種對象編寫一個類甩卓,然后為對象創(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()
當然,這是一個非常簡單的例子斥难,您需要輸入所有的代碼,而不是那些帶括號的注釋群扶。但是你應該有基本想法镀裤。把一個類放在一個類中暑劝,你把所有的函數(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ù)其當前位置,移動和向量劳翰。我馬上就會解釋物理。
另一件需要注意的事情是文檔字符串乙墙,這段時間稍微長一點生均,并解釋了類的基礎(chǔ)知識马胧。這些字符串不僅對您自己和其他程序員來說很方便,而且還可以用于解析代碼并記錄代碼的工具蛙粘。它們不會對程序產(chǎn)生很大的影響威彰,但是對于大的程序來說它們是無價的,所以這是一個很好的習慣舔痕。
4.1.1. Diversion 1: Sprites
為每個對象創(chuàng)建類的另一個原因是精靈。你在游戲中渲染的每一個圖像都是一個精靈對象盈咳,因此边翼,首先组底,每個對象的類都應該繼承精靈類。這是Python類繼承的一個很好的特性〗酰現(xiàn)在厌均,球類擁有所有與Sprite類一起的功能棺弊,并且球類的任何對象實例都將被Pygame注冊為精靈。而對于文本和背景稻艰,它們不移動侈净,可以把對象放在背景上,Pygame以不同的方式處理精靈對象元扔,當我們查看整個程序的代碼時旋膳,你會看到它溺忧。
基本上盯孙,你為那個球創(chuàng)建一個球?qū)ο蠛鸵粋€精靈對象,然后你在sprite對象上調(diào)用球的更新函數(shù)歌溉,從而更新精靈痛垛。精靈還提供了復雜的方法來確定兩個物體是否相撞。通常情況下漫谷,您可能只是在主循環(huán)中檢查它們的矩形是否重疊蹂析,但這將涉及到大量的代碼,這將是一種浪費惕稻,因為Sprite類提供了兩個功能(spritecollide and groupcollide)來為您完成這項工作蝙叛。
4.1.2. Diversion 2: Vector physics
除了球類的結(jié)構(gòu)外借帘,這段代碼值得注意的是矢量物理,用來計算球的運動宋梧。任何涉及到角運動的游戲狰挡,除非你熟悉三角學加叁,否則你不會走太遠,所以我將介紹一些你需要知道的基礎(chǔ)知識來理解calcnewpos函數(shù)展融。
首先豫柬,你會注意到球有一個屬性向量烧给,它是由角和z組成的,這個角是用弧度來表示的指么,它會告訴你球運動的方向。Z是球運動的速度晚唇。所以通過這個向量盗似,我們可以確定球的方向和速度赫舒,以及它在x軸和y軸上的移動程度:
上面的圖表說明了向量背后的基本數(shù)學。
在左手圖中并鸵,你可以看到球的投影運動是由藍線表示的园担。這條線的長度(z)表示它的速度枯夜,角度是它移動的方向。球運動的角度總是從右邊的x軸上取下咏闪,從這條線順時針方向測量鸽嫂,如圖所示征讲。
從球的角度和速度,我們可以算出它沿x軸和y軸移動了多少癣籽。因為Pygame不支持向量本身滤祖,我們只能通過沿著兩個軸移動它的矩形來移動球匠童。所以我們需要在x軸(dx)和y軸(dy)上解決這個角度和速度。這是一個簡單的三角學問題楞遏,可以用圖中所示的公式來完成首昔。
如果你以前學過基本的三角學知識勒奇,這對你來說都不應該是新聞。但是格二,為了防止健忘竣蹦,這里有一些有用的公式可以記住顶猜,這將幫助你對角度進行視覺化(用度來表示角度比弧度更直觀)。
5. User-controllable objects
到目前為止痘括,你可以創(chuàng)建一個Pygame窗口长窄,并渲染一個可以在屏幕上運行的球。
下一步是制造一些用戶可以控制的球拍纲菌。這可能比球簡單得多挠日,因為它不需要物理(除非你的用戶控制的對象會以比上下更復雜的方式移動,比如像馬里奧這樣的平臺角色嚣潜,在這種情況下你需要更多的物理知識)。用戶控制的對象很容易創(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需要被設置回它的起始位置驳阎,任何屬性都被設置回它們的必要值抗愁。
接下來馁蒂,球拍移動的方式比球要復雜一些,因為它的運動很簡單(向上/向下)蜘腌,但它依賴于使用者告訴它移動沫屡,不像球在每一幀中不斷移動。為了理解球的運動方式撮珠,看一個快速的圖來顯示事件的順序是很有幫助的:
這里發(fā)生的是控制球棒的人按下按鈕沮脖,將球棒向上移動。主游戲循環(huán)的每個迭代(每一幀),關(guān)鍵是是否進行,球拍的狀態(tài)屬性對象被設置為“移動”,moveup函數(shù)將調(diào)用,導致球的y位置降低速度屬性的值(在本例中,10)芯急。換句話說勺届,只要鍵盤被壓住,球拍就會以每幀10個像素的速度向上移動屏幕娶耍。state屬性還沒有使用免姿,但是在處理自旋還是想要一些有用的調(diào)試輸出,也是很有用的榕酒。
一旦玩家過去,第二組框被調(diào)用,球拍的狀態(tài)屬性對象將回到“靜止”狀態(tài),和movepos屬性將回到(0,0),這意味著當更新函數(shù)被調(diào)用時,它不會把球拍移動养泡。所以當玩家松開按鍵時,球拍就會停止移動奈应。簡單!
5.1.1. Diversion 3: Pygame events
那么我們怎么知道玩家什么時候把按鍵按下澜掩,然后釋放呢
有了Pygame事件隊列系統(tǒng),年青人杖挣!這是一個非常容易使用和理解的系統(tǒng)肩榕,所以這不會花很長時間:)您已經(jīng)在基本的Pygame程序中看到了事件隊列,它用于檢查用戶是否退出了應用程序惩妇。移動球拍的代碼就這么簡單:
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"
這里假設您已經(jīng)創(chuàng)建了一個bat的實例株汉,并調(diào)用了object player。
您可以看到熟悉結(jié)構(gòu)布局歌殃,它遍歷Pygame事件隊列中每個事件乔妈,并用event.get()函數(shù)檢索。當用戶點擊按鍵氓皱,按下鼠標按鈕并移動操縱桿時路召,這些動作會被注入到Pygame事件隊列中,然后直到處理波材。
所以在主游戲循環(huán)的每次迭代中股淡,你都要經(jīng)歷這些事件,檢查它們是否是你想要處理的廷区,然后適當?shù)靥幚硭鼈兾椤T谇蚺纳砩系氖录ump()函數(shù)。在每次迭代中調(diào)用update函數(shù)保持隊列流隙轻。
首先埠帕,我們檢查用戶是否退出了程序垢揩,如果他們退出了,就退出敛瓷。然后我們檢查是否有任何鍵被按下叁巨,如果是,我們檢查它們是否是移動球拍的指定鍵琐驴。如果是,然后調(diào)用對應移動功能,并設置適當?shù)腷at狀態(tài)(盡管 moveup movedown改變了moveup()和movedown()函數(shù),這使得簡潔的代碼,并且不破壞封裝,這意味著您將屬性分配給對象本身,沒有引用該對象的實例的名稱)俘种。
注意這里我們有三個狀態(tài): still, moveup, and movedown秤标。同樣绝淡,如果您想要調(diào)試或計算旋轉(zhuǎn),這些都是很方便的苍姜。我們還會檢查是否有任何鍵被“松開”(即不再被按桌谓汀),如果是衙猪,我們就會阻止球拍移動馍乙。
6. 把它們放在一起
到目前為止,您已經(jīng)學習了構(gòu)建簡單游戲所需的所有基礎(chǔ)知識垫释。您應該了解如何創(chuàng)建Pygame對象丝格,Pygame如何顯示對象,如何處理事件棵譬,以及如何使用物理將一些動作引入到您的游戲中显蝌。
現(xiàn)在,我將展示如何將所有這些代碼塊放到游戲中订咸。首先要做的是讓球觸到屏幕的兩側(cè)曼尊,讓球棒能夠擊球,否則就不會有太多的比賽了脏嚷。我們用Pygame的碰撞方法來做這個骆撇。
6.1. 讓球擊中兩邊
讓它在兩側(cè)彈跳的基本原理很容易理解。你利用球的四個角坐標父叙,檢查它們是否與屏幕邊緣的x或y坐標相對應神郊。如果右上角和左上角都有y坐標為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ū)域是否包含了球的新位置(它總是應該的,我們不需要有else子句鲸匿,盡管在其他情況下你可能想要考慮它爷怀。)
然后檢查四個角的坐標是否與該區(qū)域的邊發(fā)生碰撞,并為每個結(jié)果創(chuàng)建對象带欢。如果是的話运授,對象的值是1烤惊,或者是真值。如果不吁朦,那么價值將是零柒室,或者是假的。
然后我們看它是否擊中了頂部或底部逗宜,如果是雄右,它改變了球的方向。使用弧度纺讲,我們可以簡單地改變它的正/負的值來做到這一點擂仍。還會檢查球是否從側(cè)面消失了,如果它有的話熬甚,我們會調(diào)用offcourt函數(shù)逢渔。在游戲中,重新設置球乡括,在調(diào)用該函數(shù)時指定的玩家的分數(shù)增加1點肃廓,并顯示新分數(shù)。
最后诲泌,根據(jù)新的角度重新編譯向量盲赊。就這樣。球?qū)g快地從墻上彈回來敷扫,并以優(yōu)雅的姿態(tài)離開墻面哀蘑。
6.2. 讓球碰到球拍
把球打到球拍身上很類似,它會撞到屏幕的兩側(cè)呻澜。仍然使用碰撞法递礼,但是這次要檢查球的矩形和球拍是否碰撞。在這段代碼中羹幸,還添加了一些額外的代碼來避免各種故障脊髓。您會發(fā)現(xiàn),為了避免出現(xiàn)小故障和bug栅受,您必須添加各種額外的代碼将硝,因此習慣了它是一件好事。
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ù)進行條件。第一個故障修復是在這兩個維度縮小球員矩形3像素,停止背后的球拍抓球(如果你想象你只是把球拍這球跟蹤,矩形重疊,所以通常球?qū)⒈弧按驌簟?。
接下來檢查這些矩形是否會發(fā)生碰撞误辑,還有一個小故障沧踏。請注意,我已經(jīng)對這些奇怪的代碼進行了注釋——對于那些查看代碼的人來說巾钉,解釋一些不尋常的代碼總是好的翘狱,因此當看到它的時候,您就會理解它砰苍。如果沒有修復潦匈,球可能會擊中球棒的一角,改變方向赚导,一幀后仍然會發(fā)現(xiàn)自己在球拍內(nèi)茬缩。然后它會認為它再次被擊中了,并改變了它的方向辟癌。這種情況可能會發(fā)生幾次寒屯,使得球的運動完全不真實荐捻。
所以我們有一個變量黍少,self.click,當它被擊中時处面,我們將它設置為True厂置,然后在后面加上一個False。當我們檢查這些矩形是否發(fā)生碰撞時魂角,我們也檢查是否self命中是true/false昵济,以阻止內(nèi)部的反彈。
這里的代碼很容易理解野揪。所有矩形都有一個碰撞函數(shù)访忿,你可以在其中輸入另一個物體的矩形,如果這些矩形是重疊的斯稳,如果不是海铆,它就會返回True。我們可以通過從pi中減去當前的角度來改變方向(同樣挣惰,你可以用弧度來做一個簡單轉(zhuǎn)變卧斟,它會把角度調(diào)整90度,然后把它往正確的方向發(fā)送;你可能會發(fā)現(xiàn)憎茂,在這一點上珍语,對弧度的徹底理解是有道理的!)為了完成故障檢查竖幔,我們換了self.hit板乙。如果們被擊中后的框架,那就返回False拳氢。
然后重新編譯這個向量募逞。當然晓猛,您希望刪除前一段代碼中的同一行,這樣您只需要在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),和其他各種錯誤和故障修復汉操。