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