1. ホーム
  2. Python

tkinter モジュールを使った Python 倉庫番ゲーム

2022-02-16 12:30:33
<パス

少し前にC言語で倉庫番のキャラクター版を作ったのですが、本当に初歩的なものでした。最近Pythonを使ったので、Pythonで倉庫番のグラフィカル・インターフェースを作ってみようと思った。今回はCのように簡単にはいきません。まず、PythonのGUIをあまり使ったことがないので、Webでいろいろと教科書を探し、最終的に特に理由もなく、ただ話題になっているからということでtkinterを選びました。

次に、2つのポイントをお伝えします。ひとつは、このプログラムを実装するまでの過程、もうひとつは、執筆中に考えたことです。

I. はじめに

Development language: Python 3.7
Development tool: PyCharm 2019.2.4
Date: October 2, 2019
Author: ZackSock


今回のプッシュボックスは、C言語版とは異なり、まずグラフィカルなインターフェースを採用し、次にBGMをつけ、さらにさまざまなマップに対応できるようにしました。私は3つのマップを内蔵していますが、それは次のようなものです。



前作よりずっと進化してますね、ハハ。

II. 開発環境

この名前が正しいかどうかわかりませんが、ここでは主に使用するモジュールの話をしています。Pythonは得意ではないので、簡単な説明にとどめておきます。

まず、Python 3.7を使っているのですが、主に2つのモジュールを使っています。 tkinter パゲーム . 主な用途はやはりtkinterで、pygameは音楽再生に使っています。(pygameに手を出さなかったので、インターフェースは全てtkinterで書きました)。ライブラリのインポート 私はpycharmを使っていますが、インポートは非常に便利です。他のソフトを使う場合は、pipを使ってモジュールをインストールすることも検討できますので、詳しくはブログをご覧ください。 https://www.cnblogs.com/banzhen/p/isimulink.html .

pip install tkinter
pip install pygame


III. 原理分析

1. 地図

マップは考え方としてはあまり変わっておらず、以前と同じ2次元の配列表現を使っています。とはいえ、これが本当に効率がいいとは思えないのですが、書いてみて思いつきました。

2. 移動

移動の面では何度も修正を加え、最初はオリジナルのアルゴリズムに忠実に従いました。これはうまくいったのですが、最初のパスだけでした。マップを修正した後に一連の問題が見つかり、その問題に基づいて実際のエンカウントがもっと複雑になっていることが分かりました。Pythonは{}の代わりに強制インデントを使用しているため、コードは少し見づらいかもしれませんので、ご容赦ください。

引越しの考え方は、おおよそ次のようなものです。

/**
* 0 for blank
* 1 for wall
* 2 for people
* 3 indicates a box
* 4 indicates the end point
* 5 indicates a completed box
* 6 denotes people at the end of the line
*/
I. People
	1、Move direction is blank
		Front set to 2
		Current position is 0
	2、Move direction for the wall
		Direct return
	3、Move direction is the end	
		Front set to 6
		The current position is set to 0
	4、Move direction for the completed box
		4.1、Finished box is in front of the box
			return
		4.2、Finished box in front of completed box
			return
		4.3, the completed box is in front of the wall
			return
		4.4, completed box is blank in front
			Completed box in front of set 3
			The front position is set to 6
			The current position is set to 0
		4.5、Finished box in front of the end
			Completed the box in front of set to 5
			The front position is set to 6
			The current position is set to 0
	5、Front for the box
		5.1, the front of the box is blank
			The front position of the box is set to 3
			Front position set to 2
			The current position is set to 0
		5.2, the front of the box for the wall
			return
		5.3, the front of the box for the box
			return
		5.4, the front of the box is the completed box
			return
		5.5, the front of the box is the end
			The position in front of the box is set to 5
			The front position is set to 2
			The current position is set to 0
