【第9回】pythonでウィンドウを使ったゲームを作成する方法(解説編:ゲームの流れ)

monster_road詳細 pythonで簡単プログラミング

#python #ゲーム #ウィンドウ #流れ #中核 #機能改良 #機能追加 #イベント処理

本記事では、前回の記事で解説しなかった内容のうち、ゲームとしての流れに沿って中核となる処理について解説を行います。

本記事では、主としてWindows、および、python3を前提にしています。
スポンサーリンク

プログラムについて

解説に入る前に、プログラムを以下に示します。
実行するための前準備や、プログラムが必要とする画像ファイルなどは、前回をご参照ください。


[monster_road.py]

import tkinter as tk        # GUIモジュール
import tkinter.scrolledtext as tkscr  # GUI:スクロールテキストクラスモジュール
import PIL.Image as im      # Pillowの画像クラスモジュール
import PIL.ImageTk as imtk  # Pillowの写真画像クラスモジュール
import sys                  # システム用モジュール
import random as rd         # 乱数を扱うモジュール
import time

#
# 登場人物・キャラのクラス定義
#

class me :                  # 自分自身のクラスを定義する
    def __init__(self):     #
        self.HP = 100       # 生命点(HP)
        self.score = 0      # 得点(スコア)

class monster :             # モンスターのクラスを定義する
    #image : im.Image        # 画像を格納するメンバ変数の定義

    def __init__(self):     # モンスタークラスの初期化関数
        self.name = ""      # 名前
        self.attack_pt = 0  # 攻撃力
        self.exp_pt = 0     # 経験値(スコアになる)

    def set(self, img, name, attack_pt, exp):   # モンスターの情報を設定する関数
        self.image = img            # 画像を設定する
        self.name = name            # 名前を設定する
        self.attack_pt = attack_pt  # 攻撃力を設定する
        self.exp = exp              # 相手に与える経験値(=スコア)を設定する

    def attack(self, anyone):       # モンスターが攻撃する関数
        anyone.HP -= self.attack_pt # だれかの生命点(HP)を減らす

#
# ウィンドウアプリケーションの定義
#

