見出し画像

Pythonでマイクラを作る ⑪起動画面を実装する

Pythonでマイクラを作る 第11回目です。今回はユーザーインターフェースの実装を続けます。まず画面上にテキストを表示できるようにします。デバッグモードでは、プレイヤーの情報(位置や向きなど)を画面上で確認できるようにします。最後、起動画面(スプラッシュスクリーン)を表示して、本格的なゲームに近づける方法を学びます。

フォントの準備

フォントとはコンピューターで使われる文字の書体のことです。Panda3D のデフォルトのフォントは日本語に対応していないため、自分で日本語対応フォントを準備します。

PixelMplus(ピクセル・エムプラス)は、8bitゲーム機のビットマップフォント風のフリーのフォント(True Type Font)です。こちらを使わせていただきます。

# ディレクトリ構造
Documents/
  ├ pynecrafter/
  │  ├ fonts/
  │  │  ├ PixelMplus10-Regular.ttf
  │  │  ├ PixelMplus12-Regular.ttf
  │  │  

ダウンロードしたフォントは、fontsフォルダを作って、その中に保存してください。これでフォントの準備は完了です。

画面にテキストを表示する

"""src/utils.py"""
from direct.gui.DirectGui import *
from panda3d.core import *


class DrawImage(OnscreenImage):
    def __init__(self, parent=None, image=None, scale=(1, 1, 1), pos=(0, 0, 0)):
        super().__init__(
            parent=parent,
            image=image,
            pos=pos,
            scale=scale,
        )
        self.setName(image)
        self.setTransparency(TransparencyAttrib.M_alpha)


class DrawText(OnscreenText):
    def __init__(self, parent=None, text='', font=None, scale=0.07, pos=(0.05, -0.1), fg=(0, 0, 0, 1), bg=(0, 0, 0, 0.1)):
        super().__init__(
            parent=parent,
            text=text,
            align=TextNode.ALeft,
            pos=pos,
            scale=scale,
            font=font,
            fg=fg,
            bg=bg,
            mayChange=True,
        )
        self.start_time = None

utils.py ファイルの中に、DrawTextクラスを追記します。Panda3D の文字表示を操作する OnscreenTextクラスを継承して、super().__init__()により、OnscreenTextクラスのプロパティーを初期化します。
引数 scale は文字サイズを指定します。引数 fg は前景(ForeGround)を意味し、文字色(r、g、b、a)を指定します。引数 bg は背景(BackGround)を意味し、背景色(r、g、b、a)を指定します(デフォルト値は薄いグレー (0, 0, 0, 0.1) に指定した)。引数mayChange は文字を変更する場合に True を指定します。
インスタンス変数start_time は既定時間で文字が消える実装のときに使うので、この時点で指定しておきましょう。

"""src/mc.py"""
from direct.showbase.ShowBase import ShowBase
from panda3d.core import *
from . import *


class MC(ShowBase, UserInterface):
    def __init__(self, ground_size=128):
        # ShowBaseを継承する
        ShowBase.__init__(self)
        self.font = self.loader.loadFont('fonts/PixelMplus12-Regular.ttf')  # 追記
        UserInterface.__init__(self)

mc.py を修正します。 
インスタンス変数font に使用するフォントを代入します。self.loader.loadFontメソッドでフォントのパスを指定して、PixelMplus(ピクセル・エムプラス)フォントを読み込みます。ここでフォントを指定しておくと、UserInterface クラスでフォントを利用できるようになります。

"""src/user_interface.py"""
from time import time
from math import *
from panda3d.core import *
from .utils import *  # 修正


class UserInterface:
    hotbar_blocks = [
        ['stone', ['0-1']],
        ['grass_block', ['0-3', '0-0', '0-2']],
        ['dirt', ['0-2']],
        ['white_wool', ['4-0']],
        ['blue_wool', ['11-1']],
        ['red_wool', ['8-1']],
        ['glass', ['3-1']],
        ['gold_block', ['1-7']],
        ['bricks', ['0-7']],
    ]

    def __init__(self):
        self.selected_hotbar_num = 0
        self.selected_block = UserInterface.hotbar_blocks[0]

        # draw hotbar
        for i, block in enumerate(UserInterface.hotbar_blocks):
            block_image_name = block[1][0]
            image = DrawImage(
                parent=self.a2dBottomCenter,
                image=f'textures/{block_image_name}.png',
                scale=(16 / 164, 16 / 164, 16 / 164),
                pos=((i - 4) * 0.22, 0, 20 / 164)
            )
            self.set(f'bar{i + 1}', image)
        self.hotbar = DrawImage(
            parent=self.a2dBottomCenter,
            image='images/hotbar1.png',
            scale=(1, 1, 20 / 164),
            pos=(0, 0, 20 / 164)
        )
