見出し画像

Pythonでマイクラを作る ⑩設置するブロックを選択する

Pythonでマイクラを作る 第10回目です。前回の記事でプレイヤーの操作でブロックを設置、破壊することができるようになりました。設置場所は地面の上(Z = 0)に限定されていたので、高さ1以上の場所にもブロックを置けるように修正します。それから設置するブロックをホットバーから選ぶことができるようにします。

隣接するブロックの有無を調べる

"""src/block.py"""

Blockクラスにメソッドを追加

    def can_add_or_remove_block_at(self, position):
        diff_positions = [
            Point3(1, 0, 0),
            Point3(0, 1, 0),
            Point3(0, 0, 1),
            Point3(-1, 0, 0),
            Point3(0, -1, 0),
            Point3(0, 0, -1),
        ]

        for diff_position in diff_positions:
            x, y, z = [floor(value) for value in position + diff_position]
            key = f'{x}_{y}_{z}'
            if key in self.block_dictionary:
                return True
        else:
            return False
隣接するブロックの有無を調べる

高さ1以上の場所にブロックを置く準備として、隣接するブロックの有無を調べなくてはなりません。マイクラでは宙に浮かせてブロックを置くことは禁止されており、必ず隣接するブロックにくっつけて新しいブロックを設置します。
隣接するブロックの場所は、置こうとする場所の上下前後左右の6箇所になります(上図のガラスブロックの場所)。この6箇所にブロックがあるかどうか調べるのが、can_add_or_remove_block_atメソッドの役割です。変数diff_positions は調べる場所への差異(diff = different)を指定しています。for文で各場所にブロックがあるか調べて、ブロックがあれば True を返します(そこで、 return によりメソッドは終了する)。すべて調べ終わってブロックがないときは False を返します。


        for diff_position in diff_positions:
            x, y, z = [floor(value) for value in position + diff_position]
            key = f'{x}_{y}_{z}'
            if key in self.block_dictionary:
                return True
        else:  # 繰り返しの最後に実行する
            return False

for と同じインデントにある else ブロックは、繰り返しの最後に実行する処理を記述します。今回のように全て実行して、条件を満たさないときに最後に False を返すときなどに便利に使えます。

高さ1以上の場所にブロックを置く


def get_target_position(self):
    x0, y0, z0 = self.position
    phi, theta, _ = self.direction
    # print(x0, y0, z0, phi, theta)
    # 方向ベクトル
    direction_vec = Vec3(
        sin(radians(90 - theta)) * cos(radians(phi + 90)),
        sin(radians(90 - theta)) * sin(radians(phi + 90)),
        cos(radians(90 - theta))
    )

    if not theta == 0:
        if theta < 0:
            check_heights = [1, 0]
        else:
            check_heights = [2, 3, 4, 5, 6, 7, 8]
        for check_height in check_heights:
            z = check_height
            x = x0 + (z - z0 - self.eye_height) * direction_vec.x / direction_vec.z
            y = y0 + (z - z0 - self.eye_height) * direction_vec.y / direction_vec.z
            # print(x, y)
            target_position = Point3(x, y, z)
            if (target_position - Point3(x0, y0, z0 + self.eye_height)).length() < 8 and \
                    self.base.block.can_add_or_remove_block_at(target_position):
                return Point3(x, y, z)
        else:
            return None
    else:
        return None
target position upper

上図はプレイヤーを横から見た模式図です。プライヤーは上を向いており($${\theta}$$ はプラス)、目標点の高さは 2 以上の整数になります(ブロックの位置は整数の場所のみ設置できるルールより)。

Targetクラスの get_target_positionメソッドを修正します。
変数theta の符号によって場合分けをしています。thetaが 0のとき、地面と並行になり、目標点は得られないので None を返します。theta がマイナスのときは下向きであり、調べる高さは 1, 0 になります。プラスのときは上向きであり、調べる高さは 2, 3, 4, 5, 6, 7, 8 になります(目標点からの距離は 8 以下に設定しているので、9以上は調べなくて良い)。


        for check_height in check_heights:
            z = check_height
            x = x0 + (z - z0 - self.eye_height) * direction_vec.x / direction_vec.z
            y = y0 + (z - z0 - self.eye_height) * direction_vec.y / direction_vec.z
            # print(x, y)
            target_position = Point3(x, y, z)
            if (target_position - Point3(x0, y0, z0 + self.eye_height)).length() < 8 and \
                    self.base.block.can_add_or_remove_block_at(target_position):
                return Point3(x, y, z)
        else:
            return None