class tkApp(tk.Frame):          # GUIモジュール tkinter を使ったアプリを定義する
                                # tk.Frameを拡張したクラスにする
    def __init__(self, master=None):
        super().__init__(master)    # 基底クラスtk.Frameの初期化を実行する
        self.pack()                 # レイアウト

        # スクロールバー付きテキストボックスを作成・配置する
        self.textbox = tkscr.ScrolledText(width=63, height=12)
        self.textbox.place(x=25, y=250)

        self.titleimg = im.open("title.png")# タイトルの画像を読み込む
        self.backimg = im.open("back.png")  # 分かれ道の画像を読み込む
        slimeimg = im.open("slime.png")     # スライムの画像を読み込む
        goblinimg = im.open("goblin.png")   # ゴブリンの画像を読み込む
        dragonimg = im.open("dragon.png")   # ドラゴンの画像を読み込む

        self.slime = monster()
        self.slime.set(slimeimg, name="スライム", attack_pt=5, exp=1)

        self.goblin = monster()
        self.goblin.set(goblinimg, name="ゴブリン", attack_pt=20, exp=5)

        self.dragon = monster()
        self.dragon.set(dragonimg, name="ドラゴン", attack_pt=40, exp=20)

        self.text_r = 1      # テキストの行位置

        #
        #
        # 初期画面表示
        #
        #

        self.menu_bar = tk.Menu(wnd)            # メニューバー

        # プレイメニュー
        self.play_menu = tk.Menu(self.menu_bar, tearoff=False)
        self.play_menu.add_command(label="スタート", command=self.game_start)
        self.play_menu.add_separator()
        self.play_menu.add_command(label="終了", command=self.game_exit)

        # ヘルプメニュー
        self.help_menu = tk.Menu(self.menu_bar, tearoff=False)
        self.help_menu.add_command(label="ヘルプ", command=self.help)

        self.menu_bar.add_cascade(label="プレイ", menu=self.play_menu)
        self.menu_bar.add_cascade(label="ヘルプ", menu=self.help_menu)

        wnd.config(menu=self.menu_bar)

        self.change_image(self.titleimg) # 画像をタイトルの画像にする(初回の変更)

        # スタートボタンを作成して配置する
        self.start_btn = tk.Button(wnd, text="スタート",
                        command=self.start_btn_click,
                        width=7, height=1, bg="green", fg="white")
        self.start_btn.place(x=20, y=20)

        # 左ボタンを作成して配置する
        self.left_btn = tk.Button(wnd, text="左",
                        command=self.left_btn_click,
                        width=7, height=1, bg="steelblue", fg="white",
                        state=tk.DISABLED)
        self.left_btn.place(x=50, y=440)

        # 真ん中ボタンを作成して配置する
        self.mid_btn = tk.Button(wnd, text="真ん中",
                        command=self.mid_btn_click,
                        width=7, height=1, bg="steelblue", fg="white",
                        state=tk.DISABLED)
        self.mid_btn.place(x=230, y=440)

        # 右ボタンを作成して配置する
        self.right_btn = tk.Button(wnd, text="右",
                        command=self.right_btn_click,
                        width=7, height=1, bg="steelblue", fg="white",
                        state=tk.DISABLED)
        self.right_btn.place(x=400, y=440)

    def change_image(self,image):       # 画像を変更する関数
        png = imtk.PhotoImage(image)    # 指定された画像を写真画像として保存する
        lab = tk.Label(wnd, image=png)  # 画像をラベルに貼り付ける
        lab.image = png                 # これをやらないと画像が消されてしまう
        lab.pack()                      # レイアウトを実行する
        lab.place(x=100, y=20)          # 配置する

    def change_score(self, score):      # 得点(スコア)の表示を更新する関数
        # 得点(スコア)のタイトルだけを表示する
        lbl = tk.Label(wnd, text="スコア: ", fg="black", width=5 )
        lbl.place(x=410, y=20)

        # 得点(スコア)の数値だけを表示する
        lbl = tk.Label(wnd, text=str(score), bg="white", fg="black",
                        width=3, relief="solid", anchor="w" )
        lbl.place(x=460, y=20)

    def change_hp(self, HP):            # 生命点(HP)の表示を更新する関数
        # 生命点(HP)のタイトルだけを表示する
        lbl = tk.Label(wnd, text="生命点: ", fg="black", width=5 )
        lbl.place(x=410, y=50)

        # 生命点(HP)の数値だけを表示する
        lbl = tk.Label(wnd, text=str(HP), bg="white", fg="black",
                        width=3, relief="solid", anchor="w" )
        lbl.place(x=460, y=50)

    def enable_dir_button(self):                # 方向ボタンを全て有効にする関数
        self.left_btn["state"] = tk.NORMAL
        self.mid_btn["state"] = tk.NORMAL
        self.right_btn["state"] = tk.NORMAL

    def disable_dir_button(self):               # 方向ボタンを全て無効にする関数
        self.left_btn["state"] = tk.DISABLED
        self.mid_btn["state"] = tk.DISABLED
        self.right_btn["state"] = tk.DISABLED

    def putText(self,text):
        # 次の行にテキストを表示する
        self.textbox.insert(str(self.text_r)+'.0', text + '\n')
        self.textbox.see('end')             # 最後の行にスクロールさせる
        self.text_r += 1                    # 次に表示させる行を進める

    def game_start(self):               # ゲーム開始時の設定をする
        self.change_image(self.backimg) # 画像を分かれ道の画像にする(初回の変更)

        self.textbox.delete("1.0", "end")

        self.text_r = 1      # テキストの行位置

        self.putText("3つの分かれ道があります。")
        self.putText("どれに進みますか?")

        self.me = me()                      # 自分のクラスオブジェクトを作成する

        self.change_score(self.me.score)    # 得点(スコア)を表示する
        self.change_hp(self.me.HP)          # 生命点(HP)を表示する

        self.enable_dir_button()            # 方向ボタンを全て有効にする
    
    def game_exit(self):        # ゲームを終了する関数
        sys.exit(0)

    def help(self):             # ヘルプを表示する関数
        self.putText("**************************************")
        self.putText("**************************************")
        self.putText("")
        self.putText("使い方は説明しなくても分かりますよね😌")
        self.putText("")
        self.putText("**************************************")
        self.putText("**************************************")

    def start_btn_click(self):  # スタートボタンがクリックされた時に呼ばれる関数
        self.game_start()

    def left_btn_click(self):   # 左ボタンがクリックされた時に呼ばれる関数
        self.choose_monster(1)  # 左を選ぶ

    def mid_btn_click(self):    # 真ん中ボタンがクリックされた時に呼ばれる関数
        self.choose_monster(2)  # 真ん中を選ぶ

    def right_btn_click(self):  # 右ボタンがクリックされた時に呼ばれる関数
        self.choose_monster(3)  # 右を選ぶ

    def choose_monster(self,dir):
    
        if self.me.HP <= 0 :        # すでに生命点(HP)が無くなっていた
            return

        self.disable_dir_button()   # 方向ボタンを全て無効にする

        mon_no = rd.randint(1,3)    # 1~3の乱数を発生させモンスター番号にする
        mon_no = mon_no + dir - 1   # 進んだ方向(dir)でモンスターを変更する

        if mon_no > 3 :             # 変更した結果が3を超えたら
            mon_no = mon_no - 3     # 1に修正する(モンスターは1~3)のため

        if mon_no == 1 :            # 1はスライム
            monster = self.slime
        elif mon_no == 2 :          # 2はゴブリン
            monster = self.goblin
        else :                      # 3はドラゴン
            monster = self.dragon

        self.change_image(monster.image)    # モンスターの画像を表示する
        self.putText(monster.name + "が現れた!")
        monster.attack(self.me)             # モンスターが出現する
        self.putText("攻撃されてHPが" + str(self.me.HP) + "になった")

        if self.me.HP <= 0 :            # 生命点(HP)が無くなった
            self.putText("やられた")
            self.putText("")
            self.putText("--- GAME OVER ---")
            return

        self.me.score += monster.exp        # 経験値を得点(スコア)に加算する
        self.change_score(self.me.score)
        self.change_hp(self.me.HP)

        app.update_idletasks()              # 画面を強制的に更新
        time.sleep(1.0)                     # チョット待つ
        self.change_image(self.backimg)     # 画像を分かれ道の画像にする
        self.enable_dir_button()            # 方向ボタンを全て有効にする

