# 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 功力提升的樂趣