Python使用tkinter实现推箱子游戏:从原理到代码详解
本文详细介绍如何使用Python的tkinter模块结合pygame播放音乐,开发一款图形界面的推箱子游戏。文章从开发环境搭建、地图数据结构设计、移动算法逻辑到文件模块划分进行逐步解析,提供了完整的代码实现思路与百度网盘源码下载链接,适合Python初学者参考学习。
Python推箱子游戏开发概述
此前曾用C语言编写过字符版的推箱子,功能较为简单。近期因使用Python,决定尝试制作一个图形界面的推箱子游戏。相比C语言版本,Python实现更为复杂,主要因为我对Python图形界面开发经验不足。经过查阅大量资料,最终选择了社区支持较多的tkinter模块,未涉及其他特殊原因。
本文将重点分享两部分内容:一是程序的具体实现过程,二是开发过程中遇到的思考与问题。
一、介绍
开发语言:Python 3.7 开发工具:PyCharm 2019.2.4 日期:2019年10月2日 作者:ZackSock
本次Python版本的推箱子游戏与C语言版有显著区别:首先采用了图形界面,其次增加了背景音乐功能,并支持多地图切换。程序内置了三张地图,效果如图所示:



相比之前的版本,功能更加完善。
开发环境配置
二、开发环境
此处主要介绍项目中使用的模块。由于Python并非我的强项,以下说明较为简略。
项目基于Python 3.7版本,主要依赖两个模块:tkinter和pygame。核心界面绘制由tkinter完成,pygame仅用于播放背景音乐(因未深入研究pygame,界面部分完全使用tkinter实现)。
在PyCharm中导入库非常方便。若使用其他开发工具,可通过pip安装模块,具体操作参考:https://www.jb51.net/article/171391.htm。
pip install tkinter
pip install pygame
核心原理与地图设计
三、原理分析
1、地图
地图逻辑与之前版本一致,仍采用二维数组表示。虽然这种实现方式效率并非最高,但该想法是在代码编写完成后才产生的。
2、移动
移动逻辑经过多次修改。最初完全沿用旧算法,虽能实现第一关,但更换地图后暴露出诸多问题,实际处理情况比预想的复杂。由于Python使用强制缩进代替大括号,代码阅读可能有一定难度,敬请谅解。
移动逻辑大致如下:
/**
* 0表示空白
* 1表示墙
* 2表示人
* 3表示箱子
* 4表示终点
* 5表示已完成的箱子
* 6表示在终点上的人
*/
一、人
1、移动方向为空白
前方设置为2
当前位置为0
2、移动方向为墙
直接return
3、移动方向为终点
前面设置为6
当前位置设置为0
4、移动方向为已完成的箱子
4.1、已完成箱子前面是箱子
return
4.2、已完成箱子前面是已完成的箱子
return
4.3、已完成箱子前面是墙
return
4.4、已完成箱子前面为空白
已完成箱子前面设置3
前方位置设置为6
当前位置设置为0
4.5、已完成箱子前面为终点
已完成箱子前面设置为5
前方位置设置为6
当前位置设置为0
5、前方为箱子
5.1、箱子前方为空白
箱子前方位置设置为3
前方位置设置为2
当前位置设置为0
5.2、箱子前方为墙
return
5.3、箱子前方为箱子
return
5.4、箱子前方为已完成的箱子
return
5.5、箱子前方为终点
箱子前方位置设置为5
前方位置设置为2
当前位置设置为0
二、在终点上的人
1、移动方向为空白
前方设置为2
当前位置设置为4
2、移动方向为墙
直接return
3、移动方向为终点
前面设置为6
当前位置设置为4
4、移动方向为已完成的箱子
4.1、已完成箱子前面是箱子
return
4.2、已完成箱子前面是已完成的箱子
return
4.3、已完成箱子前面是墙
return
4.4、已完成箱子前面为空白
已完成箱子前面设置3
前方位置设置为6
当前位置设置为4
4.5、已完成箱子前面为终点
已完成箱子前面设置为5
前方位置设置为6
当前位置设置为4
5、前方为箱子
5.1、箱子前方为空白
箱子前方位置设置为3
前方位置设置为2
当前位置设置为4
5.2、箱子前方为墙
return
5.3、箱子前方为箱子
return
5.4、箱子前方为已完成的箱子
return
5.5、箱子前方为终点
箱子前方位置设置为5
前方位置设置为2
当前位置设置为4
玩家存在两种状态:站在空白处或站在终点处。经分析,两者的唯一区别在于移动后原位置的值:空白处设为0,终点处设为4。因此,在移动前判断玩家背后的元素,可简化一半代码。优化后的逻辑如下:
/**
* 0表示空白
* 1表示墙
* 2表示人
* 3表示箱子
* 4表示终点
* 5表示已完成的箱子
* 6表示在终点上的人
*/
if(当前位置为2):
#即人在空白处
back = 0
elif(当前位置为6):
#即人在终点处
back = 4
1、移动方向为空白 (可移动)
前方设置为2
当前位置为back
2、移动方向为墙
直接return
3、移动方向为终点 (可移动)
前面设置为6
当前位置设置为back
4、移动方向为已完成的箱子
4.1、已完成箱子前面是箱子
return
4.2、已完成箱子前面是已完成的箱子
return
4.3、已完成箱子前面是墙
return
4.4、已完成箱子前面为空白 (可移动)
已完成箱子前面设置3
前方位置设置为6
当前位置设置为back
4.5、已完成箱子前面为终点 (可移动)
已完成箱子前面设置为5
前方位置设置为6
当前位置设置为back
5、前方为箱子
5.1、箱子前方为空白 (可移动)
箱子前方位置设置为3
前方位置设置为2
当前位置设置为back
5.2、箱子前方为墙
return
5.3、箱子前方为箱子
return
5.4、箱子前方为已完成的箱子
return
5.5、箱子前方为终点 (可移动)
箱子前方位置设置为5
前方位置设置为2
当前位置设置为back
项目文件结构分析
四、文件分析

