---
# System prepended metadata

title: Pygame 遊戲實作

---

# Pygame 遊戲實作



## Pygame 基礎

### 淺談遊戲

根據維基百科 : 「遊戲既可以指人的一種娛樂活動，也可以指這種活動過程。」
因此要滿足「遊戲」的條件，勢必是「娛樂性佳」的，否則只是操作介面和動畫。
本次課程中，無法讓各位做出非常大型的遊戲 ( 譬如X紀帝國、X世神、Cyberxxxk等 )
但是有了基本的概念，剩下的就僅是想法、美術、和利用各種工具滿足需求了。

### 安裝Pygame
真的很簡單，只需要在終端機上面輸入 : pip install pygame
![](https://i.imgur.com/OR5xnK6.png)

### 第一支程式
```python=
import pygame, sys
from pygame.locals import *

# main
pygame.init()
# 設定視窗大小，可以改變參數設定成喜歡的大小。
DISPLAY = pygame.display.set_mode((800,600))

# 設定標題
pygame.display.set_caption("HELLO")
# 這邊會顯示黑色，可以透過修改參數的方式改成自己喜歡的顏色。
DISPLAY.fill((0,0,0)) 
# 無窮迴圈
while True:
    # 取得所有的Event
    for event in pygame.event.get():
        # 如果event是QUIT，也就是按右上角的x
        if event.type == pygame.QUIT:
            # 將pygame殺掉
            pygame.quit()
            # 終止程式
            sys.exit()
    # 一直更新pygame的畫面
    pygame.display.update()
```
<br>

![](https://i.imgur.com/vPNquVt.png)

好的，恭喜你寫了第一個「遊戲」，~~是不是覺得很快樂呢?~~ 
看到這邊的你們肯定想燒了這份講義，但是不要緊張，一定會教你們怎麼寫能玩的遊戲。

### GUI vs CLI

:::success
在上午的課中，我們介紹了print()函數並在終端機上面看結果，那就是CLI
而真正的遊戲因為有各種圖片及操作，因此可以算是一種GUI
:::

| 比較 | GUI| CLI |
| -------- | -------- | -------- |
| 中文名稱    | 圖形化介面     | 命令列介面     |
| 定義    | 使用各種圖形建立的介面     | 純文字介面     |
| 操作方式    | 點擊、鍵盤     | 只能使用鍵盤     |
| 視覺體驗    | 較豐富且生動     | 只有文字，偏無聊     |
| 對使用者來說    | 較友善且容易操作     | 較不友善且不易操作     |
| 開發難度    | __很難__     | __很難__     |
| 開發時間    | __很長__     | __很長__     |


### Game Loop

![](https://i.imgur.com/YkBh9ZS.png)

大部分的遊戲都是照著上面的 Game Loop 運作。


### 座標
:::success
數學的原點是在中心點。往右x增加，往上y增加。
但是在程式裡面 原點是在「左上角」，且往下y增加。
:::
![](https://i.imgur.com/zR5taRf.gif =100%x)



### 顏色
:::success
遊戲怎麼可能少了顏色呢? 有了好的顏色，可以讓遊戲變得更好看、更熱賣。
:::

在程式設計裡面，顏色是由三原色 ( RGB ) 組成。

常用的顏色


| 顏色 | RGB 
| -------- | --------
| 水藍色    | ( 0, 255, 255 ) 
| 黑色    | ( 0, 0, 0 ) 
| 藍色    | ( 0, 0, 255 ) 
| 紫紅色    | ( 255, 0, 255 )
| 灰色    | ( 128, 128, 128 )
| 紅色    | ( 128, 128, 128 )
| 綠色    | ( 0, 128, 0 )
| 黃色    | ( 255, 255, 0 )
| 白色    | ( 255, 255, 255 )

當然還有更多顏色等著大家去探索。連結 : https://www.peko-step.com/zhtw/tool/tfcolor.html 。

### 事件 (pygame.event)
:::success
在 Pygame 中，每個事件都是存在 pygame.event 裡面。在前面的程式中，我們看到了 for event in pygame.event.get()，這是用來將Pygame裡的每個事件取出來。
:::
以下是常用的 event 種類
#### 退出
pygame.QUIT : 若使用者按下了右上角的叉叉
#### 滑鼠
pygame.MOUSEBUTTONDOWN : 按下滑鼠按鍵
pygame.MOUSEMOTION : 移動滑鼠
pygame.mouse.get_pos() : 抓取滑鼠的位置

#### 鍵盤
pygame.KEYDOWN : 按下鍵盤按鍵
如果有偵測到 pygame.KEYDOWN 就可以進一步偵測是哪個按鍵
以下是 pygame 常用的按鍵

pygame.K_UP : 上
pygame.K_DOWN : 下
pygame.K_w : W 鍵
依此類推


### 基本圖形
:::success
好的遊戲怎麼能缺少圖案呢? 有了圖案，可以增加視覺體驗。
:::
Pygame本身提供了很多圖形，像是多邊形、方型、線段、圓形、橢圓、曲線等

#### 線段
pygame.draw.line( surface, color, ( x1, y1 ), ( x2, y2 ), width = 0 )
surface : 想畫在哪個平面
color : 顏色
x1 跟 y1 : 起始位置的 x 和 y 座標
x2 跟 y2 : 終點位置的 x 和 y 座標
width : 粗度

:::info
補充 : 一次畫出多個連續線段。
pygame.draw.lines( surface, color, closed, points, width )
closed : 是否為封閉圖形
True的話，基本上就跟下面的polygon差不多，只是差在無法做出實心的多邊形。
False的話 就是一組線段，最後一個點並不會連到第一個點。
:::
#### 多邊形 
pygame.draw.polygon( surface, color, points, width=0 )
points : 點座標集合，點越多就可以畫出越多邊 像是 [ ( 146, 0 ), ( 291, 106 ), ( 236, 277 ), ( 56, 277 ), ( 0, 106 ) ] 就可以畫出一個五邊形。
width : ( 可以不用加，預設是0 ) 增加多邊形的粗度。
width > 0 : 空心的多邊形，線段會因為 width 增加而加粗。
width = 0 : 填滿的多邊形。
width < 0 : 什麼都沒有，什麼都看不到。
#### 方形
pygame.draw.rect(surface, color, rect)
rect : Rect物件，可以直接寫( x, y, width, height )
x : 方形左上角的x座標
y : 方形左上角的y座標
width : 方形的長
height : 方形的寬

#### 圓形
pygame.draw.circle( surface, color, center_point, radius, width )
center_point : 中心點
radius : 半徑
width : 請參考多邊形的width

__以上只是舉例幾種，當然還有更多種好玩的圖形等著大家去發掘 ( Google )__

:::info
在實際撰寫遊戲時，當然不可能一個圖形接著一個這樣子慢慢畫，肯定是借助各種圖案以及模型做出複雜的圖形，接下來將會講到圖片。
:::
開始練習畫圖吧!

```python=
import pygame, sys
from pygame.locals import *
pygame.init()

# 設定螢幕大小
DISPLAYSURF = pygame.display.set_mode((500, 400), 0, 32)
pygame.display.set_caption('Drawing')

# 設定顏色
BLACK = (  0,   0,   0)
WHITE = (255, 255, 255)
RED = (255,   0,   0)
GREEN = (  0, 255,   0)
BLUE = (  0,   0, 255)

'''
判斷是否在多邊形內部
原理 : 利用射線法就可以判斷一個點是否在多邊形內部
如果焦點個數為奇數就是在多邊形內 反之則在外面
參考 : https://blog.csdn.net/leviopku/article/details/111224539
'''
def isInPolygon(p,poly):
    # 先取得輸入進來的點 放進去px, py裡面
    px,py = p
    # 先假設 flag 為 False 也就代表在外面
    flag = False
    # 用 i 來當作記錄在該陣列的第幾個位置
    # corner來當作數值
    for i, corner in enumerate(poly):
        j = i + 1 # 下一個點
        if(j>=len(poly)) : 
            j = 0
        # 先取兩個相鄰的點
        x1, y1 = corner
        x2, y2 = poly[j]
        #如果在點上 就直接寫True
        if(x1 == px and y1 == py) or (x2 == px and y2 == py):
            flag = True
            break
        
        # 如果該點在兩端點的y之間
        if(min(y1,y2)<py<=max(y1,y2)):
            # 計算 X
            # X 為 x1去加上 (py - y1) 乘以斜率分之一
            x = x1 + (py-y1) * (x2-x1)/(y2-y1)
            # 如果x在點上 就代表在內部
            if(x==px):
                flag = True
                break
            # 有焦點 讓 flag 變成 flag的相反
            elif x > px:
                flag = not flag
    return flag

# 判斷是否在方形區塊內
def isInRect(p,rect):
    x1, y1 = p
    x2, y2, len, width = rect
    if(x1<x2 or x1 > x2+len) : return False
    elif (y1<y2 or y1>y2+width) : return False
    else: return True

# 定義座標位置
POLYGON = ((146, 0), (291, 106), (236, 277), (56, 277), (0, 106))
RECT = (200, 150, 100, 50)

# 一開始設定為 True 代表每個都會顯示
DISPLAY = [True,True,True,True,True,True,True]
# 開始繪圖吧
def draw():
    DISPLAYSURF.fill(WHITE)

    if (DISPLAY[0]) : pygame.draw.polygon(DISPLAYSURF, GREEN, POLYGON)
    
    if (DISPLAY[1]) : pygame.draw.line(DISPLAYSURF, BLUE, (60, 60), (120, 60), 4)
    
    if (DISPLAY[2]) : pygame.draw.line(DISPLAYSURF, BLUE, (120, 60), (60, 120))
    
    if (DISPLAY[3]) : pygame.draw.line(DISPLAYSURF, BLUE, (60, 120), (120, 120), 4)
    
    if (DISPLAY[4]) : pygame.draw.circle(DISPLAYSURF, BLUE, (300, 50), 20, 0)
    
    if (DISPLAY[5]) : pygame.draw.ellipse(DISPLAYSURF, RED, (300, 250, 40, 80), 1)
    
    if (DISPLAY[6]) : pygame.draw.rect(DISPLAYSURF, RED, RECT)

# 點擊 被點到就設定為False

def click():
    global DISPLAY
    x,y = pygame.mouse.get_pos()
    if isInPolygon((x,y),POLYGON) : DISPLAY[0] = False
    if isInRect((x,y),(300,50,20,20)): DISPLAY[4] = False
    if isInRect((x,y),(300, 250, 40, 80)) : DISPLAY[5] = False
    if isInRect((x,y),RECT): DISPLAY[6] = False
 

def show():
    global DISPLAY
    DISPLAY = [True,True,True,True,True,True,True]

# 執行 Game loop
while True:
    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            sys.exit()
        if event.type == MOUSEBUTTONDOWN:
            click()
        if event.type == pygame.KEYDOWN:
            show()
    # 每次進來都要再重畫一遍 才能看得出特色
    draw()     
    pygame.display.update()
    
```
![](https://i.imgur.com/TZdkTHW.png)


### 匯入圖片&簡單的動畫 
為何遊戲這麼吸引人，當然是因為可以隨著使用者的想法操作動畫，比起傳統的動畫有了更多互動性。
:::success
我們所看到的動畫，在電腦裡面其實只是一張一張圖片隨著時間慢慢地改變位置。
用專業術語講的話，就是禎數，禎數越多代表每秒顯示的圖片數越多。
:::
若要執行以下程式，請先去 http://invpy.com/cat.png 下載貓的圖案，並且跟程式放在同一個資料夾。
```python=
import pygame, sys
from pygame.locals import *

# 初始化
pygame.init()
FPS = 60 # 設定每秒幾禎
fpsClock = pygame.time.Clock() # Clock

# 設定視窗
DISPLAYSURF = pygame.display.set_mode((400,300), 0, 32)
pygame.display.set_caption('Cat Animation')

# 設定一些基本的數值
WHITE = (255, 255, 255)
catImg = pygame.image.load('cat.png')
# 設定貓的x y 軸
catx = 10
caty = 10
direction = 'right' # 一開始的方向

while True: # the main game loop
     DISPLAYSURF.fill(WHITE)
    
     if direction == 'right': # 往右跑
         catx = catx + 5 
         
         if catx > DISPLAYSURF.get_width()-catImg.get_width(): # 如果跑到邊界 就讓貓往下跑
             direction = 'down'

     elif direction == 'down': # 往下跑
         caty = caty + 5
         if caty > DISPLAYSURF.get_height()-catImg.get_height(): # 如果讓貓跑到邊界 就讓貓往左跑
             direction = 'left'

     elif direction == 'left': # 往左跑
         catx = catx - 5
         if catx < 10: 
             direction = 'up'
     elif direction == 'up': # 往上跑
         caty = caty - 5
         if caty < 10:
             direction = 'right'
                
                
     DISPLAYSURF.blit(catImg, (catx, caty)) #繪製覆蓋整個視窗

     # 偵測事件
     for event in pygame.event.get():
         # 按叉叉就退出
         if event.type == QUIT:
             pygame.quit()
             sys.exit()
     #記得更新畫面
     pygame.display.update()
     #並且讓clock tick 一下
     fpsClock.tick(FPS)
```
![](https://i.imgur.com/gyKHN4q.png)
:::info
這個程式有個好玩的地方 : 當你把FPS調得越高，貓移動的速度越快。
:::
:::danger
若把fpsClock.tick(FPS)註解掉，程式就不會暫停，貓移動的速度會根據每台電腦的性能而增加 ( 像我的電腦配備R7+3080，跑特別快 )
:::

#### 匯入圖片:
catImg = pygame.image.load('cat.png')
其中catImg是個surface，透過 load 讀 cat.png 

DISPLAYSURF.blit(catImg,(catx,caty))
catImg就是上面讀的那張圖片
catx跟caty就是圖片呈現的位置(左上角)

:::success
若想要免費的素材庫 可以在 https://craftpix.net/ 中尋找，或者是在Google上搜尋 Game Sprite 即可。
:::

### 文字
:::success
怎麼樣顯示出還有多少時間以及得了多少分? 最直覺的就是用文字表達了。
:::

如果你們很認真看講義的話，搞不好你們會想做一件事 : 嘗試用draw.line()畫出文字，__但這麼做只會把你們逼瘋。__
下面是一個能夠輸出Hello World的一個程式
```python=
import pygame, sys
from pygame.locals import *

pygame.init()
DISPLAYSURF = pygame.display.set_mode((400, 300))
pygame.display.set_caption('Hello World!')

WHITE = (255, 255, 255)
GREEN = (0, 255, 0)
BLACK = (0,0,0)

fontObj = pygame.font.Font('freesansbold.ttf', 32) #創字體的物件
textSurfaceObj = fontObj.render('Hello world!', True, GREEN,BLACK) #創文字Surface
textRectObj = textSurfaceObj.get_rect() #文字方塊
textRectObj.center = (200, 150)

while True: # main game loop
    DISPLAYSURF.fill(WHITE)
    DISPLAYSURF.blit(textSurfaceObj, textRectObj)
    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            sys.exit()
    pygame.display.update()
```
要創文字，需要以下六步驟
1. 創 pygame.font.Font 物件 ( 13行 )
2. 使用 fontObj.render() 創 Surface ( 14行 )
3. 使用 get_rect() 方法 創 Rect物件 ( 15行 )
4. 設定center的位置( 16行 )
5. 使用blit畫出字體
6. 使用 pygame.display.update() 顯示在螢幕上

這樣完美的字體就能呈現了
:::info
很多人會感到疑惑，為何第14行的第二個參數要設成True? 因為當設成True時，pygame會幫我們做出反鋸齒 ( Anti-Aliasing ) ，也就能讓字體變的更柔順，更好看。
當然，如果你要做出那種像素感很重的復古遊戲，可以設定成False。
:::

### 音樂、音效
:::success
好的音樂和音效能夠增加遊戲體驗，及讓遊戲變的更有趣。
:::

```python=
soundObj = pygame.mixer.Sound('fileName.mp3')
soundObj.play()
# 當不要播時 : soundObj.stop()
```

是不是覺得比匯入圖片更簡單了呢?
如果有空的時候，可以多玩玩上述的這些功能。



### 打包遊戲
:::success
為何要學會打包遊戲 ? 直接給使用者原始碼叫他自己編譯不就好了 ?
事實上大錯特錯。回想一下，在安裝這麼多遊戲中，哪次看到原始碼了。通常為了使用者方便以及保護好程式碼，通常是不會讓使用者直接看到程式在寫什麼。所以打包是一件非常重要的事情。
:::
首先先在terminal輸入pip install auto-py-to-exe
![](https://i.imgur.com/WRzNySn.png)
然後再輸入auto-py-to-exe
![](https://i.imgur.com/gTEivgy.png)
:::info
Script Location是放遊戲的main
One Directory / One File 是打包成一個資料夾或者是一個exe檔
Console Window是指要不要顯示CLI ( 如果你們覺得很醜的話就勾Window Based )
:::

完成 :
![](https://i.imgur.com/RJaz6ST.png)
糟糕 怎麼出錯了XD
:::danger
注意 ! 打包完成後如果直接打開，會出現錯誤，因為打包的檔案並不包含上面引入的圖片或者是音樂。
若要正常執行，請將輸出的檔案放回去原本的資料夾。
:::

這樣才是正常的 :
![](https://i.imgur.com/5fjgze4.png)

### 總結
上述的這些功能只佔遊戲設計中的小小一環，要做出功能完整的遊戲需要學習的知識點遠遠不止這些。但當你們掌握今天上午的課程以及上述的這些指令後，就能做出小遊戲了。
今天學習到的概念只是製作遊戲的基礎，如果對於大型遊戲設計感到興趣的話，除了掌握今天所學外，還可以去接觸 Unity 或是 JavaScript，僅僅學習今天的東西還是不夠的XD。

## 正式的遊戲設計

### 打地鼠


![](https://i.imgur.com/9juJztP.png)
(不用你們說，我知道很醜)

資源包 : https://drive.google.com/drive/folders/1DNa6-j81sERV2FgpYOsZsCtFIO49jWmW?usp=sharing

```python=
import pygame
import time
from random import randint
#視窗大小
SCREEN_WIDTH = 400
SCREEN_HEIGHT = 400
#常用顏色
GREEN = (73,188,11)
WHITE = (255,255,255)
#地鼠座標 / 分數 /遊戲時間 / 開始時間 / 狀態
x,y = None,None
score = 0
game_time = 20
start_time = 0
state = 0 #首頁0 遊戲中1 結束2
#建立視窗及頻率鐘
screen = pygame.display.set_mode((SCREEN_WIDTH,SCREEN_HEIGHT))
pygame.display.set_caption('打地鼠')
clock = pygame.time.Clock()
#載入遊戲圖片
mallet = pygame.image.load('mallet.png') 
down_mallet = pygame.image.load('down-mallet.png')
mole = pygame.image.load('mole.png') 
grass = pygame.image.load('grass.png') 
#隱藏滑鼠座標顯示及初始化文字模組
pygame.mouse.set_visible(False)
pygame.font.init()

# FPS
FPS = 60
# 歡迎畫面
def welcome_screen():
    # 全部填滿草
    screen.blit(grass,(0,0))
    # 字體設定
    font = pygame.font.SysFont('corbel',48)
    # 文字設定
    text = font.render('Press ENTER to start',False,WHITE)
    # 畫出文字部分
    screen.blit(text,((SCREEN_WIDTH - text.get_width()) / 2, 185 ) )
    #畫出一個地鼠
    screen.blit(mole,(120,50))
    # 取得槌子的矩形範圍
    mallet_position = mallet.get_rect()
    # 將槌子的中心點設在滑鼠點的位置
    mallet_position.center = pygame.mouse.get_pos()
    if pygame.mouse.get_pressed()[0]:
         screen.blit(down_mallet, mallet_position)
    else:
         screen.blit(mallet, mallet_position)
    
def play():
   # 取用遊戲狀態、分數及開始時間資訊
   global state, score, start_time
   # 設定遊戲開始時間 time.time() 會取得目前的時間
   start_time = time.time()
   # 將分數歸 0 且狀態設定為 1 遊玩中
   score = 0
   state = 1
   # 產生新的老鼠（這邊先立一個函式之後完成）
   new_mole()
   # 產生瞬間先檢查是否有被打到（這邊先立一個函式之後完成）
   whack()
  
def end():
   # 狀態改為 2 結束遊戲
   global state
   state = 2
 
def new_mole():
   # 隨機決定下一個老鼠產生的位置
   global x, y
   # x 從螢幕最左到右邊扣掉老鼠的寬都能取, y 則向下移 30 到底部扣掉老鼠的高都能取
   x = randint(0, SCREEN_WIDTH - mole.get_width())
   y = randint(30, SCREEN_HEIGHT - mole.get_height())
 
# 判斷是否在方形區塊內
def isInRect(p,rect):
    # 先取得點座標
    x1, y1 = p
    # 再取得方形座標
    x2, y2, len, width = rect
    # 如果不在區域內 就 return false
    if(x1<x2 or x1 > x2+len) : return False
    elif (y1<y2 or y1>y2+width) : return False
    # else return true
    else: return True

def whack():
   global score
   # 取得滑鼠當前的位置
   mx, my = pygame.mouse.get_pos()
   # 取得老鼠的寬及高
   width, height = mole.get_size()
   # 將座標計算是不是點擊在老鼠的圖片上, 如果有的話要加分和產生下一隻新的
   if isInRect((mx,my),(x,y,width,height)):
       score += 1
       new_mole()
   
# 遊戲畫面
def play_screen():
    # 畫出草
    screen.blit(grass,(0,0))
    # 字體設定
    font = pygame.font.SysFont('corbel',30)
    # 分數的文字
    text_score = font.render(str(score),False,WHITE)
    # 現在的時間
    current = game_time - (time.time() - start_time)
    # 如果時間結束 就跳到 end
    if current <= 0:
          end()
    # 時間的文字
    text_time = font.render(str(int(current)),False,WHITE)
    # 如果按下了滑鼠
    if pygame.mouse.get_pressed()[0]:
        # 就在鼠標位置顯示下降的槌子
        screen.blit(down_mallet, pygame.mouse.get_pos())
    # 如果不是的話
    else:
        # 就在鼠標的位置顯示一般的槌子
        screen.blit(mallet,pygame.mouse.get_pos())
    # 顯示分數
    screen.blit(text_score,(10,0))
    #顯示時間
    screen.blit(text_time,(370,0))
    # 顯示地鼠
    screen.blit(mole,(x,y))
        
# 結束話面
def end_screen():
    # 背景填滿綠色
    screen.fill(GREEN)
    # 設定字體樣板分別顯示遊戲結束、分數及重新開始按鈕
    font = pygame.font.Font(None, 30)
    game_over = font.render("GAME OVER", False, WHITE)
    font = pygame.font.Font(None, 25)
    # 分數
    points = font.render("Score: " + str(score), False, WHITE)
    font = pygame.font.Font(None, 22)
    restart = font.render("Press ENTER to play again", False, WHITE)
    # 將上述資訊顯示到螢幕上
    screen.blit(game_over, (SCREEN_WIDTH / 2 - game_over.get_width() / 2, 100))
    screen.blit(points, (SCREEN_WIDTH / 2 - points.get_width() / 2, 200))
    screen.blit(restart, (SCREEN_WIDTH / 2 - restart.get_width() / 2, 300))

#遊戲執行
running=True
while running:
    #事件處理
    for event in pygame.event.get():
        #當遊戲視窗被關閉
        if event.type==pygame.QUIT:#當遊戲視窗被關閉
            running=False
        elif state == 0:#遊戲還沒開始的事件
           if event.type == pygame.KEYDOWN and event.key == pygame.K_RETURN:
                    play()
        elif state == 1:#遊戲中的事件
            # 如果按下滑鼠 就打
           if event.type == pygame.MOUSEBUTTONDOWN:
               whack()
        elif state == 2:#遊戲結束的事件
             if event.type == pygame.KEYDOWN and event.key ==pygame.K_RETURN:
                   play()
        if state == 0:#還沒開始的畫面
            welcome_screen()
        elif state == 1:#遊戲中的畫面
            play_screen()
        elif state == 2:#遊戲結束的畫面
            end_screen()
        clock.tick(FPS)#限制畫面最高更新 60 FPS
        pygame.display.update()#更新畫面      
pygame.quit()


```

<!-- 
將new mole
whack
play 拔掉
讓他們寫寫看

資源包 :  -->


參考資源 : 
1. Making Games with Python & Pygame ( https://inventwithpython.com/pygame/ )
2. Python 功力提升的樂趣
