見出し画像

Pythonでマイクラを作る ⑫インベントリからブロックを選択する

Pythonでマイクラを作る 第12回目です。インベントリ(アイテム一覧のホップアップ画面)を実装します。インベントリに44個のブロックを表示します。ブロックをクリックすると、ホットバーのブロックを入れ替えられるようにします。

インベントリの画像を作成

inventory.png

インベントリの枠画像をドット絵エディタなどで作成します。サイズは 1000x370 です。こちらの画像をダウンロードしてお使いください。imagesディレクトリに inventory.png を保存してください。

インベントリを画面に表示する

# ディレクトリ構造
Documents/
  ├ pynecrafter/
  │  ├ fonts/
  │  │  
  │  ├ images/
  │  │  ├ inventory.png
  │  │  
  │  ├ textures/
  │  │  
  │  ├ models/
  │  │  
  │  ├ src/
  │  │  ├ __init__.py
  │  │  ├ block.py  # ブロック関連
  │  │  ├ player.py  # プレイヤー関連
  │  │  ├ player_model.py  # プレイヤーモデル関連
  │  │  ├ camera.py  # カメラ関連
  │  │  ├ target.py  # ターゲットブロック関連
  │  │  ├ user_interface.py  # インターフェース関連
  │  │  ├ utils.py  # ユーティリティー
  │  │  ├ inventory.py  # インベントリ
  │  │  ├ mc.py  # 統合クラス

インベントリを管理する Inventoryクラスを作成するために、inventory.py をsrcディレクトリの中に作成します。

"""src/__init__.py"""
from .block import Block
from .player import Player
from .user_interface import UserInterface
from .inventory import Inventory  # 追記
from .mc import MC

__init__.py はパッケージ(=ディレクトリ)をインポートするための初期化処理を行います。
from .inventory import Inventory により、Inventoryクラスをインポートすることができるようになります。

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


class MC(ShowBase, UserInterface, Inventory):  # 追記
    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)
        Inventory.__init__(self)  # 追記

以下略

MCクラスは全てのクラスをまとめて、ゲームを起動するモジュールです。
class MC(ShowBase, UserInterface, Inventory):により Inventoryクラスを継承して、Inventory.__init__(self)により Inventoryクラスのプロパティー初期化を実行します。これで、MCクラスでインベントリを使えるようになります。

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