# ---ここから追記---
        # text window
        how_to_use = \
            '移動: W A S D\n' \
            'アイテム選択: 123456789\n' \
            'ブロックを置く: 左クリック\n' \
            'ブロックを壊す: 右クリック\n' \
            'カメラの切り替え: T\n' \
            '操作説明を表示/非表示: X'
        self.text_window = DrawText(
            parent=self.a2dTopLeft,
            text=how_to_use,
            font=self.font,
        )

        # console window
        wellcome_text = 'ようこそ Pynecrafter!'
        self.console_window = DrawText(
            parent=self.a2dTopLeft,
            text=wellcome_text,
            font=self.font,
            pos=(0.05, -1.5)
        )
        self.console_window.start_time = time()  # 実行した時間を start_time に記録
# ---ここまで追記---
        # select hotabar
        self.accept('1', self.select_hotbar, [1])
        self.accept('2', self.select_hotbar, [2])
        self.accept('3', self.select_hotbar, [3])
        self.accept('4', self.select_hotbar, [4])
        self.accept('5', self.select_hotbar, [5])
        self.accept('6', self.select_hotbar, [6])
        self.accept('7', self.select_hotbar, [7])
        self.accept('8', self.select_hotbar, [8])
        self.accept('9', self.select_hotbar, [9])

    def select_hotbar(self, i):
        self.selected_hotbar_num = i - 1
        self.selected_block = UserInterface.hotbar_blocks[i - 1]
        self.hotbar.setImage(f'images/hotbar{i}.png')
        self.hotbar.setTransparency(TransparencyAttrib.M_alpha)

user_interface.py にテキストを表示するコードを追記します。DrawTextクラスのインスタンスを生成することで、右上の操作説明と左下のコンソールを表示します。追記部分を詳しく見ていきましょう。


# ---ここから追記---
        # text window
        how_to_use = \
            '移動: W A S D\n' \
            'アイテム選択: 123456789\n' \
            'ブロックを置く: 左クリック\n' \
            'ブロックを壊す: 右クリック\n' \
            'インベントリの表示/非表示: E\n' \
            '操作説明を表示/非表示: X'
        self.text_window = DrawText(
            parent=self.a2dTopLeft,
            text=how_to_use,
            font=self.font,
        )

        # console window
        wellcome_text = 'ようこそ Pynecrafter!'
        self.console_window = DrawText(
            parent=self.a2dTopLeft,
            text=wellcome_text,
            font=self.font,
            pos=(0.05, -1.5)
        )
        self.console_window.start_time = time()  # 実行した時間を start_time に記録
# ---ここまで追記---

上記のコードがテキストを表示するコードです。
DrawTextクラスに、表示したい文字列、表示したいノード、フォントを指定してインスタンスを生成します。self.a2dTopLeft は2次元ノード(aspect2)の左上を表します。 引数pos=(0.05, -1.5)により、self.console_window を右に 0.05、下に 1.5 動かします。
self.console_window.start_time = time()により、実行した時間をインスタンス変数start_time に記録しておきます。コンソールのテキストを指定した時間で消すときに使います。

テキストを表示

08_01_main.py を実行して、テキスト表示を確認します。左上に操作説明が、左下にコンソール文字列が表示できたら成功です。
次に、文字を消す方法を実装します。操作説明は常に表示されていると邪魔なので、ユーザーが 「x」キーを押すと、表示/非表示を切り替えられるようにします。そしてコンソールの文字列は 3秒経ったら自動で消えるようにしましょう。

テキストを消す

"""src/user_interface.py"""

コンストクタの最後に追記
        
        # テキストウインドウを表示/ 非表示
        self.accept('x', self.toggle_text_window)

        # スクリーンを更新
        self.taskMgr.add(self.screen_update, "screen_update")

