@TOC初入Python(一) Pygame貪吃蛇游戲的撰寫與改進
貪吃蛇游戲是一款簡單耐玩的休閑益智類游戲,利用pygame可以實作輕松撰寫,不需要輔佐圖片等等元素,可以直接利用涂色方塊表示,吃果子變長的原理也很容易實作,將body分為一塊一塊,每塊有自己的位置屬性,從而可以輕松表示出來,通過對貪吃蛇游戲的撰寫和改進可以在pygame模塊的應用以及面向物件編程的編程方面得到不錯的練習效果,
作為一個初學者,我也打算利用這種方式對學習進行一個有趣的總結,maybe效果不錯哦!
初步撰寫
初步撰寫的代碼來自于簡書上的一個程式,看起來清晰好懂,
https://gitee.com/codetimer/Snake/blob/master/main.py
前期定義
這部分主要匯入庫以及定義游戲視窗大小
import pygame
import sys
import random
# 全域定義
SCREEN_X = 600
SCREEN_Y = 600
蛇類的定義
蛇類擁有方向和身體塊兩個屬性,蛇塊的增加、減少和移動都可以利用串列的增刪輕松實作,方向的判斷處要注意左右、上下方向不能被直接逆向改變,死亡判斷處,直接用頭部方塊的位置做if判斷即可,
# 蛇類 點以25為單位
class Snake(object):
# 初始化各種需要的屬性 [開始時默認向右/身體塊x5]
def __init__(self):
self.dirction = pygame.K_RIGHT
self.body = []
for x in range(5):
self.addnode()
# 無論何時 都在前端增加蛇塊
def addnode(self):
left,top = (0,0)
if self.body:
left,top = (self.body[0].left,self.body[0].top)
node = pygame.Rect(left,top,25,25)
if self.dirction == pygame.K_LEFT:
node.left -= 25
elif self.dirction == pygame.K_RIGHT:
node.left += 25
elif self.dirction == pygame.K_UP:
node.top -= 25
elif self.dirction == pygame.K_DOWN:
node.top += 25
self.body.insert(0,node)
# 洗掉最后一個塊
def delnode(self):
self.body.pop()
# 死亡判斷
def isdead(self):
# 撞墻
if self.body[0].x not in range(SCREEN_X):
return True
if self.body[0].y not in range(SCREEN_Y):
return True
# 撞自己
if self.body[0] in self.body[1:]:
return True
return False
# 移動!
def move(self):
self.addnode()
self.delnode()
# 改變方向 但是左右、上下不能被逆向改變
def changedirection(self,curkey):
LR = [pygame.K_LEFT,pygame.K_RIGHT]
UD = [pygame.K_UP,pygame.K_DOWN]
if curkey in LR+UD:
if (curkey in LR) and (self.dirction in LR):
return
if (curkey in UD) and (self.dirction in UD):
return
self.dirction = curkey
食物類的定義
食物類主要包含一個方塊,remove和set方法都是對食物的坐標進行判斷,這里需要注意pygame中rect方法的原理,
# 食物類 點以25為單位
class Food:
def __init__(self):
self.rect = pygame.Rect(-25,0,25,25)
def remove(self):
self.rect.x=-25
def set(self):
if self.rect.x == -25:
allpos = []
# 不靠墻太近 25 ~ SCREEN_X-25 之間
for pos in range(25,SCREEN_X-25,25):
allpos.append(pos)
self.rect.left = random.choice(allpos)
self.rect.top = random.choice(allpos)
print(self.rect)
pygame.Rect()
通過Rect可以創造一個矩形區域,可以由LEFT,TOP,WIDTH,HEIGHT這四個值創建,

