Skip to content
[EN]

Flappy Bird

《Flappy Bird》是一款2013年鳥飛類遊戲,由越南河內獨立遊戲開發者阮河東(Dong Nguyen)開發,另一個獨立遊戲開發商GEARS Studios發佈。此遊戲原本於2013年5月發佈,平台為iPhone 5。同年9月,作者將遊戲更新至iOS 6。於2014年1月,此遊戲成為美國及中國iTunes最受歡迎免費應用軟件,並在同月被英國App Store描述為「新憤怒鳥」。此遊戲更成為1月下載次數最多的免費應用軟件。此遊戲的下載次數至今已超過5000萬次。

img

[TOC]

0. 本章重點

  1. 複習如何用歐拉方法模擬物理加速度
  2. ==物件導向, python class==
  3. Python for的另一個列表語法
  4. 程式常用技巧: 只執行一次

1. 開始遊戲

python
birdPos = PVector()

def setup():
    global birdPos

    size(800, 600)
    birdPos = PVector(50, height/2)

def draw():
    background("#70C6D5")

    fill("#D5BB06")
    ellipse(birdPos.x, birdPos.y, 25, 25)
image-20221019114234582

首先我們如常,一個processing.py的程式,基本包括了宣告區,setup()draw()。開一個800x600的畫面,開一個鳥的位置(birdPos)的向量變數,在setup()區中設定初始值,之後在draw()區中繪畫。

2. 讓鳥自由落體

python
birdPos = PVector()     #鳥的位置
birdVec = PVector()     #鳥的速度
birdAccel = PVector(0, 0.3)   #鳥的向下加速度

def setup():
    global birdPos, birdVec

    size(800, 600)
    frameRate(60)
    birdPos = PVector(50, height/2)
    birdVec = PVector(0, 0)

def draw():
    global birdPos, birdVec

    background("#70C6D5")

    birdVec = birdVec.add(birdAccel)
    birdPos = birdPos.add(birdVec)

    fill("#D5BB06")
    ellipse(birdPos.x, birdPos.y, 25, 25)
bird1

跟上一章一樣,開兩個向量變數,分別是鳥的速度和加速度。在setup()區中設定初速為0,而加速度因為由始至終都不變,可以直接在宣告區中宣告其值。

python
birdVec = birdVec.add(birdAccel)
birdPos = birdPos.add(birdVec)

draw()中,每次更新時都將鳥的速度加上加速度,而位置則加上速度。你可以自行調整一下加速度和frameRate(),抄到一個不會出現殘影而又夠快的速度。

3. 讓鳥懂得飛起

python
birdPos = PVector()     #鳥的位置
birdVec = PVector()     #鳥的速度
birdAccel = PVector(0, 0.3)   #鳥的向下加速度

def setup():
    global birdPos, birdVec

    size(800, 600)
    frameRate(60)
    birdPos = PVector(50, height/2)
    birdVec = PVector(0, 0)

def draw():
    global birdPos, birdVec

    background("#70C6D5")

    birdVec = birdVec.add(birdAccel)
    birdPos = birdPos.add(birdVec)

    fill("#D5BB06")
    ellipse(birdPos.x, birdPos.y, 25, 25)


def keyPressed():
    if (key == ' '):
        birdVec.y = -8
bird2
python
def keyPressed():
    if (key == ' '):
        birdVec.y = -8

要令鳥懂得"飛"起來,其實只要每次按下控制按鍵時,將鳥速度的y方向變成一個常數,就可以做到類似突然升起的效果,但升起後又會因為加速度再次慢慢降下。

4. 製作水管

python
birdPos = PVector()  # 鳥的位置
birdVec = PVector()  # 鳥的速度
birdAccel = PVector(0, 0.3)  # 鳥的向下加速度

panSpeed = 5

myPipe = 0


def setup():
    global birdPos, birdVec, myPipe

    size(800, 600)
    frameRate(60)
    birdPos = PVector(50, height/2)
    birdVec = PVector(0, 0)

    myPipe = Pipe()


def draw():
    global birdPos, birdVec

    background("#70C6D5")

    birdVec = birdVec.add(birdAccel)
    birdPos = birdPos.add(birdVec)

    fill("#D5BB06")
    ellipse(birdPos.x, birdPos.y, 25, 25)

    myPipe.update()
    myPipe.show()


