更新於 2024/11/17閱讀時間約 60 分鐘

初學者 Pygame 超詳細實戰教學:一步步帶你撰寫 Chrome Dino 小遊戲!

Pygame是什麼?

講到編寫遊戲你會想到什麼程式語言呢?

大多數人可能第一時間會想到Unity之類的程式,但其實Python也能寫遊戲喔。其中Pygame就是為了使用Python寫遊戲所開發的套件,它是一個輕量的遊戲套件,能夠完成大部分遊戲所需要的功能,包括音樂管理、圖形處理及事件處理等等,適合用於開發2D遊戲。

初步了解Pygame後,就讓我們透過撰寫一個小遊戲(本文使用chrome的離線小恐龍遊戲),相信在跟著文章一步一步學習後,你會對於整體架構更有概念!

💡 補充:在撰寫pygame遊戲時需要用到大量Class(物件導向)的模組,若對物件導向不熟的可以先去網路上爬爬文再回來閱讀本文。我相信你很快就能理解物件導向的概念了!加油!


開始編寫程式!

設定

我使用的編譯器是vscode,但不一定要跟我一樣,用自己習慣的編譯器即可。

首先,我們需要先在終端機中安裝好pygame,指令如下

pip install pygame

請將此處的Assets資源包下載下來,裡面有這次遊戲製作中所需的所有圖片以及字體

資源包下載連結:

https://drive.google.com/drive/u/2/folders/1R1Fkk6mdl3G2PDu1yJJMVojc4mLXRsGq

完整程式



將assets拉進這次專案的資料夾後,創建一個名為「main.py」的檔案,這樣就設定完成了。

💡 補充:不一定要使用main來取名可依照個人習慣改名,但後面一定要是「.py」告訴電腦這是一個Python檔案


在main.py中寫一些基礎設定

import pygame 
import os
import random

pygame.init()

screen_height = 600
screen_width = 1100

screen = pygame.display.set_mode((screen_width, screen_height))

第一行的import pygame是要引用「pygame」這個套件,這樣才可以使用pygame中的函式。

接著,透過pygame.init() 來初始化 pygame。

import os 是引用「os」套件,它的主要功能是處理檔案相關內容。

import random會在背景移動時用到。


定義一些跟設定畫面有關的變數

  • screen_height :設定畫面的高度為600
  • screen_width :設定畫面的寬度為1100

pygame.display.set_mode((screen_width, screen_height)) :pygame的功能,藉由前面兩個高跟寬的變數設定畫面大小。

簡單來說,screen 就是一個設定好高為600、寬為1100的畫布(可以在上面顯示遊戲畫面)。


小恐龍移動

#接著上面的程式繼續寫... 

RUNNING = [
pygame.image.load(os.path.join("assets/Dino", "Chrome Dino Run.png")),
pygame.image.load(os.path.join("assets/Dino", "Chrome Dino Run 2.png"))
]
JUMPING = pygame.image.load(os.path.join("assets/Dino", "Dino Jump.png"))
DUCKING = [
pygame.image.load(os.path.join("assets/Dino", "Dino Duck.png")),
pygame.image.load(os.path.join("assets/Dino", "Dino Duck 2.png"))
]

定義遊戲中所需要的小恐龍圖片(分別是跑步、跳躍、趴下)

pygame.image.load() 是一個Pygame中引入圖片的方式,在括號中的os.path.join() 是在「os」套件中將兩個路徑連接的函式。

舉例來說pygame.image.load(os.path.join("assets/Dino", "Chrome Dino Run.png")) 就是讀取以下這個檔案









依照這方式陸續讀取跑步、跳躍、趴下的圖片。

像是RUNNING裡有兩張圖片分別是腳抬起來以及腳放下來(如下圖),會使用一個陣列將其包起來,這樣之後的程式中就可以透過RUNNING[0]以及RUNNING[1]來切換動作

Chrome Dino Run.png


Chrome Dino Run 2.png







#接著上面的程式繼續寫... 

class Dinosaur:
X_pos = 80
Y_pos = 310
Y_pos_duck = 340
set_jump_vel = 8.5

創建一個叫做「Dinosaur」的Class控制小恐龍的移動,使用變數定義一些基本設定

變數定義如下:

  • X_pos :角色在畫面中的x座標
  • Y_pos :角色在畫面中的y座標
  • Y_pos_duck :由於圖片大小不同,所以需要在將趴下時的y座標稍微往下調整
  • set_jump_vel:目前可以先將它想成跳躍的高度(後面會在解釋用法)


init 函式

#接著上面的程式繼續寫... 

class Dinosaur:
#接著上面的程式繼續寫...

def __init__(self):
self.duck_img = DUCKING
self.run_img = RUNNING
self.jump_img = JUMPING

self.dino_duck = False
self.dino_run = True
self.dino_jump = False

self.step_index = 0
self.jump_vel = self.set_jump_vel
self.image = self.run_img[0]
self.dino_rect = self.image.get_rect()
self.dino_rect.x = self.X_pos
self.dino_rect.y = self.Y_pos
💡 備註:def __init__(self)函式是在Class中的初始化定義,因此在此處定義所需的變數


self.duck_imgself.run_imgself.jump_img只是將先前所定義好的圖片在複製一份到Class中,方便後面編寫。

初始化設定self.dino_runTrue,而其餘的self.dino_duckself.dino_jump設定為False,代表遊戲開始時的狀態會是跑步,而不是趴下跳躍


self.step_indexself.jump_vel 是在後面程式中會運用到的變數(會在文章後面解釋)。

self.image這個變數是抓出run_img[0]中的圖片也就是檔名為Chrome Dino Run.png的圖片

(如下圖)

Chrome Dino Run.png

💡 補充:.get_rect():是 Pygame 圖片物件的方法,會自動取得該圖片的“矩形”資訊,包含圖片的寬度、高度,以及位置(預設會在 (0, 0))。

self.dino_rect會得到Chrome Dino Run.png的矩形資訊,並設定其x座標為X_pos(80)、y座標為Y_pos(310)


update函式

#接著上面的程式繼續寫... 

class Dinosaur:
#接著上面的程式繼續寫...

def update(self, userInput):
if self.dino_duck:
self.duck()
if self.dino_run:
self.run()
if self.dino_jump:
self.jump()

if self.step_index >= 10:
self.step_index = 0

if userInput[pygame.K_UP] and not self.dino_jump:
self.dino_duck = False
self.dino_run = False
self.dino_jump = True
elif userInput[pygame.K_DOWN] and not self.dino_jump:
self.dino_duck = True
self.dino_run = False
self.dino_jump = False
elif not (self.dino_jump or userInput[pygame.K_DOWN]):
self.dino_duck = False
self.dino_run = True
self.dino_jump = False

這裡函式「update」的名稱就沒有像__init__一樣有特別規定,而是因為這個函式是用來更新角色控制狀態所以才會以update命名。userInput通常是一個 字典 (dictionary) 或 列表 (list),其中包含使用者的輸入狀態,在這裡可以將它理解成使用者按的上下鍵就好了。

if self.dino_duck:if self.dino_run:if self.dino_jump:分別會在先前定義好的這三個變數為True時執行對應的動作函式。

這個if self.step_index >= 10:是要在step_index ≥ 10的時候將它歸零(後面用到step_index變數時會在講解)

後面三個 if 及 else if(python中的elif)的條件可以理解成

  • if userInput[pygame.K_UP] and not self.dino_jump: :如果「向上鍵被按下並且不是在跳躍中」,那麼就將self.dino_jump設定成True其餘的設定成False,代表恐龍正在執行跳躍的動作。
  • elif userInput[pygame.K_DOWN] and not self.dino_jump::若上述條件沒有成立則會來判斷,「向下鍵被按下且不是在跳躍中」,就設定self.dino_duckTrue其他的為False,表示現在型態為趴下
  • elif not (self.dino_jump or userInput[pygame.K_DOWN])::最後若以上條件都沒有成立,就判斷,「如果不是在跳躍也沒有按下向下鍵」,則將self.dino_runTrue剩下的設定為False,也就是把狀態設定回正在跑步。



duck / run函式

#接著上面的程式繼續寫... 

class Dinosaur:
#接著上面的程式繼續寫...

def duck(self):
self.image = self.duck_img[self.step_index // 5]
self.dino_rect = self.image.get_rect()
self.dino_rect.x = self.X_pos
self.dino_rect.y = self.Y_pos_duck
self.step_index += 1

def run(self):
self.image = self.run_img[self.step_index // 5]
self.dino_rect = self.image.get_rect()
self.dino_rect.x = self.X_pos
self.dino_rect.y = self.Y_pos self.step_index += 1

此函式會在上面寫的條件判斷self.dino_duck成立時被呼叫。

這裡稍稍複雜一點

要先知道在self.duck_img這個陣列中有兩張圖片(可以回去看一下上面的程式碼),也就是說self.step_index // 5(「//」在Python中是無條件捨去的意思)這一串看起來很可怕的運算,其實只會有兩種可能,也就是0跟1而已 (若數值大於1則會造成資料溢位,出現IndexError: list index out of range 這個錯誤訊息)。

run函式與duck函式差不多,我就不多解釋了,但是注意self.image = self.run_img[self.step_index // 5]這行與duck函式有稍稍不同。


這邊稍微追一下程式給各位看。

self.step_index的變化流程圖

由於在程式最後會將self.step_index加一,因此可以把self.step_index想像成一個一直增加的變數,直到上面寫的if self.step_index >= 10: 成立就將self.step_index歸零這樣才可以達到不斷在0跟1間切換的效果,也就是讓小恐龍看起來在跑步。

還對於這個運算式不太了解嗎?

那麼不妨自己在「這個角色移動段落結束時(寫完main函式)**」**後,試試看修改參數,把self.step_index // 5 改成 self.step_index // 2,並且將if self.step_index >= 10: 這個判斷式改成if self.step_index >= 4: ,觀察看看恐龍步伐速度的變化,再回來看解釋時,或許能夠幫助理解~










jump函式

#接著上面的程式繼續寫... 

class Dinosaur:
#接著上面的程式繼續寫...

def jump(self):
self.image = self.jump_img
if self.dino_jump:
self.dino_rect.y -= self.jump_vel * 5
self.jump_vel -= 0.85

if self.jump_vel < -self.set_jump_vel:
self.dino_jump = False
self.jump_vel = self.set_jump_vel

先將要顯示的圖片切換成跳躍的動作self.image = self.jump_img,接著要判斷如果是在跳躍狀態,則執行跳躍的動作。

self.dino_rect.y -= self.jump_vel * 5

self.jump_vel -= 0.85

這兩橫程式碼就是在處理跳躍的動作(改變恐龍的y座標),由於解釋上必較複雜,因此我將恐龍在跳躍時的y座標及self.jump_vel分別印出來,請觀察以下gif相信有助於你們理解。

如果還是不太懂原理的話可以自己改改看,把self.jump_vel -= 0.8改成self.jump_vel -= 1,並看看差異~


draw函式

同樣「draw」這個名稱也是自己定義的並沒有其他特別的規則,也因為他的功能是將圖片畫到畫面上才稱視為draw。

#接著上面的程式繼續寫... 

class Dinosaur:
#接著上面的程式繼續寫...

def draw(self, SCREEN):
SCREEN.blit(self.image, (self.dino_rect.x, self.dino_rect.y))
💡 blit(image, (x, y))

SCREEN會是後來要引入的畫布。而blit函式簡單來說就是將圖片畫到畫面上需要給此函式要畫的圖片,以及圖片的xy座標。

那我們恐龍移動的Class就到這邊告一個段落。


main函式

#其他程式碼... 

class Dinosaur:
...

#接在class後面寫
def main():
run = True
clock = pygame.time.Clock()
player = Dinosaur()

while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False

screen.fill((255, 255, 255))
userInput = pygame.key.get_pressed()

player.draw(screen)
player.update(userInput)
clock.tick(30)
pygame.display.update()

main()
💡 補充:在 Pygame 中,Clock 是一個用於管理遊戲更新速度和時間間隔的對象。它主要用來限制畫面的刷新頻率,讓遊戲保持平穩流暢的運行,並讓每次循環(即每一個遊戲畫面更新)之間的時間間隔穩定。


前面寫了那麼多控制角色的程式,但是都還未讓整個遊戲開始運作,因此我們需要這個main函式來執行遊戲。

遊戲運作的原理就是讓畫面不斷刷新。使用while True迴圈便能達到這個目的。

以下這段程式可以先當作是遊戲中斷的判斷就好

for event in pygame.event.get(): 
if event.type == pygame.QUIT:
run = False

接著設定畫布顏色為白色screen.fill((255, 255, 255))也可以自行修改背景顏色,以及設定按鍵輸入userInput = pygame.key.get_pressed()

最後,就可以呼叫我們先前寫好的draw函式跟update函式,並設定畫面更新速度為每秒30次clock.tick(30),然後讓pygame更新螢幕,將畫面顯示出來。

記得在main函式下呼叫他,這就是角色移動的所有部分,現在可以讓程式跑跑看,欣賞成果。

截至本段落的完整程式碼

import pygame
import os

pygame.init()

screen_height = 600
screen_width = 1100
screen = pygame.display.set_mode((screen_width, screen_height))

RUNNING = [
pygame.image.load(os.path.join("assets/Dino", "Chrome Dino Run.png")),
pygame.image.load(os.path.join("assets/Dino", "Chrome Dino Run 2.png"))
]
JUMPING = pygame.image.load(os.path.join("assets/Dino", "Dino Jump.png"))
DUCKING = [
pygame.image.load(os.path.join("assets/Dino", "Dino Duck.png")),
pygame.image.load(os.path.join("assets/Dino", "Dino Duck 2.png"))
]

class Dinosaur:
X_pos = 80
Y_pos = 310
Y_pos_duck = 340
set_jump_vel = 8.5

def __init__(self):
self.duck_img = DUCKING
self.run_img = RUNNING
self.jump_img = JUMPING

self.dino_duck = False
self.dino_run = True
self.dino_jump = False

self.step_index = 0
self.jump_vel = self.set_jump_vel
self.image = self.run_img[0]
self.dino_rect = self.image.get_rect()
self.dino_rect.x = self.X_pos
self.dino_rect.y = self.Y_pos

def update(self, userInput):
if self.dino_duck:
self.duck()
if self.dino_run:
self.run()
if self.dino_jump:
self.jump()

if self.step_index >= 20:
self.step_index = 0

if userInput[pygame.K_UP] and not self.dino_jump:
self.dino_duck = False
self.dino_run = False
self.dino_jump = True
elif userInput[pygame.K_DOWN] and not self.dino_jump:
self.dino_duck = True
self.dino_run = False
self.dino_jump = False
elif not (self.dino_jump or userInput[pygame.K_DOWN]):
self.dino_duck = False
self.dino_run = True
self.dino_jump = False

def duck(self):
self.image = self.duck_img[self.step_index // 10]
self.dino_rect = self.image.get_rect()
self.dino_rect.x = self.X_pos
self.dino_rect.y = self.Y_pos_duck
self.step_index += 1

def run(self):
self.image = self.run_img[self.step_index // 10]
self.dino_rect = self.image.get_rect()
self.dino_rect.x = self.X_pos
self.dino_rect.y = self.Y_pos
self.step_index += 1

def jump(self):
self.image = self.jump_img

if self.dino_jump:
self.dino_rect.y -= self.jump_vel * 4
self.jump_vel -= 0.8
print(f"y pos: {self.dino_rect.y}, jump vel: {self.jump_vel: .2f}")

if self.jump_vel < -self.set_jump_vel:
self.dino_jump = False
self.jump_vel = self.set_jump_vel
print("jump stop", self.jump_vel)

def draw(self, SCREEN):
SCREEN.blit(self.image, (self.dino_rect.x, self.dino_rect.y))



def main():
run = True
clock = pygame.time.Clock()
player = Dinosaur()

while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False

screen.fill((255, 255, 255))
userInput = pygame.key.get_pressed()

player.draw(screen)
player.update(userInput)
clock.tick(30)
pygame.display.update()

main()

背景移動

首先,我們需要先讀取背景的圖片(地板、雲朵)

RUNNING = ...
JUMPING = ...
DUCKING = ...

#接著上面的程式繼續寫
CLOUD = pygame.image.load(os.path.join("assets/Other", "Chrome Dinosaur Cloud.png"))
BG = pygame.image.load(os.path.join("assets/Other", "Chrome Dinosaur Track.png"))


創建一個叫做Cloud的Class處理雲朵的移動

...

class Dinosaur:
...

class Cloud:
def __init__(self):
self.x = screen_width + random.randint(500, 2000)
self.y = random.randint(50, 200)
self.image = CLOUD
self.width = self.image.get_width()

def update(self):
self.x -= game_speed
if self.x < -self.width:
self.x = screen_width + random.randint(500, 1500)
self.y = random.randint(50, 200)

def draw(self, SCREEN):
SCREEN.blit(self.image, (self.x, self.y))
💡 補充:random.randint 是 Python random 模組中的一個函式,用來產生一個指定範圍內的隨機整數。

__init__(self):初始化函式中,定義以下變數

  • self.x:將雲初始x座標設定為,螢幕的寬度(1100)在加上500到2000中隨機的數值,也就是說雲初始時的位置會在螢幕之外,然後再飄進來。
  • self.y:初始化y座標設定為一個50到200之間的數值
  • self.image:設定圖片成雲朵
  • self.width:讀取雲朵的寬度

update函式負責雲朵簡單的移動,不斷地將雲朵的x座標減去game_speed,實現雲朵依照遊戲速度由右至左移動。

接著判斷若雲朵的x座標小於負的雲朵的寬度(0 - 雲朵的寬度),就代表雲朵已經完全移動至畫面外,便可以將其x座標設定回螢幕的寬度(1100)在加上500到2000中隨機的數值,以及y座標設定為一個50到200之間的數值。

...

class Dinosaur: ...

class Cloud: ...

def main():
#接著上面的程式碼寫...

#新增以下兩行
global game_speed
game_speed = 14

while run:
...
userInput = ...

#新增以下兩行
cloud.draw(screen)
cloud.update()

這樣雲朵移動的部分就處理完了,可以將程式跑起來看看效果!


再來,我們要增加的部分是地板移動的控制。

...

class Dinosaur: ...

class Cloud: ...

def main():
#接著上面的程式碼寫...

global ..., x_pos_bg, y_pos_bg
game_speed = ...
x_pos_bg = 0
y_pos_bg = 380

def background():
global x_pos_bg, y_pos_bg
image_width = BG.get_width()
screen.blit(BG, (x_pos_bg, y_pos_bg))
screen.blit(BG, (image_width + x_pos_bg, y_pos_bg))
if x_pos_bg <= -image_width:
screen.blit(BG, (image_width + x_pos_bg, y_pos_bg))
x_pos_bg = 0
x_pos_bg -= game_speed

while run:
...
userInput = ...

cloud.draw(screen)
cloud.update()

#新增以下這行
background()

其實移動地板讓地板看起來無窮無境得跑的原理,就是利用兩塊一樣的圖片銜接。講仔細一點,兩塊地板先是連接再一起的,然而當最前面那塊完全超出畫面後,便會跳回到最後面,在與前一塊地板連接。

以下是示意影片(將兩塊地板用紅色線匡起來)


截至本段落的完整程式碼

import pygame
import os
import random

pygame.init()

screen_height = 600
screen_width = 1100
screen = pygame.display.set_mode((screen_width, screen_height))

RUNNING = [
pygame.image.load(os.path.join("assets/Dino", "Chrome Dino Run.png")),
pygame.image.load(os.path.join("assets/Dino", "Chrome Dino Run 2.png"))
]
JUMPING = pygame.image.load(os.path.join("assets/Dino", "Dino Jump.png"))
DUCKING = [
pygame.image.load(os.path.join("assets/Dino", "Dino Duck.png")),
pygame.image.load(os.path.join("assets/Dino", "Dino Duck 2.png"))
]
CLOUD = pygame.image.load(os.path.join("assets/Other", "Chrome Dinosaur Cloud.png"))
BG = pygame.image.load(os.path.join("assets/Other", "Chrome Dinosaur Track.png"))

class Dinosaur:
X_pos = 80
Y_pos = 310
Y_pos_duck = 340
set_jump_vel = 8.5

def __init__(self):
self.duck_img = DUCKING
self.run_img = RUNNING
self.jump_img = JUMPING

self.dino_duck = False
self.dino_run = True
self.dino_jump = False

self.step_index = 0
self.jump_vel = self.set_jump_vel
self.image = self.run_img[0]
self.dino_rect = self.image.get_rect()
self.dino_rect.x = self.X_pos
self.dino_rect.y = self.Y_pos

def update(self, userInput):
if self.dino_duck:
self.duck()
if self.dino_run:
self.run()
if self.dino_jump:
self.jump()

if self.step_index >= 20:
self.step_index = 0

if userInput[pygame.K_UP] and not self.dino_jump:
self.dino_duck = False
self.dino_run = False
self.dino_jump = True
elif userInput[pygame.K_DOWN] and not self.dino_jump:
self.dino_duck = True
self.dino_run = False
self.dino_jump = False
elif not (self.dino_jump or userInput[pygame.K_DOWN]):
self.dino_duck = False
self.dino_run = True
self.dino_jump = False

def duck(self):
self.image = self.duck_img[self.step_index // 10]
self.dino_rect = self.image.get_rect()
self.dino_rect.x = self.X_pos
self.dino_rect.y = self.Y_pos_duck
self.step_index += 1


def run(self):
self.image = self.run_img[self.step_index // 10]
self.dino_rect = self.image.get_rect()
self.dino_rect.x = self.X_pos
self.dino_rect.y = self.Y_pos
self.step_index += 1

def jump(self):
self.image = self.jump_img
if self.dino_jump:
self.dino_rect.y -= self.jump_vel * 4
self.jump_vel -= 0.8
print(f"y pos: {self.dino_rect.y}, jump vel: {self.jump_vel: .2f}")
if self.jump_vel < -self.set_jump_vel:
self.dino_jump = False
self.jump_vel = self.set_jump_vel
print("jump stop", self.jump_vel)

def draw(self, SCREEN):
SCREEN.blit(self.image, (self.dino_rect.x, self.dino_rect.y))


class Cloud:
def __init__(self):
self.x = screen_width + random.randint(500, 2000)
self.y = random.randint(50, 200)
self.image = CLOUD
self.width = self.image.get_width()

def update(self):
self.x -= game_speed
if self.x < -self.width:
self.x = screen_width + random.randint(500, 1500)
self.y = random.randint(50, 200)

def draw(self, SCREEN):
SCREEN.blit(self.image, (self.x, self.y))

def main():
global game_speed, x_pos_bg, y_pos_bg
run = True
clock = pygame.time.Clock()
cloud = Cloud()
player = Dinosaur()
game_speed = 14
x_pos_bg = 0
y_pos_bg = 380

def background():
global x_pos_bg, y_pos_bg
image_width = BG.get_width()
bg_rect = BG.get_rect()
screen.blit(BG, (x_pos_bg, y_pos_bg))
screen.blit(BG, (image_width + x_pos_bg, y_pos_bg)
if x_pos_bg <= -image_width:
screen.blit(BG, (image_width + x_pos_bg, y_pos_bg))
x_pos_bg = 0
x_pos_bg -= game_speed
bg_rect.x = x_pos_bg


while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False

screen.fill((255, 255, 255))
userInput = pygame.key.get_pressed()

cloud.draw(screen)
cloud.update()

background()


player.draw(screen)
player.update(userInput)

clock.tick(30)
pygame.display.update()


main() screen.fill((255, 255, 255))


新增障礙物

將大、小的仙人掌以及鳥的圖片引入至程式中。

RUNNING = ...
JUMPING = ...
DUCKING = ...
CLOUD = ...
BG = ...
#接著上面的程式繼續寫
SMALL_CACTUS = [
pygame.image.load(
os.path.join("assets/Cactus", "Chrome Dinosaur Small Cactus.png")),
pygame.image.load(
os.path.join("assets/Cactus", "Chrome Dinosaur Small Cactus (1).png")),
pygame.image.load(
os.path.join("assets/Cactus", "Chrome Dinosaur Small Cactus (2).png"))
]
LARGE_CACTUS = [
pygame.image.load(
os.path.join("assets/Cactus", "Chrome Dinosaur Large Cactus.png")),
pygame.image.load(
os.path.join("assets/Cactus", "Chrome Dinosaur Large Cactus (1).png")),
pygame.image.load(
os.path.join("assets/Cactus", "Chrome Dinosaur Large Cactus (2).png")),
]
BIRD = [
pygame.image.load(
os.path.join("assets/Bird", "Chrome Dinosaur Bird1.png")),
pygame.image.load(
os.path.join("assets/Bird", "Chrome Dinosaur Bird2.png")),
]

class Dinosaur: ...

class Cloud: ...

#接著上面的程式繼續寫

class Obstacle:
def __init__(self, image, type):
self.image = image
self.type = type
self.rect = self.image[self.type].get_rect()
self.rect.x = screen_width

def update(self):
self.rect.x -= game_speed
if self.rect.x < -self.rect.width:
obstacles.pop()

def draw(self, SCREEN):
SCREEN.blit(self.image[self.type], self.rect)


__init__(self) 這裡大多數的概念都與前面的相似,差別只在與多了一個選擇形狀的type變數(大、小仙人掌都有不同的形狀)。

update函式與前面幾乎一模一樣了,都是將x座標不斷的減去game_speed達到往左移動的效果,並且在完全超出畫面時,將其從儲存障礙物的陣列中刪除。

class Dinosaur: ...

class Cloud: ...

class Obstacle:...

#接著上面的程式繼續寫
class SmallCactus(Obstacle):
def __init__(self, image):
self.type = random.randint(0, 2)
super().__init__(image, self.type)
self.rect.y = 325

class LargeCactus(Obstacle):
def __init__(self, image):
self.type = random.randint(0, 2)
super().__init__(image, self.type)
self.rect.y = 300

class Bird(Obstacle):
def __init__(self, image):
self.type = 0
super().__init__(image, self.type)
self.rect.y = 250
self.index = 0

def draw(self, SCREEN):
if self.index >= 10:
self.index = 0
SCREEN.blit(self.image[self.index//5], self.rect)
self.index += 1

SmallCactusLargeCactus都會隨機選擇0到2中的一個數值(在SMALL_CACTUS陣列中有三個元素,因此是選擇0到2這個區間),並且將這個type丟回給剛剛所寫好的Obstacle Class,最後設定其障礙物的y座標。

Bird__init__(self)與前兩個也很相似,就不多做解釋了,draw函式的部分可以參考恐龍移動的部分概念上是一樣的。

在main的函式中,我們需要先將儲存障礙物的陣列定義出來。

def main():
global ..., ..., ..., obstacles
run = ...
...
y_pos_bg = ...
obstacles = []
def main():
...

while run:
...
background()...

#新增以下程式碼
if len(obstacles) == 0:
if random.randint(0, 2) == 0:
obstacles.append(SmallCactus(SMALL_CACTUS))
elif random.randint(0, 2) == 1:
obstacles.append(LargeCactus(LARGE_CACTUS))
elif random.randint(0, 2) == 2:
obstacles.append(Bird(BIRD))

for obstacle in obstacles:
obstacle.draw(screen)
obstacle.update()
if player.dino_rect.colliderect(obstacle.rect):
pygame.draw.rect(screen, (255, 0, 0), player.dino_rect, 2)
💡 補充:在 Pygame 中,.colliderect 是一個用於檢測兩個矩形物件是否發生碰撞的函式。

那一串ifelse if 在做得事就是判斷,若儲存障礙物的陣列obstalces是空的話,那麼就隨機將大、小仙人掌以及鳥放進去此陣列中。

確認陣列中有東西時,便利用for迴圈將陣列中所有元素都提取出來,並畫在畫面上。最後,利用pygame的.colliderect函式判斷恐龍是否有碰到障礙物,而目前為了方面觀察碰到時,先使用框框將恐龍框起來。


分數計算以及遊戲重置畫面

定義以下變數及字體

def main():
global ..., ..., ..., obstacles, points
obstacles = ...

#新增以下變數
death_count = 0
points = 0
font = pygame.font.Font(os.path.join("assets/font", "ARCADECLASSIC.TTF"), 30)
...

def main():

...

def background():
...

#接著上面的程式寫
def score():
global points, game_speed
points += 1
if points % 100 == 0:
game_speed += 1

text = font.render(str(points), True, (94, 94, 94))
textRect = text.get_rect()
textRect.center = (1000, 60)
screen.blit(text, textRect)


while run:
...

background()

#新增以下程式
score()

for obstacle in obstacles:
obstacle.draw(screen)
obstacle.update()
if player.dino_rect.colliderect(obstacle.rect):
#pygame.draw.rect(screen, (255, 0, 0), player.dino_rect, 2)
pygame.time.delay(2000)
death_count += 1
menu(death_count)


判斷當分數 / 100的餘數為零的時候就將game_speed + 1,簡單來說,就是分數每增加100就將遊戲加速。

後面就是在設定字的位置以及顏色。

在判斷是否碰到障礙物的for迴圈中將畫紅框那行註解或刪除,並增加以下幾行程式

  • pygame.time.delay(2000):遊戲暫停2秒
  • death_count += 1:死亡次數+1
  • menu(death_count):呼叫結束畫面的函式
def main():
...

#main() 記得將這裡呼叫的main函式「刪除或註解」
#接著上面的程式寫
def menu(death_count):
global points
run = True
while run:
screen.fill((255, 255, 255))
font = pygame.font.Font(os.path.join("assets/font", "ARCADECLASSIC.TTF"), 30)

if death_count == 0:
text = font.render("Press any key to start", True, (94, 94, 94))
elif death_count > 0:
text = font.render("Press any key to restart", True, (94, 94, 94))
score = font.render("Your Score " + str(int(points)), True, (94, 94, 94))
scoreRect = score.get_rect()
scoreRect.center = (screen_width // 2, screen_height // 2 + 50)
screen.blit(score, scoreRect)

textRect = text.get_rect()
textRect.center = (screen_width // 2, screen_height // 2)
screen.blit(text, textRect)
screen.blit(RUNNING[0], (screen_width // 2 - 20, screen_height // 2 - 140))
pygame.display.update()
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
if event.type == pygame.KEYDOWN:
main()


menu(death_count=0)


遊戲結束介面

這一整段程式碼就是在創建這個畫面,僅此而已。

結束!!!!

謝謝你有耐心得將這麽攏長的文章看完,也希望你在這段過程有所收穫!


完整程式碼以及assets都在github


分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.