也可以用來裁剪圖片
eg:
加載圖片img = pygame.image.load(r’D:\Python\images\xxx.png’)
剪切圖片rect = pygame.Rect(LEFT,TOP,WIDTH,HEIGHT)
顯示與主函式
show_text沒有什么可說的,對應好引數就OK,main函式的邏輯基本可以分解為——初始化、進入回圈、檢測事件、運動并更新蛇身、判斷是否吃到并完成食物投遞、判斷死亡、顯示分數,蛇運動的速度由clock.tick()中的引數決定,它決定了游戲繪制的幀率,引數越大,運動速度越大,
def show_text(screen, pos, text, color, font_bold = False, font_size = 60, font_italic = False):
#獲取系統字體,并設定文字大小
cur_font = pygame.font.SysFont("宋體", font_size)
#設定是否加粗屬性
cur_font.set_bold(font_bold)
#設定是否斜體屬性
cur_font.set_italic(font_italic)
#設定文字內容
text_fmt = cur_font.render(text, 1, color)
#繪制文字
screen.blit(text_fmt, pos)
def main():
pygame.init()
screen_size = (SCREEN_X,SCREEN_Y)
screen = pygame.display.set_mode(screen_size)
pygame.display.set_caption('Snake')
clock = pygame.time.Clock()
scores = 0
isdead = False
# 蛇/食物
snake = Snake()
food = Food()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
if event.type == pygame.KEYDOWN:
snake.changedirection(event.key)
# 死后按space重新
if event.key == pygame.K_SPACE and isdead:
return main()
screen.fill((255,255,255))
# 畫蛇身 / 每一步+1分
if not isdead:
scores+=1
snake.move()
for rect in snake.body:
pygame.draw.rect(screen,(20,220,39),rect,0)
# 顯示死亡文字
isdead = snake.isdead()
if isdead:
show_text(screen,(100,200),'YOU DEAD!',(227,29,18),False,100)
show_text(screen,(150,260),'press space to try again...',(0,0,22),False,30)
# 食物處理 / 吃到+50分
# 當食物rect與蛇頭重合,吃掉 -> Snake增加一個Node
if food.rect == snake.body[0]:
scores+=50
food.remove()
snake.addnode()
# 食物投遞
food.set()
pygame.draw.rect(screen,(136,0,21),food.rect,0)
# 顯示分數文字
show_text(screen,(50,500),'Scores: '+str(scores),(223,223,223))
pygame.display.update()
clock.tick(10)
if __name__ == '__main__':
main()
初步效果
請大家自行測驗初步效果,死亡后按空格重新開始,

