【Python】那些年我們一起玩過的遊戲(四)-貪吃蛇(上)

channel
24 min readMay 13, 2020

--

Blockade(封鎖線)這款你可能不知道的貪吃蛇類史祖,問世於1976年街機平台上,其可兩人同時對戰的設計與簡約卻又不簡單的遊戲模式,在當時還真讓不少人為之瘋狂,這也是我們本次教學所要致敬的對象Blockade(封鎖線),希望在完成遊戲後也能讓您感受到70年代人們對此遊戲的瘋狂,廢話不多說以下開始本次教學。

依照慣例我們還是先來看看完成後的遊戲影片:

以下會以”A、提案企劃 > B、執行企劃 > C、製作日誌”的順序寫作,各部份代表的分工如下:

A、提案企劃:主要用來紀錄點子,平常想到就可以陸續不斷的寫,等有專案要執行時就從中挑選出一份合適的企劃來執行。

B、執行企劃:在確定要執行的提案企劃後,接下來會在針對需要執行的內容作更完整的細節規格與製作規畫,以方便讓製作人員了解到要如何執行此專案。

C、製作日誌執行專案時,每位製作人員會將製作時的心得、經驗與規劃方式紀錄下來,以方便往後維護時可以給自己或接手的人參考。

A、提案企劃

一句話形容這個遊戲

  • 闔家娛樂的對戰遊戲

遊戲類型

  • 休閒類

遊戲特色

  • 對戰
  • 闔家娛樂

發想概念

  • 藉由重製Blockade(封鎖線)遊戲學習Python語言

遊戲玩法

  • 需兩人同時一起遊玩
  • 控制不斷前進與的長條並阻擋對手繼續前進

目標族群

  • 用Python學遊戲

發行平台

  • Windows
  • MAC
  • Linux

預計製作期

  • 7天

美術風格

  • 復古字元風格

遊戲周邊

  • 復古遊戲機

製作人員需求

  • 企劃 x 1
  • 美術 x 1
  • 程式 x 1

收費方式

  • 佛家弟子不談錢談緣,什麼是緣。。。50元(好冷)

製作預算

  • 繼續燒熱血當預算

參考資料

B、執行企劃

前言

本企劃的重點依然還是以重製Blockade(封鎖線)遊戲來學習使用Python語言,希望讓您能在學習程式語言同時也能了解到遊戲的運作原理。

使用解析度

  • 800 x 600

遊戲玩法

遊戲流程
遊戲畫面示意圖
細節說明
  • 畫面上箭頭會持續朝前方移動
  • 箭頭每移動一格線段就會增加一格
  • 在箭頭移動中玩家可操控左右上下轉向
  • 箭頭碰到我方或對方線段就算失敗,失敗後對方分數加1
  • 雙方任一方先達到分數6遊戲結束

遊戲操作

鍵盤

Esc:結束遊戲

Enter:開始遊戲

P:開啟/關閉遊戲除錯模式

W:1P上移動

A:1P左移動

S:1P下移動

D:1P右移動

上:2P上移動

左:2P左移動

下:2P下移動

右:2P右移動

美術元件列表

  • 字元”▨”,編碼1
  • 字元”▦”,編碼2
  • 字元”⠀”,編碼3
  • 字元”6”,編碼6
  • 字元”▴”,編碼10
  • 字元”▾”,編碼11
  • 字元”◂”,編碼12
  • 字元”▸”,編碼13
  • 字元”▵”,編碼20
  • 字元”▿”,編碼21
  • 字元”◃”,編碼22
  • 字元”▹”,編碼23
C、開發日誌

開發工具

使用語言與版本

Python 3.7.0

安裝套件

在安裝套件前建議先檢查一下pip管理工具並更新,請在命令列輸入以下指令:

python -m pip install --upgrade pip

以下為所需安裝套件:

  • PyGame - 遊戲開發套件

請在命令列輸入以下指令以進行安裝:

pip install pygame

第2~3天

系統分析與設計

關於文字模式

經歷過DOS時代的前輩們都非常熟習這個模式,在這模式下只能使用系統提供的ASCII碼來作畫面顯示,其實在Windows上也可以體驗文字模式,只要開啟 ”命令提示字元”程式。