class Inventory:
    inventory_blocks = [
        ['stone', ['0-1']],
        ['grass_block', ['0-3', '0-0', '0-2']],
        ['dirt', ['0-2']],
        ['cobblestone', ['1-0']],
        ['oak_planks', ['0-4']],
        ['bedrock', ['1-1']],
        ['water', ['3-22']],
        ['lava', ['3-21']],
        ['sand', ['1-2']],
        ['oak_log', ['1-4', '1-5']],
        ['oak_leaves', ['3-4']],
        ['glass', ['3-1']],
        ['white_wool', ['4-0']],
        ['orange_wool', ['13-2']],
        ['magenta_wool', ['12-2']],
        ['light_blue_wool', ['13-1']],
        ['yellow_wool', ['10-2']],
        ['lime_wool', ['9-2']],
        ['pink_wool', ['8-2']],
        ['gray_wool', ['7-2']],
        ['light_gray_wool', ['14-1']],
        ['cyan_wool', ['11-2']],
        ['purple_wool', ['12-1']],
        ['blue_wool', ['11-1']],
        ['brown_wool', ['10-1']],
        ['green_wool', ['9-1']],
        ['red_wool', ['8-1']],
        ['black_wool', ['7-1']],
        ['gold_block', ['1-7']],
        ['iron_block', ['1-6']],
        ['diamond_block', ['1-8']],
        ['emerald_block', ['1-9']],
        ['lapis_block', ['9-0']],
        ['bricks', ['0-7']],
        ['tnt', ['0-8', '0-9', '0-10']],
        ['bookshelf', ['2-3', '0-4']],
        ['furnace', ['2-12', '2-13', '3-14']],
        ['burning_furnace', ['3-13', '2-13', '3-14']],
        ['piston', ['6-12', '6-11', '6-13']],
        ['snow', ['4-2']],
        ['ice', ['4-3']],
        ['cactus', ['4-5', '4-6']],
        ['jack_o_lantern', ['7-8', '7-6', '6-6']],
        ['chest', ['6-19', '6-18', '6-17']],
    ]

    def __init__(self):
        self.inventory_node = self.aspect2d.attachNewNode("inventory_node")

        # background
        cm = CardMaker('inventory_card')
        cm.setFrame(-1.5, 1.5, -1, 1)
        self.inventory_background = self.inventory_node.attachNewNode(cm.generate())
        self.inventory_background.setTransparency(1)
        self.inventory_background.setColor(0, 0, 0, 0.5)

        # frame
        self.frame = DrawImage(
            'images/inventory.png',
            self.inventory_node,
            scale=(11 / 9, 11 / 9, 74 * 11 / 200 / 9),
            pos=(0, 0, 0),
        )

        # buttons
        for i, block in enumerate(Inventory.inventory_blocks):
            block_image_name = block[1][0]
            row_num = i // 11
            column_num = i % 11
            button = DirectButton(
                image=f'textures/{block_image_name}.png',
                parent=self.inventory_node,
                scale=(16 / 164, 1, 16 / 164),
                pos=((column_num - 5) * 0.22, 0, - 0.11 - (row_num - 2) * 0.22),
                relief=None,  # ボタンを持ち上げて表示しない
            )
            button.setTransparency(TransparencyAttrib.MAlpha)

inventory.py にインベントリに関するコードを記載していきます。
Inventoryクラスはインベントリを管理します。クラス変数inventory_blocks に44個のブロック情報をリストで保存します(お好みで変更してください)。inventory_blocksリストの要素もリストになっています。子リストの第1要素がブロックID で、第2要素が画像ファイル名のリストになっています。

self.aspect2d.attachNewNode("inventory_node")により、2次元ノード(aspect2d)に inventory_nodeノードを作成し、そこに背景、枠、ブロックの画像を表示します。

背景はCardMakerで作成します。カードは長方形の平面のことで4つの頂点を持つポリゴンです。背景や地面、床、壁などに使われます。cm.setFrame(-1.5, 1.5, -1, 1)により、サイズは 3x2 になり、画面全体を覆うことができます。
self.inventory_node.attachNewNode(cm.generate())により、inventory_nodeノードにカードを配置します。setTransparency(1)で透明度が設定できるようにして、setColor(0, 0, 0, 0.5)により、半透明な黒で塗りつぶします。

DirectButtonクラスはボタンを作成するクラスです。image(画像のパス)、parent(表示するノード)、scale(ボタンサイズ)、pos(表示する位置)を指定して、インスタンスを生成します。引数relief=None はボタンを立体的に見せる効果を無効にします。setTransparency(TransparencyAttrib.MAlpha)により、画像の透明度を有効化します。
for文で、クラス変数inventory_blocks からブロック情報を一つずつ読み込んで、 4 x 11 の碁盤上に並べていきます。

インベントリの表示

11_01_main.py を実行して、インベントリを表示します。まだ表示、非表示のコードを実装していないので、前面に表示されたままになっています。
次にインベントリを表示 / 非表示を切り替えられるようにします。

インベントリを表示 / 非表示を切り替える

"""src/inventory.py"""

末尾に追記

    # インベントリを非表示
    self.inventory_node.stash()

    # インベントリを表示
    self.accept('e', self.toggle_inventory)

def toggle_inventory(self):
    if self.inventory_node.isStashed():
        self.inventory_node.unstash()
    else:
        self.inventory_node.stash()

