「バトル画面を作りたい!!」Python初心者でもRPGを作りたい!!#2 ver2.0.0

目次

前回のあらすじ

前回はコマンドRPGのバトルっぽい動作を行うCUIプログラムを初心者なりに作ってみた。

あわせて読みたい
Python初心者でもコマンドRPGを作りたい!!#1「バトルの基礎」ver1.0.0 【関係ないかもあるかもしれない前置き「ひらめき」】 最近、「TRICK」シリーズがアマプラで配信されてたのでドラマの初回シーズンから見直している。 TRICKをご存じな...

けれど、今回の記事にまとめた変更によってその殆どが泡沫のごとく消え去ったので前回までの努力は水の泡。泣けるぜ

関係あるっちゃあるかもしれない前置き

自分でゲームを0から作る時、いちばん大事な能力とはなんだろう。私は想像力がその一つだと思う。頭の中で「あれもやりたい!これもやりたい!」という想像を膨らませ続けること(やり過ぎは良くない)、これがたまらなく楽しい。それにまた、幸運なことに私はその道のスペシャリストであるので全くそれに苦労をしない

正直ココが楽しみのピークだ。だってその妄想もそれを実際に形にできるコーディング能力がないとタダの妄想で終わってしまうのだから

そんな私の中二妄想力の原点、起源、礎となる存在がファイナルファンタジーシリーズである。特にゲームに関してはその影響がめっぽう強い。

たくさんの作品があるけれど、中でも最も印象的というか「This is the FINALFANTASY for me」とも言える存在なのが

FINALFANTASY VII
FINALFANTASY VIII
FINALFANTASY XIII
FINALFANTASY XIII-2
LIGHTNING RETURNS FINALFANTASY XIII
FINALFANTASY XV (Versus XIII)

である。一番好きなのはファイナルファンタジー9なのだけど、FFのイメージと言うとこの6作品がやっぱり強い。
FFシリーズ経験者ならこれらタイトルの共通点がパッと頭に浮かぶと思うのだけれど、まさにその通りだ。(ノムリッシュは卒業済み、ピクリともこない)

わからない人は、「野村哲也 キャラクターデザイン」とかで検索してくれればなんとなーくわかるんじゃないかと思う。

んで、それもあってこの頃FF8のオープニングとかヴェルサス(であった頃)のトレーラーを制作のヒントとしてたまに見ているのだけれど、なんだかCUI、つまり文字列じゃ面白くないなぁと思えてきた。例えばFF8と前回のバトル画面を比べてみるとこんな感じ。

(ゲームシステムとは対極的に)シリーズでも随一のシンプルなUIを採用したFF8と”とりあえず情報を詰め込んだ”コレを比べるのも酷かもしれないが、やっぱり文字列よりきちんとしたウィジェットを配置したほうが操作しやすい。

もっというと、FF8のようにプレイヤーキャラクターを表示しようとした思った時「文字列」で表現するとなるとこうなる

O
/|\
/ \

うーん。これじゃあなんだか「映え」がないし、戦っていて面白くもない、それに”棒”じゃあ「このキャラクターはHPが1000あります」と表示されても説得力のかけらもないし、何よりイケてない。つまずいただけでポキッっと逝ってしまいそうだもの。

文字列のゲーム。いわゆるCUIゲームでも「ローグ」とかとんでもなく面白く深いゲームがあることは知っているけれど、私が作りたいのは上のようなFFライクなモノなので方向性が違うような気がする。

いうわけで今回からGUI(グラフィカルインターフェイス)を導入してみた。

ソースコード

ソースコードは長いのでGoogleDriveに置いておきます。そこからダウンロードしてGUI_BATTLE.pyを実行して遊んでみてください。

こちら

動作環境や本連載のテーマについてはこちらの記事にまとめてあります。

あわせて読みたい
やねうら流プログラミングカテゴリの仕様書:動作環境、テーマ、教材…etc 当サイト(以下やねうら)のプログラミングカテゴリについての仕様書。(都度更新) 【やねうらのプログラミングカテゴリについて】 やねうらに投稿されているプログラ...

前回(ver)からの変更点

  • CUIからGUIへのエヴォルブ
  • バトルシステムの一部変更(ターン制バトル→リアルタイムタイムバトル・被ダメージ関数をより簡略化・「重量」ステータス値の導入等)

最低限必要な知識

このプログラムはPythonのTkinter・TtkinterというGUI作成モジュールとThreadingモジュールによる並列処理を使って作成されているので、tkinterと並列処理という単語を見てちんぷんかんぷんな方は厳しいかもしれません。

