見出し画像

Pythonでマイクラを作る ⑨ブロックの設置と破壊を実装する

Pythonでマイクラを作る 第9回目です。
今回は、プレイヤーがブロックを設置、破壊できるようにします。まずプレイヤーの視線の先にターゲット(目標)ブロックを置くことから始めます。次にマウスクリックで、ターゲットの場所に石ブロックを置くことができるようにします。

ターゲットブロックのテクスチャー画像を作成

target_block.png

プレイヤーがブロックを設置、破壊するときに目標となるターゲットブロックを準備します。ドット絵エディターでサイズ 16x16のテクスチャー画像を作成してください。target_block.png の名前をつけて、texturesディレクトリの中に保存してください。

ご自分で作成されないときは、上のファイルをダウンロードしてお使いください。解凍して、中身の pngファイルを texturesディレクトリに移動します。

ターゲットブロックのモデルを作成

"""03_03_egg_model_maker_1.py"""

coordinate = """\
<CoordinateSystem> { Z-Up }

"""
vertex_pool = """\
<VertexPool> box {
  <Vertex> 1 {
    0 1 1
    <UV> { 1 1 }
  }
  <Vertex> 2 {
    1 1 1
    <UV> { 0 1 }
  }
  <Vertex> 3 {
    0 0 1
    <UV> { 0 1 }
  }
  <Vertex> 4 {
    1 0 1
    <UV> { 1 1 }
  }
  <Vertex> 5 {
    0 1 0
    <UV> { 1 0 }
  }
  <Vertex> 6 {
    1 1 0
    <UV> { 0 0 }
  }
  <Vertex> 7 {
    0 0 0
    <UV> { 0 0 }
  }
  <Vertex> 8 {
    1 0 0
    <UV> { 1 0 }
  }
  <Vertex> 9 {
    0 1 1
    <UV> { 0 0 }
  }
  <Vertex> 10 {
    1 1 1
    <UV> { 1 0 }
  }
  <Vertex> 11 {
    0 1 0
    <UV> { 0 1 }
  }
  <Vertex> 12 {
    1 1 0
    <UV> { 1 1 }
  }
}

"""
group = """\
<Group> box {
  <Polygon> {
    <TRef> { one }
    <Normal> { 0 1 0 }
    <VertexRef> { 3 7 8 4 <Ref> { box } }
  }
  <Polygon> {
    <TRef> { one }
    <Normal> { 0 1 0 }
    <VertexRef> { 2 6 5 1 <Ref> { box } }
  }
  <Polygon> {
    <TRef> { one }
    <Normal> { -1 0 0 }
    <VertexRef> { 1 5 7 3 <Ref> { box } }
  }
  <Polygon> {
    <TRef> { one }
    <Normal> { 1 0 0 }
    <VertexRef> { 4 8 6 2 <Ref> { box } }
  }
  <Polygon> {
    <TRef> { one }
    <Normal> { 0 0 1 }
    <VertexRef> { 9 3 4 10 <Ref> { box } }
  }
  <Polygon> {
    <TRef> { one }
    <Normal> { 0 0 -1 }
    <VertexRef> { 7 11 12 8 <Ref> { box } }
  }
}

"""


class EggModel:
    def __init__(self, model_name, texture1):
        self.model_name = model_name
        self.texture1 = texture1

    def make(self):
        model = coordinate
        model += f'<Texture> one {{\n  "../textures/{self.texture1}.png"\n}}\n\n'
        model += vertex_pool + group

        print(model)

        # クライアントから送られてきたデータをファイルに書き出す
        with open(f"models/{self.model_name}.egg", "w") as f:
            f.write(model)


