作者自我介紹:大爽歌, b站小UP主 ,直播程式設計+紅警三 ,python1對1輔導老師 。
本教學步驟明確,過程清晰簡明,最終程式碼量250行上下,適合學習pygame的新手。
專案程式碼已上傳到我的github: https://github.com/BigShuang/simple-brick-games-by-pygame
遊戲已錄製成視訊,投稿至本人b站:點選前往b站觀看遊戲視訊
遊戲執行效果,截圖如下
電腦開啟cmd命令視窗,輸入pip install pygame
如果電腦上安的是pip3則是pip3 install pygame
補充說明:
由於眾所周知的原因,安裝過程中下載可能十分緩慢,甚至由此導致安裝失敗
此時建議大家嘗試使用映象下載
---國內源---
清華:https://pypi.tuna.tsinghua.edu.cn/simple
阿里雲:http://mirrors.aliyun.com/pypi/simple/
中國科技大學: https://pypi.mirrors.ustc.edu.cn/simple/
華中理工大學:http://pypi.hustunique.com/
山東理工大學:http://pypi.sdutlinux.org/
豆瓣:http://pypi.douban.com/simple/
使用辦法 pip install xxxx -i jinxiangurl
具體到pygame,則是:
pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple
car_racing.py
檔案,內容如下import pygame
WIN_WIDTH = 600 # 視窗寬度
WIN_HEIGHT = 900 # 視窗高度
pygame.init() # pygame初始化,必須有,且必須在開頭
# 建立主表單
win=pygame.display.set_mode((WIN_WIDTH,WIN_HEIGHT))
此時執行car_racing.py
,會發現一個一閃而逝的視窗,
pygame.display.update()
語句進行介面的更新pygame.time.Clock
用於去控制迴圈重新整理的頻率,建立pygame.time.Clock
物件後,呼叫該物件的tick()
方法,函數引數為每秒重新整理次數,就可以設定迴圈每秒重新整理頻率,術語叫做影格率可前往官方檔案觀看pygame.time.Clock的更多細節,https://www.pygame.org/docs/ref/time.html
car_racing.py
後如下import pygame
C, R = 11, 20 # 11列, 20行
CELL_SIZE = 40 # 格子尺寸
FPS=60 # 遊戲影格率
WIN_WIDTH = CELL_SIZE * C # 視窗寬度
WIN_HEIGHT = CELL_SIZE * R # 視窗高度
pygame.init() # pygame初始化,必須有,且必須在開頭
# 建立主表單
clock = pygame.time.Clock() # 用於控制迴圈重新整理頻率的物件
win = pygame.display.set_mode((WIN_WIDTH,WIN_HEIGHT))
while True:
clock.tick(FPS) # 控制迴圈重新整理頻率,每秒重新整理FPS對應的值的次數
pygame.display.update()
此時執行car_racing.py
, 就可以得到一個最最最基礎的視窗了,
所以需要自己去重新實現這個視窗關閉功能,需要在迴圈體內新增如下程式碼
# 獲取所有事件
for event in pygame.event.get():
if event.type == pygame.QUIT:
# 判斷當前事件是否為點選右上角退出鍵
pygame.quit()
sys.exit() # 需要提前 import sys
同時我們一般會希望能夠設定下背景的顏色
比如,這個遊戲的背景色是輕灰色(200, 200, 200)
那麼設定背景的程式碼為
bg_color = (200, 200, 200)
win.fill(bg_color)
不過需要注意的是,這段程式碼放在不同的位置會產生不同的效果。
放在while迴圈之前,代表只繪製一次背景,會被後面繪製的東西遮住。
放在while迴圈中,則是每一幀都會繪製一次背景,一般用於去覆蓋掉那些希望刪掉的元素。
3 - 給視窗設定標題
在win = pygame.display.set_mode((WINWIDTH,WINHEIGHT))
後面,
新增程式碼如下,設定視窗標題(Big Shuang
是我的英文名,可以刪掉或者修改為你的名字)
pygame.display.set_caption('Car Racing by Big Shuang')
本階段最後car_racing.py
如下
import pygame
import sys
FPS=60 # 遊戲影格率
WINWIDTH = 600 # 視窗寬度
WINHEIGHT = 900 # 視窗高度
pygame.init() # pygame初始化,必須有,且必須在開頭
# 建立主表單
clock = pygame.time.Clock() # 用於控制迴圈重新整理頻率的物件
win = pygame.display.set_mode((WINWIDTH,WINHEIGHT))
pygame.display.set_caption('Car Racing by Big Shuang')
bg_color = (200, 200, 200)
win.fill(bg_color)
while True:
# 獲取所有事件
for event in pygame.event.get():
if event.type == pygame.QUIT:
# 判斷當前事件是否為點選右上角退出鍵
pygame.quit()
sys.exit()
clock.tick(FPS) # 控制迴圈重新整理頻率,每秒重新整理FPS對應的值的次數
pygame.display.update()
到這裡,基礎視窗就完成了~
pygame裡面,繪製一個小方格實際上是很簡單的。
新建一個方格色塊(放在while迴圈之前)
area = pygame.Surface((CELL_SIZE, CELL_SIZE))
enemy_color = (50, 50, 50)
area.fill(enemy_color)
再將方格色塊放在視窗物件上(放在while迴圈中,clock.tick(FPS)
之前)
win.blit(area, (CELL_SIZE, 0))
此時執行,效果如圖
不過這個繪製方法的問題在於, 後面的移動操作管理起來頗為不便。
要在while迴圈中編寫各種程式碼來實現area這個色塊的位置變換的話,程式碼寫起來麻煩,管理起來也亂。
pygame 給我們提供了一個Sprite
類,用於實現可以移動的二維影象物件。
我們將繼承這個類,並封裝一些需要的方法,方便移動以及管理。
In computer graphics, a sprite is a two-dimensional bitmap that is integrated into a larger scene, most often in a 2D video game.
在計算機圖學中,精靈是一種二維點陣圖,它被整合到一個更大的場景中,通常在二維電動遊戲中。
個人理解,sprite是一個計算機術語,代表介面中可以移動的二維點陣圖。
繼承Sprite
, 新建Block
類如下(在新建win物件,應該是14行,後面新增如下程式碼)
class Block(pygame.sprite.Sprite):
def __init__(self, c, r, color):
super().__init__()
self.cr = [c, r]
self.x = c * CELL_SIZE
self.y = r * CELL_SIZE
self.image = pygame.Surface((CELL_SIZE, CELL_SIZE))
self.image.fill(color)
self.rect = self.image.get_rect()
self.rect.move_ip(self.x, self.y)
刪掉前面 1 最基礎的繪製方法中, area的相關程式碼
在while迴圈之前, 新增如下程式碼
enemy_color = (50, 50, 50)
block = Block(1, 1, enemy_color)
win.blit(block.image, block.rect)
此時執行效果和1中相同。
首先,給Block
類新增移動到指定行列的類方法如下
def move_cr(self, c, r):
self.cr[0] = c
self.cr[1] = r
self.x = c * CELL_SIZE
self.y = r * CELL_SIZE
self.rect.left = self.x
self.rect.top = self.y
但是這個方法還是不夠的的,
因為遊戲中的移動,一般都是操作上下左右來移動。
那麼,我們需要把上下左右,轉換成c、r的變換。
所以建立字典如下
DIRECTIONS = {
"UP": (0, -1), # (dc, dr)
"DOWN": (0, 1),
"LEFT": (-1, 0),
"RIGHT": (1, 0),
}
然後再給Block
類新增按方向移動的方法如下
def move(self, direction):
dc, dr = DIRECTIONS[direction]
next_c, next_r = self.cr[0] + dc, self.cr[1] + dr
self.move_cr(next_c, next_r)
1 中只是新增了移動的方法,但是玩家要通過鍵盤來移動的話,
還需要程式中有能夠響應處理鍵盤操作
所以在while
迴圈中的for event in pygame.event.get()
迴圈裡,新增程式碼如下
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT or event.key == ord('a'):
block.move("LEFT")
if event.key == pygame.K_RIGHT or event.key == ord('d'):
block.move("RIGHT")
if event.key == pygame.K_UP or event.key == ord('w'):
block.move("UP")
if event.key == pygame.K_DOWN or event.key == ord('s'):
block.move("DOWN")
同時移動後,需要再進行繪製才能看到在上面的for
迴圈後面(外面)新增
win.blit(block.image, block.rect)
但是這個時候會有一個問題,移動後原來的位置色塊還在,沒有消失掉(被清掉)。
多移動幾下後,結果就像下圖一樣
所以我們需要擦除之前繪製的色塊。
pygame裡面,一般採用重新繪製整個介面的方式擦除之前的繪製。
重新繪製介面後,再在介面上新增新的需要繪製的東西。
即在while
迴圈中,win.blit(block.image, block.rect)
之前新增程式碼:
win.fill(bg_color)
此時, while
迴圈之前的這兩句程式碼刪不刪除沒啥區別
win.fill(bg_color)
win.blit(block.image, block.rect)
(個人視為多餘的,所以刪除)
此時移動小方塊就不會有之前的色塊殘留了
不過此時還有一個小小的問題,就是小方塊可以移動到介面邊界外
雖然可以再移動回來,但是這不符合我們這個程式的規則。
所以需要再進行邊界處理,使其無法移到邊界外。
給Block
類新增檢查能否移動的方法如下
def check_move(self, direction=""):
move_c, move_r = DIRECTIONS[direction]
next_c, next_r = self.cr[0] + move_c, self.cr[1] + move_r
if 0 <= next_c < C and 0 <= next_r < R:
return True
return False
再在while
迴圈中的for
迴圈中的每次呼叫move
方法前,使用check_move
檢查是否 能移動。
修改後的while
迴圈如下
while True:
# 獲取所有事件
for event in pygame.event.get():
if event.type == pygame.QUIT:
# 判斷當前事件是否為點選右上角退出鍵
pygame.quit() # 關閉視窗
sys.exit() # 停止程式
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT or event.key == ord('a'):
if block.check_move("LEFT"):
block.move("LEFT")
if event.key == pygame.K_RIGHT or event.key == ord('d'):
if block.check_move("RIGHT"):
block.move("RIGHT")
if event.key == pygame.K_UP or event.key == ord('w'):
if block.check_move("UP"):
block.move("UP")
if event.key == pygame.K_DOWN or event.key == ord('s'):
if block.check_move("DOWN"):
block.move("DOWN")
win.fill(bg_color)
win.blit(block.image, block.rect)
clock.tick(FPS) # 控制迴圈重新整理頻率,每秒重新整理FPS對應的值的次數
pygame.display.update()
當然這麼寫不夠優雅,if block.check_move("RIGHT"):
重複了四次,
有的朋友可能會覺得把check_move
的呼叫放在move
方法裡面開頭更好。
這裡之所以不這麼做,是因為後面會在方塊組成的賽車類裡,進行這個check_move
的工作。
具體見下文。
無論是玩家的車,還是敵人的車,
都是由多個方塊組成的。
所以首先要定義下,玩家車和敵人車的方格組成,程式碼如下
CARS = { # 車的形狀,即格子位置
"player": [
[0, 1, 0],
[1, 1, 1],
[1, 0, 1],
],
"enemy": [
[1, 0, 1],
[1, 1, 1],
[0, 1, 0],
]
}
根據這個方格祖,繪製賽車方法如下:
(放在while
迴圈中,重置背景之後)
car_c, car_r = 2, 2
for ri, row in enumerate(CARS["enemy"]):
for ci, cell in enumerate(row):
if cell == 1:
i_block = Block(car_c + ci, car_r + ri, enemy_color)
win.blit(i_block.image, i_block.rect)
Group
管理方塊接下來要建立一個賽車類,用來管理這些方塊。
pygame為我們提供了一個管理多個Sprite的工具pygame.sprite.Group
。
這個Group
類可以使用draw
方法,將內部的Sprite
一起繪製在螢幕視窗上。
這裡賽車類就繼承這個Group
類,如下。
class Car(pygame.sprite.Group):
def __init__(self, c, r, car_kind, car_color):
super().__init__()
for ri, row in enumerate(CARS[car_kind]):
for ci, cell in enumerate(row):
if cell == 1:
block = Block(c+ci, r+ri, car_color)
self.add(block)
同時,給這個賽車類新增移動方法,用於統一移動內部的方塊
def move(self, direction=""):
if all(block.check_move(direction) for block in self.sprites()):
for block in self.sprites():
block.move(direction)
然後刪掉其他的建立方塊與繪製方塊的程式碼
此時while
迴圈上下程式碼如下
bg_color = (200, 200, 200)
enemy_color = (50, 50, 50)
player_color = (65, 105, 225) # RoyalBlue
bottom_center_c = (C - len(CARS["player"][0])) // 2
bottom_center_r = R - len(CARS["player"])
car = Car(bottom_center_c, bottom_center_r, "player", player_color)
while True:
# 獲取所有事件
for event in pygame.event.get():
if event.type == pygame.QUIT:
# 判斷當前事件是否為點選右上角退出鍵
pygame.quit() # 關閉視窗
sys.exit() # 停止程式
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT or event.key == ord('a'):
car.move("LEFT")
if event.key == pygame.K_RIGHT or event.key == ord('d'):
car.move("RIGHT")
if event.key == pygame.K_UP or event.key == ord('w'):
car.move("UP")
if event.key == pygame.K_DOWN or event.key == ord('s'):
car.move("DOWN")
win.fill(bg_color)
car.draw(win)
clock.tick(FPS) # 控制迴圈重新整理頻率,每秒重新整理FPS對應的值的次數
pygame.display.update()
此時效果如圖,小車可以左右移動
敵人的賽車繪製,同上面第四部分。
不過敵人的賽車
Car
新增新的方法這裡有幾個問題需要先解決,
Car
新增無邊界限制的移動方法free_move
如下 def free_move(self, direction=""):
for block in self.sprites():
block.move(direction)
再修改原有的move
方法如下
def move(self, direction=""):
if all(block.check_move(direction) for block in self.sprites()):
self.free_move(direction)
Block
新增方法is_out
如下 def is_out(self):
if 0 <= self.cr[0] < C and 0 <= self.cr[1] < R:
return False
return True
再為Car
新增方法is_out
如下
def is_out(self):
return all(block.is_out() for block in self.sprites())
我們需要一個管理敵人賽車的類EnemyManager
。
其程式碼如下
import random # 在程式碼檔案開頭部分新增
# 在 enemy_color 宣告之後的位置新增
class EnemyManager():
def __init__(self):
self.enemies = []
self.move_count = 0
def gen_new_enemies(self): # 生成敵人賽車
# 設定敵人賽車的生成間隔, 隔兩倍的敵人賽車行數+1
if self.move_count % (2 * len(CARS["enemy"]) + 1) == 1:
ec = random.randint(1, C - len(CARS["enemy"][0]))
enemy = Car(ec, 0, "enemy", enemy_color)
self.enemies.append(enemy)
def move(self): # 自動向下移動敵人賽車
# 超出邊界後,自動清理掉
to_delete = []
for i, enemy in enumerate(self.enemies):
enemy.free_move("DOWN")
if enemy.is_out():
to_delete.append(i)
for di in to_delete[::-1]: # 倒著按序號來刪除
self.enemies.pop(di)
self.move_count += 1
self.gen_new_enemies()
def draw(self, master):
# 繪製敵人賽車
for enemy in self.enemies:
enemy.draw(master)
在while迴圈之前範例化EnemyManager
,程式碼如下
emg = EnemyManager()
在while
中對敵車進行繪製,即在win.fill(bg_color)
之後新增程式碼如下
emg.move()
emg.draw(win)
但是此時敵人賽車運動的太快了,是每幀一次移動。
所以需要設定一下,讓敵人的賽車每過MOVE_SPACE
幀才進行一次移動
在開頭新增程式碼如下
MOVE_SPACE = 5
再在while
迴圈之前新增幀計數器
frame_count = 0
在while
迴圈中的開頭新增
frame_count += 1
再修改剛才的敵人賽車繪製程式碼如下
if frame_count % MOVE_SPACE == 0:
emg.move()
emg.draw(win)
此時執行效果如圖
要實現碰撞檢測,只需給敵人賽車管理類EnemyManager
新增方法
遍歷其中現有的敵人賽車,檢查是否與玩家賽車相撞即可。
賽車相撞的判斷方法為兩車的方塊有重疊,即有相同位置的方塊。
所以給類Car
新增check_collide
方法,程式碼如下
def check_collide(self, other_car):
for block in self.sprites():
bcr1 = tuple(block.cr)
for other_block in other_car.sprites():
bcr2 = tuple(other_block.cr)
if bcr1 == bcr2:
return True
return False
再給敵人賽車管理類EnemyManager
新增check_collide
方法,程式碼如下
def check_collide(self, player):
for enemy in self.enemies:
if enemy.check_collide(player):
return True
return False
在while
迴圈中,繪製完賽車後新增碰撞檢測
即在clock.tick(FPS)
之前新增程式碼如下
if emg.check_collide(car):
break
此時玩家賽車碰到敵人賽車,遊戲就會結束。
不過,此時遊戲結束後會直接關閉視窗。
玩家體驗並不好,所以需要進一步的優化下。
最好是遊戲結束後,視窗不關閉,展示Game Over
的提示,並告訴玩家得分。
遊戲流程上,執行程式碼後,敵人賽車就直接迎面而來,留給玩家的緩衝準備時間太短了。
所以這裡修改為程式碼執行後,遊戲處於等待狀態,玩家按鍵盤任意鍵開始遊戲。
同時遊戲結束後,玩家可按鍵盤任意鍵重新開始。
首先需要在while
迴圈前,新增一個遊戲執行狀態變數,預設遊戲未開始
running = False
再修改while
迴圈如下
while True:
frame_count += 1
# 獲取所有事件
for event in pygame.event.get():
if event.type == pygame.QUIT:
# 判斷當前事件是否為點選右上角退出鍵
pygame.quit() # 關閉視窗
sys.exit() # 停止程式
if event.type == pygame.KEYDOWN:
if running: # 遊戲開始時,響應上下左右按鍵
if event.key == pygame.K_LEFT or event.key == ord('a'):
car.move("LEFT")
if event.key == pygame.K_RIGHT or event.key == ord('d'):
car.move("RIGHT")
if event.key == pygame.K_UP or event.key == ord('w'):
car.move("UP")
if event.key == pygame.K_DOWN or event.key == ord('s'):
car.move("DOWN")
else: # 遊戲結束後,響應任意按鍵,開始遊戲
# reset game, 重置變數和遊戲狀態
car = Car(bottom_center_c, bottom_center_r, "player", player_color)
frame_count = 0
emg = EnemyManager()
running =True
if running:
win.fill(bg_color)
if frame_count % MOVE_SPACE == 0:
emg.move()
emg.draw(win)
car.draw(win)
if emg.check_collide(car):
running = False # 撞車後,遊戲狀態改變為結束
clock.tick(FPS) # 控制迴圈重新整理頻率,每秒重新整理FPS對應的值的次數
pygame.display.update()
此時遊戲流程就修改好了,但是沒有文字提示,玩家會不明就裡,感到迷惑,所以需要文字在開始時和結束時進行提示。
在遊戲開頭新增大中小三種字型(必須要在pygame.init()
之後,能後不能前)
# 大中小三種字型,48,36,24
FONTS = [
pygame.font.Font(pygame.font.get_default_font(), font_size) for font_size in [48, 36, 24]
]
程式碼執行後,遊戲尚未開始,需要文字提示玩家按任意鍵開始遊戲
即在while
迴圈之前新增程式碼如下
score_color = (0,128,0)
start_info = FONTS[2].render("Press any key to start game", True, score_color)
text_rect = start_info.get_rect(center=(WIN_WIDTH / 2, WIN_HEIGHT / 2))
win.blit(start_info, text_rect)
遊戲過程中,展示玩家得分(玩家堅持時間)
在while
迴圈中,car.draw(win)
之後新增程式碼如下
text_info = FONTS[2].render("Scores: %d" % (frame_count / FPS), True, score_color)
win.blit(text_info, dest=(0, 0))
玩家撞車後,一輪遊戲結束,需要展示Game Over
,玩家得分,與按任意鍵結束遊戲
修改while
迴圈中,原有的程式碼
if emg.check_collide(car):
running = False # 撞車後,遊戲狀態改變為結束
為
if emg.check_collide(car):
running = False # 撞車後,遊戲狀態改變為結束
over_color = (255,0,0)
texts = ["Game Over", "Scores: %d" % (frame_count / FPS), "Press Any Key to Restart game"]
for ti, text in enumerate(texts):
over_info = FONTS[ti].render(text, True, over_color)
text_rect = over_info.get_rect(center=(WIN_WIDTH / 2, WIN_HEIGHT / 2 + 48 * ti))
win.blit(over_info, text_rect)
我們注意到,關於顏色這個常數,都是直接寫在用的地方。
後面多了後,容易找起來不方便,所以建議把所有的顏色宣告都統一移到開頭位置。
這裡放在CELL_SIZE = 40
之後,移動後顏色設定程式碼如下
bg_color = (200, 200, 200)
enemy_color = (50, 50, 50)
player_color = (65, 105, 225) # RoyalBlue
score_color = (0,128,0) # SpringGreen
over_color = (255, 0, 0)
car_racing.py
就是這樣寫的1 car racing
資料夾下的car_racing_plus1.py
中實現。