変数check_heights は調べる高さのリストです。for文で順番に目標点の座標target_position を計算して求めます。条件式で「target_position とプレイヤーの頭の中心の距離が8以下」であり、「隣接するブロックが存在する」とき、target_position が返り値として返されます(return でメソッドは終了)。全て調べて条件を満たさないときは、最後のelseブロックが実行され、None が返されます。

ブロックの設置と破壊

08_01_main.py を実行して、1以上の場所にブロックを設置、破壊ができることを確認してください。これで全ての場所にブロックを設置、破壊ができるようになりました。
次に、設置するブロックを選べるようにします。

ホットバー

hotbar

設置するブロックは画面下に表示されているホットバーから数字キーで選びます。ホットバーの枠の画像をドット絵エディターなどで準備します。選ばれているブロックは薄いグレーの枠で囲みたいので、画像は9枚(hotbar1.png - hotbar9.png)必要になります。画像サイズは 820 x 100 で作成してください。

hotbar1.png

ホットバーの枠画像は、私の作ったものをダウンロードしてお使いください。解凍して、ディレクトリの中に入っている9枚の画像 hotbar1.png - hotbar9.png を imagesディレクトリの中に保存します。

ホットバーのような規則的な画像は、プログラミングで作成した方が楽です。私は Procssingの Pytonバージョンで作りました。ご要望がありましたら、ホットバーの作成コードを番外編で公開いたします。

ホットバーを表示する

# ディレクトリ構造
Documents/
  ├ pynecrafter/
  │  ├ images/
  │  │  ├ 48-488312_blockcss-minecraft-terrain-png-1-0-0.png
  │  │  ├ hotbar1.png
  │  │  ├ hotbar2.png
  │  │  ├ hotbar3.png
  │  │  ├ hotbar4.png
  │  │  
  │  ├ textures/
  │  │  ├ 0-1.png
  │  │  ├ 0-2.png
  │  │  
  │  ├ models/
  │  │  ├ grass_block.egg
  │  │  ├ stone.egg
  │  │  ├ dirt.egg
  │  │  
  │  ├ src/
  │  │  ├ __init__.py
  │  │  ├ block.py  # ブロック関連
  │  │  ├ player.py  # プレイヤー関連
  │  │  ├ player_model.py  # プレイヤーモデル関連
  │  │  ├ camera.py  # カメラ関連
  │  │  ├ target.py  # ターゲットブロック関連
  │  │  ├ user_interface.py  # インターフェース関連
  │  │  ├ utils.py  # ユーティリティー
  │  │  ├ mc.py  # 統合クラス
  │  │  
  │  ├ 08_01_main.py  # 統合クラスをインポートしてゲームを起動する

srcディレクトリの中に user_interface.py を作成します。UserInterfaceクラスは、ホットバーなどのユーザーインターフェースを操作するクラスです。srcディレクトリの中に utils.py を作成します。このファイル(=モジュール)には、文字や画像の表示などを操作するクラスをまとめて記載していきます。

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

src/__init__.py に UserInterfaceクラスをインポートする1行を追記します。

"""src/mc.py"""
import sys
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)
        UserInterface.__init__(self)

MCクラスは全てのクラスをまとめて、ゲームを起動します。UserInterfaceクラスを継承して、UserInterface.__init__(self)により、UserInterfaceクラスのプロパティーを初期化します。これで MCクラスで、UserInterfaceのインスタンス変数とメソッドを使えるようになりました。