if __name__ == "__main__":
    blocks_1 = {
        # 'stone': ['0-1'],
        # 'dirt': ['0-2'],
        # 'bricks': ['0-7'],
        # 'cobblestone': ['1-0'],
        # 'bedrock': ['1-1'],
        # 'sand': ['1-2'],
        # 'iron_block': ['1-6'],
        # 'gold_block': ['1-7'],
        # 'diamond_block': ['1-8'],
        # 'emerald_block': ['1-9'],
        # 'gold_ore': ['2-0'],
        # 'iron_ore': ['2-1'],
        # 'coal_ore': ['2-2'],
        # 'mossy_cobblestone': ['2-4'],
        # 'obsidian': ['2-5'],
        # 'sponge': ['3-0'],
        # 'glass': ['3-1'],
        # 'diamond_ore': ['3-2'],
        # 'redstone_ore': ['3-3'],
        # 'oak_leaves': ['3-4'],
        # 'dark_oak_leaves': ['3-5'],
        # 'stone_bricks': ['3-6'],
        # 'lava': ['3-21'],
        # 'water': ['3-22'],
        # 'white_wool': ['4-0'],
        # 'mob_spawner': ['4-1'],
        # 'snow': ['4-2'],
        # 'ice': ['4-3'],
        # 'black_wool': ['7-1'],
        # 'gray_wool': ['7-2'],
        # 'red_wool': ['8-1'],
        # 'pink_wool': ['8-2'],
        # 'lapis_block': ['9-0'],
        # 'green_wool': ['9-1'],
        # 'lime_wool': ['9-2'],
        # 'lapis_ore': ['10-0'],
        # 'brown_wool': ['10-1'],
        # 'yellow_wool': ['10-2'],
        # 'blue_wool': ['11-1'],
        # 'cyan_wool': ['11-2'],
        # 'purple_wool': ['12-1'],
        # 'magenta_wool': ['12-2'],
        # 'spruce_planks': ['12-6'],
        # 'jungle_planks': ['12-7'],
        # 'light_blue_wool': ['13-1'],
        # 'orange_wool': ['13-2'],
        # 'birch_planks': ['13-6'],
        # 'light_gray_wool': ['14-1'],
        'target_block': ['target_block'],  # 追記
    }

    for key, value in blocks_1.items():
        egg_model = EggModel(key, value[0])
        egg_model.make()

③ブロックのモデルを作成する でブロックモデルを自動生成するプログラムを作成しました。03_03_egg_model_maker_1.py(すべての面が同じテクスチャー)に1行追記して、ターゲットブロックを作成します。存在するブロックを再生成しないようにするため、作成済みのブロック部分はコメントアウトしておきましょう。03_03_egg_model_maker_1.py を実行すると、modelsディレクトリの中に target_block.egg が自動で作成されます。

"""03_04_egg_model_viewer.py"""
from direct.showbase.ShowBase import ShowBase
from panda3d.core import *