Second, at the end of the end of the people
	1、Moving direction is blank
		Front position set to 2
		Current position is set to 4
	2、Move in the direction of the wall
		Directlyreturn
	3、Move direction is the end
		Front set to 6
		The current position is set to 4
	4、Move direction for the completed box
		4.1、Finished box is in front of the box
			return
		4.2、Finished box in front of completed box
			return
		4.3, the completed box is in front of the wall
			return
		4.4, completed box is blank in front
			Completed box in front of set 3
			The front position is set to 6
			The current position is set to 4
		4.5、Finished box in front of the end
			Completed the box in front of set to 5
			The front position is set to 6
			The current position is set to 4
	5、Front for the box
		5.1, the front of the box is blank
			The front position of the box is set to 3
			Front position is set to 2
			The current position is set to 4
		5.2, the front of the box for the wall
			return
		5.3, the front of the box for the box
			return
		5.4, the completed box in front of the box
			return
		5.5, the front of the box is the end
			The position in front of the box is set to 5
			The front position is set to 2
			Current position is set to 4


まず、人物の状態には2種類あって、空白に立つ場合と端に立つ場合があります。後でわかったのですが、人が空白にいる場合と端にいる場合の違いは、人が移動した後、人の元の位置の一方を空白の0に、一方を端の4に設定することだけなのです。なので、移動前に人の後ろがどうなっているかを判断することで、一般的なコードを節約することができますね。上のロジックは、以下のように変更することができます。

/**
* 0 means blank
* 1 for wall
* 2 for people
* 3 indicates a box
* 4 indicates the end point
* 5 indicates a completed box
* 6 denotes people at the end of the line
*/
if(current position is 2):
	# i.e. person in the blank
	back = 0
elif(current position is 6):
	# that is, the person is at the end
	back = 4

1, the direction of movement is blank (can be moved)
	Front set to 2
	The current position is back
2, the direction of movement for the wall
	Direct return
3、Moving direction is end (movable)
	Front set to 6
	The current position is set to back
4、Moving direction for the completed box
	4.1、Finished box in front of the box
		return
	4.2、Finished box in front of completed box
		return
	4.3, the completed box is in front of the wall
		return
	4.4, completed box in front of blank (moveable)
		Completed box in front of set 3
		The front position is set to 6
		The current position is set to back
	4.5, has completed the box in front of the end (can be moved)
		Completed box front set to 5
		The front position is set to 6
		The current position is set to back
5, in front of the box
	5.1、Front of the box is blank (movable)
		The front position of the box is set to 3
		Front position set to 2
		The current position is set to back
	5.2, the front of the box for the wall
		return
	5.3, the front of the box for the box
		return
	5.4, the front of the box is the completed box
		return
	5.5, box in front of the end (movable)
		The position in front of the box is set to 5
		The front position is set to 2
		Current position is set to back


IV. ドキュメント分析


ディレクトリ構成は以下のとおりで、BoxGame、initGame、Painterの3つのメインファイルがあります。それからtestファイルはテスト用で、実用性はありません。次に、各ファイルの機能について説明します。

  1. BoxGame。ゲームの正面玄関として、主な流れはそこにあります。正直、Pythonは比較的勉強してきたし、Pythonのオブジェクト指向もあまり詳しくないので、このフローはプロセス指向の考え方に近いです。
  2. initGame: マップデータ、人々の位置、マップサイズ、レベルなど、いくつかのデータを初期化または保存します。
  3. ペインターです。このファイルではPainterオブジェクトを定義しており、主にマップの描画に使用します

それ以外は画像リソースと音楽リソースだけです。

V. コード解析

1. 箱庭ゲーム
from tkinter import *
from initGame import *
from Painter import Painter
from pygame import mixer

# Create the interface and set the properties
# Create a window
root = Tk()	
#Set the window title
root.title("pushbox")
# set the window size, when the brackets for "widhtxheight" form, will be judged to set the width and height here note that "x" is an important identifier
root.geometry(str(width*step) + "x" + str(height*step))
# set margins, when the brackets are "+left+top" form, will be judged as set margins
root.geometry("+400+200")
# This sentence means that width can change 0, height can change 0, prohibit change can also be written as resizable(False, False)
root.resizable(0, 0)

