這就是陣列元素對應畫面技巧,只要依照在二維陣列內填入代表各種圖形的數字,然後繪圖的時候在判斷二維陣列內的數字將相對應到的字元顯示在畫面上就可以了,這也是本次遊戲教學在顯示畫面時所使用到的技巧。
主程式
所有Blockade(封鎖線)的邏輯運作與繪圖方法都在這邊執行,已將程式碼放置GitHub Gist上,點選以下連結查看:
程式碼內已寫上詳細說明,後面再針對細節作個別解說:
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(封鎖線)變形而來的這個冷知識,希望大家會喜歡,本次教學也到這邊結束囉,接下來敬請期待下篇的貪吃蛇教學。