Python使用tkinter模塊實現推箱子游戲

前段時間用C語言做了個字符版的推箱子,着實是比較簡陋。正好最近用到了Python,然後想着用Python做一個圖形界面的推箱子。這回可沒有C那麼簡單,首先Python的圖形界面我是沒怎麼用過,在網上找了一大堆教材,最後選擇了tkinter,沒什麼特別的原因,只是因爲網上說的多。

接下來就來和大家分享一下,主要分享兩點,第一就是這個程序的實現過程,第二點就是我在編寫過程中的一些思考。

一、介紹

開發語言:Python	3.7
開發工具:PyCharm 2019.2.4
日期:2019102日
作者: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來幫忙完成了,因爲修改後的代碼不是非常瞭解,所有我分享的是我原本的代碼。源碼兩份我都會上傳,感謝大家支持。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章