#
# 入り口の処理
#

# 画面作成
# ウィンドウを作成する

wnd = tk.Tk()                   # ウィンドウを作成する
wnd.geometry('500x500')         # ウィンドウサイズを500 ✕ 500ドットにする
wnd.title('モンスターロード')   # ウィンドウタイトルを設定する

# ウィンドウのフレームを作成する

app = tkApp(wnd)

# ウィンドウを表示してメニュー・ボタン等の待受け処理に入る
# ウィンドウを終了するまで戻ってこない無限ループ

wnd.mainloop()

今回の解説範囲について

前回は以下の2つの大きな観点で、特に1.に関して解説しました。

1. オブジェクトクラス
2.処理の流れ

本記事では、2.の部分の以下の分類の中で、b.について解説します。
b.は付録のように見えるかもしれませんが、これは画面処理が大半を占めていたからであって、下記説明文が悪いせいでもあります。😂
中核となる処理になるため、重要です。

a.画面処理
b.画面以外のゲームに関する処理

ゲームの開始と画面準備処理

前回に解説した下図の”メイン”がゲームの開始処理、”クラスtkAppのウィンドウオブジェクト”が画面の準備処理を行っています。
オブジェクト の作成や画面の準備処理のみであり、ゲームの流れとしての重要な処理は行っていません。

モンスターロード:メインとウィンドウの処理の流れ
処理の流れ その1

画面関連と中核の処理

ウィンドウオブジェクトの処理を下図に示しますが、画面関連と中核の処理(色付き部分)に分かれます。

処理の流れ その2

ゲームとしての中核処理は、以下の2箇所に集中しています。

1.ゲームをスタートする(game_start関数)
2.モンスターを選択して戦う(choose_monster関数)

以降、上記の処理について解説します。

ゲームをスタートする(game_start関数)

この関数はスタートボタンを押すか、メニューから開始したときに呼ばれて実行される関数です。
プログラムを以下に示します。

    def game_start(self):               # ゲーム開始時の設定をする
        self.change_image(self.backimg) # 画像を分かれ道の画像にする(初回の変更)

        self.textbox.delete("1.0", "end")

        self.text_r = 1      # テキストの行位置

        self.putText("3つの分かれ道があります。")
        self.putText("どれに進みますか?")

        self.me = me()                      # 自分のクラスオブジェクトを作成する

        self.change_score(self.me.score)    # 得点(スコア)を表示する
        self.change_hp(self.me.HP)          # 生命点(HP)を表示する

        self.enable_dir_button()            # 方向ボタンを全て有効にする

コメントにも記載していますが、以下の流れで処理をしています。

1.画像を分かれ道の画像にする(初回の変更)
2.テキストボックス内を消去
3.テキストの行位置を1に設定
4.“3つの分かれ道があります。””どれに進みますか?”を表示
5.自分(プレーヤー)のクラスオブジェクトを作成する
6.得点(スコア)、生命点(HP)を表示する
7.方向ボタンを全て有効にする

上記のほとんどすべては、前回の記事で解説済みですので、リンクをクリックして参照してください。
本記事では、3のみがポイントです。

