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

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

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

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

一、介绍

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

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

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

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

二、开发环境

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

首先我使用的是Python3.7,主要用了两个模块,tkinterpygame。其中主要使用的还是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文件的话就是测试用的,没有实际用处。然后讲一下各个文件的功能:

  1. BoxGame:作为游戏的主入口,游戏的主要流程就在里面。老实说我Python学习的内容比较少,对Python的面向对象不是很熟悉,所有这个流程更偏向于面向过程的思想。
  2. initGame:初始化或存储一些数据,如地图数据,人的位置,地图的大小,关卡等
  3. 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主要流程如下:

  1. 导入模块
  2. 创建窗口并设置属性
  3. 播放背景音乐
  4. 创建画板
  5. 在画板上绘制地图
  6. 将画板铺到窗口上
  7. 让窗口关联监听事件
  8. 游戏循环了
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没有太大用处,主要参数有一下几个:

  1. back:人背后的东西(前面分析过了)
  2. width、height:宽高
  3. boxs:箱子的个数
  4. x、y:人的坐标
  5. 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,1)时,因为单元的间隔为step(30),所以对应的像素坐标为(30, 30)。(2,2)对应(60,60),即(x*step,y*step),而终点位置为(x*step+step,y*step+step)。
  2. 图片:绘制图片只需要一个坐标,左上角坐标,这个是前面一样为(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来帮忙完成了,因为修改后的代码不是非常了解,所有我分享的是我原本的代码。源码两份我都会上传,感谢大家支持。

目录
相关文章
|
28天前
|
开发者 Python
如何在Python中管理模块和包的依赖关系?
在实际开发中,通常会结合多种方法来管理模块和包的依赖关系,以确保项目的顺利进行和可维护性。同时,要及时更新和解决依赖冲突等问题,以保证代码的稳定性和可靠性
45 4
|
8天前
|
Python
Python Internet 模块
Python Internet 模块。
102 74
|
26天前
|
算法 数据安全/隐私保护 开发者
马特赛特旋转算法:Python的随机模块背后的力量
马特赛特旋转算法是Python `random`模块的核心,由松本真和西村拓士于1997年提出。它基于线性反馈移位寄存器,具有超长周期和高维均匀性,适用于模拟、密码学等领域。Python中通过设置种子值初始化状态数组,经状态更新和输出提取生成随机数,代码简单高效。
104 63
|
27天前
|
测试技术 Python
手动解决Python模块和包依赖冲突的具体步骤是什么?
需要注意的是,手动解决依赖冲突可能需要一定的时间和经验,并且需要谨慎操作,避免引入新的问题。在实际操作中,还可以结合使用其他方法,如虚拟环境等,来更好地管理和解决依赖冲突😉。
|
27天前
|
持续交付 Python
如何在Python中自动解决模块和包的依赖冲突?
完全自动解决所有依赖冲突可能并不总是可行,特别是在复杂的项目中。有时候仍然需要人工干预和判断。自动解决的方法主要是提供辅助和便捷,但不能完全替代人工的分析和决策😉。
|
1月前
|
JSON Linux 数据格式
Python模块:从入门到精通,只需一篇文章!
Python中的模块是将相关代码组织在一起的单元,便于重用和维护。模块可以是Python文件或C/C++扩展,Python标准库中包含大量模块,如os、sys、time等,用于执行各种任务。定义模块只需创建.py文件并编写代码,导入模块使用import语句。此外,Python还支持自定义模块和包,以及虚拟环境来管理项目依赖。
Python模块:从入门到精通,只需一篇文章!
|
28天前
|
Python
Python的模块和包
总之,模块和包是 Python 编程中非常重要的概念,掌握它们可以帮助我们更好地组织和管理代码,提高开发效率和代码质量
38 5
|
27天前
|
数据可视化 Python
如何在Python中解决模块和包的依赖冲突?
解决模块和包的依赖冲突需要综合运用多种方法,并且需要团队成员的共同努力和协作。通过合理的管理和解决冲突,可以提高项目的稳定性和可扩展性
|
1月前
|
Python
在Python中,可以使用内置的`re`模块来处理正则表达式
在Python中,可以使用内置的`re`模块来处理正则表达式
47 5
|
1月前
|
数据可视化 开发者 Python
Python GUI开发:Tkinter与PyQt的实战应用与对比分析
【10月更文挑战第26天】本文介绍了Python中两种常用的GUI工具包——Tkinter和PyQt。Tkinter内置于Python标准库,适合初学者快速上手,提供基本的GUI组件和方法。PyQt基于Qt库,功能强大且灵活,适用于创建复杂的GUI应用程序。通过实战示例和对比分析,帮助开发者选择合适的工具包以满足项目需求。
119 7

热门文章

最新文章