def keyPressed():
    if (key == ' '):
        birdVec.y = -8
    if (key == 'R' or key == 'r'):
        setup()
        
# =======================Pipe Object=======================================

class Pipe(object):
    x = 0
    y = 0
    w = 80  #水管的寬度
    h = 0   #水管的高度
    gap = 80 #兩水管中間的間隔

    def __init__(self):
        self.x = width + self.w  #令水管再後一點一點
        self.y = random(100, height - 100) #下水管的左上角位置
        self.h = height

    def update(self):
        self.x -= panSpeed

    def show(self):
        fill(0, 204, 0)
        rect(self.x, self.y, self.w, self.h) #上水管
        rect(self.x, self.y - self.gap, self.w, -self.h) #下水管
bird3

要製作水管,就要先討論一下本章最重點的學習內容:物件導向和class。

4.1 物件導向與class

詳見這裡

python
class Pipe(object):
    x = 0
    y = 0
    w = 80  #水管的寬度
    h = 0   #水管的高度
    gap = 80 #兩水管中間的間隔

    def __init__(self):
        self.x = width + self.w  #令水管再後一點一點
        self.y = random(100, height - 100) #下水管的左上角位置
        self.h = height

    def update(self):
        self.x -= panSpeed

    def show(self):
        fill(0, 204, 0)
        rect(self.x, self.y, self.w, self.h) #上水管
        rect(self.x, self.y - self.gap, self.w, -self.h) #下水管

建立一個class來做水管的類別。水管包括有它的x位置,y位置,寬度和高度,還有上下水管的縫隙間距。設定水管一開始在畫面外的右側再多一點點,下水管我們用-self.h令劃水管時的方向不是向下而是向上。

5. 測試gameover

python
birdPos = PVector()  # 鳥的位置
birdVec = PVector()  # 鳥的速度
birdAccel = PVector(0, 0.3)  # 鳥的向下加速度

panSpeed = 5

myPipe = 0


def setup():
    global birdPos, birdVec, myPipe

    size(800, 600)
    frameRate(60)
    birdPos = PVector(50, height/2)
    birdVec = PVector(0, 0)

    myPipe = Pipe()


def draw():
    global birdPos, birdVec

    background("#70C6D5")

    birdVec = birdVec.add(birdAccel)
    birdPos = birdPos.add(birdVec)

    fill("#D5BB06")
    ellipse(birdPos.x, birdPos.y, 25, 25)

    myPipe.update()
    myPipe.show()
    
    if myPipe.collide(birdPos) == True or birdPos.y > height:
        GAMEOVER = True
        println("Game Over")


def keyPressed():
    if (key == ' '):
        birdVec.y = -8
    if (key == 'R' or key == 'r'):
        setup()
        
# =======================Pipe Object=======================================

class Pipe(object):
    x = 0
    y = 0
    w = 80  #水管的寬度
    h = 0   #水管的高度
    gap = 80 #兩水管中間的間隔

    def __init__(self):
        self.x = width + self.w  #令水管再後一點一點
        self.y = random(100, height - 100) #下水管的左上角位置
        self.h = height

    def update(self):
        self.x -= panSpeed

    def show(self):
        fill(0, 204, 0)
        rect(self.x, self.y, self.w, self.h) #上水管
        rect(self.x, self.y - self.gap, self.w, -self.h) #下水管
        
    def collide(self, _birdPos):
        if (_birdPos.x > self.x and _birdPos.x < self.x+self.w):
            if (_birdPos.y > self.y or _birdPos.y < self.y - self.gap):
                return True
image-20221020172152227
python
def collide(self, _birdPos):
    if (_birdPos.x > self.x and _birdPos.x < self.x+self.w):
        if (_birdPos.y > self.y or _birdPos.y < self.y - self.gap):
            return True

Pipe的class中新增一個函數叫collide(),這個函數有1個輸入(self不是輸入是格式),輸入鳥的位置,如果鳥的位置和水管的範圍重疊,則return True

python
if myPipe.collide(birdPos) == True or birdPos.y > height:
	GAMEOVER = True
	println("Game Over")