#Play background music
mixer.init()
mixer.music.load('bgm.mp3') #load music
mixer.music.play() #Play music, the song will stop automatically when it finishes playing

#Create a white canvas with the following parameters: parent window, background, height, width
cv = Canvas(root, bg='white', height=height*step, width=width*step)

# Draw the map
painter = Painter(cv, map, step)
painter.drawMap()

#Associate Canvas
cv.pack()

#Define the listener method
def move(event):
	pass
	
#bind the listener event, the first parameter of the keyboard event is fixed to "

", the second parameter is the method name (no parentheses)	

root.bind("
"
, move)
#Enter loop
root.mainloop()


moveのコードは長いので割愛し、後で説明する。BoxGameの主な流れは以下の通りだ。

  1. モジュールのインポート
  2. ウィンドウの作成とプロパティの設定
  3. BGMを再生する
  4. ドローイングボードの作成
  5. ドローイングボードに地図を描く
  6. 窓の上に画板を置く
  7. ウィンドウをイベントのリスニングに関連付ける
  8. ゲームがループするようになりました
2. initGame
Some parameters needed for #game
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, 0, 1, 1, 0, 0]
    ],
    [
        [0, 0, 0, 1, 1, 1, 1, 1, 1, 0],
        [0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0],
        [1, 1, 4, 0, 3, 1, 1, 1, 0, 1, 1, 1],
        [1, 4, 4, 3, 0, 3, 0, 0, 0, 2, 1],
        [1, 4, 4, 0, 3, 0, 3, 0, 0, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0],
        [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0], [0, 0, 0, 0, 0, 1, 1, 0]
    ],
    [
        [0, 0, 1, 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, 0, 1, 1],
        [1, 0, 0, 1, 3, 3, 0, 1],
        [1, 0, 0, 2, 0, 0, 0, 0, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1
    ],
    [
        [1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 0, 0, 1, 0, 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, 0, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1
    ]
]
map = mapList[3]

# of things behind the person
back = 0
#The width and height of the map
width, height = 0, 0
# number of boxes in the map
boxs = 0
# coordinates of the people in the map
x = 0
y = 0
#Screen size
step = 30

def start():
    global width, height, boxs, x, y, map
    # Make loop variables
    m, n = 0, 0
    for i in map:
        for j in i:
            # Get the width, each time the number of inner loops is the same, just record width the first time
            if (n == 0):
                width += 1
            # traversal to the box when the number of boxes + 1
            if (j == 3):
                boxs += 1
            # when 2 or 6, for traversal to people
            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. ペインター
from tkinter import PhotoImage, NW

# When drawing an image with Canvas, the image must be a global variable
img = []
class Painter():
    def __init__(self, cv, map, step):
    	"""Painter's constructor, on the cv drawing board, draws a map of size step based on map"""
    	# pass in the drawing board to be used to draw
        self.cv = cv
        #Pass in the map data
        self.map = map
        #pass in the map size
        self.step = step
    def drawMap(self):
        """Used to draw a map based on the map list"""
        # length of the img list
        imgLen = 0
        global img
        #loop variables
        x, y = 0, 0
        for i in self.map:
            for j in list(i):
            	# Record the actual position
                lx = x * self.step
                ly = y * self.step

                # Draw the blank space
                if (j == 0):
                    self.cv.create_rectangle(lx, ly, lx + self.step, ly + self.step,
                                             fill="white", width=0)
                # Draw wall
                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])
                # Draw the box
                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



ここでは,create_image と create_rectangle の 2 つの cv メソッドが使用されています.

# Draw rectangle
cv.create_rectangle(sx, sy, ex, ey, key=value...)
1, the first two parameters sx, sy (s for start) for the upper left corner coordinates
2, the last two parameters ex, ey (e for end) means the lower right corner coordinates
3, and the key=value... indicates multiple key = value form of parameters (the order is not fixed)
Such as.
# fill color is red
fill = "red"
# border color is black
outline = "black"
#border width is 5
width = 5

