侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

Python使用tkinter模块实现推箱子游戏

Python  /  管理员 发布于 7年前   342

前段时间用C语言做了个字符版的推箱子,着实是比较简陋。正好最近用到了Python,然后想着用Python做一个图形界面的推箱子。这回可没有C那么简单,首先Python的图形界面我是没怎么用过,在网上找了一大堆教材,最后选择了tkinter,没什么特别的原因,只是因为网上说的多。

接下来就来和大家分享一下,主要分享两点,第一就是这个程序的实现过程,第二点就是我在编写过程中的一些思考。

一、介绍

开发语言:Python 3.7
开发工具:PyCharm 2019.2.4
日期:2019年10月2日
作者:ZackSock

这次的推箱子不同与C语言版的,首先是使用了图形界面,然后添加了背景音乐,还有就是可以应对多种不同的地图。我内置了三张地图,效果图如下:




比上次的高级多了,哈哈。

二、开发环境

我也不知道这么取名对不对,这里主要讲的就是使用到的模块。因为Python不是我的强项,所以我只能简单说一下。

首先我使用的是Python3.7,主要用了两个模块,tkinter和pygame。其中主要使用的还是tkinter,而pygame是用来播放音乐的。(因为没去了解pygame,所有界面我是用tkinter写的)。

库的导入我使用的是pycharm,导入非常方便。如果使用其它软件可以考虑用pip安装模块,具体操作见下文:https:///article/171391.htm。

pip install tkinterpip install pygame

三、原理分析

1、地图

地图在思想方面没有太大改变,还是和以前一样使用二维数组表示。不过我认为这样确实不是非常高效的做法,不过这个想法也是在我写完之后才有的

2、移动

在移动方面我修改了很多遍,先是完全按照原先的算法。这个确实也实现了,不过只能在第一关有效,在我修改地图之后发现了一系列问题,然后根据问题发现实际遇到的情况要复杂很多。因为Python是用强制缩进替代了{},所以代码在观看中会有些难度,希望大家见谅。

移动的思想大致如下:

/***0表示空白*1表示墙*2表示人*3表示箱子*4表示终点*5表示已完成的箱子*6表示在终点上的人*/一、人1、移动方向为空白前方设置为2当前位置为02、移动方向为墙直接return3、移动方向为终点前面设置为6当前位置设置为04、移动方向为已完成的箱子4.1、已完成箱子前面是箱子return4.2、已完成箱子前面是已完成的箱子return4.3、已完成箱子前面是墙return4.4、已完成箱子前面为空白已完成箱子前面设置3前方位置设置为6当前位置设置为04.5、已完成箱子前面为终点已完成箱子前面设置为5前方位置设置为6当前位置设置为05、前方为箱子5.1、箱子前方为空白箱子前方位置设置为3前方位置设置为2当前位置设置为05.2、箱子前方为墙return5.3、箱子前方为箱子return5.4、箱子前方为已完成的箱子return5.5、箱子前方为终点箱子前方位置设置为5前方位置设置为2当前位置设置为0二、在终点上的人1、移动方向为空白前方设置为2当前位置设置为42、移动方向为墙直接return3、移动方向为终点前面设置为6当前位置设置为44、移动方向为已完成的箱子4.1、已完成箱子前面是箱子return4.2、已完成箱子前面是已完成的箱子return4.3、已完成箱子前面是墙return4.4、已完成箱子前面为空白已完成箱子前面设置3前方位置设置为6当前位置设置为44.5、已完成箱子前面为终点已完成箱子前面设置为5前方位置设置为6当前位置设置为45、前方为箱子5.1、箱子前方为空白箱子前方位置设置为3前方位置设置为2当前位置设置为45.2、箱子前方为墙return5.3、箱子前方为箱子return5.4、箱子前方为已完成的箱子return5.5、箱子前方为终点箱子前方位置设置为5前方位置设置为2当前位置设置为4

首先,人有两种状态,人可以站在空白处,也可以站在终点处。后面我发现,人在空白处和人在终点唯一的区别是,人移动后,人原先的位置一个设置为0,即空白,一个设置为4,即终点。所以我在移动前判断人背后的东西,就可以省去一般的代码了。上面的逻辑可以改为如下:

/***0表示空白*1表示墙*2表示人*3表示箱子*4表示终点*5表示已完成的箱子*6表示在终点上的人*/if(当前位置为2):#即人在空白处back = 0elif(当前位置为6):#即人在终点处back = 41、移动方向为空白(可移动)前方设置为2当前位置为back2、移动方向为墙直接return3、移动方向为终点(可移动)前面设置为6当前位置设置为back4、移动方向为已完成的箱子4.1、已完成箱子前面是箱子return4.2、已完成箱子前面是已完成的箱子return4.3、已完成箱子前面是墙return4.4、已完成箱子前面为空白(可移动)已完成箱子前面设置3前方位置设置为6当前位置设置为back4.5、已完成箱子前面为终点(可移动)已完成箱子前面设置为5前方位置设置为6当前位置设置为back5、前方为箱子5.1、箱子前方为空白(可移动)箱子前方位置设置为3前方位置设置为2当前位置设置为back5.2、箱子前方为墙return5.3、箱子前方为箱子return5.4、箱子前方为已完成的箱子return5.5、箱子前方为终点(可移动)箱子前方位置设置为5前方位置设置为2当前位置设置为back

四、文件分析


目录结构如下,主要有三个文件BoxGame、initGame和Painter。test文件的话就是测试用的,没有实际用处。然后讲一下各个文件的功能:

  1. BoxGame:作为游戏的主入口,游戏的主要流程就在里面。老实说我Python学习的内容比较少,对Python的面向对象不是很熟悉,所有这个流程更偏向于面向过程的思想。
  2. initGame:初始化或存储一些数据,如地图数据,人的位置,地图的大小,关卡等
  3. Painter:我在该文件里定义了一个Painter对象,主要就是用来绘制地图

除此之外就是图片资源和音乐资源了。

五、代码分析

1、BoxGame

from tkinter import *from initGame import *from Painter import Painterfrom 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()#关联Canvascv.pack()#定义监听方法def move(event):pass#绑定监听事件,键盘事件第一个参数固定为"<Key>",第二个参数为方法名(不能加括号)root.bind("<Key>", move)#进入循环root.mainloop()

因为move的代码比较长,就先不写出来,后面讲解。BoxGame主要流程如下:

  1. 导入模块
  2. 创建窗口并设置属性
  3. 播放背景音乐
  4. 创建画板
  5. 在画板上绘制地图
  6. 将画板铺到窗口上
  7. 让窗口关联监听事件
  8. 游戏循环了

2、initGame

#游戏需要的一些参数mission = 0mapList = [ [ [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 = 0y = 0#画面大小step = 30def 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 = nstart()

因为我还没有实现关卡切换,所以这里的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"#边框宽度为5width = 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模块实现推箱子游戏,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!


  • 上一条:
    python内置函数sorted()用法深入分析
    下一条:
    python生成器推导式用法简单示例
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • 在python语言中Flask框架的学习及简单功能示例(0个评论)
    • 在Python语言中实现GUI全屏倒计时代码示例(0个评论)
    • Python + zipfile库实现zip文件解压自动化脚本示例(0个评论)
    • python爬虫BeautifulSoup快速抓取网站图片(1个评论)
    • vscode 配置 python3开发环境的方法(0个评论)
    • 近期文章
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • Laravel从Accel获得5700万美元A轮融资(0个评论)
    • 在go + gin中gorm实现指定搜索/区间搜索分页列表功能接口实例(0个评论)
    • 在go语言中实现IP/CIDR的ip和netmask互转及IP段形式互转及ip是否存在IP/CIDR(0个评论)
    • PHP 8.4 Alpha 1现已发布!(0个评论)
    • Laravel 11.15版本发布 - Eloquent Builder中添加的泛型(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2016-10
    • 2016-11
    • 2018-04
    • 2020-03
    • 2020-04
    • 2020-05
    • 2020-06
    • 2022-01
    • 2023-07
    • 2023-10
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客