之後就可以去到draw()的最後,加入這句,如果鳥撞到柱或者y座標大於heightprintln("Game Over")

6. 測試gameover(續)

python
birdPos = PVector()  # 鳥的位置
birdVec = PVector()  # 鳥的速度
birdAccel = PVector(0, 0.3)  # 鳥的向下加速度

panSpeed = 5

myPipe = 0

GAMEOVER = False

def setup():
    global birdPos, birdVec, myPipe, GAMEOVER

    size(800, 600)
    frameRate(60)
    birdPos = PVector(50, height/2)
    birdVec = PVector(0, 0)

    myPipe = Pipe()
    
    GAMEOVER = False


def draw():
    global GAMEOVER
    if (GAMEOVER == False):
        runGame()
    else:
        textSize(64)
        textAlign(CENTER, CENTER)
        fill(255, 255, 0)
        text("GAME OVER", width/2, height/2)

def runGame():
    global birdPos, birdVec, GAMEOVER

    background("#70C6D5")

    birdVec = birdVec.add(birdAccel)
    birdPos = birdPos.add(birdVec)

    fill("#D5BB06")
    ellipse(birdPos.x, birdPos.y, 25, 25)

    myPipe.update()
    myPipe.show()
    
    if myPipe.collide(birdPos) == True or birdPos.y > height:
        GAMEOVER = True
        println("Game Over")

def keyPressed():
    if (key == ' '):
        birdVec.y = -8
    if (key == 'R' or key == 'r'):
        setup()
        
# =======================Pipe Object=======================================

class Pipe(object):
    x = 0
    y = 0
    w = 80  #水管的寬度
    h = 0   #水管的高度
    gap = 80 #兩水管中間的間隔

    def __init__(self):
        self.x = width + self.w  #令水管再後一點一點
        self.y = random(100, height - 100) #下水管的左上角位置
        self.h = height

    def update(self):
        self.x -= panSpeed

    def show(self):
        fill(0, 204, 0)
        rect(self.x, self.y, self.w, self.h) #上水管
        rect(self.x, self.y - self.gap, self.w, -self.h) #下水管
        
    def collide(self, _birdPos):
        if (_birdPos.x > self.x and _birdPos.x < self.x+self.w):
            if (_birdPos.y > self.y or _birdPos.y < self.y - self.gap):
                return True
bird3
python
def runGame():
    global birdPos, birdVec, GAMEOVER

    background("#70C6D5")

    birdVec = birdVec.add(birdAccel)
    birdPos = birdPos.add(birdVec)

    fill("#D5BB06")
    ellipse(birdPos.x, birdPos.y, 25, 25)

    myPipe.update()
    myPipe.show()
    
    if myPipe.collide(birdPos) == True or birdPos.y > height:
        GAMEOVER = True
        println("Game Over")

接著,跟之前的教學一樣,開一個函數叫runGame(),將原本在draw()的內容複製到這個runGame()中。

python
def draw():
    if not GAMEOVER:
        runGame()
    else:
        textSize(64)
        textAlign(CENTER, CENTER)
        fill(255, 255, 0)
        text("GAME OVER", width/2, height/2)

而原本在draw()中,改寫為當GAMEOVERFalse時遊戲才運行,否則gameover的話就在畫面上寫上文字。==記得在最上面開一個新的boolean變數為GAMEOVER,而且在每個需要更新GAMEOVER的函數都要加入global==

7. 新增多條水管

python
birdPos = PVector()  # 鳥的位置
birdVec = PVector()  # 鳥的速度
birdAccel = PVector(0, 0.3)  # 鳥的向下加速度

panSpeed = 5

myPipe = []

GAMEOVER = False

score = 0

def setup():
    global birdPos, birdVec, myPipe, GAMEOVER, score

    size(800, 600)
    frameRate(60)
    birdPos = PVector(50, height/2)
    birdVec = PVector(0, 0)

    myPipe.append(Pipe())
    
    GAMEOVER = False
    
    score = 0

def draw():
    global GAMEOVER
    if (GAMEOVER == False):
        runGame()
    else:
        textSize(64)
        textAlign(CENTER, CENTER)
        fill(255, 255, 0)
        text("GAME OVER", width/2, height/2)