目录结构包含BoxGame、initGame和Painter三个主要文件,test文件仅用于测试,无实际功能。各文件功能说明如下:
- BoxGame:游戏主入口,包含主要流程。由于对Python面向对象编程不熟悉,该文件逻辑更偏向面向过程风格。
- initGame:负责初始化或存储数据,包括地图数据、玩家坐标、地图尺寸及关卡信息等。
- Painter:定义Painter对象,专门用于绘制地图。
此外,项目还包含图片资源和音乐资源文件。
关键代码实现解析
五、代码分析
1、BoxGame
from tkinter import *
from initGame import *
from Painter import Painter
from pygame import mixer
#创建界面并设置属性
#创建一个窗口
root = Tk()
#设置窗口标题
root.title("推箱子")
#设置窗口大小,当括号中为"widhtxheight"形式时,会判断为设置宽高这里注意“x”是重要标识
root.geometry(str(width*step) + "x" + str(height*step))
#设置边距, 当括号中为"+left+top"形式,会判断为设置边距
root.geometry("+400+200")
#这句话的意思是width可以改变0,height可以改变0,禁止改变也可以写成resizable(False, False)
root.resizable(0, 0)
#播放背景音乐
mixer.init()
mixer.music.load('bgm.mp3') #加载音乐
mixer.music.play() #播放音乐,歌曲播放完会自动停止
#创建一个白色的画板,参数分别是:父窗口、背景、高、宽
cv = Canvas(root, bg='white', height=height*step, width=width*step)
#绘制地图
painter = Painter(cv, map, step)
painter.drawMap()
#关联Canvas
cv.pack()
#定义监听方法
def move(event):
pass
#绑定监听事件,键盘事件第一个参数固定为"<Key>",第二个参数为方法名(不能加括号)
root.bind("<Key>", move)
#进入循环
root.mainloop()
由于move函数代码较长,此处暂不展示,后续单独讲解。BoxGame的主要流程如下:
- 导入所需模块
- 创建窗口并配置属性
- 播放背景音乐
- 创建画板
- 在画板上绘制地图
- 将画板添加到窗口
- 绑定窗口事件监听
- 进入游戏主循环
2、initGame
#游戏需要的一些参数
mission = 0
mapList = [
[
[0, 0, 1, 1, 1, 0, 0, 0],
[0, 0, 1, 4, 1, 0, 0, 0],
[0, 0, 1, 0, 1, 1, 1, 1],
[1, 1, 1, 3, 0, 3, 4, 1],
[1, 4, 0, 3, 2, 1, 1, 1],
[1, 1, 1, 1, 3, 1, 0, 0],
[0, 0, 0, 1, 4, 1, 0, 0],
[0, 0, 0, 1, 1, 1, 0, 0]
],
[
[0, 0, 0, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 0, 0, 0, 0, 1, 0],
[1, 1, 4, 0, 3, 1, 1, 0, 1, 1],
[1, 4, 4, 3, 0, 3, 0, 0, 2, 1],
[1, 4, 4, 0, 3, 0, 3, 0, 1, 1],
[1, 1, 1, 1, 1, 1, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 0]
],
[
[0, 0, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 4, 4, 1, 0, 0],
[0, 1, 1, 0, 4, 1, 1, 0],
[0, 1, 0, 0, 3, 4, 1, 0],
[1, 1, 0, 3, 0, 0, 1, 1],
[1, 0, 0, 1, 3, 3, 0, 1],
[1, 0, 0, 2, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
],
[
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 1, 0, 0, 0, 1],
[1, 0, 3, 4, 4, 3, 0, 1],
[1, 2, 3, 4, 5, 0, 1, 1],
[1, 0, 3, 4, 4, 3, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
]
]
map = mapList[3]
#人背后的东西
back = 0
#地图的宽高
width, height = 0, 0
#地图中箱子的个数
boxs = 0
#地图中人的坐标
x = 0
y = 0
#画面大小
step = 30
def start():
global width, height, boxs, x, y, map
# 做循环变量
m, n = 0, 0
for i in map:
for j in i:
# 获取宽,每次内循环的次数都是一样的,只需要第一次记录width就可以了
if (n == 0):
width += 1
#遍历到箱子时箱子数量+1
if (j == 3):
boxs += 1
#当为2或者6时,为遍历到人
if (j == 2 or j == 6):
x, y = m, n
m += 1
m = 0
n += 1
height = n
start()
因尚未实现关卡切换功能,mapList和mission变量作用有限。主要参数包括:
back:玩家背后的元素值(前文已分析) width、height:地图宽高 boxs:箱子数量 x、y:玩家坐标 step:每个正方形格子的边长。因对Canvas绘制图片不熟悉,固定图片大小为30px。
由于initGame中未定义类,引用该文件时相当于直接执行其中的代码。
3、Painter
from tkinter import PhotoImage, NW
#在用Canvas绘制图片时,图片必须是全局变量
img = []
class Painter():
def __init__(self, cv, map, step):
"""Painter的构造函数,在cv画板上,根据map画出大小为step的地图"""
#传入要拿来画的画板
self.cv = cv
#传入地图数据
self.map = map
#传入地图大小
self.step = step
def drawMap(self):
"""用来根据map列表绘制地图"""
#img列表的长度
imgLen = 0
global img
#循环变量
x, y = 0, 0
for i in self.map:
for j in list(i):
#记录实际位置
lx = x * self.step
ly = y * self.step
# 画空白处
if (j == 0):
self.cv.create_rectangle(lx, ly, lx + self.step, ly+self.step,
fill="white", width=0)
# 画墙
elif (j == 1):
img.append(PhotoImage(file="imgs/wall.png"))
self.cv.create_image(lx, ly, anchor=NW, image=img[imgLen - 1])
elif (j == 2):
img.append(PhotoImage(file="imgs/human.png"))
self.cv.create_image(lx, ly, anchor=NW, image=img[imgLen - 1])
# 画箱子
elif (j == 3):
img.append(PhotoImage(file="imgs/box.png"))
self.cv.create_image(lx, ly, anchor=NW, image=img[imgLen - 1])
elif (j == 4):
img.append(PhotoImage(file="imgs/terminal.png"))
self.cv.create_image(lx, ly, anchor=NW, image=img[imgLen - 1])
elif (j == 5):
img.append(PhotoImage(file="imgs/star.png"))
self.cv.create_image(lx, ly, anchor=NW, image=img[imgLen - 1])
elif (j == 6):
img.append(PhotoImage(file="imgs/t_man.png"))
self.cv.create_image(lx, ly, anchor=NW, image=img[imgLen - 1])
x += 1
x = 0
y += 1
此处说明cv方法的使用,主要涉及create_image和create_rectangle两个方法:
#绘画矩形
cv.create_rectangle(sx, sy, ex, ey, key=value...)
1、前两个参数sx、sy(s代表start)为左上角坐标
2、后两个参数ex、ey(e代表end)表示右下角坐标
3、而后面的key=value...表示多个key=value形式的参数(顺序不固定)
如:
#填充色为红色
fill = "red"
#边框色为黑色
outline = "black"
#边框宽度为5
width = 5
具体使用例如:
#在左上角画一个边长为30,的黑色矩形
cv.create_rectangle(0, 0, 30, 30, fill="black", width=0)
接下来是图片绘制部分:
#这里要注意img必须是全局对象
self.cv.create_image(x, y, anchor=NW, img)
1、前两个参数依旧是坐标,但是这里不一定是左上角坐标,x,y默认是图片中心坐标
2、anchor=NW,设置anchor后,x,y为图片左上角坐标
3、img是一个PhotoImage对象(PhotoImage对象为tkinter中的对象),PhotoImage对象的创建如下
#通过文件路径创建PhotoImage对象
img = PhotoImage(file="img/img1.png")
因个人理解有限,更细节的内容无法深入解释。
关于实际坐标问题,前述坐标基于数组索引,而绘图时需转换为像素坐标。绘制对象分为矩形和图片两类。
- 矩形:需两个坐标点。当数组坐标为(1,1)时,因单元间隔为step(30),对应像素坐标为(30, 30)。例如(2,2)对应(60,60),即(x*step,y*step),而终点位置为(x*step+step,y*step+step)。
- 图片:绘制图片仅需一个坐标,即左上角坐标,计算方式与前述一致,为(x*step, y*step)。
另一个关键点:最初定义img列表用于存储图片对象。尝试使用单个图片对象时,绘图仅显示最后一张;改用img列表后成功解决此问题(因基础不扎实,具体原理难以详述)。
绘制图片包含以下两个步骤:
#根据数组元素,创建相应的图片对象,添加到列表末尾
img.append(PhotoImage(file="imgs/wall.png"))
#在传入图片对象参数时,使用img[imgLen - 1],imgLen为列表当前长度,而imgLen-1就是最后一个元素,即刚刚创建的图片对象
self.cv.create_image(lx, ly, anchor=NW, image=img[imgLen - 1])
4、move
def move(event):
global x, y, boxs, back, mission,mapList, map
direction = event.char
#判断人背后的东西
# 在空白处的人
if (map[y][x] == 2):
back = 0 #讲back设置为空白
# 在终点上的人
elif (map[y][x] == 6):
back = 4 #将back设置为终点
#如果按的是w
if(direction == 'w'):
#获取移动方向前方的坐标
ux, uy = x, y-1
#如果前方为墙,直接return
if(map[uy][ux] == 1):
return
# 前方为空白(可移动)
if (map[uy][ux] == 0):
map[uy][ux] = 2 #将前方设置为人
# 前方为终点
elif (map[uy][ux] == 4):
map[uy][ux] = 6 #将前方设置为终点
# 前方为已完成的箱子
elif (map[uy][ux] == 5):
#已完成箱子前面为箱子已完成箱子或者墙都不能移动
if (map[uy - 1][ux] == 3 or map[uy - 1][ux] == 5 or map[uy - 1][ux] == 1):
return
# 已完成前面为空白(可移动)
elif (map[uy - 1][ux] == 0):
map[uy - 1][ux] = 3 #箱子向前移动
map[uy][ux] = 6 #已完成箱子处原本是终点,人移动上去之后就是6了
boxs += 1 #箱子移出,箱子数量要+1
#已完成箱子前面为终点(可移动)
elif (map[uy - 1][ux] == 4):
map[uy - 1][ux] = 5 #前方的前方设置为已完成箱子
map[uy][ux] = 6 #前方的箱子处原本是终点,人移动上去后是6
# 前方为箱子
elif (map[uy][ux] == 3):
# 箱子不能移动
if (map[uy - 1][ux] == 1 or map[uy - 1][ux] == 3 or map[uy - 1][ux] == 5):
return
# 箱子前方为空白
elif (map[uy - 1][ux] == 0):
map[uy - 1][ux] = 3
map[uy][ux] = 2
# 箱子前方为终点
elif (map[uy - 1][ux] == 4):
map[uy - 1][ux] = 5
map[uy][ux] = 2
boxs -= 1
#前面只是改变了移动方向的数据,当前位置还是2或6,此时把当前位置设置为back
map[y][x] = back
#记录移动后的位置
y = uy
# 清除屏幕,并绘制地图
cv.delete("all")
painter.drawMap()
if(boxs == 0):
print("游戏结束")
此处仅展示一个方向的逻辑,其他方向代码类似,未列出。主要区别在于前方坐标及“前方的前方”坐标,具体如下:
- 向前:前方ux,uy=x,y-1,前方的前方ux,uy-1
- 向下:前方ux,uy=x,y+1,前方的前方ux,yu+1
- 向左:前方ux,uy=x-1,y,前方的前方ux-1,uy
- 向右:前方ux,uy=x+1,y,前方的前方ux+1,uy
总结与源码获取
六、总结
由于对Python语言掌握不足,博客中可能存在解释不清或错误之处,敬请谅解。
该游戏主要采用面向过程思想,改进空间较大。改进工作由Python专家Clever_Hui协助完成。因对修改后的代码理解不深,本文分享的是原始代码。两份源码均已上传,感谢支持。
原版:链接: https://pan.baidu.com/s/1KJgDFr3nwYW8BUAw-JjZbQ 提取码: a7kn
改进版:链接: https://pan.baidu.com/s/1UOEKVdjSPidkK9SZdO0jLw 提取码: ipi2
以上为Python使用tkinter模块实现推箱子游戏的介绍,希望有所帮助。如有任何疑问,请留言,小编会及时回复。感谢大家对本站的支持! 若本文对您有帮助,欢迎转载,请注明出处,谢谢!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