class App(ShowBase):
    # コンストラクタ
    def __init__(self):
        # ShowBaseを継承する
        ShowBase.__init__(self)

        # textured cube
        blocks = {
            # blocks_1
            'stone': ['0-1'],
            'dirt': ['0-2'],
            'bricks': ['0-7'],
            'cobblestone': ['1-0'],
            'bedrock': ['1-1'],
            'sand': ['1-2'],
            'iron_block': ['1-6'],
            'gold_block': ['1-7'],
            'diamond_block': ['1-8'],
            'emerald_block': ['1-9'],
            'gold_ore': ['2-0'],
            'iron_ore': ['2-1'],
            'coal_ore': ['2-2'],
            'mossy_cobblestone': ['2-4'],
            'obsidian': ['2-5'],
            'sponge': ['3-0'],
            'glass': ['3-1'],
            'diamond_ore': ['3-2'],
            'redstone_ore': ['3-3'],
            'oak_leaves': ['3-4'],
            'oak_plants': ['0-4'],
            'stone_bricks': ['3-6'],
            'lava': ['3-21'],
            'water': ['3-22'],
            'white_wool': ['4-0'],
            'mob_spawner': ['4-1'],
            'snow': ['4-2'],
            'ice': ['4-3'],
            'black_wool': ['7-1'],
            'gray_wool': ['7-2'],
            'red_wool': ['8-1'],
            'pink_wool': ['8-2'],
            'lapis_block': ['9-0'],
            'green_wool': ['9-1'],
            'lime_wool': ['9-2'],
            'lapis_ore': ['10-0'],
            'brown_wool': ['10-1'],
            'yellow_wool': ['10-2'],
            'blue_wool': ['11-1'],
            'cyan_wool': ['11-2'],
            'purple_wool': ['12-1'],
            'magenta_wool': ['12-2'],
            'spruce_planks': ['12-6'],
            'jungle_planks': ['12-7'],
            'light_blue_wool': ['13-1'],
            'orange_wool': ['13-2'],
            'birch_planks': ['13-6'],
            'light_gray_wool': ['14-1'],
            'target_block': ['target_block'],  # 追記
            # blocks_1_5
            'oak_log': ['1-4', '1-5'],
            'bookshelf': ['2-3', '0-4'],
            'crafting_table': ['3-11', '2-11'],
            'cactus': ['4-5', '4-6'],
            'jukebox': ['4-10', '4-11'],
            'spruce_log': ['7-4', '1-5'],
            'binch_log': ['7-5', '1-5'],
            'jungle_log': ['9-9', '1-5'],
            # blocks_1_5_6
            'grass_block': ['0-3', '0-0', '0-2'],
            'tnt': ['0-8', '0-9', '0-10'],
            'sticky_piston': ['6-12', '6-10', '6-13'],
            'piston': ['6-12', '6-11', '6-13'],
            # blocks_1_2_5
            'furnace': ['2-12', '2-13', '3-14'],
            'burning_furnace': ['3-13', '2-13', '3-14'],
            'chest': ['6-19', '6-18', '6-17'],
            'pumpkin': ['7-7', '7-6', '6-6'],
            'jack_o_lantern': ['7-8', '7-6', '6-6'],
            # blocks_1_2_3_4_5_6
            'player_head': ['head/1', 'head/2', 'head/3', 'head/4', 'head/5', 'head/6'],
            'player_body': ['body/1', 'body/2', 'body/3', 'body/4', 'body/5', 'body/6'],
            'player_hand': ['hand/1', 'hand/2', 'hand/3', 'hand/4', 'hand/5', 'hand/6'],
            'player_leg': ['leg/1', 'leg/2', 'leg/3', 'leg/4', 'leg/5', 'leg/6'],
        }

        for i, name in enumerate(blocks):
            self.cube = self.loader.loadModel(f'models/{name}')
            self.cube.setPos(i % 10 - 5, 30, int(i / 10) * 2 - 7)
            self.cube.reparentTo(self.render)


app = App()
app.run()
ターゲットブロックの追加

03_04_egg_model_viewer.py に1行追記して実行します。これでターゲットブロックが作成できたことが確認できました。これで使えるブロックは70個になりました。

目標地点にターゲットブロックを置く

"""src/player.py"""
from math import *
from panda3d.core import *
from direct.showbase.ShowBaseGlobal import globalClock
from .player_model import PlayerModel
from .camera import Camera
from .target import Target  # 追記


class Player(PlayerModel, Camera, Target):  # 追記
    heading_angular_velocity = 15000
    pitch_angular_velocity = 5000
    max_pitch_angle = 60
    speed = 10
    eye_height = 1.6  # 追記

    # コンストラクタ
    def __init__(self, base):
        self.base = base
        PlayerModel.__init__(self)
        Camera.__init__(self)
        Target.__init__(self)  # 追記

        self.position = Point3(0, 0, 0)
        self.direction = VBase3(0, 0, 0)
        self.velocity = Vec3(0, 0, 0)
        self.mouse_pos_x = 0
        self.mouse_pos_y = 0
        self.target_position = None  # 追記

以下略

player.py を修正します。
from .target import Targetにより、Targetクラスをインポートして使えるようにします。class Player(PlayerModel, Camera, Target):により、Tagetクラスを継承して、コンストラクタ内に Target.__init__(self)を追記して Targetクラスのプロパティーを初期化します。これで Targetクラスを取り込んで使えるようになりました。
新しいクラス変数eye_heightは、プレイヤーの目線の高さを定義します。地上 1.6メートルの位置から眺めている設定です。
インスタンス変数target_position はターゲットブロックの位置情報を表します。

"""src/target.py"""
from math import *
from panda3d.core import *