3.の処理はテキストボックスの出力処理に必要となる行位置を1に初期化する処理です。
この現在行は、以下のputText関数で呼び出すself.textbox.insert関数がテキストを挿入する先の行位置に渡されます。
そして、その後、”self.text_r += 1″で、1加算されます。
以降、putText関数が呼び出されるたびに、テキストボックスの最終行にテキストを挿入し続けます。

    def putText(self,text):
        # 次の行にテキストを表示する
        self.textbox.insert(str(self.text_r)+'.0', text + '\n')
        self.textbox.see('end')             # 最後の行にスクロールさせる
        self.text_r += 1                    # 次に表示させる行を進める

モンスターを選択して戦う(choose_monster関数)

この関数のプログラムを以下に示します。

    def choose_monster(self,dir):

        if self.me.HP <= 0 :        # すでに生命点(HP)が無くなっていた
            return

        self.disable_dir_button()   # 方向ボタンを全て無効にする

        mon_no = rd.randint(1,3)    # 1~3の乱数を発生させモンスター番号にする
        mon_no = mon_no + dir - 1   # 進んだ方向(dir)でモンスターを変更する

        if mon_no > 3 :             # 変更した結果が3を超えたら
            mon_no = mon_no - 3     # 1に修正する(モンスターは1~3)のため

        if mon_no == 1 :            # 1はスライム
            monster = self.slime
        elif mon_no == 2 :          # 2はゴブリン
            monster = self.goblin
        else :                      # 3はドラゴン
            monster = self.dragon

        self.change_image(monster.image)    # モンスターの画像を表示する
        self.putText(monster.name + "が現れた!")
        monster.attack(self.me)             # モンスターが出現する
        self.putText("攻撃されてHPが" + str(self.me.HP) + "になった")

        if self.me.HP <= 0 :            # 生命点(HP)が無くなった
            self.putText("やられた")
            self.putText("")
            self.putText("--- GAME OVER ---")
            return

        self.me.score += monster.exp        # 経験値を得点(スコア)に加算する
        self.change_score(self.me.score)
        self.change_hp(self.me.HP)

        app.update_idletasks()              # 画面を強制的に更新
        time.sleep(1.0)                     # チョット待つ
        self.change_image(self.backimg)     # 画像を分かれ道の画像にする
        self.enable_dir_button()            # 方向ボタンを全て有効にする

実は上記の処理は、”【第6回】テキストアドベンチャーゲームで基本構文を学ぶ(解説編)“の内容を持ってきて改造したものです。

大きく分けて以下の処理になります。

1.以降の処理を行なうための前処理
2.モンスターの選択処理
3.戦闘処理
4.戦闘結果反映処理
5.画面更新と後始末処理

以降の処理を行なうための前処理

最初の下記の行は、すでに生命点(HP)が無くなっていた場合に関数の呼び出し元に即座に戻って、画面表示もなにも動かないようにしています。
choose_monster関数は、”処理の流れ その2”の図のとおり、何箇所から何度も呼び出されるこのゲームの中枢の処理であるため、呼び出された冒頭でこのような処理が必要となります。

        if self.me.HP <= 0 :        # すでに生命点(HP)が無くなっていた
            return

次に下記の行ですが、これは、この後の処理を行なうにあたって、方向ボタンが有効なままであると不都合になるために加えている処理です。

        self.disable_dir_button()   # 方向ボタンを全て無効にする

ウィンドウアプリケーションは、 イベント処理 と呼ばれる処理の塊なので、ある処理を実行している途中に、何らかのイベントが発生して同時に動く処理が動作します。
そのため、予期しない動作をしないように注意してプログラミングする必要があります。

モンスターの選択処理

次の行では、rd.randint()という関数を実行しています。

        mon_no = rd.randint(1,3)    # 1~3の乱数を発生させモンスター番号にする

ここで、”rd.”は何かですが、これは、定義部にある下記の乱数を扱うモジュールです。

import random as rd         # 乱数を扱うモジュール

乱数とは、常に不規則(random= ランダム )に変化する数のことをいいます。
そして、”import random as rd”の行では、randomモジュールを”rd”として(=as)インポートしています。(asは長くなる記述を短縮するために使います)

よって、”rd.randint()”は、randomモジュールのrandint関数を実行することを意味します。
randint関数では、入力を2つ指定します。
”mon_no = randint(1,3)”は、1から3までの乱数を発生させて、mon_no変数に入れています。

ここで、進んだ方向によって3種類のモンスターのどれかが決まるようにするために、下記の行で計算をしています。

        mon_no = mon_no + dir - 1   # 進んだ方向(dir)でモンスターを変更する