def runGame():
    global birdPos, birdVec, GAMEOVER, score
    
    score += 1
    if (score % 100 == 0):
        myPipe.append(Pipe())

    background("#70C6D5")

    birdVec = birdVec.add(birdAccel)
    birdPos = birdPos.add(birdVec)

    fill("#D5BB06")
    ellipse(birdPos.x, birdPos.y, 25, 25)
    
    for p in myPipe:
        p.update()
        p.show()
        
        if p.collide(birdPos) == True or birdPos.y > height:
            GAMEOVER = True
            println("Game Over")

def keyPressed():
    if (key == ' '):
        birdVec.y = -8
    if (key == 'R' or key == 'r'):
        setup()
        
# =======================Pipe Object=======================================

class Pipe(object):
    x = 0
    y = 0
    w = 80  #水管的寬度
    h = 0   #水管的高度
    gap = 80 #兩水管中間的間隔

    def __init__(self):
        self.x = width + self.w  #令水管再後一點一點
        self.y = random(100, height - 100) #下水管的左上角位置
        self.h = height

    def update(self):
        self.x -= panSpeed

    def show(self):
        fill(0, 204, 0)
        rect(self.x, self.y, self.w, self.h) #上水管
        rect(self.x, self.y - self.gap, self.w, -self.h) #下水管
        
    def collide(self, _birdPos):
        if (_birdPos.x > self.x and _birdPos.x < self.x+self.w):
            if (_birdPos.y > self.y or _birdPos.y < self.y - self.gap):
                return True
image-20221020182007370
python
myPipe = []

GAMEOVER = False

score = 0

將原本最上方宣告區的myPipe由原本宣告成整數,變成宣告成列表。另外新增一個變數叫score來紀錄鳥飛得有多遠。

python
def setup():
    global birdPos, birdVec, myPipe, GAMEOVER, score

    size(800, 600)
    frameRate(60)
    birdPos = PVector(50, height/2)
    birdVec = PVector(0, 0)

    myPipe.append(Pipe())

    GAMEOVER = False

    score = 0

setup()區中,用 myPipe.append(Pipe())將新的Pipe()物件新增到列表中。另外初始化GAMEOVERscore

image-20221020183804359

runGame()中,新增每次運行時都將score加1,用來紀錄鳥飛行了多少影格。if (score % 100 == 0):的意思是score除以100,餘數為0時就執行。即每100影格就新增一條水管。

下面16-21行,跟之前幾乎是一樣的,但新增了一行for p in myPipe:

for常見有兩種格式,一種是for i in range(n):,就是直接告訴程式將以下的程式重覆n次,每次重覆時都會將i加1。而另一種常見的格式是for obj in list:,就是直接重覆list的長度,每次重覆時,obj的內容就是對應的list[i]

image-20221020185335687

即,for obj in list:for i in range(len(list)):是一樣的。

image-20221020182026620

但如果你按下r鍵重設遊戲時,你會發現會有上面的bugs。原因是myPipe這個列表只有新增,沒有減少,也沒有清空。

8. 清空和減少水管列表

PYTHON
birdPos = PVector()  # 鳥的位置
birdVec = PVector()  # 鳥的速度
birdAccel = PVector(0, 0.3)  # 鳥的向下加速度

panSpeed = 5

myPipe = []

GAMEOVER = False

score = 0

def setup():
    global birdPos, birdVec, myPipe, GAMEOVER, score

    size(800, 600)
    frameRate(60)
    birdPos = PVector(50, height/2)
    birdVec = PVector(0, 0)
    
    myPipe = []
    myPipe.append(Pipe())
    
    GAMEOVER = False
    
    score = 0

def draw():
    global GAMEOVER
    if (GAMEOVER == False):
        runGame()
    else:
        textSize(64)
        textAlign(CENTER, CENTER)
        fill(255, 255, 0)
        text("GAME OVER", width/2, height/2)

def runGame():
    global birdPos, birdVec, GAMEOVER, score
    
    score += 1
    if (score % 100 == 0):
        myPipe.append(Pipe())

    background("#70C6D5")

    birdVec = birdVec.add(birdAccel)
    birdPos = birdPos.add(birdVec)

    fill("#D5BB06")
    ellipse(birdPos.x, birdPos.y, 25, 25)
    
    for p in myPipe:
        p.update()
        p.show()
        
        # if p.collide(birdPos) == True or birdPos.y > height:
        #     GAMEOVER = True
        #     println("Game Over")
        
        if (p.x < - p.w):
            myPipe.remove(p)
    
    # delete me after testing=============
    for p in myPipe:
        print(p)
    println(' ')
    #=====================================

