要想用《Flappy Bird》來重現Ronald及Schoenauer的研究成果,首先必須要針對《Flappy Bird》,寫個能讓神經演化可以在其中運作的專屬版本。
在《Flappy Bird》中有兩個主角:小鳥和水管,所以實作時,我們可以用兩個類別來描述這兩個主角。我們把描述小鳥的類別叫做Bird;把描述水管的類別叫做Pipe。現在,就先來設計Bird類別。因為在玩遊戲時,小鳥只會在同一個地點上下移動,所以x軸方向的位置是固定的,只有y軸方向的位置會變動。這也就是說,只需要一個純量就可以用來描述小鳥的位置和速度,不需要用到向量。
為了更進一步簡化程式,在計算小鳥的速度時,我們把小鳥拍翅向上飛的作用力直接加進速度中,而不是用以前的做法,也就是把作用力累加進加速度中,然後再用加速度來計算速度。除了固定會有的update()方法和show()方法之外,我們新加入一個flap()方法,用來處理小鳥拍翅向上飛這件事。完整的Bird類別程式碼如下:
class Bird:
def __init__(self):
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()
self.radius = 8
# 小鳥的位置;其中x軸方向的位置是固定不變的。
self.x, self.y = 50, 120
# 因為小鳥只會上下移動,所以速度和作用力都是純量
self.velocity = 0
self.gravity = 0.5
self.flap_force = -10
def flap(self):
# 小鳥拍翅向上飛
self.velocity += self.flap_force
def update(self):
self.velocity += self.gravity
self.y += self.velocity
self.velocity *= 0.95
# 小鳥跌落地面
if self.y > self.height:
self.y = self.height
self.velocity = 0
def show(self):
# 用圓來代表小鳥
pygame.draw.circle(self.screen, (0, 0, 0), (self.x, self.y), self.radius)
在設計Bird類別時,我們用圓來代表小鳥,這樣比較省事一些;畢竟我們的重點是要讓程式能順暢運作,而不是畫隻漂亮的小鳥。
接下來,來設計Pipe類別。
在遊戲中,水管有幾個特性。首先,水管只會由右向左等速移動,而不會朝其他方向移動。所以,要描述水管的速度,就和描述小鳥的速度一樣,只需用一個純量就可以了,不需要用到向量。另外,既然水管會由右向左持續等速移動,所以它最後會移到畫面之外。
除了上述的特性之外,水管還有另一個特性:每根水管都有個長度一樣的缺口,不過缺口的位置是隨機的。
綜合以上水管的特性,描述水管的Pipe類別可以這樣設計:
class Pipe:
def __init__(self):
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()
# 水管開口長度
self.spacing = 100
# 水管開口頂部位置
self.top = random.randint(0, self.height - self.spacing)
# 水管開口底部位置
self.bottom = self.top + self.spacing
# 水管一開始的位置是在螢幕最右方
self.x = self.width
# 水管寬度
self.w = 20
# 水管會水平等速移動
self.velocity = 2
def update(self):
# 水管從右向左移動
self.x -= self.velocity
def off_screen(self):
# 水管是否已移至畫面外?
return self.x < -self.w
def show(self):
# 開口上方之水管
pipe = pygame.Rect(self.x, 0, self.w, self.top)
pygame.draw.rect(self.screen, (0, 0, 0), pipe)
# 開口下方之水管
pipe = pygame.Rect(self.x, self.bottom, self.w, self.height-self.bottom)
pygame.draw.rect(self.screen, (0, 0, 0), pipe)
Bird類別和Pipe類別設計好了,不過這樣還不算完工,還有個關鍵的部分需要處理:碰撞。
《Flappy Bird》的玩法是要讓小鳥平安穿過水管,所以我們必須不斷檢查小鳥和水管是不是產生碰撞;說得比較直白一點,就是要知道小鳥和水管有沒有撞在一起。
小鳥和水管的碰撞可以寫個collides()方法來處理。不過,這個collides()方法要放在哪呢?是放在Bird類別,還是放在Pipe類別?其實都可以啦,就看怎麼看小鳥和水管撞在一起這件事。如果認為是小鳥撞上水管,那就把collides()方法放在Bird類別中;如果認為是水管撞上小鳥,那就把collides()方法放在Pipe類別中。在這裡,我們就把collides()方法放在Pipe類別中,也就是去偵測水管有沒有撞上小鳥。
要檢查水管和小鳥有沒有發生碰撞,做法有很多種。其中一種做法,就是先檢查小鳥的高度,看看是不是位於開口之上的水管或開口之下的水管中,然後再檢查小鳥的水平位置,看看是不是位於水管的寬度範圍內。當小鳥的高度位於開口之上的水管或開口之下的水管中,而且水平位置位於水管的寬度範圍內時,那就代表水管和小鳥發生碰撞了。所以,collides()方法可以這樣寫:
def collides(self, bird):
# 小鳥的高度是否位於開口之上的水管或開口之下的水管中?
self.vertical_collision = (bird.y < self.top) or (bird.y > self.bottom)
# 小鳥的水平位置是否位於水管的寬度範圍內?
self.horiziontal_collision = self.x < bird.x < self.x+self.w
return self.vertical_collision and self.horiziontal_collision
這裡要注意一下。雖然我們是用一個圓來代表小鳥,不過在寫collides()方法時,我們其實是用圓心而不是用圓來檢查碰撞。這樣做的好處是程式可以簡化,而且會比較好寫;不過代價是碰撞的偵測不是那麼精準,有可能明明圓的邊邊已經切過水管了,但因為圓心沒有碰到水管,所以collides()方法不會偵測到發生碰撞。
設計好Bird類別和Pipe類別之後,接下來就是主程式的部分。
因為水管會由右向左不斷移動,最後會跑到畫面之外。所以,我們設定每隔100幀畫面就會有新的水管出現,免得最後畫面上都沒有水管。當然啦,跑出畫面的水管最好從程式中給移除掉,免得佔用資源。這部分程式的寫法其實和粒子系統的寫法差不多,就用一個list存放水管,跑出畫面的水管就從list中移除,要新增水管時,就用append()方法加到list中。
Example 11.1: Flappy Bird Clone
按滑鼠按鍵小鳥會往上飛。撞到水管時,會顯示「OOPS!」字樣。

# python version 3.13.9
import random
import sys
import pygame # version 2.6.1
pygame.init()
pygame.display.set_caption("Example 11.1: Flappy Bird Clone")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
bird = Bird()
pipes = [Pipe()]
font = pygame.font.SysFont('courier', 24)
text = font.render('OOPS!', True, (0, 0, 0))
counter = 0
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN:
# 按滑鼠按鍵則小鳥拍翅向上飛
bird.flap()
screen.fill(WHITE)
for pipe in pipes:
pipe.show()
pipe.update()
for pipe in pipes:
if pipe.collides(bird):
text_rect = text.get_rect(center=(pipe.x+15, pipe.top+20))
screen.blit(text, text_rect)
break
# 將移至畫面外之水管移除?
for pipe in pipes.copy():
if pipe.off_screen():
pipes.remove(pipe)
bird.update()
bird.show()
# 每100幀畫面加入一根新水管
counter += 1
if counter == 100:
counter = 0
pipes.append(Pipe())
pygame.display.update()
frame_rate.tick(FPS)
Exercise 11.1
在畫面中以紅色字體顯示分數。

見下圖。當小鳥由區域A進入區域B,再由區域B進入區域C,如果這期間沒有發生碰撞,那就代表小鳥安全穿越水管。

Bird類別和Pipe類別不變,主程式如下:
# python version 3.13.9
import random
import sys
import pygame # version 2.6.1
pygame.init()
pygame.display.set_caption("Exercise 11.1")
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
bird = Bird()
pipes = [Pipe()]
font = pygame.font.SysFont('courier', 24)
text = font.render('OOPS!', True, (0, 0, 0))
counter = 0
score = 0
# 小鳥是否正在穿越水管?
going_thru = False
# 是否發生碰撞?
collide = False
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN:
# 按滑鼠按鍵則小鳥拍翅向上飛
bird.flap()
screen.fill(WHITE)
for pipe in pipes:
pipe.show()
pipe.update()
for pipe in pipes:
if pipe.collides(bird):
collide = True
text_rect = text.get_rect(center=(pipe.x+15, pipe.top+20))
screen.blit(text, text_rect)
break
for pipe in pipes:
if pipe.x <= bird.x <= pipe.x + pipe.w:
# 小鳥正在穿越水管
going_thru = True
break
else:
# 小鳥不在水管範圍內
if going_thru:
# 小鳥已穿越水管
if not collide:
score += 1
collide = False
going_thru = False
# 將移至畫面外之水管移除?
for pipe in pipes.copy():
if pipe.off_screen():
pipes.remove(pipe)
bird.update()
bird.show()
total_score = font.render(f'{score}', True, (255, 0, 0), (255, 255, 255))
score_rect = total_score.get_rect(center=(WIDTH//2, 20))
screen.blit(total_score, score_rect)
# 每100幀畫面加入一根新水管
counter += 1
if counter == 100:
counter = 0
pipes.append(Pipe())
pygame.display.update()
frame_rate.tick(FPS)