けれど、私も見様見真似で試行錯誤しながら作ったので、これから紹介するような”とりあえず動けるアプリ”程度なら味付け無しの寒天くらい薄味の知識でも出来ます。

尚、GUI以外の要素(特にバトル系の)については前回からほとんど変更がなく、またボツになる予感がとってもプンプンしてるのでこの記事ではあまり触れません。

大まかな流れ

各UIの説明

バトルの流れ

今回は、行動ターンがプレイヤー→敵→プレイヤーと順に切り替わっていく前回までのオーソドックスなターン制を廃し、各々の重量値(行動スピード)によって行動が可能となるリアルタイムバトルを作ってみた。

  • キャラクターが何か行動を起こすには特定の量の行動ポイントを消費する必要がある。
    例えば「ATTACK」コマンドなら10消費。「ARTS」コマンドなら20消費というように
  • 行動ポイントは一定間隔で増加しつづけ、最大値(50)まで到達すると今度はAPを回復し始める。行動ポイントもAPも最大なのなら増加処理を行われない。
    また、行動ポイントの増加率はキャラクターの重量値によって増減する。重量が軽いほうが1秒あたりの行動ポイントの増加率は大きく、重量が重ければ重いほど増加率は少なくなる。
  • APは特定のコマンドを実行する際に行動ポイントとともに消費される。

すっごい大雑把な流れ

このプログラムは

  • CHARACTERファイル:プレイヤーや敵のclassを書き込んである
  • GUIBATTLEファイル:GUIとバトル部分の処理

という2つから構成されている。CHARACTERファイルに関しては前回と一部の関数名こそ違うもののほとんど同じなので本記事では触れない。

GUIBATTLEファイルは

  • 各classの呼び出し
  • 関数(GUIの更新・勝敗の判定等)
  • tkinterウィジェット
  • 関数を並列処理するThread

の4つから構成されている。classの呼び出しは文字通りのことをやっているので説明は略して今からご説明するのは下の3つ。

各部分の解説

running = True

ゲームが実行中であるかどうかを識別するグローバル関数。プレイヤーか敵どちらかが死亡した場合(VICTORY/GAMEOVER)やexit_applicationボタンを押された場合は = False になる。

runningがFalseになることによってrunning=Trueをwhileループの条件にしていた関数はbreakする。

各関数

def exit_application():
    global running
    root.destroy()
    running = False

アプリを終了させる関数。実行された場合、runnnigをFalseにし、GUIを終了させる。

def judge():
    global running
    while running:
        if enemy.hp < 0:
            running = False
            victory_canvas = tk.Canvas(root, bg='#ffffff', width=1000,
                                       height=1000)
            victory_canvas.place(x=50, y=50)
            tk.Label(text='VICTORY',
                     font=('BIZ UDGothic', '100', 'bold')).place(x=300, y=400)
            tk.Label(text='thank you for playing!!:)').place(x=300, y=500)
            break
        if player.hp < 0:
            running = False
            game_over_canvas = tk.Canvas(root, bg='#ffffff', width=1000,
                                         height=1000)
            game_over_canvas.place(x=50, y=50)
            tk.Label(text='GAME OVER',
                     font=('BIZ UDGothic', '100', 'bold')).place(x=300, y=400)
            tk.Label(text='thank you for playing!!:)').place(x=300, y=500)
            break
        time.sleep(3)

勝利orゲームオーバーを判断する関数。後述するTreadsで回すことによってGUIを実行している間も並列で処理されている。
もしどちらかのHPがゼロになった場合、対応するtk.Canvasとtk.Labelを表示する。

if player.hp < 0:

if enemy.hp < 0:

def enemy_actions():
    while running:
        time.sleep(enemy.action_speed)
        enemy.attack(player)

敵を行動させる関数。処理進行をtime.sleepで敵の重量値から計算されるaction_speed分止めることによって、高速で攻撃してこないようにしている。
enemy.attack(player)はattackする相手をplayer変数で指定している。

def plus_gauge():
    while running:
        if player.action_gauge == player.MAX_action_gauge:
            if player.ap < player.MAX_ap:
                player.ap += 1
                print('アクションポイント増加______')
        if player.action_gauge < player.MAX_action_gauge:
            player.action_gauge += 1
            print('アクションゲージ増加______')

        else:
            print('パスしちゃうよん')
            pass
        time.sleep(player.action_speed)


def update_action_var(action_var, var):
    while running:
        var.set(player.action_gauge)
        action_var.update()
        time.sleep(0.1)

行動ポイントと連動して上下するゲージバーの処理。


plus_gaugeでは、行動ポイントとAPどちらを増加させるかの判断
updateでは、guiを0.1秒間隔で上書きすることによって「ゲージが増減する動き」を演出している

def update_info(info_label):
    while running:
        info_label.config(text=MANAGEMENT.INFO_TEXT)
        info_label.update()
        time.sleep(0.1)
        # print('update_info呼び出されてます')


def clear_info():
    while running:
        MANAGEMENT.INFO_TEXT = ''
        time.sleep(1)
        print('clear_info呼び出されてます')

戦闘の状況を知らせてくれるinfoくんの処理。


これも先のゲージと同じくupdateで表示の上書きをしている
clearはinfoを空にする処理

def update_player_hp(player_hp_label):
    while running:
        player_hp_label.config(text=str(player.hp))
        player_hp_label.update()
        time.sleep(0.1)


def update_player_ap(player_ap_label):
    while running:
        player_ap_label.config(text=str(player.ap))
        player_ap_label.update()
        time.sleep(0.1)


def update_player_gauge(player_gauge_label):
    while running:
        player_gauge_label.config(text=str(player.action_gauge))
        player_gauge_label.update()
        time.sleep(0.1)

playerのhp/ap/行動ポイントを上書きする処理。

tkinter

font_ap = f.Font(family='BIZ UDGothic', size=40, slant='italic')
    font_info = f.Font(family='BIZ UDGothic', size=30, weight='bold')
    font_command = f.Font(family='BIZ UDGothic', size=20, weight='bold')
    font_name = f.Font(family='BIZ UDGothic', size=25, weight='bold')

    # window
    root.geometry('1100x1100')
    root.title('archetype ver2.0.0')

各ステータス値のフォントの設定とwindowの大きさと名前の設定

フォントはMacに標準搭載のBIZ Gothicを使用しているからMacならば心配いらないと思うけれど、BIZ Gothicが見つからなくてエラーが出るのならば適当なフォントに書き直せば解消出来ると思う。

def gui():
    def toggle_ex_button():
        if ex_arts_1_button.winfo_ismapped():
            ex_arts_1_button.place_forget()
            ex_arts_2_button.place_forget()
            ex_arts_3_button.place_forget()
            ex_arts_4_button.place_forget()
            ex_arts_5_button.place_forget()
            ex_arts_6_button.place_forget()
        else:
            ex_arts_1_button.place(x=200, y=800)
            ex_arts_2_button.place(x=200, y=840)
            ex_arts_3_button.place(x=200, y=880)
            ex_arts_4_button.place(x=350, y=800)
            ex_arts_5_button.place(x=350, y=840)
            ex_arts_6_button.place(x=350, y=880)

最初の動画を見ていただけたらわかるように、EX-ARTSボタンは子EX-ARTSボタンを表示する親の役割をしている。その表示・非表示の切り替えをする関数。

# visual
    canvas = tk.Canvas(root, bg='#ffffff', width=1000, height=1000)
    canvas.place(x=50, y=50)
    command_canvas = tk.Canvas(root, bg='#ffffff', width=1000, height=300)
    command_canvas.place(x=50, y=750)
    info_kun = tk.Label(text='(info)',
                        foreground='#292d2e', background='#ffffff',
                        font=font_info)
    info_kun.place(x=70, y=700)
    info_label = tk.Label(text=f'{MANAGEMENT.INFO_TEXT}',
                          foreground='#292d2e', background='#ffffff',
                          font=font_info)
    info_label.place(x=180, y=700)

    # character
    character_img = tk.PhotoImage(file='io.png', height=400, width=400)
    canvas.create_image(600, 200, image=character_img, anchor=tk.NW)

    enemy_img = tk.PhotoImage(file='sample_enemy.png', height=400, width=400)
    canvas.create_image(100, 400, image=enemy_img, anchor=tk.NW)

    # status
    chara_name = tk.Label(text='KAIN', foreground='#292d2e',
                          background='#ffffff', font=font_name)
    chara_name.place(x=650, y=770)

    hp_label = tk.Label(text='HP', foreground='#292d2e', background='#ffffff',
                        font=font_name)
    hp_label.place(x=750, y=770)
    player_hp_label = tk.Label(text=player.hp, foreground='#292d2e',
                               background='#ffffff', font=font_ap)
    player_hp_label.place(x=780, y=755)

    ap_label = tk.Label(text='AP', foreground='#292d2e', background='#ffffff',
                        font=font_name)
    ap_label.place(x=900, y=770)
    player_ap_label = tk.Label(text=player.ap, foreground='#292d2e',
                               background='#ffffff', font=font_ap)
    player_ap_label.place(x=930, y=755)