def keyPressed():
    if (key == ' '):
        birdVec.y = -8
    if (key == 'R' or key == 'r'):
        setup()
        
# =======================Pipe Object=======================================

class Pipe(object):
    x = 0
    y = 0
    w = 80  #水管的寬度
    h = 0   #水管的高度
    gap = 80 #兩水管中間的間隔

    def __init__(self):
        self.x = width + self.w  #令水管再後一點一點
        self.y = random(100, height - 100) #下水管的左上角位置
        self.h = height

    def update(self):
        self.x -= panSpeed

    def show(self):
        fill(0, 204, 0)
        rect(self.x, self.y, self.w, self.h) #上水管
        rect(self.x, self.y - self.gap, self.w, -self.h) #下水管
        
    def collide(self, _birdPos):
        if (_birdPos.x > self.x and _birdPos.x < self.x+self.w):
            if (_birdPos.y > self.y or _birdPos.y < self.y - self.gap):
                return True
bird4

你可以留意一下console的位置,我將每次runGame()時將所有的myPipe物件的id都列印出來,然後每次都隔一行,你可以見到,每次列印出來時也只有2個id,證明程式現在懂得將超過畫面的Pipe()物件從myPipe列表中移除。

python
myPipe = []

setup()中新增了一句。每次初始化時,都將myPipe的內容清空變成空的列表。

python
def runGame():
    global birdPos, birdVec, GAMEOVER, score

    score += 1
    if (score % 100 == 0):
        myPipe.append(Pipe())

    background("#70C6D5")

    birdVec = birdVec.add(birdAccel)
    birdPos = birdPos.add(birdVec)

    fill("#D5BB06")
    ellipse(birdPos.x, birdPos.y, 25, 25)

    for p in myPipe:
        p.update()
        p.show()

        # if p.collide(birdPos) == True or birdPos.y > height:
        #     GAMEOVER = True
        #     println("Game Over")
        
        if (p.x < - p.w):
            myPipe.remove(p)
    
    # delete me after testing=============
    for p in myPipe:
        print(p)
    println(' ')
    #=====================================

**在runGame()中,20-22行,暫時停止gameover功能方便debug,**之後24-25行新增了當水管已經移出了畫面時,就在myPipe列表中移除當下這個物件。要注意的是,移除一定要在最後才執行,否則移除後下面繼續執行的都是針對下一個id)。

之後就在每次runGame()中,將所有myPipe列表的內容都列印出來,在python中列印一件物件,只會列印其id,這個id是電腦儲存這個物件的暫存記憶體地址。這一段是用來debug的,測試後沒有問題就可以刪掉。

9. 水管計分

python
birdPos = PVector()  # 鳥的位置
birdVec = PVector()  # 鳥的速度
birdAccel = PVector(0, 0.3)  # 鳥的向下加速度

panSpeed = 5

myPipe = []

GAMEOVER = False

score = 0
pipeCount = 0

def setup():
    global birdPos, birdVec, myPipe, GAMEOVER, score, pipeCount

    size(800, 600)
    frameRate(60)
    birdPos = PVector(50, height/2)
    birdVec = PVector(0, 0)
    
    myPipe = []
    myPipe.append(Pipe())
    
    GAMEOVER = False
    
    score = 0
    pipeCount= 0

def draw():
    global GAMEOVER
    if (GAMEOVER == False):
        runGame()
    else:
        textSize(64)
        textAlign(CENTER, CENTER)
        fill(255, 255, 0)
        text("GAME OVER", width/2, height/2)