ユーザーが「x」キーを押したとき、操作説明文を消すコードを実装します。
self.accept('x', self.toggle_text_window)により、「x」キーを押したとき toggle_text_windowメソッドが実行されます。
コンソールの文字列が3秒後に消える実装のため、タスクマネージャーのtaskMgr.addメソッドで screen_updateメソッドを指定します。

"""src/user_interface.py"""

メソッドの追加

def toggle_text_window(self):
    if self.text_window.isHidden():
        self.text_window.show()
    else:
        self.text_window.hide()

def screen_update(self, task):
    # 3秒でコンソールの文字を消す
    if self.console_window.getText() and \
            self.console_window.start_time and time() - self.console_window.start_time > 3:
        self.console_window.setText('')
    return task.cont

toggle_text_windowメソッドは、テキストウインドウの表示/非表示を管理します。isHiddenメソッドで、テキストウインドウが非表示(hide)であるか確認して、表示⇄非表示を切り替えます。
screen_updateメソッドは、フレームごとに画面を更新します。
time() - self.console_window.start_time はコンソール文字列が表示されてからの経過時間を表し、経過時間が3秒以上になったら、self.console_window.setText('')により、テキストを消します。

08_01_main.py を実行して、所定の動作が行われるか確認してください。
次にデバッグモードを実装します。操作説明の代わりにプレイヤーの情報を画面上で確認できるようにします。

プレイヤーの情報を表示する

"""src/user_interface.py"""

def screen_update(self, task):
    # 3秒でコンソールの文字を消す
    if self.console_window.getText() and \
            self.console_window.start_time and time() - self.console_window.start_time > 3:
        self.console_window.setText('')
    # デバッグモード
    if self.mode == 'debug':
        position = self.player.position
        direction = self.player.direction
        velocity = self.player.velocity
        text = f'player x: {round(position[0], 1)}\n' \
               f'player y: {round(position[1], 1)}\n' \
               f'player z: {round(position[2], 1)}\n' \
               f'player heading: {int(direction[0])}\n' \
               f'player pitch: {int(direction[1])}\n' \
               f'player roll: {int(direction[2])}\n' \
               f'player velocity x: {round(velocity[0], 1)}\n' \
               f'player velocity y: {round(velocity[1], 1)}\n' \
               f'player velocity z: {round(velocity[2], 1)}\n'
        self.text_window.setText(text)
    return task.cont

プレイヤーの位置、方向、速度を画面に表示するために screen_updateメソッドにコードを追記します。
フレームごとに、position、direction、velocity を取得して、テキストを書き換えてやります。

"""src/mc.py"""
from direct.showbase.ShowBase import ShowBase
from panda3d.core import *
from . import *


class MC(ShowBase, UserInterface):
    def __init__(self, ground_size=128, mode='normal'):  # 追記
        self.mode = mode  # 追記
        # ShowBaseを継承する
        ShowBase.__init__(self)
        self.font = self.loader.loadFont('fonts/PixelMplus12-Regular.ttf')
        UserInterface.__init__(self)


        # ウインドウの設定
        self.properties = WindowProperties()
        self.properties.setTitle('Pynecrafter')
        self.properties.setSize(1200, 800)
        self.win.requestProperties(self.properties)
        self.setBackgroundColor(0, 1, 1)

        # ブロック
        self.block = Block(self, ground_size)

        # プレイヤー
        self.player = Player(self)

        # ゲーム終了
        self.accept('escape', exit)

    def get(self, var):
        try:
            return getattr(self, var)
        except AttributeError:
            return None

    def set(self, var, val):
        setattr(self, var, val)

MCクラスを修正します。
コンストラクターの引数に mode='normal' を追記します。これでモードを指定してゲームを実行できるようになります。normal(通常モード)、debug(デバッグモード)の2つのモードを切り替えることができる設計にしました。

"""11_01_main.py"""
from math import *
from src import MC


class Game(MC):
    def __init__(self):
        # MCを継承する
        MC.__init__(self, ground_size=32, mode='debug')  # 追記

        # 座標軸
        self.axis = self.loader.loadModel('models/zup-axis')
        self.axis.setPos(0, 0, 0)
        self.axis.setScale(1.5)
        self.axis.reparentTo(self.render)

        # 壁
        for i in range(5):
            for j in range(3):
                self.block.add_block(i, 5, j, 'gold_block')