Specific use example.
#draw a black rectangle with a side length of 30, in the upper left corner
cv.create_rectangle(0, 0, 30, 30, fill="black", width=0)


続いて、画像の描画です。

#Note here that img must be a global object
self.cv.create_image(x, y, anchor=NW, img)
1, the first two parameters are still coordinates, but here is not necessarily the upper left corner coordinates, x, y default is the center of the picture coordinates
2, anchor=NW, after setting anchor, x, y is the upper left corner of the picture coordinates
3, img is a PhotoImage object (PhotoImage object for the object in tkinter), PhotoImage object is created as follows

# PhotoImage object created by the file path
img = PhotoImage(file="img/img1.png")


私自身がよく知らないので、これ以上詳しくは言えませんが。

次に実際の座標の問題ですが、これは上記のように配列に参照されます。そして、実際のプロットは、特定のピクセルで行う必要があります。描画処理では、矩形と絵の2種類を描く必要があります。

  1. 矩形。矩形は2つの座標を必要とします。配列座標が (1, 1) の場合、セルの間隔はステップ (30) なので、対応するピクセル座標は (30, 30) となります。(2, 2)は(60, 60)に相当し、(x*step, y*step)となり、終了位置は(x*step+step, y*step+step)となる。
  2. 画像:画像を描くのに必要な座標は左上の1つだけで、これは前と同じように(x*step, y*step)とします。

上記でもう一つ重要な点があります。私は一番最初にimgリストを定義して、画像オブジェクトを保持するようにしました。最初、1つの画像オブジェクトを使おうとしたのですが、画像を描画するときに1つしか表示されないので、代わりにimgリストを使おうと考えたら、うまくいったのです。(あまりしっかり勉強していないので、うまく説明できませんが)。

画像の描画には2つのステップがあります。

# Based on the array elements, create the corresponding image object and add it to the end of the list
img.append(PhotoImage(file="imgs/wall.png"))

# When passing in the image object parameter, use img[imgLen - 1], imgLen is the current length of the list, and imgLen-1 is the last element, the image object just created
self.cv.create_image(lx, ly, anchor=NW, image=img[imgLen - 1])

def move(event):
    global x, y, boxs, back, mission,mapList, map
    direction = event.char

    # Determine what's behind the person
    # People in the margin
    if (map[y][x] == 2):
        back = 0 # speak back set to blank
    # People at the end of the line
    elif (map[y][x] == 6):
        back = 4 # set back to the end point
	
	# If the press is w
    if(direction == 'w'):
    	#Get the coordinates in front of the direction of movement
        ux, uy = x, y-1
        #If the front is a wall, directly return
        if(map[uy][ux] == 1):
            return
        # The front is blank (moveable)
        if (map[uy][ux] == 0):
            map[uy][ux] = 2 # Set the front to human
        # Set front to end
        elif (map[uy][ux] == 4):
            map[uy][ux] = 6 # Set the front to the end

        # Front as completed box
        elif (map[uy][ux] == 5):
        	# completed boxes in front of the box completed boxes or walls can not be moved
            if (map[uy - 1][ux] == 3 or map[uy - 1][ux] == 5 or map[uy - 1][ux] == 1):
                return
            # Completed preceded by blank (moveable)
            elif (map[uy - 1][ux] == 0):
                map[uy - 1][ux] = 3 # box moves forward
                map[uy][ux] = 6 # completed box at the original end, after the person moved up is 6
                boxs += 1 # box moved out, the number of boxes to +1
            # has been completed in front of the box is the end (can be moved)
            elif (map[uy - 1][ux] == 4):
                map[uy - 1][ux] = 5 # the front of the front set to completed boxes
                map[uy][ux] = 6 # the front of the box was originally the end, after the person moved up is 6
        # The front for the box
        elif (map[uy][ux] == 3):
            # The box can not be moved
            if (map[uy - 1][ux] == 1 or map[uy - 1][ux] == 3 or map[uy - 1][ux] == 5):
                return
            # The front of the box is blank
            elif (map[uy - 1][ux] == 0):
                map[uy - 1][ux] = 3
                map[uy][ux] = 2
            # The front of the box is the end
            elif (map[uy - 1][ux] == 4):
                map[uy - 1][ux] = 5
                map[uy][ux] = 2
                boxs -= 1
        
        # The previous data only changed the direction of movement, the current position is still 2 or 6, then set the current position to back
        map[y][x] = back
        # record the position after the move
        y = uy

    # Clear the screen and draw the map
    cv.delete("all")
    painter.drawMap()
    if(boxs == 0):
        print("Game over")


4.移動
def move(event):
    global x, y, boxs, back, mission,mapList, map
    direction = event.char

    # Determine what's behind the person
    # People in the margin
    if (map[y][x] == 2):
        back = 0 # speak back set to blank
    # People at the end of the line
    elif (map[y][x] == 6):
        back = 4 # set back to the end point
	
	# If the press is w
    if(direction == 'w'):
    	#Get the coordinates in front of the direction of movement
        ux, uy = x, y-1
        #If the front is a wall, directly return
        if(map[uy][ux] == 1):
            return
        # The front is blank (moveable)
        if (map[uy][ux] == 0):
            map[uy][ux] = 2 # Set the front to human
        # Set front to end
        elif (map[uy][ux] == 4):
            map[uy][ux] = 6 # Set the front to the end

        # Front as completed box
        elif (map[uy][ux] == 5):
        	# completed boxes in front of the box completed boxes or walls can not be moved
            if (map[uy - 1][ux] == 3 or map[uy - 1][ux] == 5 or map[uy - 1][ux] == 1):
                return
            # Completed preceded by blank (moveable)
            elif (map[uy - 1][ux] == 0):
                map[uy - 1][ux] = 3 # box moves forward
                map[uy][ux] = 6 # completed box at the original end, after the person moved up is 6
                boxs += 1 # box moved out, the number of boxes to +1
            # has been completed in front of the box is the end (can be moved)
            elif (map[uy - 1][ux] == 4):
                map[uy - 1][ux] = 5 # the front of the front set to completed boxes
                map[uy][ux] = 6 # the front of the box was originally the end, after the person moved up is 6
        # The front for the box
        elif (map[uy][ux] == 3):
            # The box can not be moved
            if (map[uy - 1][ux] == 1 or map[uy - 1][ux] == 3 or map[uy - 1][ux] == 5):
                return
            # The front of the box is blank
            elif (map[uy - 1][ux] == 0):
                map[uy - 1][ux] = 3
                map[uy][ux] = 2
            # The front of the box is the end
            elif (map[uy - 1][ux] == 4):
                map[uy - 1][ux] = 5
                map[uy][ux] = 2
                boxs -= 1
        
        # The previous data only changed the direction of movement, the current position is still 2 or 6, then set the current position to back
        map[y][x] = back
        # record the position after the move
        y = uy

    # Clear the screen and draw the map
    cv.delete("all")
    painter.drawMap()
    if(boxs == 0):
        print("Game over")


他の方向コードは非常に類似しているため、ここでは1つの方向のみを取り上げています。唯一の違いは、前面の座標と前面の座標を次のように指定することである。

  • 前方:前方 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の手前側

VI. 概要

私自身のPython言語に対する理解不足のため、ブログを書く上で不明瞭な説明や間違いがあることは避けられませんので、大変申し訳ありませんが、ご容赦いただければ幸いです。

このゲームはよりプロセス指向の考え方を用いており、改良の余地がたくさんあります。Pythonの人にもやってもらうことにします。 クレバー_ホイ 修正後のコードがよくわからないので、オリジナルのコードを共有します。ソースコードの両方のコピーをアップロードします、よろしくお願いします。