class Target:

    # コンストラクタ
    def __init__(self):
        self.target_node = self.base.render.attachNewNode(PandaNode('target_node'))
        self.target_block_model = self.base.loader.loadModel('models/target_block')
        self.target_block_model.reparentTo(self.target_node)

        self.base.taskMgr.add(self.set_target_block, "set_target_block")

    def get_target_position(self):
        x0, y0, z0 = self.position
        phi, theta, _ = self.direction

        # 方向ベクトル
        direction_vec = Vec3(
            sin(radians(90 - theta)) * cos(radians(phi + 90)),
            sin(radians(90 - theta)) * sin(radians(phi + 90)),
            cos(radians(90 - theta))
        )
        if theta < 0:
            z = 0
            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)
            if (Point3(x, y, z) - self.position).length() < 5:
                return Point3(x, y, z)
            else:
                return None
        else:
            return None

    def set_target_block(self, task):
        self.target_position = self.get_target_position()
        if self.target_position:
            if self.target_node.isHidden():
                self.target_node.show()
            self.target_node.setPos(
                floor(self.target_position.x),
                floor(self.target_position.y),
                floor(self.target_position.z),
            )
        else:
            if not self.target_node.isHidden():
                self.target_node.hide()
        return task.cont

srcディレクトリ内に target.py を作成し、コードを書いていきます。
Targetクラスは、ブロックを設置、破壊する場所(目標地点)を操作します。ターゲットブロックを目標地点において、建築作業の目印にすることができます。


    # コンストラクタ
    def __init__(self):
        self.target_node = self.base.render.attachNewNode(PandaNode('target_node'))
        self.target_block_model = self.base.loader.loadModel('models/target_block')
        self.target_block_model.reparentTo(self.target_node)

        self.base.taskMgr.add(self.set_target_block, "set_target_block")

コンストラクタで Targetクラスの初期化を行います。target_nodeノードを作成し、ターゲットブロックのモデルを表示します。ターゲットブロックを動かすときは target_nodeノードと一緒に動かす設計です。
taskMgrクラスのaddメソッドで、フレームごとに実行する処理を指定します。set_target_blockメソッドを指定しました。


    def get_target_position(self):
        x0, y0, z0 = self.position
        phi, theta, _ = self.direction

        # 方向ベクトル
        direction_vec = Vec3(
            sin(radians(90 - theta)) * cos(radians(phi + 90)),
            sin(radians(90 - theta)) * sin(radians(phi + 90)),
            cos(radians(90 - theta))
        )
        if theta < 0:
            z = 0
            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)
            if (Point3(x, y, z) - Point3(x0, y0, z0 + self.eye_height)).length() < 8:
                return Point3(x, y, z)
            else:
                return None
        else:
            return None

get_target_positionメソッドは、プレイヤーの視線の先を計算して、ブロックを置く(破壊する)ことができる場所の位置情報を返すメソッドです。いきなりすべての場所を調べるのは難しいため、最初は z = 0(地面に接する場所)のみ位置情報を計算します。

target_position

上図は、プレイヤーを横から見て、頭モデルと目標地点の関係を示しています。頭モデルの中心は (x0, y0, z0 + self.eye_height) です。Playerクラスのクラス変数eye_height は self.eye_height で取得できます。

$$
極座標 (r, \theta, \phi) を直交座標 (x, y, z) に変換すると\\
x = r \sin \theta cos \phi\\
y = r \sin \theta sin \phi\\
z = r \cos \theta
$$

上記の座標変換公式に $${r = 1, \theta = 90 - theta, \phi = phi + 90}$$ を代入すると次のコードになります。このコードから、視線の方向ベクトルが得られます。($${\theta}$$ の方向に注意)


            direction_vec = Vec3(
                sin(radians(90 - theta)) * cos(radians(phi + 90)),
                sin(radians(90 - theta)) * sin(radians(phi + 90)),
                cos(radians(90 - theta))
            )

$$
点 (x_0, y_0, z_0) を通り、方向ベクトル (a, b, c) の直線の方程式\\
\frac{x - x_0}{a} = \frac{y - y_0}{b} = \frac{z - z_0}{c}
$$


            z = 0
            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

上記、3次元直線の公式を変形して、上記のコードから目標点の座標が得られます。


            if (Point3(x, y, z) - Point3(x0, y0, z0 + self.eye_height)).length() < 8:
                return Point3(x, y, z)
            else:
                return None

