如何用Python自带的Tkinter库实现一个简单的贪吃蛇——一个菜鸟的自娱自乐

Phemia ·
更新时间:2024-11-10
· 806 次阅读

写在前面

特殊时期大学生都成了家里蹲大学的同学,实在憋得慌。周末偶然了解Tkinter库,发现其功能丰富,用法简单,于是萌生了用它来画贪吃蛇的这么一个想法。用一个设计界面的库来玩贪吃蛇,可以说是大材小用,但自学Python从入门到(入土)能够画出一个贪吃蛇,对于一个入门小白来说还是很有趣的。
代码附在文末,在Python3以上版本的编辑器下都可以直接拷贝运行。
上代码演示效果:
在这里插入图片描述
下面是我自娱自乐的心路历程

整理思路

首先重温一下贪吃蛇,其实现逻辑还是相对简单的。在一个平铺的像素网格背景中,初始化一条蛇,随机生成一个食物。蛇移动一格,判断头部撞到自己或者覆盖食物,往复循环。若覆盖事物,再次随机生成食物,若撞到自己,游戏结束。

Tkinter库中所需要的组件 核心组件只有一个,Canvas,也就是画布,需要依托它来进行窗口的画图 Canvas(window, width = Width, height = Height, bg = 'white', ) 琐碎的方法包括pack(), palce(), create_rectangle()等等 同时需要进行窗口的更新(否则贪吃蛇将会变成一条不动的咸鱼蛇)mainloop(), update(), after() 这些后文都会涉及。 准备工作做完,接下来就到了写bug的时间了! 构建窗口,背景和贪吃蛇

有了以上的工具,我们首先当然要把库import进来,然后正儿八经地搞出一个自己的窗口来。

import tkinter as tk Unit_size = 20 ''' 这是一个单位像素的长度(体现在窗口下的真实长度) 有了它,我们就可以更容易地表示方块所在行和列 ''' global Row, Column #这里的Row和Column分别对应行数和列数,也就是y坐标最大和x坐标最大 Row = 20 Column = 20 Height = Row * Unit_size Width = Column * Unit_size win = tk.Tk() win.title('Python Snake') #给你的小程序窗口取名,任你皮 canvas = tk.Canvas(win, width = Width, height = Height + 2 * Unit_size) canvas.pack() #用pack()放置对应地Canvas到窗口下

我们要画下一个个像素(或者说方块),于是先写 画出一个像素 的方法

def draw_a_unit(canvas, col, row, unit_color = "green") : x1 = col * Unit_size y1 = row * Unit_size x2 = (col + 1) * Unit_size y2 = (row + 1) * Unit_size #用画布对象中的组件进行绘画,从(x0, y0)到(x1, y1)对角线构成的矩形 canvas.create_rectangle(x1, y1, x2, y2, fill = unit_color, outline = "white")

canvas中有自带的create_rectangle()方法,可以帮助我们具体画一个矩形:

canvas.create_rectangle(x1, y1, x2, y2, fill = unit_color, outline = "white") ''' 在这里,(x1, y1) (x2, y2)分别对应的这个矩形的左上和右下坐标,坐标是建立在界面初始坐标系内的。 界面初始的坐标系:左上角为原点,向右为x轴正方向,向下为y轴正方向 fill参数对应着网格内的颜色 outline则是边框颜色 '''

有了这样一个以方块为基本单元填充的方法,用一个简单的for循环,自然就可以形成一个网格:

def put_a_backgroud(canvas, color = 'silver') : #几番尝试,个人觉得silver这个颜色最适合做背景色 for x in range(Column) : for y in range(Row) : draw_a_unit(canvas, x, y, unit_color = color)

综上,我们成功地画出了一个背景。
一鼓作气,我们再接着用同样的方法把蛇给画好 :

global snake_list snake_list = [[11, 10], [10, 10], [9, 10]] ''' 在这里用列表来记录蛇身子的坐标[x, y],有助于接下来实现移动操作 snake_list[0]为头,snake_list[1]为尾 ''' def draw_the_snake (canvas, snake_list, color = 'green') : for i in snake_list : draw_a_unit(canvas, i[0], i[1], unit_color = color)

一条失去梦想一动不动的贪吃蛇就此诞生:

实现贪吃蛇的简单移动

在这一步,我们的首要目标是让这条小绿蛇活跃一点,让它动起来。也仅仅让它动起来,不考虑结束判断和是否吃到食物(连食物都还没有呢)。
而这个实现,前面声明snake_list列表变量就显得未雨绸缪了,让思路变得简单起来。
移动实际上到这里就分成了两步:

更新snake_list[ ] 更新画布上的有关像素

首先考虑更新snake_list[ ]的操作,毫无疑问,我们需要删除列表中的最后一个元素(也就是尾巴),添加沿方向(变量名:Direction)的新的元素到最前一位。

