在那Nokia手機風靡全球的年代,因該有不少人玩過手機內建的
貪吃蛇遊戲,記得當時年紀小在還是學生的那個年代就經常利用上電腦課的時候偷偷用我那隻好不容易打工購買的Nokia手機玩這款遊戲,玩到最後還利用電腦課的時間用BASIC寫出了一款簡易版的
貪吃蛇遊戲,所以本次在寫
貪吃蛇教學的過程中讓我回想起那段瘋狂的學生時代,除了回味外現在想想還好遊戲魂還在對寫遊戲還是一樣熱情,希望能一直保持下去,好了廢話不多說讓我們開始本次教學。
依照慣例我們還是先來看看完成後的遊玩影片:
以下會以”A、提案企劃 > B、執行企劃 > C、製作日誌”的順序寫作,各部份負責說明如下:
- A、提案企劃:主要用來紀錄點子,平常想到就可以陸續不斷的寫,等有專案要執行時就從裡面挑出一份合適的企劃來執行。
- B、執行企劃:在確定要執行的提案企劃後,接下來會在針對需要執行的內容作更完整的細節規格與製作規畫,以方便讓製作人員了解到要如何執行此專案。
- C、製作日誌:執行專案時,每位製作人員會將製作時的心得、經驗與規劃方式紀錄下來,以方便往後維護時可以給自己或接手的人參考。
A、提案企劃書
一句話形容這個遊戲
遊戲類型
遊戲特色
發想概念
遊戲玩法
- 控制移動中的長條吃畫面上的數字
- 移動中長條吃下畫面上的數字後,長條數量會增加所吃數字數量
- 玩家必須控制長條避免觸碰到四周牆壁與自己
目標族群
發行平台
預計製作期
美術風格
製作人員需求
收費方式
製作預算
B、執行企劃書
前言
玩法與市面上玩到的貪吃蛇遊戲大同小異,玩家控制不斷前進的貪吃蛇去吃畫面上的數字,每吃一個數字貪吃蛇身長就會增加吃的數字長度,同時分數也會增加,直到貪吃蛇碰到牆壁或自己遊戲就宣告結束。
使用解析度
遊戲流程
遊戲玩法說明
遊戲畫面示意圖
遊戲操作
鍵盤
上下左右鍵:控制貪吃蛇移動方向
D鍵:開啟與關閉除錯訊息
Enter:遊戲結束時重新開始遊戲
Esc鍵:離開遊戲
C、開發日誌
開發工具
安裝套件
在安裝套件前建議先檢查一下pip管理工具並更新,請在命令列輸入以下指令:
python -m pip install --upgrade pip
以下為所需安裝套件:
請在命令列輸入以下指令以進行安裝:
pip install pygame
第2~3天
系統分析與設計
關於繪製圖形
這次使用了微軟釋出的免費等寬字型(Cascadia.ttf),當作字型庫,在字形庫內可以找到⬛圖形,我們遊戲畫面的組成主要會以這個方塊與數字為主。
以下是字型庫下載網址,進入後請下載Cascadia.ttf檔案:
關於二維陣列對應繪圖畫面技巧
這是筆者在製作遊戲時經常使用到的技巧之一,主要原理是宣告一個二維陣列並在此陣列內填入數字,等要繪製圖形到畫面上時就判斷此陣列內的數字並在畫面上相對應的位置繪製圖形。
第4~7天
程式碼說明
主程式
所有
貪吃蛇的邏輯運作與繪圖方法都在這裡執行,請點選以下連接觀看所有程式碼,大部分的運作說明也都在程式碼內,後續筆者會再針對細節部分個別作詳細解說:
# 初始位置.
CONST_STARTING_SNAKE_POS_X = 32
CONST_STARTING_SNAKE_POS_Y = 24
16 ~ 18:設定遊戲區二維陣列大小
# 遊戲區大小.
game_area_width = 64
game_area_height = 48
25 ~ 26:設定遊戲區二維陣列
# 遊戲區陣列.
gameAreaArray =[[0]*game_area_height for i in range(game_area_width)]
# 前進方向.
# 0:上 1:下 2:左 3:右.
direction = 1
# 要吃的節點資料.
# 0:x 1:y 2:產生蛇身數量.
eat_data = [32,24,5]
#-----------------------------------------------------------------
# 函數:亂數產生要吃的數字節點.
#-----------------------------------------------------------------
def randomEatData():
r = True
while r:
# 亂數陣列位置.
eat_data[0] = random.randint(2, 61)
eat_data[1] = random.randint(7, 45)
# 亂數產生增加蛇身數量.
eat_data[2] = random.randint(3, 9)
# 陣列位置為空.
if(gameAreaArray[eat_data[0]][eat_data[1]] == 0):
# 設定產生蛇身數量.
gameAreaArray[eat_data[0]][eat_data[1]] = eat_data[2]
# 離開迴圈.
r = False
此函數會在遊戲區域內亂數產生一個3~9的數字,並會判斷產生的位置不能是在蛇身位置上。
76 ~ 85:此函數處理在畫面上秀字
#-----------------------------------------------------------------
# 函數:秀字.
#-----------------------------------------------------------------
def showFont( text, x, y, color, size):
global canvas
if(size==24):
text = font_24.render(text, True, color)
else:
text = font_40.render(text, True, color)
canvas.blit( text, (x,y))
傳入參數說明:
text:要秀的字串
x, y:字串顯示在畫面上的位置
color:字串顯示的顏色
size:字體大小,傳入24為顯示24x24字體大小,傳入非24顯示為40x40字體大小
87 ~ 107:呼叫此函數後會立即重新開始遊戲
#-----------------------------------------------------------------
# 函數:重新開始遊戲.
#-----------------------------------------------------------------
def resetGame():
global game_mode, score,snake_x, snake_y
# 分數.
score = 0
# 開始位置.
snake_x = CONST_STARTING_SNAKE_POS_X
snake_y = CONST_STARTING_SNAKE_POS_Y
# 蛇身位置串列.
for i in range(snake_body_linkedList.size()-3):
snake_body_linkedList.remove_first()
# 清除畫面陣列.
for y in range(game_area_height):
for x in range(game_area_width):
gameAreaArray[x][y] = 0
# 亂數產生要吃的數字節點.
randomEatData()
# 開始遊戲.
game_mode = 10
程式碼主要是在初始分數、初始貪吃蛇位置、將畫面陣列內容清除為0並亂數產生要吃的數字,最後將狀態設定為開始遊戲(game_mode = 10)。
113 ~ 118:初始pygame、設定視窗Title設定視窗大小
# 初始.
pygame.init()
# 顯示Title.
pygame.display.set_caption(u"貪吃蛇")
# 建立畫佈大小.
canvas = pygame.display.set_mode((canvas_width, canvas_height))
120 ~ 121:建物時脈物件
# 時脈.
clock = pygame.time.Clock()
123 ~ 125:建立兩組字體物件以供遊戲使用
# 設定字型.
font_24 = pygame.font.Font("Fonts/Cascadia.ttf", 24)
font_40 = pygame.font.Font("Fonts/Cascadia.ttf", 40)
這邊分別建立了兩組字體大小的物件供程式使用,字體大小分別為24x24跟40x40。
24x24字體使用在顯示蛇身、蛇要吃的數字與除錯訊息
40x40字體使用畫面左上角顯示的分數與”GAME OVER”訊息
127 ~ 128:在畫面上產生要給貪吃蛇吃的數字
# 亂數產生要吃的數字節點.
randomEatData()
130 ~ 309:遊戲主迴圈
#----------------------------------------------------------------
# 主迴圈.
#----------------------------------------------------------------
running = True
while running:
。
。
。
主迴圈內處理貪吃蛇遊戲的所有輸入、邏輯與繪圖程式運算
135 ~ 136:設定主回圈更新率
# 每秒執行fps次
clock.tick(fps)
這邊設定主迴圈每秒更新8次(fps = 8)
138 ~ 176:處理遊戲中所有輸入處理
#-----------------------------------------------------------------
# 判斷輸入.
#-----------------------------------------------------------------
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_d:
debug_message = not debug_message
# 10:遊戲開始.
if(game_mode == 10):
#-----------------------------------------------------
# 上.
if event.key == pygame.K_UP:
direction = 0
#-----------------------------------------------------
# 下.
elif event.key == pygame.K_DOWN:
direction = 1
#-----------------------------------------------------
# 左.
elif event.key == pygame.K_LEFT:
direction = 2
#-----------------------------------------------------
# 右.
elif event.key == pygame.K_RIGHT:
direction = 3
# 11:Game Over.
elif (game_mode == 11):
if event.key == pygame.K_RETURN:
resetGame()
- 按下Esc按鈕後會將running設定為 False,這將會讓程式離開主迴圈並關閉遊戲視窗
- 按下D按鈕後會反轉debug_message變數以啟動將除錯訊息顯示在畫面上的功能
- 在遊戲模式下(game_mode= 10)判斷玩家按下上下左右按鈕來控制貪吃蛇行進方向
- 在GAME OVER模式下(game_mode= 11)判斷玩家按下Enter按鈕以重新開始遊戲
178 ~ 230: 這區段在處理遊戲邏輯
#-----------------------------------------------------------------
# 邏輯運算.
#-----------------------------------------------------------------
# 10:遊戲開始.
if(game_mode == 10):
。
。
。
183 ~ 192:處理貪吃蛇吃到數字後的邏輯處理
# 判斷吃到數字節點.
if(gameAreaArray[snake_x][snake_y] >= 1 and gameAreaArray[snake_x]
[snake_y] <= 9):
# 加分數.
score += gameAreaArray[snake_x][snake_y]
# 設定要產生的蛇身體數量.
generate_node = gameAreaArray[snake_x][snake_y]
# 清除產生蛇身體數量節點.
gameAreaArray[snake_x][snake_y] = 0
# 亂數產生要吃的數字節點.
randomEatData()
吃到數字後首先會增加分數,然後增加蛇身並清除畫面上的數字,最後在呼叫randomEatData()函數產生新數字在畫面上。
194 ~ 224: 處理貪吃蛇前進相關邏輯處理
# 蛇前進.
if(gameAreaArray[snake_x][snake_y] == 0):
# 設定蛇身體陣列編號.
gameAreaArray[snake_x][snake_y] = 10
# 將節點加入串列鏈節.
snake_body_linkedList.insert_front([snake_x,snake_y])
# 增加節點.
if(generate_node > 0):
generate_node-=1
else:
# 取得尾節點.
p = snake_body_linkedList.fetch(snake_body_linkedList.size()-1)
# 清除尾節點陣列編號.
gameAreaArray[p[0]][p[1]] = 0
# 刪除尾節點.
snake_body_linkedList.remove_last()
# 控制蛇前進方向.
# 0:上.
if (direction == 0):
snake_y -= 1
# 1:下.
elif (direction == 1):
snake_y += 1
# 2:左.
elif (direction == 2):
snake_x -= 1
# 3:右.
elif (direction == 3):
snake_x += 1
一開始先判斷前進的位置是空(陣列編號為0),確定後將前進的位置設定為蛇身(陣列編號為10)
# 設定蛇身體陣列編號.
gameAreaArray[snake_x][snake_y] = 10
並將這個節點的座標加入蛇身串列鏈節頭的位置
# 將節點加入串列鏈節.
snake_body_linkedList.insert_front([snake_x,snake_y])
接下來從蛇身串列鏈節的尾端取出一個座標節點,並清除這個座標節點的蛇身,然後從蛇身串列鏈節內刪除這個座標節點
# 取得尾節點.
p = snake_body_linkedList.fetch(snake_body_linkedList.size()-1)
# 清除尾節點陣列編號.
gameAreaArray[p[0]][p[1]] = 0
# 刪除尾節點.
snake_body_linkedList.remove_last()
以上動作執行完畢後就可以看到畫面上的貪吃蛇前進了一格,接下來在判斷玩家輸入的方向狀態(direction)繼續往前行進,不斷的執行這段程式碼,就會看到貪吃蛇不停在畫面上行走的動畫效果。
225 ~ 230:判斷到貪吃蛇前進的位置不為0
# 失敗.
else:
# 清除產生節點數.
generate_node = 0
# 11:GameOver.
game_mode = 11
貪吃蛇前進的位置如果不為0表示撞到牆壁或自己了,這時要將遊戲狀態設定為GameOver(game_mode = 11)。
232 ~ 309:繪製外框
#-----------------------------------------------------------------
# 繪製畫面.
#-----------------------------------------------------------------
# 清除畫面.
canvas.fill(darkBlock)
# 外框.
for x in range(game_area_width):
if(gameAreaArray[x][3]==0):
gameAreaArray[x][3] = 10
if(gameAreaArray[x][5]==0):
gameAreaArray[x][5] = 10
if(gameAreaArray[x][game_area_height-1]==0):
gameAreaArray[x][game_area_height-1] = 10
for y in range(5,game_area_height):
if(gameAreaArray[0][y]==0):
gameAreaArray[0][y] = 10
if(gameAreaArray[game_area_width-1][y]==0):
gameAreaArray[game_area_width-1][y] = 10
。
。
。
執行以上程式碼後會在畫面上畫出以下結果
252 ~ 253:在畫面左上角顯示得分
# 顯示分數.
showFont( str(score), 14, 0, darkGreen, 40)
255 ~ 302:繪製遊戲區
# 繪製遊戲區.
ix = 15
iy = 2
for y in range(game_area_height):
for x in range(game_area_width):
# 顯示數字-1.
if(gameAreaArray[x][y]==1):
showFont( u"1", ix, iy, darkGreen, 24)
# 顯示數字-2.
elif(gameAreaArray[x][y]==2):
showFont( u"2", ix, iy, darkGreen, 24)
# 顯示數字-3.
elif(gameAreaArray[x][y]==3):
showFont( u"3", ix, iy, darkGreen, 24)
# 顯示數字-4.
elif(gameAreaArray[x][y]==4):
showFont( u"4", ix, iy, darkGreen, 24)
# 顯示數字-5.
elif(gameAreaArray[x][y]==5):
showFont( u"5", ix, iy, darkGreen, 24)
# 顯示數字-6.
elif(gameAreaArray[x][y]==6):
showFont( u"6", ix, iy, darkGreen, 24)
# 顯示數字-7.
elif(gameAreaArray[x][y]==7):
showFont( u"7", ix, iy, darkGreen, 24)
# 顯示數字-8.
elif(gameAreaArray[x][y]==8):
showFont( u"8", ix, iy, darkGreen, 24)
# 顯示數字-9.
elif(gameAreaArray[x][y]==9):
showFont( u"9", ix, iy, darkGreen, 24)
# 方塊.
elif(gameAreaArray[x][y]==10):
showFont( u"⬛", ix, iy, darkGreen, 24)
# 空.
elif(gameAreaArray[x][y]==11):
showFont( u"⠀", ix, iy, darkGreen, 24)
# 除錯.
if(debug_message):
if(gameAreaArray[x][y]!=0):
# 顯示除錯訊息.
showFont( str(gameAreaArray[x][y]), ix, iy, (255, 0, 0), 24)
# 顯示FPS.
showFont( u"FPS:" + str(int(clock.get_fps())), 8, 2, (255, 0, 0), 24)
ix+=12
ix = 15
iy+=12
使用兩個for迴圈尋歷所有遊戲區陣列(gameAreaArray)的內容,並依照陣列位置將數字或方塊繪製到畫面上。
同時也會判斷如果除錯變數(debug_message)開啟,就在畫面上顯示除錯訊息。
304 ~ 306:顯示GameOver
# 顯示 GameOver.
if(game_mode == 11):
showFont( u"GAME OVER", 300, 280, darkGreen, 40)
在GameOver模式下(game_mode = 11)在畫面中間顯示GAME OVER字串
308 ~ 309:這邊會依照我們設定的FPS(clock.tick(fps))更新畫面
# 更新畫面.
pygame.display.update()
311 ~ 313:結束程式
# 離開遊戲.
pygame.quit()
quit()
在按下Esc按鈕後離開遊戲主迴圈就會進入這邊執行結束程式的動作
關於串列鏈節(doublyLinkedList.py)
本次使用到網路大神寫的串列鏈節模組,使用方法建議大家可以直接連到大神的教學頁面,裡面有這個模組的詳細解說。。
執行遊戲
- 請在命令列下輸入python play.py 以執行遊戲
GitHub下載原始碼
後記
呼~~終於又完成了一篇,感覺整個腦袋都快被榨乾了,寫遊戲一直是筆者的休閒興趣之一,所以一有空就會動動手寫一些小遊戲來玩,順道練練程式功力,接下來也會繼續保持這個習慣,下一款小遊戲教學也正在構思中,希望不會讓大家等太久。