言歸正傳提到文字模式主要是想說明本次的教學我們會以摸擬文字模式的風格來繪製遊戲畫面,也就是說我們遊戲的畫面繪製會把”字元”當作圖形來顯示,這樣會讓畫面的感覺更趨近街機版的Blockade(封鎖線),接下來我們需要先選一套裡面有本次遊戲所需用到的圖案字型庫,在這邊我選用了微軟免費等寬字型(Cascadia Code)來當主要的字型庫,以下是字型庫下載網址,進入後請下載Cascadia.ttf檔案:

下載完畢後在Cascadia.ttf上快速點擊兩下,會出現以下視窗:

點擊”安裝”將此字形檔安裝進您的Windows系統,並且打開字元對應表程式:

將字形選到”Cascadia Code”,這些字元裡面的某些部分稍後會被我們選作遊戲的基本圖形使用,這在稍後的遊戲程式說明裡會在進一步解說。

關於陣列元素對應畫面技巧

何謂陣列元素對應畫面技巧?我們實際舉個例子來說明,會比較容易理解, 首先我們設定8x8二維陣列如下:

接下來在二維陣列內填入數字1如下:

然後判斷二維陣列只要填入1的地方就在畫面相對位置上顯示字元■,完成後就可以看到如下圖案:

這就是陣列元素對應畫面技巧,只要依照在二維陣列內填入代表各種圖形的數字,然後繪圖的時候在判斷二維陣列內的數字將相對應到的字元顯示在畫面上就可以了,這也是本次遊戲教學在顯示畫面時所使用到的技巧。

其實這技巧也常被用在PRG遊戲的地圖顯示上,這等以後有機會分享RPG類遊戲教學時在詳細說明。

第4~7天

程式碼說明

主程式

所有Blockade(封鎖線)的邏輯運作與繪圖方法都在這邊執行,以下先列出所有程式碼,大部分的運作說明也都在程式碼內,後續我們會再針對細節在個別作詳細解說:

19 ~ 21:宣告字元遊戲區大小變數

# 遊戲區大小. 
game_area_width = 64
game_area_height = 48

27 ~ 28:設定字元遊戲區二維陣列

# 遊戲區陣列. 
gameAreaArray =[[0]*game_area_height for i in range(game_area_width)]

設定後就可以使用如gameAreaArray[0][0] = 1 來設定字元編號

75 ~ 81:秀字函數

# — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — # 函數:秀字. 
# — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — def showFont( text, x, y, color):
global canvas
text = font_24.render(text, True, color)
canvas.blit( text, (x,y))

此函數是本次遊戲畫面顯示的重點,前面解說過會以字元來顯示畫面,所以都是通過此函數來將要顯示的字元或文字顯示在畫面上。

83 ~ 114:判斷邊界-1P

# — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — # 函數:判斷邊界-1P. 
# — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — def ifBoundary1P(direction):
global gameAreaArray, game1P_x, game1P_y, playFial, score2P
# 判斷邊界.
if(game1P_x > game_area_width-2):
game1P_x = game_area_width-1
elif(game1P_x < 1):
game1P_x = 0
if(game1P_y > game_area_height-2):
game1P_y = game_area_height-1
elif(game1P_y < 1):
game1P_y = 0

# 設定失敗.
if(gameAreaArray[game1P_x][game1P_y] != 0):
playFial = 1
gameAreaArray[game1P_x][game1P_y] = 2
# 加對方分數.
score2P += 1
else:
# 箭頭.
if(direction==0):
gameAreaArray[game1P_x][game1P_y] = 10
elif(direction==1):
gameAreaArray[game1P_x][game1P_y] = 11
elif(direction==2):
gameAreaArray[game1P_x][game1P_y] = 12
elif(direction==3):
gameAreaArray[game1P_x][game1P_y] = 13

以上是此函數的所有程式碼,以下在針對程式細節一一解說。

# 判斷邊界. 
if(game1P_x > game_area_width-2):
game1P_x = game_area_width-1
elif(game1P_x < 1):
game1P_x = 0
if(game1P_y > game_area_height-2):
game1P_y = game_area_height-1
elif(game1P_y < 1):
game1P_y = 0

這邊在判斷1P玩家的箭頭是否移動到超出邊界,是的話會將變數修正再邊界上。

# 設定失敗. 
if(gameAreaArray[game1P_x][game1P_y] != 0):
playFial = 1
gameAreaArray[game1P_x][game1P_y] = 2
# 加對方分數.
score2P += 1
else:
# 箭頭.
if(direction==0):
gameAreaArray[game1P_x][game1P_y] = 10
elif(direction==1):
gameAreaArray[game1P_x][game1P_y] = 11
elif(direction==2):
gameAreaArray[game1P_x][game1P_y] = 12
elif(direction==3):
gameAreaArray[game1P_x][game1P_y] = 13