然后要更新画布上的有关像素,这里有几种思路:

1. 重新把整个画布重新画一遍(果断pass) 2. 只重新画蛇的部分(我的一开始的思路) 用之前draw_the_snake (canvas, snake_list, color)函数 先调用一次,传入原来的snake_list,和背景色'silver',以达到清除原有蛇的目的 再调用一次,传入更新后的snake_list,color = 'green',以达到更新蛇的目的 ''' 一开始我是这么写的,但第一次运行的时候,发现蛇身子越长,越容易卡,于是我改了几处看起来可以 减少循环的地方,这里是一处,最终采用了思路3 ''' 3. 只更新头部和尾部,原有的蛇不变,擦去最后一个像素snake_list[-1], 画出更新后的第一个像素snake_list[0]

如上所述,最后思路三竞标成功,被甲方采纳。
于是整个snake_move的雏形如下:

def snake_move(snake_list, dire) : #通过event的外部事件绑定实现对direction的改变 global Row global Column new_coord = [0, 0] if dire % 2 == 1: new_coord[0] = snake_list[0][0] new_coord[1] = snake_list[0][1] + dire else : new_coord[0] = snake_list[0][0] + (int)(dire / 2) new_coord[1] = snake_list[0][1] snake_list.insert(0, new_coord) #进行一个取模处理,形成越过边界后的效果 for coord in snake_list : ''' coord[0] = coord[0] % Column coord[1] = coord[1] % Row # 第一个版本的代码,也是为了尽量减少计算时间,for循环内更改如下 ''' if coord[0] not in range(Column) : coord[0] %= Column break elif coord[1] not in range(Row) : coord[1] %= Row break draw_a_unit(canvas, snake_list[-1][0], snake_list[-1][1], unit_color = "silver") draw_a_unit(canvas, snake_list[0][0], snake_list[0][1], ) snake_list.pop() return snake_list

这里补充一下,在第一次运行之前写写代码的时候的时候,我把上面这个函数中的倒数第三、四行调了个顺序,也就是这样:

draw_a_unit(canvas, snake_list[0][0], snake_list[0][1], ) draw_a_unit(canvas, snake_list[-1][0], snake_list[-1][1], unit_color = "silver")

实际上这是涉及先擦尾部方块or先画头部方块的问题,看起来好像并没有什么区别,但如果采取1.0版本顺序——先画头再擦尾,就出现一个bug:

gif演示中可以看出,当新头部方块碰到旧尾巴方块时,按照规则没有GameOver,但是由于先画再擦,于是把新的头给擦掉了,蛇消失又出现,十分神奇。

下面解释一下snake_move(snake_list, dire)中的dire变量是什么,这就是蛇的移动的另外一个重要参数:方向。
贪吃蛇在前进过程中,需要知道自己的朝向,才能给新的头部方块坐标赋值,这其实是很容易实现的:

global Direction Direction = 2 ''' Direction可以有四个取值为-1,1,-2,2,分别代表Up,Down,Left,Right 于是结合方向计算新的头部元素方法如下: ''' new_coord = [0, 0] if dire % 2 == 1: new_coord[0] = snake_list[0][0] new_coord[1] = snake_list[0][1] + dire else : new_coord[0] = snake_list[0][0] + (int)(dire / 2) new_coord[1] = snake_list[0][1] snake_list.insert(0, new_coord) ''' 将这段代码插入到snake_move()函数内即可 '''

最后让游戏实现被键盘操作,我们需要一个键鼠事件(event)的绑定操作,监控键盘对应摁键的情况:

#绑定键盘鼠标事件关系 def callback (event) : ''' 判断是否可以向上向下操作 如果snake_list[0] 和 [1] 的x轴坐标相同,意味着不可以改变上、下方向 若y轴坐标相同,意味着不可以改变左、右方向 ''' global Direction ch = event.keysym if ch == 'Up': if snake_list[0][0] != snake_list[1][0] : Direction = -1 elif ch == 'Down' : if snake_list[0][0] != snake_list[1][0] : Direction = 1 elif ch == 'Left' : if snake_list[0][1] != snake_list[1][1] : Direction = -2 elif ch == 'Right' : if snake_list[0][1] != snake_list[1][1] : Direction = 2 return canvas.focus_set() canvas.bind("", callback) canvas.bind("", callback) canvas.bind("", callback) canvas.bind("", callback)

这里我绑定的是键盘右下角的上、下、左、右(PgUp、PgDn、Home、End)按键。

到此为止,蛇的简单移动我们就已经实现了!

随机生成食物

对于随机生成食物,只要一个random库中的choice就可以随机生成一个食物的位置坐标了。注意到食物当然不能和蛇本身重叠,于是我们可以先实现global一个全局变量game_map[ ],覆盖地图上所有像素坐标,再用snake_list[ ]的蛇位置坐标完成去重,随即摘取即可:

global game_map game_map = [] ''' 直接通过前面初始化背景像素网格的时候,添加坐标即可 ''' def put_a_backgroud(canvas, color = 'silver') : global game_map for x in range(Column) : for y in range(Row) : draw_a_unit(canvas, x, y, unit_color = color) game_map.append([x, y]) def food(canvas, snake_list) : ''' 在这里,Have_food用于记录当前是否有食物,有数值为1,无数值为2 Food_coord = [x, y],用于记录食物的坐标,当蛇头覆盖(也就是吃掉)食物的时候Have_food重新置为0,如此往复 ''' global Column, Row, Have_food, Food_coord global game_map if Have_food : #用return结束函数,无实际返回值 return food_map = [i for i in game_map if i not in snake_list] Food_coord = random.choice(food_map) draw_a_unit(canvas, Food_coord[0], Food_coord[1], unit_color = 'red') Have_food = 1 再接下来,实现蛇的成长、结束判定

在前面我们已经有了食物的坐标,是否吃掉食物也就是判断新蛇头的坐标是否和食物坐标重合;同时实现长度改变。
写新的函数费劲,直接在snake_move函数里悄悄改两行就好了:

def snake_move(snake_list, dire) : #通过event的外部事件绑定实现对direction的改变 #或者默认方向调用实现 #return 新的snake_list global Row, Column global Have_food global Food_coord global Score new_coord = [0, 0] if dire % 2 == 1: new_coord[0] = snake_list[0][0] new_coord[1] = snake_list[0][1] + dire else : new_coord[0] = snake_list[0][0] + (int)(dire / 2) new_coord[1] = snake_list[0][1] snake_list.insert(0, new_coord) #进行一个取模处理,形成穿越边界的效果 for coord in snake_list : if coord[0] not in range(Column) : coord[0] %= Column break elif coord[1] not in range(Row) : coord[1] %= Row break #更改为以下内容即可 if snake_list[0][0] == Food_coord[0] and snake_list[0][1] == Food_coord[1] : ''' 若蛇头部与食物坐标重合,吃掉食物同时不进行pop()弹出尾部坐标,不擦去尾部,代表长长 ''' draw_a_unit(canvas, snake_list[0][0], snake_list[0][1], ) Have_food = 0 else : ''' 其他情况照常移动 ''' draw_a_unit(canvas, snake_list[0][0], snake_list[0][1], ) draw_a_unit(canvas, snake_list[-1][0], snake_list[-1][1], unit_color = "silver") snake_list.pop() return snake_list

当然,没有结束判定也不行,这样就不惊险刺激了,乐趣减半!

def snake_death_judge (snake_list) : #return 0代表没有死亡 #return 1代表死亡 #判断列表是否有重复元素的方法 #涉及列表查重方法 ''' 切片获得除头部的snake_list的其他坐标 ''' list = snake_list[1 :] if snake_list[0] in set_list : return 1 else : return 0

现在为止,万事俱备,只剩下把这些函数统一应用起来了。

最后的game_loop()

最后我们需要一个循环反复执行上述代码,控制游戏总体进程,如果没有这个循环game_loop(),以上写的东西全部白给,无法实现。
这里用到了窗口的刷新update(),after(),实现让贪吃蛇动起来的目的。
再利用Tkinter库中Tk窗口内的Label组件添加上分数和Game Over标签。

def game_loop() : global FPS global snake_list win.update() food(canvas, snake_list) snake_list = snake_move(snake_list, Direction) flag = snake_death_judge(snake_list) if flag : over_lavel = tk.Label(win, text = 'Game Over', font = ('楷体', 25), width = 15, height = 1) over_lavel.place(x = 40, y = Height / 2, bg = None) return ''' FPS在这里代表单位时间传输的帧数,FPS越低,贪吃蛇看起来的速度将会越快。 ''' win.after(FPS, game_loop)

到此为止,贪吃蛇小程序就全部写完了。
能力有限,也只能写写这种逻辑简单、库也不难的小程序。纯属自娱自乐吧,希望大家不会嫌弃。