先ほど設定したwindowの中身の設定。placeを使用して座標でボタンやラベルが配置できるようにしてある。

action_var = ttk.Progressbar(root,
                                 orient="horizontal",
                                 maximum=player.MAX_action_gauge,
                                 mode="determinate",
                                 variable=var,
                                 length=350)
    action_var.place(x=650, y=800)
    action_var_label = tk.Label(text=player.action_gauge, foreground='#292d2e',
                                background='#ffffff', font=font_ap)
    action_var_label.place(x=600, y=770)

連動ゲージの設定。ttkinterのプログレスバーを使用している。

# command_button
    attack_button = tk.Button(root, text='ATTACK',
                              command=lambda: player.attack(enemy),
                              width=8,
                              height=1,
                              font=font_command)
    attack_button.place(x=75, y=800)

「ATTACK」コマンドボタン、「ARTS」と「ARCHE」ボタンも同様。

commandでボタンを押したときにplayerクラスの対応した関数を呼び出すように設定してある。lamdaを使わないとうまく作動しないので必須。

ex_arts_1_button = tk.Button(root, text='EX-ARTS_1',
                                 command=lambda: player.ex_arts_1(enemy),
                                 width=8,
                                 height=1,
                                 font=font_command)
    ex_arts_1_button.place_forget()

子EX-ARTSボタンの設定。最初は表示されていないため、place_forgot()を使い非表示にしている。

Thread

judge_thread = threading.Thread(target=judge)

    gauge_thread = threading.Thread(target=plus_gauge)

    update_action_var_thread = threading.Thread(target=update_action_var,
                                                args=(action_var, var))

    update_info_thread = threading.Thread(target=update_info,
                                          args=(info_label,))

    clear_info_thread = threading.Thread(target=clear_info)

    update_player_hp_thread = threading.Thread(target=update_player_hp,
                                               args=(player_hp_label,))

    update_player_ap_thread = threading.Thread(target=update_player_ap,
                                               args=(player_ap_label,))

    update_player_gauge_thread = threading.Thread(target=update_player_gauge,
                                                  args=(action_var_label,))

    enemy_thread = threading.Thread(target=enemy_actions)

    gauge_thread.start()
    update_action_var_thread.start()
    update_info_thread.start()
    clear_info_thread.start()
    update_player_hp_thread.start()
    update_player_ap_thread.start()
    update_player_gauge_thread.start()
    judge_thread.start()
    #enemy_thread.start()

updateを使った関数達をThreadを使って並列処理するための設定。

Threadに引数を渡すときはタプルで渡さないとだめなので一つの引数でも’ ,’を入れてある。
数が多すぎて処理がきれいに気持ちよく行われてないのであんまり良くない。

まとめと課題と次回予告と

今回はPythonのGUI作成モジュールtkinterを使用してアプリを作ってみた。

改めて前回のそれと比べてみると雲泥の差

そこそこゲームらしくなったんじゃないだろうか

けれど、もちろん課題も山積みだ。

スレッド数が多すぎて処理遅延が起こるとか何が起こってるのかわかりにくいとか動きがなくてつまらないとか…頭を抱えても重すぎて支えられないくらいたっくさんある。それにUIもなんだか気に入らないしまたリメイクだな。たぶん

とりあえず、処理遅延は最優先で修正するとしてその次はやっぱり仲間の実装だろうか?
個人的にFF13以降の仲間が直接操作できないシステムは好きじゃないので(ガンビットも面白いけどあんま好きじゃない)やっぱり仲間は逐一指示できるようにしたい。でもそうすると現在の(アクションゲージ量で行動が増えていく)バトルシステムは複雑すぎてつまらない、そもそもあまり気に入ってすらない。

バトルシステムはオーソドックスで5分で理解できるシンプルなヤツ、あるいはものすごく突飛で独特で取っ付き難いのに慣れたらとっても面白いヤツのどちらか両極端がいい。シャドウハーツのジャッジメントリングとかエンドオブエタニティのトライアタックバトルとかね。ゼノギアスの△▢◯を組み合わせて「はいやーっ」するやつも良いね。

まぁでもその前にやっぱりその前にとっても楽しみなFF7リバースを消化しないと

次回

目次