以上程式碼上半是在判斷1P是否碰上障礙物,只要判斷要前進的陣列位置內容是否不為0就表示發生碰撞了,如果要前進的陣列位置內容為0表示尚未發生碰撞,這時會進入程式碼下半段,這邊是將要前進的位置改成箭頭編號。

116 ~ 147:判斷邊界-2P

判斷邊界2P的運作原理與判斷邊界1P相同,只是將1P的相關變數改成2P的相關變數。

149 ~ 174:重新開始遊戲,初始遊戲變數

176 ~ 453:主程式區段

180 ~200:初始pygame與遊戲變數

# 初始. 
pygame.init()
# 顯示Title.
pygame.display.set_caption(u”封鎖線遊戲”)
# 建立畫佈大小.
canvas = pygame.display.set_mode((canvas_width, canvas_height))
# 時脈.
clock = pygame.time.Clock() # 設定字型.
font_24 = pygame.font.Font("Fonts/Cascadia.ttf", 24) # 重新開始遊戲.
restart()
# 1P位置.
gameAreaArray[game1P_x][game1P_y] = 11
# 2P位置.
gameAreaArray[game2P_x][game2P_y] = 20
# 設定字型.
font_24 = pygame.font.Font(“Fonts/Cascadia.ttf”, 24)
# 重新開始遊戲.
restart()
# 1P位置.
gameAreaArray[game1P_x][game1P_y] = 11
# 2P位置.
gameAreaArray[game2P_x][game2P_y] = 20

主要是初始pygame基本設定,如設定Title、設定畫佈大小、時脈、設定字型,還有重新設定遊戲變數跟1P與2P的初始位置。

202 ~449:主迴圈

210 ~273:判斷輸入

# — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — # 判斷輸入. 
# — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — for event in pygame.event.get():
# 離開遊戲.
if event.type == pygame.QUIT:
running = False
# 判斷按下按鈕
if event.type == pygame.KEYDOWN:
# 判斷按下ESC按鈕
if event.key == pygame.K_ESCAPE:
running = False
# 除錯訊息開關.
elif event.key == pygame.K_p:
debug_message = not debug_message
# 0:遊戲結束.
if gameMode == 0:
# 開始遊戲.
if event.key == pygame.K_RETURN:
# 初始分數.
score1P = 0
score2P = 0
# 重新開始遊戲.
restartTime = 0
restart()
# 設定開始遊戲.
gameMode = 1

以上程式碼主要是在判斷玩家按下Esc按鈕後處理結束遊戲、按下P按鈕後顯示除錯訊息與按下Enter按鈕開始遊戲。

    # 1:遊戲中. 
elif gameMode == 1:
# — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
# 1P-上.
if event.key == pygame.K_w:
game1P_direction = 0
# — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
# 1P-下.
elif event.key == pygame.K_s:
game1P_direction = 1
# — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
# 1P-左.
elif event.key == pygame.K_a:
game1P_direction = 2
# — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
# 1P-右.
elif event.key == pygame.K_d:
game1P_direction = 3
# — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
# 2P-上.
if event.key == pygame.K_UP:
game2P_direction = 0
# — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
# 2P-下.
elif event.key == pygame.K_DOWN:
game2P_direction = 1
# — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
# 2P-左.
elif event.key == pygame.K_LEFT:
game2P_direction = 2
# — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
# 2P-右.
elif event.key == pygame.K_RIGHT:
game2P_direction = 3

這段程式碼主要處理遊戲中判斷1P與2P的控制箭頭方向判斷(控制按鍵請參考上方遊戲操作說明)。

281 ~293:遊戲結束畫面顯示

# — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — 
# 遊戲模式.
# 0:遊戲結束.
if gameMode == 0:
# 清除顯示區.
for y in range(18,24):
for x in range(25,40):
gameAreaArray[x][y] = 3
# 顯示GameOver與雙方分數.
showFont( “GAME”, 380, 240, green)
showFont( “OVER”, 380, 262, green)
showFont( str(score1P), 340, 262, green)
showFont( str(score2P), 460, 262, green)

gameMode=0表示遊戲結束狀態,接下來是先用迴圈把中間區域清空,然後顯示遊戲結束訊息與雙方分數。