上完整代码 import tkinter as tk import random ''' @Row 为高方向的单元数 @Column 为长方向上的单元数 @Unit_size 为单个单元的边长 @Height 为整体的高度 @Width 为整体的长度 ''' global Row, Column Row = 20 Column = 20 Unit_size = 20 Height = Row * Unit_size Width = Column * Unit_size global Direction Direction = 2 global FPS FPS = 150 global Have_food Have_food = 0 global Food_coord Food_coord = [0, 0] global Score Score = 0 global snake_list snake_list = [[11, 10], [10, 10], [9, 10]] global game_map game_map = [] # Dire为前进方向全局变量-1,1,-2,2代表Up,Down,Left,Right def draw_a_unit(canvas, col, row, unit_color = "green") : # 画一个以左上角为参照的(c, r)的方块 x1 = col * Unit_size y1 = row * Unit_size x2 = (col + 1) * Unit_size y2 = (row + 1) * Unit_size # 用画布对象中的组件进行绘画,从(x0, y0)到(x1, y1)对角线构成的矩形 canvas.create_rectangle(x1, y1, x2, y2, fill = unit_color, outline = "white") def put_a_backgroud(canvas, color = 'silver') : # 画布上构建像素网格 for x in range(Column) : for y in range(Row) : draw_a_unit(canvas, x, y, unit_color = color) game_map.append([x, y]) def draw_the_snake (canvas, snake_list, color = 'green') : ''' @description: 画蛇函数 @param {type} snake_list为整数列表,默认元素为列表[x, y] @return: None ''' for i in snake_list : draw_a_unit(canvas, i[0], i[1], unit_color = color) def snake_move(snake_list, dire) : #通过event的外部事件绑定实现对direction的改变 #或者默认方向调用实现 #return 新的snake_list global Row, Column global Have_food global Food_coord global Score new_coord = [0, 0] if dire % 2 == 1: new_coord[0] = snake_list[0][0] new_coord[1] = snake_list[0][1] + dire else : new_coord[0] = snake_list[0][0] + (int)(dire / 2) new_coord[1] = snake_list[0][1] snake_list.insert(0, new_coord) #进行一个取模处理,形成穿越边界的效果 for coord in snake_list : if coord[0] not in range(Column) : coord[0] %= Column break elif coord[1] not in range(Row) : coord[1] %= Row break if snake_list[0] == Food_coord : draw_a_unit(canvas, snake_list[0][0], snake_list[0][1], ) Have_food = 0 Score += 10 str_score.set('Your Score:' + str(Score)) else : #顺序也很重要,否则蛇头会有bug draw_a_unit(canvas, snake_list[-1][0], snake_list[-1][1], unit_color = "silver") draw_a_unit(canvas, snake_list[0][0], snake_list[0][1], ) snake_list.pop() return snake_list #保证蛇头不可以朝原有的蛇的方向前进,event为绑定的键盘鼠标事件 def callback (event) : #判断是否可以向上向下操作 global Direction ch = event.keysym if ch == 'Up': if snake_list[0][0] != snake_list[1][0] : Direction = -1 elif ch == 'Down' : if snake_list[0][0] != snake_list[1][0] : Direction = 1 elif ch == 'Left' : if snake_list[0][1] != snake_list[1][1] : Direction = -2 elif ch == 'Right' : if snake_list[0][1] != snake_list[1][1] : Direction = 2 return #判断当前状态下蛇是否撞上自己 def snake_death_judge (snake_list) : #return 0代表没有死亡 #return 1代表死亡 #涉及列表查重的方法 set_list = snake_list[1 :] if snake_list[0] in set_list : return 1 else : return 0 def food(canvas, snake_list) : #随机生成位置(x1, y1) global Column, Row, Have_food, Food_coord global game_map if Have_food : return Food_coord[0] = random.choice(range(Column)) Food_coord[1] = random.choice(range(Row)) while Food_coord in snake_list : Food_coord[0] = random.choice(range(Column)) Food_coord[1] = random.choice(range(Row)) draw_a_unit(canvas, Food_coord[0], Food_coord[1], unit_color = 'red') Have_food = 1 def game_loop() : global FPS global snake_list win.update() food(canvas, snake_list) snake_list = snake_move(snake_list, Direction) flag = snake_death_judge(snake_list) if flag : over_lavel = tk.Label(win, text = 'Game Over', font = ('楷体', 25), width = 15, height = 1) over_lavel.place(x = 40, y = Height / 2, bg = None) return win.after(FPS, game_loop) ## 以上为所有函数 win = tk.Tk() win.title('Python Snake') canvas = tk.Canvas(win, width = Width, height = Height + 2 * Unit_size) canvas.pack() str_score = tk.StringVar() score_label = tk.Label(win, textvariable = str_score, font = ('楷体', 20), width = 15, height = 1) str_score.set('Your Score:' + str(Score)) score_label.place(x = 80, y = Height) put_a_backgroud(canvas) draw_the_snake(canvas, snake_list) #绑定键盘鼠标事件关系 canvas.focus_set() canvas.bind("", callback) canvas.bind("", callback) canvas.bind("", callback) canvas.bind("", callback) #游戏进程代码 game_loop() win.mainloop()
作者:逍遥之自在



用python tkinter 贪吃蛇 Python 菜鸟

需要 登录 后方可回复, 如果你还没有账号请 注册新账号