前段时间用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://www.cnblogs.com/banzhen/p/isimulink.html。
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学习的内容比较少,对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/1NSmOeSlpHqU1OFFTcS_kcA 提取码:r0ba
-
改进版:链接:https://pan.baidu.com/s/1-_uiZKRdH-OBVq1-ES9JaA 提取码:53yt
转载:https://blog.csdn.net/ZackSock/article/details/101934416