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

写在前面

特殊时期大学生都成了家里蹲大学的同学,实在憋得慌。周末偶然了解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列表变量就显得未雨绸缪了,让思路变得简单起来。
移动实际上到这里就分成了两步:

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

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

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

 1. 重新把整个画布重新画一遍(果断pass2. 只重新画蛇的部分(我的一开始的思路)
    用之前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("<KeyPress-Left>",  callback)
canvas.bind("<KeyPress-Right>", callback)
canvas.bind("<KeyPress-Up>",    callback)
canvas.bind("<KeyPress-Down>",  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("<KeyPress-Left>",  callback)
canvas.bind("<KeyPress-Right>", callback)
canvas.bind("<KeyPress-Up>",    callback)
canvas.bind("<KeyPress-Down>",  callback)
#游戏进程代码
game_loop()
win.mainloop()
发布了1 篇原创文章 · 获赞 1 · 访问量 230

猜你喜欢

转载自blog.csdn.net/xiaoyaozizai_19/article/details/104485956