game = Game()
game.run()

08_01_main.py をコピーして、11_01_main.py を作成します。
MCクラスのコンストラクタを実行するコードに、引数mode='debug' を指定します。これでデバッグモードでゲームを実行できます。

プレイヤーの情報を表示

11_01_main.py を実行すると、デバッグモードでゲームが開始されます。プレイヤーの情報が画面左上に表示されます。プレイヤーを動かすと、プレイヤー情報が更新されます。今後は、このデバッグモードでプレイヤー情報を確認しながら開発を進めていきましょう。

最後に起動画面(スプラッシュスクリーン)を実装します。起動画面はゲームの演出として利用されます。起動画面が表示された 5秒後にゲームが開始されるように実装していきます。

起動画面の画像を作成

splash screen image

ドット絵エディターで起動画面に表示する画像を作成します。
上図は Aseprite で起動画面を作成中のスクリーンショットです。サイズは 120x80で作成し、出力時に 1200x800 にリサイズしました。右上のキャラクターはパンダのアバターで、このゲームが Panda3D で作られていることからデザインしました。
ご自分で作成されない方は以下のファイルをダンロードしてお使いください。imagesディレクトリに、pynecrafter_splash.png の名前をつけて保存してください。

aspect2d(2次元の専用ノード)

aspect2d

シーングラフについて説明します。
Panda3D では2次元のオブジェクトを render2dをルートとして、シーングラフで管理します。通常の2Dオブジェクトは aspect2dノードの下に配置します。2Dオブジェクトを操作(移動したり、消したり)するには、aspect2dノードの下に新しいノードを配置して、その中に2Dオブジェクトを配置します。

起動画面を表示し、5秒後に消す

 """src/user_interface.py"""

コンストラクタの最後に追記

        # スプラッシュスクリーン
        self.splash_screen_node = self.aspect2d.attachNewNode("splash_screen_node")
        self.splash_image = DrawImage(
            parent=self.splash_screen_node,
            image='images/pynecrafter_splash.png',
            scale=(3 / 2, 1, 1),
            pos=(0, 0, 0),
        )
        loading_text = 'creating a new world...'
        loading_text = '新しい世界を創造しています...'
        self.loading_text = DrawText(
            parent=self.splash_screen_node,
            text=loading_text,
            font=self.font,
            pos=(-.2, -.5, 0),
            scale=0.1,
        )
        self.taskMgr.doMethodLater(3, self.close_splash_screen, "close_splash_screen")

メソッドを追加

    def close_splash_screen(self, task):
        self.splash_screen_node.detachNode()
        self.console_window.start_time = time()  # 実行した時間を start_time に記録
        return task.done

起動画面を表示・非表示するコードを追記します。
self.aspect2d.attachNewNode("splash_screen_node")により、aspect2dノードの下に新しいノード(splash_screen_nodeノード)を作成します。DrawImageクラスを使って、スプラッシュスクリーンの画像を splash_screen_nodeノードに表示します。起動画面に表示するテキストは、DrawTextクラスを使って、splash_screen_nodeノードに表示します。
taskMgr.doMethodLater は遅延実行を行うメソッドです。第一引数で指定した数字の秒数が経過すると、第2引数のメソッドが実行されます。
close_splash_screenメソッドは、splash_screen_nodeノードをデタッチ(再利用しないノードを完全に削除)します。起動画面が消えてから 3秒後にコンソールの文字列が消えるように、self.console_window.start_time = time()を記載します。
以上で、今回のプログラミングは完成です。

起動画面

11_01_main.py を実行して、ゲームを開始します。起動画面が表示され、5秒後に消えることを確認してください。起動画面が実装されると、ゲームらしくなりましたね。

次回はインベントリ(アイテム一覧のホップアップ画面)を実装します。ユーザーが「e」キーを押すとインベントリが表示されます。そしてインベントリからホットバーに表示されているブロックを変更できるようにします。お楽しみに。


前の記事
Pythonでマイクラを作る ⑩設置するブロックを選択する
次の記事
Pythonでマイクラを作る ⑫インベントリからブロックを選択する

その他のタイトルはこちら


いいなと思ったら応援しよう!