条件式により、頭の中心より 8 より近い点は、目標地点を返し、それ以外はNone を返すようにしました。これで get_target_positionメソッドの説明は終わりです。


def set_target_block(self, task):
    self.target_position = self.get_target_position()
    if self.target_position:
        if self.target_node.isHidden():
            self.target_node.show()
        self.c.setPos(
            floor(self.target_position.x),
            floor(self.target_position.y),
            floor(self.target_position.z),
        )
    else:
        if not self.target_node.isHidden():
            self.target_node.hide()

set_target_blockメソッドは、ターゲットブロックを目標地点に置きます。self.target_position が存在するときは、target_nodeノードを showメソッドで表示して、setPosメソッドでターゲットブロックの位置を決定します。存在しない(None)ときは、target_nodeノードを hideメソッドで非表示にします。

set target block

前回作成した 08_01_main.py を実行すると、プレイヤーの視線の先にターゲットブロックを置くことができます。
次にプレイヤーの操作で石ブロックを置けるようにします。

プレイヤー操作でブロックを設置、破壊する

"""src/block.py"""

クラスの最後にメソッドを追加

    def is_block_at(self, position):
        x, y, z = [floor(value) for value in position]
        key = f'{x}_{y}_{z}'
        return key in self.block_dictionary

ブロックがある場所に重ねてブロック置いたり、ブロックがない場所を破壊したりしないように、block.py を修正します。
is_block_atメソッドは、その場所にブロックがあるかどうか判定します。辞書block_dictionary はキーに位置情報を保存しているので、該当箇所のキーが存在するか調べることで、ブロックの有無を判定します。
リスト内表記 [floor(value) for value in position]により、小数で与えらる位置情報を整数に変換してから、key = f'{x}_{y}_{z}'により辞書のキーを指定します。

"""src/player.py"""

ユーザーのキー操作を追記

        # ユーザーのキー操作
        base.accept('w', self.update_key_map, ["w", 1])
        base.accept('w-up', self.update_key_map, ["w", 0])
        base.accept('a', self.update_key_map, ["a", 1])
        base.accept('a-up', self.update_key_map, ["a", 0])
        base.accept('s', self.update_key_map, ["s", 1])
        base.accept('s-up', self.update_key_map, ["s", 0])
        base.accept('d', self.update_key_map, ["d", 1])
        base.accept('d-up', self.update_key_map, ["d", 0])
        base.accept('mouse1', self.player_remove_block)  # 追記
        base.accept('mouse3', self.player_add_block)  # 追記

player.py を修正します。
ユーザーのキー操作の部分に、マウスクリックでブロックを設置、破壊するメソッドを実行するコードを追記します。引数mouse1 は左クリック、引数mouse3 は右クリックを意味します。それぞれ、player_remove_blockメソッド、player_add_blockメソッドと関連づけます。

"""src/player.py"""

クラスの最後にメソッドを追記

def player_add_block(self):
    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,
            'stone'
        )

def player_remove_block(self):
    if self.target_position and \
            self.base.block.is_block_at(self.target_position):
        self.base.block.remove_block(
            self.target_position.x,
            self.target_position.y,
            self.target_position.z
                )

player_add_blockメソッドはブロックをターゲットブロックの位置に設置します。self.base.block.is_block_at(self.target_position)によりブロックが存在しないとき、self.base.block.add_blockメソッドで石ブロックを設置します。
player_remove_blockメソッドは、ターゲットブロックの位置にブロックが存在するとき、self.base.block.remove_blockメソッドでブロックを破壊します。

add or break blocks

08_01_main.py を実行して、プレイヤーの操作でブロックを設置、破壊できることを確認してください。地面に接した場所(z = 0)限定ですが、石ブロックを置くことができるようになりました。

次回は、今回の続きでブロックの設置と破壊の実装を行います。ブロックを高さ1 以上に置けるようにすることと、設置するブロックの種類を選べるように機能を拡張します。


前の記事
Pythonでマイクラを作る ⑧カメラ機能を拡張する
次の記事
Pythonでマイクラを作る ⑩設置するブロックを選択する

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


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