特殊时期大学生都成了家里蹲大学的同学,实在憋得慌。周末偶然了解Tkinter库,发现其功能丰富,用法简单,于是萌生了用它来画贪吃蛇的这么一个想法。用一个设计界面的库来玩贪吃蛇,可以说是大材小用,但自学Python从入门到(入土)能够画出一个贪吃蛇,对于一个入门小白来说还是很有趣的。
代码附在文末,在Python3以上版本的编辑器下都可以直接拷贝运行。
上代码演示效果:
下面是我自娱自乐的心路历程
首先重温一下贪吃蛇,其实现逻辑还是相对简单的。在一个平铺的像素网格背景中,初始化一条蛇,随机生成一个食物。蛇移动一格,判断头部撞到自己或者覆盖食物,往复循环。若覆盖事物,再次随机生成食物,若撞到自己,游戏结束。
核心组件只有一个,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[ ]的操作,毫无疑问,我们需要删除列表中的最后一个元素(也就是尾巴),添加沿方向(变量名: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()