実は、dirの値を足さなくても、違うモンスターが出てくるので、ゲームのプレーヤにとっては、選んだ方向と関係なく出てきているのか、選んだから出てきているのかは分かりません。
しかし、方向を選んだ後に、『実はモンスターはここにいたのだー』というような表示をする場合には、役に立ちます。

さて、次の”if mon_no > 3 :”ですが、

        if mon_no > 3 :             # 変更した結果が3を超えたら
            mon_no = mon_no - 3     # 1に修正する(モンスターは1~3)のため

これは、dirを足した結果、mon_noの値が3を超えてしまった場合に、”mon_no = mon_no – 3”として、1~3におさまるように引き算しています。

次に、以下の行でようやく、mon_noに従って、該当するモンスターの オブジェクト をmonster変数で参照できるように代入しています。

        if mon_no == 1 :            # 1はスライム
            monster = self.slime
        elif mon_no == 2 :          # 2はゴブリン
            monster = self.goblin
        else :                      # 3はドラゴン
            monster = self.dragon

戦闘処理

次の行で、

        self.change_image(monster.image)    # モンスターの画像を表示する
        self.putText(monster.name + "が現れた!")
        monster.attack(self.me)             # モンスターが出現する
        self.putText("攻撃されてHPが" + str(self.me.HP) + "になった")

モンスターの画像を表示して
『~が現れた!』のメッセージを表示した後、
攻撃力で生命点を減らす処理を行い、
『攻撃されてHPが~になった』のメッセージを表示しています。

そして、以下の行で、生命点がなくなってしまったら、GAME OVERとしてchoose_monster関数の呼び出し元に戻っています。

        if self.me.HP <= 0 :            # 生命点(HP)が無くなった
            self.putText("やられた")
            self.putText("")
            self.putText("--- GAME OVER ---")
            return

戦闘結果反映処理

生き残った結果、戦闘結果として経験値を得るため、得点(スコア)に経験値を加算して反映しています。
また、得点と生命点を画面に表示します。

        self.me.score += monster.exp        # 経験値を得点(スコア)に加算する
        self.change_score(self.me.score)
        self.change_hp(self.me.HP)

画面更新と後始末処理

次に以下の処理を行います。

        app.update_idletasks()              # 画面を強制的に更新
        time.sleep(1.0)                     # チョット待つ

”app.update_idletasks()”は画面を強制的に更新して表示します。
そして、”time.sleep(1.0)”で1秒間だけ処理を停止しています。

上記の処理は、後ろで”self.change_image(self.backimg)” の行で、分かれ道の画像を表示させるために行っています。
もし、モンスターの画像を表示させたまま、分かれ道の方向ボタンを押させていいのであれば、上記処理は必要ありません。
”wnd.mainloop()”関数の中で”app.update_idletasks()”が実行され、画面が更新されるためです。

モンスターとの戦闘は1秒間だけにして、その後、別れ道を表示させたいからこそ、上記の処理が必要になります。

最後に以下の処理を行います。

        self.change_image(self.backimg)     # 画像を分かれ道の画像にする
        self.enable_dir_button()            # 方向ボタンを全て有効にする

別れ道の画像の表示と、この関数の冒頭で無効化した方向ボタンを有効化します。

最後に

今回はゲームの流れに沿って、プログラムの中身を解説しました。

以下のような構造でした。

・ ゲームの開始と画面準備処理
・ 画面関連と中核の処理
 - ゲームをスタートする(game_start関数)
 - モンスターを選択して戦う(choose_monster関数)
  1.以降の処理を行なうための前処理
  2.モンスターの選択処理
  3.戦闘処理
  4.戦闘結果反映処理
  5.画面更新と後始末処理

中核の処理は、骨子となるものであることがわかります。

また、中核の処理をどのように作り上げるかで、今後のプログラムの改良や機能追加のしやすさが決まります
ゲームにおいてはゲームの新しい方向性も決まってきますし、新しいゲームを派生させて誕生させやすくなります

上記の構造は、モンスターとの戦闘を行なうターン制の進行形ゲームであれば、ほとんど当てはまると思います。

また、細かな所で言えば、今回、モンスタークラス(monster)には攻撃力と経験値しかありません。
それでもわざわざクラスとして定義したのには、拡張性を持たせたいという理由があるため
です。
魔法力(MP=Magic Point)や、逃げる・攻撃をかわすなどの挙動の追加もしやすくなっています。

次回は何をするかは未決定ですが、面白い記事になるよう努めますので、ご期待ください。

この記事へのお問い合わせや、一歩踏み込んだサポートが必要な場合は、
ホームページからメールで受け付けています。お気軽にご連絡ください。

0

コメント

タイトルとURLをコピーしました