ゲームが開始した時点では、インベントリは非表示にしましょう。
self.inventory_node.stash()により、inventory_nodeノードを隠します。stashメソッドはノードを非表示にして、タッチ操作も無効化されます。
(対して、hideメソッドは単に見えなくするだけで、タッチ操作は無効化されないので背面をタッチできなくなってしまいます)。
acceptメソッドで、「e」キーで toggle_inventoryメソッドを実行するように設定します。
toggle_inventoryメソッドは、isStashedメソッドでノードが stashされているかどうかチェックして表示 / 非表示を切り替えます。

11_01_main.py を実行して、「e」キーでインベントリの表示 / 非表示が切り替わるか確認してください。
次にボタンを押すと、ホットバーのブロックを入れ替えることができるようにします。

ホットバーのブロックを入れ替える

"""src/inventory.py"""

    # buttons
    for i, block in enumerate(Inventory.inventory_blocks):
        block_image_name = block[1][0]
        row_num = i // 11
        column_num = i % 11
        button = DirectButton(
            image=f'textures/{block_image_name}.png',
            parent=self.inventory_node,
            scale=(16 / 164, 1, 16 / 164),
            pos=((column_num - 5) * 0.22, 0, - 0.11 - (row_num - 2) * 0.22),
            relief=None,  # ボタンを持ち上げて表示しない
            command=self.select_inventory_block,  # 追記
            extraArgs=[i],  # 追記
        )
        button.setTransparency(TransparencyAttrib.MAlpha)

略

末尾に追記

    def select_inventory_block(self, i):
        selected_block = Inventory.inventory_blocks[i]
        block_image_name = selected_block[1][0]
        hotbar_num = self.selected_hotbar_num
        self.hotbar_blocks[hotbar_num] = selected_block
        bar_num = hotbar_num + 1
        bar_image = self.get(f'bar{bar_num}')
        bar_image.setImage(
            f'textures/{block_image_name}.png'
        )
        bar_image.setTransparency(TransparencyAttrib.M_alpha)

インベントリのブロック画像を押して、ホットバーのブロックを入れ替えるメソッドを実行できるようにします。
DirectButtonクラスの引数command に select_inventory_blockメソッドを指定します。select_inventory_blockメソッドに渡す引数は、引数extraArgs にリスト形式で指定します。
select_inventory_block は引数として整数(クラス変数inventory_blocks の要素のインデックス)を受け取ります。selected_block = Inventory.inventory_blocks[i]によりブロックの情報を取り出して、クラス変数hotbar_blocks の情報を書き換えます。ホットバーのブロック画像もsetImageメソッドで変更します。画像を変更したときは、再度setTransparency(TransparencyAttrib.M_alpha)を指定しないと透明度が有効にならない点に注意が必要です。

ホットバーのブロックを入れ替える

11_01_main.py を実行し、「e」キーを押してインベントリを表示します。数字キーでホットバーのブロックを選択してからインベントリのブロックを押すとホットバーのブロックが入れ替わります。上図は、ホットバーのブロックを全て羊毛(ウール)に入れ替えた状態です。
「e」キーを押してインベントリを非表示にしてから、入れ替えたブロックが正しく設置できることを確認してください。

細かいバグ修正

インベントリを表示している状態でも、プレイヤーの向きが変更されてしまうバグを修正します。

"""src/player.py"""

def update_direction(self):
    if self.base.mouseWatcherNode.hasMouse() and \
            self.base.inventory_node.isStashed():  # 追記

以下略

Playerクラスを修正します。
update_directionメソッドの条件式に、インベントリが表示されていないことを確認するコード(self.base.inventory_node.isStashed())を追記します。
今回はこれで全て完了です。

今回はインベントリを実装しました。使えるブロックが44個に増えて、これで建築が捗りますね。
次回はメニュー画面を実装し、より本格的なゲームに近づけていきます。お楽しみに。


前の記事
Pythonでマイクラを作る ⑪起動画面を実装する
次の記事
Pythonでマイクラを作る ⑬メニュー画面を作成する

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


この記事が気に入ったらサポートをしてみませんか?