301 ~340:1P、2P箭頭移動

# 還沒分出勝負. 
if (playFial == 0):
if(move):
# 1P位置.
gameAreaArray[game1P_x][game1P_y] = 1
# 1P前進方向.
# 0:上.
if (game1P_direction == 0):
game1P_y -= 1
# 1:下.
elif (game1P_direction == 1):
game1P_y += 1
# 2:左.
elif (game1P_direction == 2):
game1P_x -= 1
# 3:右.
elif (game1P_direction == 3):
game1P_x += 1
# 1P判斷邊界.
ifBoundary1P(game1P_direction)
else:
# 2P位置.
gameAreaArray[game2P_x][game2P_y] = 1
# 2P前進方向.
# 0:上.
if (game2P_direction == 0):
game2P_y -= 1
# 1:下.
elif (game2P_direction == 1):
game2P_y += 1
# 2:左.
elif (game2P_direction == 2):
game2P_x -= 1
# 3:右.
elif (game2P_direction == 3):
game2P_x += 1
# 1P判斷邊界.
ifBoundary2P(game2P_direction)
move = not move

在變數playFial=0時表示遊戲尚未分出勝負,程式會繼續讓1P或2P往箭頭前方前進,其中1P的前進方向會判斷game1P_direction變數內容( 0:上 1:下 2:左 3:右.),處理加減陣列元素位置變數game1P_x、game1P_y,2P前進方向會判斷game2P_direction變數內容( 0:上 1:下 2:左 3:右.),處理加減陣列元素位置變數game2P_x、game2P_y,然後再看到move變數,主要是讓1P與2P同時間只能有一個移動,也就以1P移動一步 →2P移動一步→1P移動一步 →2P移動一步如此重複移動直到玩家箭頭發生碰撞。

342 ~366:分出勝負後處理

# 分出勝負. 
else:
# 1P失敗,閃爍失敗處.
if(playFial==1):
if(gameAreaArray[game1P_x][game1P_y]==2):
gameAreaArray[game1P_x][game1P_y] = 3
else:
gameAreaArray[game1P_x][game1P_y] = 2
# 2P失敗,閃爍失敗處.
elif(playFial==2):
if(gameAreaArray[game2P_x][game2P_y]==2):
gameAreaArray[game2P_x][game2P_y] = 3
else:
gameAreaArray[game2P_x][game2P_y] = 2
# 重新開始遊戲時間.
restartTime += 1
if((restartTime / fps) == 3):
# 判斷遊戲結束.
if(score1P >=6 or score2P >= 6):
# 設定遊戲結束.
gameMode = 0
else:
restartTime = 0
restart() # 重新開始遊戲.

變數playFial等於1代表1P失敗,然後在碰撞陣列位置輪流設定圖形編號3與2,這樣等繪製畫面時會在畫面上呈現閃爍的失敗點,變數playFial等於 2代表2P失敗,程式同樣會輪流顯示圖形編號3與2,然後等待restartTime變數累加到24(算法為fps變數預設是8,24/8=3)的時候就進入判斷遊戲結束狀態,進入後如果1P分數(score1P)與2P分數(score2P)其中一個分數大於6表示分出勝負了,所以將變數gameMode狀態設定為0讓程式去顯示遊戲結束畫面,反之就初始變數重新開始遊戲。

368 ~378:在畫面上繪製閃爍的失敗點與分數

# 在畫面上繪製閃爍的失敗點與分數
if (playFial > 0):
# 1P失敗,2P閃爍分數.
if(playFial==1):
if(gameAreaArray[game1P_x][game1P_y]==2):
showFont( str(score2P), ((CONST_STARTING_2P_POS_X)*12)+15, ((CONST_STARTING_2P_POS_Y + 2)*12), green)
showFont( str(score1P), ((CONST_STARTING_1P_POS_X)*12)+15, ((CONST_STARTING_1P_POS_Y — 2)*12), green)
# 2P失敗,1P閃爍分數.
elif(playFial==2):
if(gameAreaArray[game2P_x][game2P_y]==2):
showFont( str(score1P), ((CONST_STARTING_1P_POS_X)*12)+15, ((CONST_STARTING_1P_POS_Y — 2)*12), green)
showFont( str(score2P), ((CONST_STARTING_2P_POS_X)*12)+15, ((CONST_STARTING_2P_POS_Y + 2)*12), green)

這區塊的程式碼在處理將失敗點與分數繪製在畫面上,並進行閃爍。