"""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)

utils.py にDrawImageクラスを書いていきます。DrawImageクラスはPanda3D のデフォルトのクラスである OnscreenImageクラス継承して作リます。このクラスは画面上の画像を操作します。
self.setTransparency(TransparencyAttrib.M_alpha)は透明(transparency)な画像を正しく表示するために記載します。この文がないと、例えばガラスブロックのような透明画像は背景が黒で表示されてしまうことになります。

"""src/user_interface.py"""
from math import *
from panda3d.core import *
from .utils import DrawImage


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']],
        # ['diamond_block', ['1-8']],
        # ['emerald_block', ['1-9']],
        ['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, 1, 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)
        )

UserInterfaceクラスは、ホットバーのようなインターフェースを実装します。(今後、このクラスにメニュー画面や画面上のテキスト表示などを付け加えていく予定です。)
クラス変数hotbar_blocks はホットバーに表示するブロックの情報を9つ含んでいるリストです。リストは多重のリストになっており、子リストはブロックID と画像のファイル名を含みます。(今後、ホットバーのブロックは変更できるように実装予定です。)
インスタンス変数selected_hotbar_num はホットバーの何番目を選択しているかを表します。最初のブロックを選択しているので、0 を代入します。インスタンス変数 select_blockは現在選択中のブロックの情報を表します。
DrawImageメソッドを使って、9枚のブロックテクスチャーの画像とホットバーの枠を画面上に表示します。


        self.hotbar = DrawImage(
            parent=self.a2dBottomCenter,
            image='images/hotbar1.png',
            scale=(1, 1, 20 / 164),
            pos=(0, 0, 20 / 164)
        )

DrawImageメソッドを詳しく見てみましょう。image は画像ファイルのパスを指定します。parent は画面上の位置を指定します。self.a2dBottomCenter は画面の下中央が画像の中心になります。a2d は aspect2d の略で、2次元のオブジェクトを格納する専用ノードです。Bottom(底)Center(中央)以外にも Top(天井)Right(右)Left(左)を組み合わせて指定できます。
scale は画像の縦横サイズを指定します。指定しないときは縦横同じサイズに引き伸ばされて表示されます。scale=(1, 1, 20 / 164)でZ方向を縮小するとホットバーは正しい縮尺で表示できます。(Y方向は必ず 1 を指定すること)
pos は画像の位置を微調整する引数です。(0, 0, 0) のときは画像の下半分がはみ出てしまうので、pos=(0, 0, 20 / 164)によりZ方向に動かしてやります。
(scaleとposの設定はクセがあり、慣れるまで難しいです。何度か設定値を変更して試行錯誤をして、感覚を掴んでおきましょう。)

ホットバーの表示

08_01_main.py を実行してください。画面の下にホットバーが表示できれば成功です。
次にホットバーのブロックを数字キーで選択する機能を追加します。

数字キーで設置するブロックを選ぶ

"""src/user_interface.py"""

UserInterfaceクラスに追記

        # select hotbar
        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)

UserInterfaceクラスに、数字キーで設置するブロックを選ぶコードを追記します。
acceptメソッドはユーザーのキー操作を受け取り、メソッドを実行できます。ユーザーが数字キーを押すと、select_hotbarメソッドが実行されます。第3引数は select_hotbarメソッドに渡す引数をリスト形式で指定します。
select_hotbarメソッドは引数を受け取り、インスタンス変数selected_hotbar_num に代入します。インスタンス変数selected_block をクラス変数hotbar_blocks から選び直します。
self.hotbar.setImage(f'images/hotbar{i}.png')により、ホットバーの枠画像を変更して、選んだテクスチャーが薄いグレーで囲まれるようにします。self.hotbar.setTransparency(TransparencyAttrib.M_alpha)により、変更した枠画像の背景を透明にします。

"""src/player.py"""
    
    def player_add_block(self):
        block_id = self.base.hotbar_blocks[self.base.selected_hotbar_num][0]
        if self.target_position and \
                not self.base.block.is_block_at(self.target_position):
            self.base.block.add_block(
                self.target_position.x,
                self.target_position.y,
                self.target_position.z,
                block_id
            )

Playerクラスの player_add_blockメソッドを修正します。
設置するブロックID は 、self.base.hotbar_blocks[self.base.selected_hotbar_num][0]のコードで得られます。self.base.block.add_blockメソッドの第4引数を block_id に変更して終了です。

設置するブロックをホットバーから選ぶ

08_01_main.py を実行して、設置するブロックを数字キーで変更できることを確認してください。これで今回のミッションは完了です。

次回は画面に文字を表示できるようにします。操作方法を画面上に表示できるようになります。デバッグモードを実装し、プレイヤー情報を画面上で確認できるようにします。
そして起動画面(スプラッシュスクリーン)を表示して、よりゲームに近づけていきます。


前の記事
Pythonでマイクラを作る ⑨ブロックの設置と破壊を実装する
次の記事
Pythonでマイクラを作る ⑪起動画面を実装する

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


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