1. 程式人生 > 程式設計 >Python使用tkinter模組實現推箱子游戲

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

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

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

一、介紹

開發語言:Python3.7
開發工具:PyCharm 2019.2.4
日期:2019年10月2日
作者:ZackSock

這次的推箱子不同與C語言版的,首先是使用了圖形介面,然後添加了背景音樂,還有就是可以應對多種不同的地圖。我內建了三張地圖,效果圖如下:


在這裡插入圖片描述

在這裡插入圖片描述
在這裡插入圖片描述

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

二、開發環境

我也不知道這麼取名對不對,這裡主要講的就是使用到的模組。因為Python不是我的強項,所以我只能簡單說一下。

首先我使用的是Python3.7,主要用了兩個模組,tkinterpygame。其中主要使用的還是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檔案的話就是測試用的,沒有實際用處。然後講一下各個檔案的功能:

  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,1,0],[0,4,1],[1,3,2,0]
 ],[
 [0,1]
 ],[
 [1,5,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,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,anchor=NW,image=img[imgLen - 1])
 elif (j == 2):
  img.append(PhotoImage(file="imgs/human.png"))
  self.cv.create_image(lx,image=img[imgLen - 1])
 # 畫箱子
 elif (j == 3):
  img.append(PhotoImage(file="imgs/box.png"))
  self.cv.create_image(lx,image=img[imgLen - 1])
 elif (j == 4):
  img.append(PhotoImage(file="imgs/terminal.png"))
  self.cv.create_image(lx,image=img[imgLen - 1])
 elif (j == 5):
  img.append(PhotoImage(file="imgs/star.png"))
  self.cv.create_image(lx,image=img[imgLen - 1])
 elif (j == 6):
  img.append(PhotoImage(file="imgs/t_man.png"))
  self.cv.create_image(lx,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,30,fill="black",width=0)

然後是繪製圖片:

#這裡要注意img必須是全域性物件
self.cv.create_image(x,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,image=img[imgLen - 1])

4、move

def move(event):
 global x,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,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模組實現推箱子游戲,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回覆大家的。在此也非常感謝大家對我們網站的支援!
如果你覺得本文對你有幫助,歡迎轉載,煩請註明出處,謝謝!