def runGame():
    global birdPos, birdVec, GAMEOVER, score
    
    score += 1
    if (score % 100 == 0):
        myPipe.append(Pipe())

    background("#70C6D5")

    birdVec = birdVec.add(birdAccel)
    birdPos = birdPos.add(birdVec)

    fill("#D5BB06")
    ellipse(birdPos.x, birdPos.y, 25, 25)
    
    for p in myPipe:
        p.update(birdPos)
        p.show()
        
        # if p.collide(birdPos) == True or birdPos.y > height:
        #     GAMEOVER = True
        #     println("Game Over")
        
        if (p.x < - p.w):
            myPipe.remove(p)
    
    textSize(60)
    fill(255, 255, 0)
    textAlign(CENTER, CENTER)
    text(pipeCount, width/2, 50)

def keyPressed():
    if (key == ' '):
        birdVec.y = -8
    if (key == 'R' or key == 'r'):
        setup()
        
# =======================Pipe Object=======================================

class Pipe(object):
    x = 0
    y = 0
    w = 80  #水管的寬度
    h = 0   #水管的高度
    gap = 80 #兩水管中間的間隔
    isPass = False

    def __init__(self):
        self.x = width + self.w  #令水管再後一點一點
        self.y = random(100, height - 100) #下水管的左上角位置
        self.h = height

    def update(self, _birdPos):
        global pipeCount
        self.x -= panSpeed
        if (self.isPass == False and _birdPos.x > self.x+self.w):
            self.isPass = True
            pipeCount += 1

    def show(self):
        fill(0, 204, 0)
        rect(self.x, self.y, self.w, self.h) #上水管
        rect(self.x, self.y - self.gap, self.w, -self.h) #下水管
        
    def collide(self, _birdPos):
        if (_birdPos.x > self.x and _birdPos.x < self.x+self.w):
            if (_birdPos.y > self.y or _birdPos.y < self.y - self.gap):
                return True
bird5
python
class Pipe(object):
    x = 0
    y = 0
    w = 80  #水管的寬度
    h = 0   #水管的高度
    gap = 80 #兩水管中間的間隔
    isPass = False

    def __init__(self):
        self.x = width + self.w  #令水管再後一點一點
        self.y = random(100, height - 100) #下水管的左上角位置
        self.h = height

    def update(self, _birdPos):
        global pipeCount
        self.x -= panSpeed
        if (self.isPass == False and _birdPos.x > self.x+self.w):
            self.isPass = True
            pipeCount += 1

    def show(self):
        fill(0, 204, 0)
        rect(self.x, self.y, self.w, self.h) #上水管
        rect(self.x, self.y - self.gap, self.w, -self.h) #下水管
        
    def collide(self, _birdPos):
        if (_birdPos.x > self.x and _birdPos.x < self.x+self.w):
            if (_birdPos.y > self.y or _birdPos.y < self.y - self.gap):
                return True

在水管的class當中,加入一個變數isPassed,顧名思義是判斷鳥有否通過了水管。在update()中,加入,如果isPassedFalse而且鳥的x位置大於水管的x位置加水管寬度,就將isPassed設成True並且pipeCount加一。另外你可以留意到, update()函數中間,除了self外,今次多了一個輸入是_birdPos,所以在之後更新runGame(),也要記得更新這runGame()中的b.update()變成b.update(birdPos)

P.S. : 一個編程常用的小技巧,如果你只想某段程式只執行一次,那就可以設定一個boolean變數,例如今次是False,在if的條件中,加入判斷這個變數,當變數是False時而且有其他你想要的特定條件,就執行你想要的內容,並且將這個變數設成True,那麼下次程式再執行時,由於這個變數已經是True了,就不會再執行。例如下面的例子:

image-20221024095738208

10. 考考你

python
# if p.collide(birdPos) == True or birdPos.y > height:
        #     GAMEOVER = True
        #     println("Game Over")

現在你可以將runGame()中,原本暫停了的這兩句重新啟動,遊戲就可以運行了。

  1. 改變變數,遊戲一開始時,水管的縫隙寬一點容易玩一點,例如是160,但當通過的水管數量越來越多,例如每通過5條水管,水管的縫隙就越來越窄,每次減少20,直到縫隙只有60
  2. 將所有有關鳥的變數和函數都打包變成一個class,跟水管的做法一樣,之後有機會教教大家用神經網絡和遺傳演算法,自製一個AI去自動玩這個遊戲。不過第一步,你可以嘗試同一時間有2隻鳥在玩,player1按spacebar來控制,player2按ENTER鍵來控制。

Last updated: