發(fā)實(shí)戰(zhàn):用pygame鞏固編程基礎(chǔ),打造第一款小游戲)
用Python寫(xiě)一個(gè)貪吃蛇小游戲最大的價(jià)值不是把游戲做完而是把Python的基礎(chǔ)語(yǔ)法串起來(lái)跑一遍。輸入處理、列表操作、條件判斷、循環(huán)控制、函數(shù)封裝這些平時(shí)單獨(dú)寫(xiě)很容易懂但一旦放進(jìn)一個(gè)不斷變化的游戲循環(huán)里很多隱藏問(wèn)題就會(huì)冒出來(lái)。這個(gè)練手項(xiàng)目尤其適合剛學(xué)完P(guān)ython基礎(chǔ)、想找一個(gè)能完整運(yùn)行的小項(xiàng)目的人。我最近又按這個(gè)思路用pygame把貪吃蛇重新寫(xiě)了一遍結(jié)論是只要環(huán)境裝好按“窗口創(chuàng)建 - 蛇移動(dòng) - 食物生成 - 碰撞判斷”的順序拆解新手也能在幾個(gè)小時(shí)內(nèi)跑出第一版。下面直接進(jìn)入正題。我會(huì)先說(shuō)明這個(gè)項(xiàng)目到底在練什么再給環(huán)境準(zhǔn)備和完整源碼最后把常見(jiàn)的報(bào)錯(cuò)、邊界和擴(kuò)展思路一起整理出來(lái)。如果你只是想復(fù)制代碼可以直接跳到第四節(jié)如果你想理解每一步為什么這么寫(xiě)建議從頭順序看。1. 先搞懂這個(gè)練手項(xiàng)目到底在練什么1.1 它適合哪類Python學(xué)習(xí)者這個(gè)項(xiàng)目適合的并不是完全沒(méi)寫(xiě)過(guò)代碼的人而是已經(jīng)認(rèn)識(shí)if、for、while、列表和函數(shù)的讀者。如果這些概念還沒(méi)接觸過(guò)我建議先花半天把基礎(chǔ)語(yǔ)法過(guò)一遍否則看代碼時(shí)會(huì)卡在“為什么這里用列表存蛇身”這類問(wèn)題上。貪吃蛇的代碼量不大一套完整可運(yùn)行版本通常也就一兩百行。但它的邏輯鏈路比普通練習(xí)復(fù)雜很多窗口要持續(xù)刷新鍵盤要實(shí)時(shí)響應(yīng)蛇的位置每幀都要更新食物消失后要重新生成。這些東西組合在一起正好能把零散的語(yǔ)法點(diǎn)變成一套完整程序。更重要的是它能讓初學(xué)者感受到“寫(xiě)小游戲”和“寫(xiě)腳本”的區(qū)別。腳本往往是順序執(zhí)行一遍就結(jié)束游戲需要在一個(gè)while循環(huán)里反復(fù)處理輸入、更新?tīng)顟B(tài)、繪制畫(huà)面。這種循環(huán)思想是很多圖形程序和游戲程序的基礎(chǔ)。1.2 一次游戲要用到的核心語(yǔ)法點(diǎn)先列一下這個(gè)項(xiàng)目里會(huì)反復(fù)用到的內(nèi)容列表蛇身用列表存儲(chǔ)蛇頭是第一個(gè)元素蛇尾是最后一個(gè)元素。元組坐標(biāo)用(x, y)表示字典用方向鍵映射方向向量。while循環(huán)游戲主循環(huán)每一幀執(zhí)行一次更新。for循環(huán)遍歷蛇身繪制矩形或者遍歷事件列表。if / elif / else判斷鍵盤事件、判斷是否撞墻、是否吃到食物。函數(shù)把初始化、生成食物、繪制、檢查碰撞分別封裝成函數(shù)。模塊導(dǎo)入import pygame、import random。常量定義窗口大小、格子大小、顏色、游戲速度用大寫(xiě)常量統(tǒng)一管理。這些點(diǎn)覆蓋了Python基礎(chǔ)中最常用的一部分。把它們放進(jìn)一個(gè)項(xiàng)目的上下文里理解比單獨(dú)背語(yǔ)法記得更牢。1.3 為什么游戲比打印九九乘法表更能鞏固基礎(chǔ)打印九九乘法表確實(shí)能練習(xí)循環(huán)但它的缺點(diǎn)是無(wú)論循環(huán)多少次外部狀態(tài)幾乎不變。貪吃蛇不一樣蛇身列表會(huì)越來(lái)越長(zhǎng)食物的位置會(huì)不斷變化游戲速度會(huì)隨著得分提高。每一步都要關(guān)心“當(dāng)前狀態(tài)是什么”和“狀態(tài)如何遷移”這其實(shí)是編程里非常重要的思維訓(xùn)練。另外游戲項(xiàng)目天然有可視化反饋。代碼出錯(cuò)時(shí)你能直觀看到畫(huà)面是否正常、蛇是否亂跑、碰撞是否誤判。這種即時(shí)反饋能大大提高調(diào)試效率也能讓人更有動(dòng)力繼續(xù)改下去。對(duì)新手來(lái)說(shuō)能看到自己寫(xiě)的程序變成一個(gè)能動(dòng)的小游戲比起單純?cè)诮K端里打印結(jié)果要有成就感得多。2. 搭建運(yùn)行環(huán)境Python和pygame缺一不可2.1 檢查Python版本和pip命令我默認(rèn)你已經(jīng)裝好了Python。判斷方式很簡(jiǎn)單打開(kāi)終端或命令行輸入python --version如果提示不是內(nèi)部或外部命令再試python3 --versionWindows下一般用pythonmacOS和Linux下可能要用python3。安裝Python時(shí)建議勾選“Add Python to PATH”否則后面在命令行里找不到python命令會(huì)很麻煩。接著檢查pip是否可用pip --version如果pip也沒(méi)找到可以試python -m pip --version。這一步的目的不是做表面檢查而是避免后面安裝pygame時(shí)出現(xiàn)“找不到pip”或“把包裝到了另一個(gè)Python環(huán)境”這種問(wèn)題。2.2 安裝pygame并確認(rèn)導(dǎo)入成功貪吃蛇最常用的圖形庫(kù)是pygame。它專門用來(lái)做2D小游戲已經(jīng)封裝了窗口、事件、圖片和聲音等常用功能。安裝命令很簡(jiǎn)單pip install pygame或者使用Python模塊方式python -m pip install pygame安裝完成后在Python交互環(huán)境或腳本里檢查import pygame print(pygame.ver)如果打印出版本號(hào)說(shuō)明安裝成功。如果報(bào)ModuleNotFoundError先確認(rèn)你執(zhí)行python和pip的環(huán)境是不是同一個(gè)。很多時(shí)候用戶裝了多個(gè)Python版本pip裝到了3.8但命令運(yùn)行的是3.11兩邊自然對(duì)不上。2.3 如果沒(méi)有pygame還能用tkinter嗎可以。Python自帶tkinter不需要額外安裝圖形庫(kù)。用tkinter寫(xiě)貪吃蛇也完全可以只是事件處理方式不同pygame用pygame.event.get()輪詢事件tkinter用bind綁定鍵盤事件。從學(xué)習(xí)目的看兩者都能鞏固基礎(chǔ)。不過(guò)我個(gè)人建議第一次做這個(gè)項(xiàng)目時(shí)直接使用pygame。原因是pygame的坐標(biāo)系、幀率和矩形繪制更符合游戲開(kāi)發(fā)直覺(jué)網(wǎng)上參考資料也多。如果你在安裝pygame時(shí)遇到網(wǎng)絡(luò)問(wèn)題可以換國(guó)內(nèi)鏡像源安裝pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple安裝完成后再用import pygame驗(yàn)證一次。環(huán)境問(wèn)題解決了后面寫(xiě)代碼才不會(huì)一頭霧水。3. 拆解貪吃蛇的四個(gè)核心模塊3.1 初始化窗口和游戲參數(shù)第一件事是定義游戲窗口的大小和格子大小。貪吃蛇的移動(dòng)是離散的每次走一個(gè)格子所以需要把窗口分成很多小格子。我一般這樣設(shè)定CELL_SIZE 20 GRID_WIDTH 30 GRID_HEIGHT 20 WINDOW_WIDTH CELL_SIZE * GRID_WIDTH WINDOW_HEIGHT CELL_SIZE * GRID_HEIGHT這里的CELL_SIZE是每個(gè)格子的邊長(zhǎng)單位是像素。GRID_WIDTH和GRID_HEIGHT分別是橫向和縱向的格子數(shù)量。窗口總寬等于格子大小乘數(shù)量這樣蛇頭坐標(biāo)可以精確落在格子里不會(huì)出現(xiàn)畫(huà)出來(lái)對(duì)不齊的情況。為什么不讓窗口寬度直接寫(xiě)600再讓CELL_SIZE隨便定因?yàn)楹竺媾袛噙吔绾蜕墒澄飼r(shí)都要依賴格子坐標(biāo)。如果窗口尺寸不是格子大小的整數(shù)倍蛇走到邊緣時(shí)會(huì)出現(xiàn)半格偏移碰撞判斷就會(huì)變得很麻煩。先把窗口和格子解耦后續(xù)改起來(lái)也容易。游戲速度用一個(gè)FPS常量控制也就是每秒刷新的幀數(shù)。FPS越低蛇移動(dòng)越慢FPS越高蛇移動(dòng)越快。初版建議從FPS 10開(kāi)始這個(gè)速度對(duì)新手比較友好。3.2 蛇的移動(dòng)和數(shù)據(jù)存儲(chǔ)方式蛇身可以用一個(gè)列表存儲(chǔ)每個(gè)元素是一個(gè)坐標(biāo)元組。比如snake [(15, 5), (14, 5), (13, 5)]這里(15, 5)是蛇頭后面兩個(gè)是身體。坐標(biāo)含義是“第幾列、第幾行”不是像素位置。畫(huà)圖的時(shí)候再根據(jù)坐標(biāo)乘以CELL_SIZE得到左上角的像素位置。蛇的移動(dòng)分為四步根據(jù)當(dāng)前方向dir計(jì)算新的蛇頭坐標(biāo)。把新蛇頭插入列表最前面。如果沒(méi)有吃到食物就把列表最后一個(gè)元素彈出。如果吃到了食物就不彈出蛇身長(zhǎng)度自然增加一格。這個(gè)做法的關(guān)鍵是“彈出尾巴”。如果不彈出蛇每幀都會(huì)變長(zhǎng)最終填滿整個(gè)窗口。如果彈出的位置不對(duì)蛇看起來(lái)就像拖著一條斷尾在跑。代碼里要保證彈出的順序先插入新頭再刪除舊尾。順序反了可能吃到食物時(shí)丟掉尾巴或者沒(méi)吃到食物時(shí)蛇頭跑到錯(cuò)誤位置。方向控制上需要做“禁止反向”的判斷。否則蛇正在向右走時(shí)按左鍵蛇頭會(huì)直接穿進(jìn)身體。常見(jiàn)寫(xiě)法是新方向與當(dāng)前方向相加不等于零向量時(shí)才允許更新方向。例如當(dāng)前方向是(1, 0)新方向(-1, 0)相加等于(0, 0)說(shuō)明是反向此時(shí)忽略按鍵。3.3 食物生成邏輯食物可以用一個(gè)坐標(biāo)元組表示例如food (5, 5)。每次生成食物時(shí)需要保證食物不在蛇身上。最簡(jiǎn)單的做法是while True: food (random.randint(0, GRID_WIDTH - 1), random.randint(0, GRID_HEIGHT - 1)) if food not in snake: break這里random.randint(0, GRID_WIDTH - 1)生成的是0到29之間的整數(shù)對(duì)應(yīng)橫向格子索引。為什么不從1開(kāi)始因?yàn)樯哳^坐標(biāo)也允許在0到29之間如果從1開(kāi)始邊緣的一圈就永遠(yuǎn)不會(huì)有食物看起來(lái)不夠自然。食物生成后要判斷是否被蛇吃到。判斷條件通常是蛇頭坐標(biāo)等于食物坐標(biāo)。如果相等就把一個(gè)“需要增長(zhǎng)”的標(biāo)記設(shè)為True。在下一次移動(dòng)時(shí)不彈出蛇尾蛇就增長(zhǎng)了一格。還有一個(gè)細(xì)節(jié)如果蛇很長(zhǎng)幾乎占滿了整個(gè)窗口while循環(huán)可能會(huì)嘗試很多次才找到空位。對(duì)于貪吃蛇來(lái)說(shuō)這種概率很低可以忽略。但如果要追求嚴(yán)謹(jǐn)可以把所有空格子收集到一個(gè)列表里再?gòu)闹须S機(jī)選一個(gè)這樣每次生成效率都穩(wěn)定。初版可以不用糾結(jié)先跑通再說(shuō)。3.4 碰撞檢測(cè)和死亡判定貪吃蛇的死亡條件有兩個(gè)撞墻和撞自己。撞墻的判斷很簡(jiǎn)單if new_head[0] 0 or new_head[0] GRID_WIDTH or new_head[1] 0 or new_head[1] GRID_HEIGHT: running False注意使用而不是。因?yàn)楹戏ㄏ聵?biāo)是0到GRID_WIDTH-1一旦等于GRID_WIDTH就已經(jīng)超出邊界。撞自己的判斷稍微復(fù)雜一點(diǎn)。常見(jiàn)寫(xiě)法是判斷新蛇頭是否在蛇身列表里。但這里有個(gè)坑如果新蛇頭是用切片或復(fù)制生成的要小心列表長(zhǎng)度。更穩(wěn)妥的方式是if new_head in snake_body: running False這里的snake_body是去掉舊蛇尾后的身體。因?yàn)槿绻澄镌谖舶偷奈恢每赡茉试S蛇頭移動(dòng)到尾巴位置時(shí)尾巴剛好移開(kāi)從而不算碰撞。但初版可以不考慮這個(gè)細(xì)節(jié)點(diǎn)直接用整個(gè)蛇身判斷也夠用。等你想做“完美”版本時(shí)再根據(jù)移動(dòng)順序調(diào)整。死亡后常見(jiàn)處理是彈出一段文字“Game Over”然后pygame.quit()退出。也可以改成等待按鍵重新開(kāi)始后面第五節(jié)會(huì)講。4. 完整源碼從啟動(dòng)到游戲結(jié)束4.1 完整代碼下面是我寫(xiě)過(guò)的一版完整代碼基于pygame實(shí)現(xiàn)去掉了復(fù)雜裝飾保留了核心邏輯。代碼可以直接保存為snake.py運(yùn)行。import pygame import random # 初始化 pygame.init() # 基礎(chǔ)參數(shù) CELL_SIZE 20 GRID_WIDTH 30 GRID_HEIGHT 20 WINDOW_WIDTH CELL_SIZE * GRID_WIDTH WINDOW_HEIGHT CELL_SIZE * GRID_HEIGHT FPS 10 # 顏色 BLACK (0, 0, 0) WHITE (255, 255, 255) RED (200, 30, 30) GREEN (30, 180, 60) # 方向映射 DIRECTIONS { pygame.K_UP: (0, -1), pygame.K_DOWN: (0, 1), pygame.K_LEFT: (-1, 0), pygame.K_RIGHT: (1, 0), } # 創(chuàng)建窗口 screen pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) pygame.display.set_caption(貪吃蛇) clock pygame.time.Clock() # 初始狀態(tài) snake [(GRID_WIDTH // 2, GRID_HEIGHT // 2)] snake.append((snake[0][0] - 1, snake[0][1])) snake.append((snake[0][0] - 2, snake[0][1])) direction (1, 0) # 初始向右移動(dòng) score 0 font pygame.font.Font(None, 36) def random_food(): while True: pos (random.randint(0, GRID_WIDTH - 1), random.randint(0, GRID_HEIGHT - 1)) if pos not in snake: return pos food random_food() def draw(): screen.fill(BLACK) # 畫(huà)蛇 for i, (x, y) in enumerate(snake): rect pygame.Rect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE) # 蛇頭用亮色蛇身用深色 if i 0: pygame.draw.rect(screen, GREEN, rect) else: pygame.draw.rect(screen, (0, 120, 40), rect) # 畫(huà)食物 pygame.draw.rect( screen, RED, pygame.Rect(food[0] * CELL_SIZE, food[1] * CELL_SIZE, CELL_SIZE, CELL_SIZE) ) # 顯示得分 score_surface font.render(fScore: {score}, True, WHITE) screen.blit(score_surface, (10, 10)) # 游戲主循環(huán) running True game_over False while running: # 事件處理 for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.KEYDOWN: if event.key in DIRECTIONS: new_dir DIRECTIONS[event.key] # 禁止反向 if new_dir[0] direction[0] ! 0 or new_dir[1] direction[1] ! 0: direction new_dir elif event.key pygame.K_SPACE and game_over: # 重新開(kāi)始 snake [(GRID_WIDTH // 2, GRID_HEIGHT // 2)] snake.append((snake[0][0] - 1, snake[0][1])) snake.append((snake[0][0] - 2, snake[0][1])) direction (1, 0) score 0 food random_food() game_over False if not game_over: # 計(jì)算新蛇頭 head_x, head_y snake[0] dx, dy direction new_head (head_x dx, head_y dy) # 撞墻判斷 if (new_head[0] 0 or new_head[0] GRID_WIDTH or new_head[1] 0 or new_head[1] GRID_HEIGHT): game_over True else: snake.insert(0, new_head) # 撞自己判斷 if new_head in snake[1:]: game_over True else: # 是否吃到食物 if new_head food: score 1 food random_food() else: snake.pop() # 繪制 draw() # 游戲結(jié)束提示 if game_over: over_surface font.render(Game Over! Press SPACE to restart, True, WHITE) screen.blit(over_surface, (WINDOW_WIDTH // 2 - 200, WINDOW_HEIGHT // 2)) pygame.display.flip() clock.tick(FPS) pygame.quit()這份代碼里按空格可以重新開(kāi)始方便測(cè)試時(shí)不反復(fù)關(guān)閉窗口。如果你不需要這個(gè)功能可以把K_SPACE分支刪掉留下QUIT處理即可。4.2 代碼里幾個(gè)容易改錯(cuò)的地方第一處是顏色元組的范圍。pygame里RGB每個(gè)分量通常取0到255如果寫(xiě)成(0, 0, 256)有些環(huán)境會(huì)直接報(bào)溢出錯(cuò)誤。建議保持在0到255之間。第二處是蛇的初始長(zhǎng)度和方向。我定義了3個(gè)格子初始向右。如果你改成初始向左要注意后面的身體坐標(biāo)也要保持一條直線否則蛇在啟動(dòng)時(shí)會(huì)扭成奇怪形狀。第三處是撞自己的判斷位置。代碼里先snake.insert(0, new_head)再判斷new_head in snake[1:]。因?yàn)樾律哳^已經(jīng)插入列表判斷時(shí)要用snake[1:]排除掉新蛇頭本身否則永遠(yuǎn)會(huì)認(rèn)為撞到自己游戲一開(kāi)局就結(jié)束。第四處是畫(huà)食物時(shí)使用pygame.Rect它的四個(gè)參數(shù)是(x, y, width, height)不是(x1, y1, x2, y2)。寫(xiě)代碼時(shí)要注意不要把第四參數(shù)寫(xiě)成結(jié)束坐標(biāo)。4.3 如何驗(yàn)證代碼是否正常第一次運(yùn)行前我會(huì)做三個(gè)層面的驗(yàn)證。第一啟動(dòng)不報(bào)錯(cuò)。雙擊運(yùn)行或命令行執(zhí)行python snake.py后窗口能正常彈出標(biāo)題欄顯示“貪吃蛇”。第二初始畫(huà)面正常。窗口里有黑色背景綠色蛇身一個(gè)紅色食物。蛇頭位置在窗口偏中間蛇身向左延伸。第三操作邏輯正確。按方向鍵蛇能移動(dòng)按反向鍵不會(huì)立刻掉頭吃掉食物后得分增加蛇身變長(zhǎng)撞墻或撞自己后畫(huà)面出現(xiàn)“Game Over”。如果這三步都通過(guò)說(shuō)明核心邏輯沒(méi)問(wèn)題。之后再去擴(kuò)展功能才有意義。5. 單局跑通后如何擴(kuò)展成更像樣的游戲5.1 增加計(jì)分和速度遞增初版得分只是數(shù)字變化玩久了會(huì)嫌單調(diào)。更常見(jiàn)的做法是得分越高移動(dòng)速度越快。實(shí)現(xiàn)方式很簡(jiǎn)單把clock.tick(FPS)中的FPS改成動(dòng)態(tài)變量例如current_speed min(FPS score // 5, 30) clock.tick(current_speed)每吃5個(gè)食物速度加1上限設(shè)為30。這里設(shè)置上限是因?yàn)樗俣瘸^(guò)30后畫(huà)面刷新很快鍵盤操作難度急劇上升反而不適合普通玩家。如果想讓速度變化更平滑可以記錄吃到食物的數(shù)量而不是直接依賴score。因?yàn)閟core可能因?yàn)閿U(kuò)展功能而出現(xiàn)加分項(xiàng)容易和速度掛鉤混亂。5.2 增加重新開(kāi)始和暫停初版代碼已經(jīng)加了空格重新開(kāi)始。暫停功能也很簡(jiǎn)單在游戲循環(huán)里增加一個(gè)暫停標(biāo)記paused False按P鍵切換暫停。暫停時(shí)只處理事件和繪制提示文字不更新蛇的位置。注意事件處理部分要單獨(dú)寫(xiě)一個(gè)狀態(tài)判斷避免玩家在暫停時(shí)還能移動(dòng)蛇。重新開(kāi)始時(shí)需要把所有狀態(tài)重置蛇身恢復(fù)初始長(zhǎng)度方向恢復(fù)向右分?jǐn)?shù)歸零食物重新隨機(jī)生成游戲結(jié)束標(biāo)記設(shè)為False。這些代碼可以封裝成一個(gè)reset_game()函數(shù)避免在按鍵分支里寫(xiě)一大串重復(fù)代碼。5.3 增加簡(jiǎn)單排行榜或最高分記錄如果你想練一下文件讀寫(xiě)可以把最高分保存到一個(gè)txt文件里。每次游戲結(jié)束時(shí)判斷當(dāng)前得分是否大于歷史最高分如果大于則寫(xiě)入文件。try: with open(high_score.txt, r) as f: high_score int(f.read().strip()) except FileNotFoundError: high_score 0這里使用try except是為了處理文件不存在的情況。第一次運(yùn)行時(shí)沒(méi)有歷史記錄直接讓最高分為0。當(dāng)游戲結(jié)束時(shí)再判斷一次更新分?jǐn)?shù)。這里要注意文件編碼和換行符。讀取時(shí)最好用strip()去掉空白否則int()可能會(huì)因?yàn)閾Q行符報(bào)錯(cuò)。寫(xiě)入時(shí)用str(high_score)寫(xiě)入整數(shù)。這個(gè)擴(kuò)展能幫你練習(xí)異常處理和文件操作算是從游戲邏輯跳到通用編程能力的過(guò)渡。6. 常見(jiàn)報(bào)錯(cuò)和排查順序6.1 啟動(dòng)時(shí)報(bào) ModuleNotFoundError最常見(jiàn)的原因是pygame沒(méi)有安裝成功。先執(zhí)行pip show pygame如果顯示W(wǎng)ARNING: Package(s) not found說(shuō)明確實(shí)沒(méi)裝上。接著執(zhí)行pip install pygame。如果已經(jīng)裝上了還是報(bào)錯(cuò)就要懷疑Python環(huán)境不一致。比如你用pip安裝但運(yùn)行腳本時(shí)用的是另一個(gè)解釋器。可以這樣驗(yàn)證python -c import pygame; print(pygame.ver)如果這個(gè)命令能正常打印版本號(hào)但運(yùn)行snake.py卻報(bào)錯(cuò)檢查腳本文件是不是被命名成了pygame.py。如果腳本名字和庫(kù)名相同Python導(dǎo)入時(shí)會(huì)優(yōu)先導(dǎo)入當(dāng)前目錄下的同名文件導(dǎo)致找不到真實(shí)的pygame。這個(gè)問(wèn)題很隱蔽我見(jiàn)過(guò)不少人踩坑。6.2 窗口一閃而過(guò)窗口出現(xiàn)后立刻關(guān)閉多半是腳本拋出了異常。因?yàn)閜ygame窗口模式下如果代碼運(yùn)行到pygame.quit()或拋出未捕獲異常控制臺(tái)會(huì)顯示錯(cuò)誤但窗口可能已經(jīng)退出。解決辦法是不要直接雙擊運(yùn)行而是在命令行里執(zhí)行腳本python snake.py這樣能看到完整的報(bào)錯(cuò)信息。常見(jiàn)錯(cuò)誤包括未初始化pygame就使用相關(guān)函數(shù)、坐標(biāo)越界、函數(shù)名拼寫(xiě)錯(cuò)誤。排查時(shí)先看終端里的Traceback定位到具體行再根據(jù)錯(cuò)誤類型修正。6.3 方向鍵沒(méi)反應(yīng)或蛇反向移動(dòng)方向鍵沒(méi)反應(yīng)先確認(rèn)事件類型是不是pygame.KEYDOWN。有些例子用KEYUP那就可能只在按鍵彈起時(shí)觸發(fā)手感會(huì)很奇怪。本項(xiàng)目使用KEYDOWN按下即觸發(fā)。反向移動(dòng)的原因是方向更新邏輯沒(méi)有過(guò)濾掉180度掉頭。檢查代碼里是否寫(xiě)了下述判斷if new_dir[0] direction[0] ! 0 or new_dir[1] direction[1] ! 0: direction new_dir如果你直接寫(xiě)成direction new_dir那么快速連按“上、左”時(shí)蛇可能瞬間掉頭并撞進(jìn)自己身體。注意這個(gè)判斷要在同一幀事件處理中反復(fù)執(zhí)行時(shí)仍然有效如果玩家在同一個(gè)幀內(nèi)按了多個(gè)鍵最終方向會(huì)以最后一個(gè)合法方向?yàn)闇?zhǔn)這是正常現(xiàn)象。6.4 蛇撞到身體卻沒(méi)有結(jié)束這個(gè)現(xiàn)象通常是“允許蛇頭移動(dòng)到蛇尾原位置”造成的。因?yàn)樯咴谝苿?dòng)時(shí)如果尾部會(huì)同時(shí)移開(kāi)那么蛇頭可以安全地移動(dòng)到尾巴之前的位置。但判斷碰撞如果用整體蛇身在新蛇頭等于舊尾巴時(shí)會(huì)被誤判為碰撞于是游戲提前結(jié)束。解決辦法是先判斷是否吃到食物。如果吃到食物尾巴不彈出此時(shí)蛇頭撞到尾巴就是真撞如果沒(méi)吃到食物尾巴即將彈出新蛇頭等于舊尾巴不算撞。具體實(shí)現(xiàn)可以這樣if new_head food: # 增長(zhǎng)不彈出尾巴所以舊尾巴還在撞到就算撞 snake.insert(0, new_head) if new_head in snake[1:]: game_over True else: score 1 food random_food() else: # 先彈出尾巴再判斷碰撞 tail snake[-1] snake.pop() snake.insert(0, new_head) # 注意此時(shí)舊尾巴已經(jīng)不在蛇身里 if new_head in snake[1:]: game_over True不過(guò)初版直接用整體蛇身判斷也能玩只是偶爾會(huì)在靠近尾巴時(shí)提前判定。如果你想做出“完美”手感就需要關(guān)注這個(gè)細(xì)節(jié)。7. 容易被忽略的邊界和性能細(xì)節(jié)7.1 窗口大小的取整問(wèn)題pygame窗口大小可以是任意整數(shù)但貪吃蛇這種格子游戲最好保證總寬和總高是格子大小的整數(shù)倍。不然你設(shè)置GRID_WIDTH 29、CELL_SIZE 20窗口寬580沒(méi)問(wèn)題但如果GRID_WIDTH取小數(shù)pygame會(huì)直接報(bào)錯(cuò)。另外格子數(shù)量也不要設(shè)置太大。比如橫向100格、縱向80格窗口會(huì)非常大普通屏幕根本放不下。我建議橫向30到40格縱向20到30格配合20像素格子大小窗口在600x400到800x600之間比較合適。7.2 速度并非越快越好初版FPS 10意味著每秒刷新10幀蛇每秒移動(dòng)10格。這個(gè)速度適合第一次測(cè)試。如果你開(kāi)局就把FPS調(diào)到60蛇會(huì)像飛一樣方向鍵根本來(lái)不及反應(yīng)過(guò)來(lái)。實(shí)際測(cè)試時(shí)我建議把速度增長(zhǎng)門檻設(shè)得高一點(diǎn)。比如每吃10個(gè)食物速度加1上限為20。這樣新手能玩一兩分鐘又不會(huì)覺(jué)得太慢。如果想要挑戰(zhàn)模式可以單獨(dú)做一個(gè)變量控制難度不要把速度寫(xiě)死在多個(gè)地方。7.3 蛇身列表的順序千萬(wàn)不能搞反很多變形bug都出在列表順序上。蛇頭必須是列表第一個(gè)元素蛇尾是最后一個(gè)。繪制蛇時(shí)如果遍歷順序錯(cuò)了蛇頭可能被畫(huà)在蛇身下面看起來(lái)像身體蓋住了頭。計(jì)算移動(dòng)時(shí)如果pop()寫(xiě)成pop(0)會(huì)刪掉蛇頭蛇身瞬間亂套。我可以給一個(gè)簡(jiǎn)單的自我檢查方法打印snake[0]和snake[-1]看看是否分別對(duì)應(yīng)蛇頭和蛇尾。如果蛇頭坐標(biāo)不是當(dāng)前方向應(yīng)該到達(dá)的位置說(shuō)明移動(dòng)邏輯有問(wèn)題。7.4 批量測(cè)試和自動(dòng)運(yùn)行思路調(diào)試游戲時(shí)每次手動(dòng)操作很慢。如果想驗(yàn)證碰撞邏輯可以讓蛇自動(dòng)朝某個(gè)方向移動(dòng)并在特定坐標(biāo)觸發(fā)食物。做法是臨時(shí)把鍵盤事件注釋掉用一套預(yù)設(shè)的方向序列讓蛇自動(dòng)走觀察是否在預(yù)期位置game over。這種自動(dòng)化測(cè)試思路對(duì)以后寫(xiě)更復(fù)雜項(xiàng)目很有用。游戲雖然是交互式的但核心狀態(tài)更新函數(shù)可以獨(dú)立測(cè)試。比如把移動(dòng)、碰撞、食物生成拆成純函數(shù)就可以編寫(xiě)測(cè)試腳本驗(yàn)證邊界條件。這里的經(jīng)驗(yàn)是任何項(xiàng)目只要邏輯足夠清晰都可以把自動(dòng)測(cè)試加進(jìn)來(lái)。小游戲也不例外。最后一個(gè)建議當(dāng)代碼跑通后不要急著加各種炫酷功能。先保存一份初始版本作為對(duì)照。后面改動(dòng)過(guò)程中如果出現(xiàn)新bug可以快速回到初始版本確認(rèn)是“原有邏輯問(wèn)題”還是“新擴(kuò)展引入的問(wèn)題”。我在實(shí)際寫(xiě)這個(gè)項(xiàng)目時(shí)就吃過(guò)虧一開(kāi)始直接加音效、動(dòng)畫(huà)和排行榜結(jié)果一個(gè)bug查了很久最后發(fā)現(xiàn)只是蛇身列表順序?qū)懛戳恕;A(chǔ)邏輯不穩(wěn)功能堆得越多排查越難。貪吃蛇雖然只是一個(gè)練手項(xiàng)目但它把Python編程里最實(shí)用的幾個(gè)部分都過(guò)了一遍。如果你能不看源碼自己獨(dú)立寫(xiě)出一版能跑的貪吃蛇說(shuō)明你已經(jīng)具備了用Python處理簡(jiǎn)單狀態(tài)變化的能力。這個(gè)基礎(chǔ)上再去看Web開(kāi)發(fā)、數(shù)據(jù)分析或自動(dòng)化腳本都會(huì)順很多。