380~392:設定外框編號

# — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — 
# 設定外框.
gameAreaArray[32][0]=6
for x in range(game_area_width):
if(gameAreaArray[x][0]==0):
gameAreaArray[x][0] = 1
if(gameAreaArray[x][game_area_height-1]==0):
gameAreaArray[x][game_area_height-1] = 1
for y in range(game_area_height):
if(gameAreaArray[0][y]==0):
gameAreaArray[0][y] = 1
if(gameAreaArray[game_area_width-1][y]==0):
gameAreaArray[game_area_width-1][y] = 1

以上程式碼主要在陣列內設定外框編號。

394~446:繪製遊戲畫面

# — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — 
# 繪製遊戲區.
ix = 15
iy = 2
for y in range(game_area_height):
for x in range(game_area_width):
# 方塊.
if(gameAreaArray[x][y]==1):
showFont( u”▨”, ix, iy, green)
# 死亡.
elif(gameAreaArray[x][y]==2):
showFont( u”▦”, ix, iy, green)
# 空白.
elif(gameAreaArray[x][y]==3):
showFont( u”⠀”, ix, iy, green)
# 6
elif(gameAreaArray[x][y]==6):
showFont( u”6", ix, iy, green)
# 1p-上箭頭.
elif(gameAreaArray[x][y]==10):
showFont( u”▴”, ix, iy, green)
# 1p-下箭頭.
elif(gameAreaArray[x][y]==11):
showFont( u”▾”, ix, iy, green)
# 1p-左箭頭.
elif(gameAreaArray[x][y]==12):
showFont( u”◂”, ix, iy, green)
# 1p-右箭頭.
elif(gameAreaArray[x][y]==13):
showFont( u”▸”, ix, iy, green)
# 2p-上箭頭.
elif(gameAreaArray[x][y]==20):
showFont( u”▵”, ix, iy, green)
# 2p-下箭頭.
elif(gameAreaArray[x][y]==21):
showFont( u”▿”, ix, iy, green)
# 2p-左箭頭.
elif(gameAreaArray[x][y]==22):
showFont( u”◃”, ix, iy, green)
# 2p-右箭頭.
elif(gameAreaArray[x][y]==23):
showFont( u”▹”, ix, iy, green)
# 除錯.
if(debug_message):
if(gameAreaArray[x][y]!=0):
# 顯示陣列編碼.
showFont( str(gameAreaArray[x][y]), ix, iy, (255, 0, 0))
# 顯示FPS.
showFont( u”FPS:” + str(int(clock.get_fps())), 8, 2, (255, 255, 255))
ix+=12
ix = 15
iy+=12

這邊使用兩個迴圈來判斷gameAreaArray二維陣列內的編號,然後在依照編號所代表的字元圖形繪製到畫面上,各字元編號所代表的圖形請參考上面美術元件列表。

436~442:除錯訊息

# 除錯. 
if(debug_message):
if(gameAreaArray[x][y]!=0):
# 顯示陣列編碼.
showFont( str(gameAreaArray[x][y]), ix, iy, (255, 0, 0))
# 顯示FPS.
showFont( u”FPS:” + str(int(clock.get_fps())), 8, 2, (255, 255, 255))

在按下P按鍵後可以顯示除錯訊息,這邊會將gameAreaArray二維陣列內的數字編號顯示在畫面上的相對位置,以方便我們查看gameAreaArray二維陣列內的編號與對應的字元圖形是否正確。

執行遊戲

  • 請在命令列下輸入python play.py 以執行遊戲

GitHub下載原始碼

後記

這次貪吃蛇系列很罕見的分成上下兩期來寫,主要是因為在找貪吃蛇相關資料的時候,發現原來貪吃蛇的玩法是從Blockade(封鎖線)這款遊戲變形而來,並且在看了Blockade(封鎖線)遊玩影片後發現其封閉式的雙打體驗還滿吸引我的,所以就決定上篇先以致敬Blockade(封鎖線)為開頭,下篇再將大家帶入主題貪吃蛇,順便也讓大家了解到原來貪吃蛇是從Blockade(封鎖線)變形而來的這個冷知識,希望大家會喜歡,本次教學也到這邊結束囉,接下來敬請期待下篇的貪吃蛇教學。

  • 如果您喜歡本篇文章請幫忙按讚請作者喝杯咖啡
  • 作者其他文章
  • Facebook紛絲團

--

--

No responses yet