優化程序
有了上面的成果,一個貪吃蛇游戲也就基本完成了,有意思么,還行吧,不過可能是個人對于游戲的癡迷讓我覺得這個游戲總是少了點什么,或者說,太不完善了,只是單純有它該有的功能罷了,什么游戲UI,闖關的感覺,都是沒有的,所以,讓我們來開動腦筋,試著去改造它吧,我將其與平常玩的游戲做了點小對比,提出了一點點優化目標,其中有些比較簡單,有些也會有點困難,需要我們推翻現在的代碼重新進行架構,
優化目標
1.cfg和函式的封裝;
2.開始ui和結束ui;
3.難度設定(選擇蛇的運行速度);
4.障礙物的設定\特殊方塊的設定(高得分方塊、暫時減速方塊);
*5.對于幀數的優化(幀數固定,即每次運動之間進行多次檢測和輸出);
*6.分數排行榜(玩之前輸入用戶名,這需要涉及到檔案讀寫的操作),
目前大概就這么多
cfg檔案和函式的封裝
cfg檔案的封裝我覺得對于開發來說至關重要,就像csgo中的config檔案,玩家也可以改來改去,添加一些想要的引數(指插入二刺螈圖片做背景),增加了游戲的可玩性,雖然這個貪吃蛇游戲沒有太多的引數需要設定,但是我覺得養成一個好習慣是不錯的,
import pygame
# 方塊大小
cube = 25
pygame.init()
# 螢屏大小
SCREENSIZE_X = 600
SCREENSIZE_Y = 600
# highest scores的text
pos1 = (50, 500)
text1 = 'Highest Scores: '
color1 = (223, 223, 223)
font_size1 = 30
# curscores 的text
pos2 = (50, 550)
text2 = 'Scores: '
color2 = (223, 223, 223)
font_size2 = 30
# UI字體
UIfont_size_big = 60
UIfont_size_small = 30
UIfont1 = pygame.font.SysFont("宋體", UIfont_size_big)
UIfont2 = pygame.font.SysFont("宋體", UIfont_size_small)
UIcolor1 = (255, 151, 23)
UIcolor2 = (255, 151, 23)
cfg檔案代碼如上,其實有些引數我并沒有設定過來,因為config中也不是需要包含所有引數的,有的引數如果改起來可行性不高我覺得也沒必要全部放在這里,反而會很亂,
部分后面要用的函式封裝如下(當然所有優化指標還沒完全實作,因此必然還會改變):
'''other_function.py'''
import pygame
import sys
def show_scores(screen, scores, pos, text, color, font_size=30):
pass
def StartUI(screen, cfg):
pass
def EndUI(screen, cfg):
pass
當然,蛇類和食物類也要封裝起來,就直接封裝就好,
'''Food.py'''
import pygame
import random
class Food(object):
def __init__(self):
self.rect = pygame.Rect(-25, 0, 25, 25)
def remove(self):
self.rect.x = -25
def set(self, SCREENSIZE_X=600):
if self.rect.x == -25:
allpos = []
# 不靠近墻
for pos in range(25, SCREENSIZE_X - 25, 25):
allpos.append(pos)
self.rect.left = random.choice(allpos)
self.rect.top = random.choice(allpos)
print(self.rect)
'''Snake.py'''
import pygame
class Snake(object):
def __init__(self):
# 設定初始方向為向右 初始身體為空
self.direction = pygame.K_RIGHT
self.body = []
# 初始化5個身體塊
for x in range(5):
self.addnote()
def addnote(self):
# left top為身體塊的定位位置
left, top = (0, 0)
if self.body:
left, top = (self.body[0].left, self.body[0].top)
node = pygame.Rect(left, top, 25, 25)
if self.direction == pygame.K_LEFT:
node.left -= 25
elif self.direction == pygame.K_RIGHT:
node.left += 25
elif self.direction == pygame.K_UP:
node.top -= 25
elif self.direction == pygame.K_DOWN:
node.top += 25
self.body.insert(0, node)
# 洗掉身體塊
def delnote(self):
self.body.pop()
def isdead(self, SCREENSIZE_X=600, SCREENSIZE_Y=600):
# 撞墻
if self.body[0].x not in range(SCREENSIZE_X):
return True
if self.body[0].y not in range(SCREENSIZE_Y):
return True
# 撞自己
if self.body[0] in self.body[1:]:
return True
return False
def move(self):
# 在前進方向上頭部增加一個方塊 尾部減少一個方塊
self.addnote()
self.delnote()
def changedirection(self, curkey):
LR = [pygame.K_LEFT, pygame.K_RIGHT]
UD = [pygame.K_UP, pygame.K_DOWN]
if curkey in LR + UD:
if (curkey in LR) and (self.direction in LR):
return
if (curkey in UD) and (self.direction in UD):
return
self.direction = curkey
需要注意的點有:
1.利用cfg檔案分裝后,檔案存放目錄要進行改變,像下圖一樣:

同時main函式中的庫的匯入也應該改變(面向物件的基礎~),比如:
import snake_cfg
from functions.Snake import *
from functions.Food import *
from functions.other_function import *
2.呼叫cfg中的引數時,要加前綴“cfg.”當然這取決于你匯入的方式,
3.每個函式的引數都需要重寫,同時為了讓主函式回圈運行,采用了如下的結構:
if __name__ == '__main__':
while True:
if not main(snake_cfg):
break
main函式的回傳值是由結束UI決定的,這部分需要結合后面的結束部分自己去理解,
def main(cfg):
pygame.init()
#省略......
...
while not isdead:
...
...
...
# 設定時間間隔
clock.tick(difficulty)
# 如果死亡 回傳輸出UI
return EndUI(screen, cfg)
開始UI和結束UI
開始UI和結束UI是什么,其實經常玩游戲的人都很清楚,一個背景,幾個選項,不同的選項導向不同的結果,比如開始游戲、繼續游戲,選擇難度、退出游戲等等,這里我們就實行一個簡單的UI,開始UI包括兩個難度的選擇level1、level2(當然你想寫10個都行),這與下一部分難度設定相重合,結束UI包括restart和quit.
UI的設計也是主要依托pygame中的顯示功能,前面初步撰寫時也并沒有談到pygame中顯示方式的使用,比如pygame.display.set_mode、screen.fill、pygame.draw.rect、font.render、surface.blit,因為這部分的內容實在是太繁瑣,需要自己去查找與學習,
簡單來說,開始UI分為幾個部分:
文字的定義與顯示、方塊的創建與按鈕的系結、事件的選擇,
def StartUI(screen, cfg):
# 歡迎為大號字體
cur_font1 = cfg.UIfont1
text_fmt1 = cur_font1.render('Welcome!', False, cfg.UIcolor1)
# 選項為小號字體 選項1為level1 選項2為level2
cur_font2 = cfg.UIfont2
text_fmt2 = cur_font2.render('level1', False, cfg.UIcolor2)
text_fmt3 = cur_font2.render('level2', False, cfg.UIcolor2)
# alpha通道起到濾鏡效果 填充一層模糊界面
surface = screen.convert_alpha()
surface.fill((127, 255, 212, 2))
# 定義文字位置并創建按鈕
text_rect1 = text_fmt1.get_rect()
text_rect1.centerx, text_rect1.centery = cfg.SCREENSIZE_X / 2, cfg.SCREENSIZE_Y / 2 - 50
# 顯示文字1 即歡迎選項
surface.blit(text_fmt1, text_rect1)
# 定義按鈕的位置 以螢屏中心位置開始進行微調(先隨便給個數再調也行)
button_width, button_height = 100, 40
button_start_x_left = cfg.SCREENSIZE_X / 2 - button_width - 20
button_start_x_right = cfg.SCREENSIZE_X / 2 + 20
button_start_y = cfg.SCREENSIZE_Y / 2 - button_height / 2 + 20
pygame.draw.rect(surface, (0, 255, 255), (button_start_x_left, button_start_y, button_width, button_height))
# 定義按鈕1的矩形
text_level1_rect = text_fmt2.get_rect()
text_level1_rect.centerx, text_level1_rect.centery = button_start_x_left + button_width / 2, button_start_y + button_height / 2
surface.blit(text_fmt2, text_level1_rect)
# 定義按鈕2的矩形
pygame.draw.rect(surface, (0, 255, 255), (button_start_x_right, button_start_y, button_width, button_height))
text_level2_rect = text_fmt3.get_rect()
text_level2_rect.centerx, text_level2_rect.centery = button_start_x_right + button_width / 2, button_start_y + button_height / 2
surface.blit(text_fmt3, text_level2_rect)
while True:
screen.blit(surface, (0, 0))
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN and event.button:
# 滑鼠選擇level1 回傳死亡為False 速度為10
if text_level1_rect.collidepoint(pygame.mouse.get_pos()):
return False, 10
# 滑鼠選擇level2 回傳死亡為False 速度為15
if text_level2_rect.collidepoint(pygame.mouse.get_pos()):
return False, 15
# 更新顯示狀態
pygame.display.update()
你問我字體的引數和操作?查百度,你問我矩形和按鈕怎么創建系結?查百度,你問我鍵盤輸入輸出key,滑鼠點擊判斷是怎么回事?查百度,
一些資料:
pygame.key 鍵值說明:
https://blog.csdn.net/stoneyyhit/article/details/52259993
pygame 字體設定:
https://blog.csdn.net/zengxiantao1994/article/details/58590594
pygame 螢屏顯示
https://zhuanlan.zhihu.com/p/99450316
RGB查詢:
https://www.fontke.com/tool/rgb/ffffff/
結束UI和開始UI幾乎完全一致,只需要改變下文本和按鈕的系結即可:
def EndUI(screen, cfg):
# 歡迎為大號字體
cur_font1 = cfg.UIfont1
text_fmt1 = cur_font1.render('Game Over!', False, cfg.UIcolor1)
# 選項為小號字體 選項1為level1 選項2為level2
cur_font2 = cfg.UIfont2
text_fmt2 = cur_font2.render('restart', False, cfg.UIcolor2)
text_fmt3 = cur_font2.render('quit', False, cfg.UIcolor2)
# alpha通道起到濾鏡效果 填充一層模糊界面
surface = screen.convert_alpha()
surface.fill((127, 255, 212, 2))
# 定義文字位置并創建按鈕
text_rect1 = text_fmt1.get_rect()
text_rect1.centerx, text_rect1.centery = cfg.SCREENSIZE_X / 2, cfg.SCREENSIZE_Y / 2 - 50
# 顯示文字1 即歡迎選項
surface.blit(text_fmt1, text_rect1)
# 定義按鈕的位置 螢屏中心位置
button_width, button_height = 100, 40
button_start_x_left = cfg.SCREENSIZE_X / 2 - button_width - 20
button_start_x_right = cfg.SCREENSIZE_X / 2 + 20
button_start_y = cfg.SCREENSIZE_Y / 2 - button_height / 2 + 20
pygame.draw.rect(surface, (0, 255, 255), (button_start_x_left, button_start_y, button_width, button_height))
# 定義按鈕1的矩形
text_restart_rect = text_fmt2.get_rect()
text_restart_rect.centerx, text_restart_rect.centery = button_start_x_left + button_width / 2, button_start_y + button_height / 2
surface.blit(text_fmt2, text_restart_rect)
# 定義按鈕2的矩形
pygame.draw.rect(surface, (0, 255, 255), (button_start_x_right, button_start_y, button_width, button_height))
text_quit_rect = text_fmt3.get_rect()
text_quit_rect.centerx, text_quit_rect.centery = button_start_x_right + button_width / 2, button_start_y + button_height / 2
surface.blit(text_fmt3, text_quit_rect)
while True:
screen.blit(surface, (0, 0))
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN and event.button:
# 滑鼠選擇restart 回傳死亡為False
if text_restart_rect.collidepoint(pygame.mouse.get_pos()):
return True
# 滑鼠選擇quit 回傳死亡為True
if text_quit_rect.collidepoint(pygame.mouse.get_pos()):
return False
pygame.display.update()
難度設定
開始UI的撰寫和難度設定是同時進行的,觀察前面的開始UI可以發現開始UI中的選項level1和level2分別對應了兩組不同的return值,在main函式中,在回圈正式開始前,會有這么一行代碼:
isdead, difficulty = StartUI(screen, cfg)
其中,isdead是while回圈繼續的判斷條件,也是游戲繼續進行的“鑰匙”,而difficulty則是開始UI的第二個回傳值,它代表了難度,我在這里簡化(偷懶)了一點,回傳值為10或者15,還記得前面說過的clock.tick么,它代表了游戲的幀數,也決定了蛇運動的速度,將diffculty直接作為引數,那么就會直接控制蛇的速度,從而實作難度的區分,
這樣的區分好像確實有點太簡單了,所以我才提出了后面的優化——添加障礙物,不同難度會對應不同大小、數量的障礙物,說實話,帶障礙物的貪吃蛇我還真沒怎么玩過,不過應該會很有意思,
當然,為了增加游戲性,你也可以發動腦筋,想出不同的策略為游戲添磚加瓦,比如食物有幾率變成幸運方塊,得分會增加或者可以讓你的蛇暫時慢下來等等,聽起來就很有意思的樣子,
…
未完待續
其他改進
這個蛇頭的顏色還是要變一下的,不然分不清蛇頭真的蛋疼,
# 畫出貪吃蛇的每個身體塊
pygame.draw.rect(screen, (4, 150, 254), snake.body[0], 0)
for rect in snake.body[1:]:
pygame.draw.rect(screen, (20, 220, 39), rect, 0)
還有對于輸入的判斷,多加些條件可以使得程式更穩定:
# 如果按鍵為方向鍵 則改變蛇的運動方向
if event.key in [pygame.K_UP, pygame.K_DOWN, pygame.K_LEFT, pygame.K_RIGHT]:
snake.changedirection(event.key)
分數的判斷也要改變一下,每次吃方塊得1分,每次運動不得分(當然這個隨便你怎么改了),
curscores += 1
食物類的remove方法也可以改進一下
def remove(self):
self.rect.x = -25
貌似改成等于號會更穩定一點,之前-=好像有可能會出bug
另外,在游玩時也發現,同時按兩個方向鍵,會直接死亡,啊這,優化方法我還沒找到(其實有個快捷死亡的bug也不錯0.0),
當前效果
懶得搞gif圖了,湊合看吧,



未完待續
鑒于目前還有不少作業以及一些別的學習,這個優化任務打算先放一放,過幾天再回來做吧,總結什么的,哎,懶得寫了,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/208773.html
標籤:其他
