見出し画像

Pythonでマイクラを作る ⑤ブロックの設置と破壊

Pythonでマイクラを作る 第5回目です。今回は Blockクラスを作り、ブロックの設置と破壊が行えるようにします。いよいよ本格的にマイクラクローンを作っていきます。頑張っていきましょう。

ブロックの設計

マイクラにおいて、ブロックは自由にどこにでも置けるわけではありません。ブロックの設置の条件は次の2点です。

接する面があるときにブロックを設置できる
  1. 接する面があるときにブロックを設置できる

  2. ブロックを置いていない位置にブロックを設置できる

条件 1 はブロックを空間に(浮かせて)置くことができないということです。上下左右前後のいずれかの面にくっつけてブロックを追加していく必要があります。条件 2 は同じ位置に別のブロックを置けないということです。ブロックを同じ位置に重ねて置くことはできません。

ブロックを設置しようとするとき、この2つの条件が満たされるかチェックしなければなりません。チェックを簡単にするために、ブロックに次の制限を加えます。

  1. ブロックのサイズは全て同じ(1 x 1 x 1 の立方体)である

  2. ブロックは整数の位置にだけ設置できる

ブロックが置けるかチェックする

この2つの制限があると、上下左右前後の6箇所を調べてブロックが存在したら置けると判定できます(図のガラスブロックの場所)。そして置こうとした場所にブロックが存在したら置けないと判定できます(図の石ブロックの場所)。つまり7箇所のブロックの存在を調べるだけでよくなります。

以上でブロックの設計ができました。次に Blockクラスを含む、全体のクラス設計をします。

クラスの設計

これから作成するマイクラクローンは、たくさんの機能を持つ巨大なプログラムになります。うまくクラスを設計して、見通しの良いプログラムを作りましょう。基本的な考え方として、機能ごとにクラスを分けて、それらを統合するクラスからゲームを起動することにしました。

# ディレクトリ構造
Documents/
  ├ pynecrafter/
  │  ├ images/
  │  │  ├ 48-488312_blockcss-minecraft-terrain-png-1-0-0.py
  │  │  
  │  ├ textures/
  │  │  ├ 0-1.png
  │  │  ├ 0-2.png
  │  │  
  │  ├ models/
  │  │  ├ grass_block.egg
  │  │  ├ stone.egg
  │  │  ├ dirt.egg
  │  │  
  │  ├ src/
  │  │  ├ __init__.py
  │  │  ├ block.py  # ブロック関連
  │  │  ├ mc.py  # 統合クラス
  │  │  
  │  ├ 01_01_showbase.py
  │  ├ 01_02_showbase.py
  │  ├ 01_03_showbase.py
  │  ├ xxx.py
  │  ├ 05_01_main.py  # 統合クラスをインポートしてゲームを起動する

上図のディレクトリ構造でディレクトリとファイルを作成します。src(sourceの略)ディレクトリにクラスを定義するファイル(モジュール)をまとめます。
次に、各モジュールを一つずつ説明していきます。

__init__.py

"""src/__init__.py"""
from .block import Block
from .mc import MC

__init__.py は、srcディレクトリ内のモジュールを呼び出す初期化処理を行います。srcディレクトリ外のファイルから import src とすることで、srcディレクトリ中のモジュール全てのクラス等にアクセスできるようになります。
またsrcディレクトリ内の別ファイルからは import . することで全てのクラス等にアクセスできます(. ドットは同じディレクトリを表す)。

mc.py

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


class MC(ShowBase):
    def __init__(self, ground_size=128):
        # ShowBaseを継承する
        ShowBase.__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.disableMouse()
        # カメラの設定
        self.camera.setPos(60, -150, 90)
        self.camera.lookAt(0, 0, 0)

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

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

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

MCクラスはマイクラ(MineCraft の略)の名前を持つ統合クラスです。srcディレクトリ内のモジュールを全て使用できます。main.py からインポートされ、ゲームを起動できるクラスです。
ShowBase を継承することで、Panda3Dの画面描画機能等が使えます。panda3d.core に含まれる便利機能も使用できます。
from . import * とすることで、srcディレクトリにある全てのクラス等を呼び出すことができます。
self.block = Block(self, ground_size) とすることで、Blockインスタンスをインスタンス変数block に代入して使えるようにしています。
getメソッド、setメソッドは、getattr関数、setattr関数をラップしたもので、インスタンス変数の名前変数で指定することができるようになり、大変便利です。Blockクラス内で使用されているので、使い方はそちらで説明いたします。

block.py

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


class Block:
    def __init__(self, base, ground_size):
        self.base = base
        self.ground_size = ground_size
        self.block_dictionary = {}

        # グラウンドを作成
        self.set_flat_world()

    def add_block_dictionary(self, x, y, z, block_id):
        key = f'{floor(x)}_{floor(y)}_{floor(z)}'
        self.block_dictionary[key] = block_id

    def add_block_model(self, x, y, z, block_id):
        key = f'{floor(x)}_{floor(y)}_{floor(z)}'
        self.base.set(key, self.base.render.attachNewNode(PandaNode(key)))
        placeholder = self.base.get(key)
        placeholder.setPos(floor(x), floor(y), floor(z))
        block = self.base.loader.loadModel(f'models/{block_id}')
        block.reparentTo(placeholder)

    def add_block(self, x, y, z, block_id):
        self.add_block_dictionary(x, y, z, block_id)
        self.add_block_model(x, y, z, block_id)

    def remove_block_dictionary(self, x, y, z):
        key = f'{floor(x)}_{floor(y)}_{floor(z)}'
        if key in self.block_dictionary:
            del self.block_dictionary[key]

    def remove_block_model(self, x, y, z):
        key = f'{floor(x)}_{floor(y)}_{floor(z)}'
        placeholder = self.base.get(key)
        if placeholder:
            placeholder.removeNode()

    def remove_block(self, x, y, z):
        self.remove_block_dictionary(x, y, z)
        self.remove_block_model(x, y, z)

    def set_flat_world(self):
        ground_size = self.ground_size
        for i in range(ground_size):
            for j in range(ground_size):
                x = i - ground_size // 2
                y = j - ground_size // 2
                z = -1
                self.add_block(x, y, z, 'grass_block')

Blockクラスはブロックの設置と破壊を操作します。マイクラはブロックを操作するゲームですから、このクラスがゲームの根本を操作するクラスということになります。今後の作業のためにも、しっかり理解するようにしてください。詳しく説明していきます。

self.base = base

インスタンス変数base は、MCインスタンスが代入されます。self.base.xxx  のドット記法で、Blockクラスの中からMCインスタンスの変数やメソッドにアクセスできるようになります。

self.block_dictionary = {}

ブロックの位置情報を管理するため、インスタンス変数 block_dictionary を辞書として定義します。辞書のキーは位置情報を表し、x, y, z座標を_で繋いだ文字列(x_y_z)を使用します。x, y, z には整数が入ります。辞書の値はブロックID を登録します。

# 座標 (1, 0, 0) に草ブロックが置かれているとき
self.block_dictionary = {
    '1_0_0': 'grass_block',
}

例として、座標 (1, 0, 0) に草ブロックが置かれているとき、block_dictionary は次のキーと値のペアを持ちます。

    def add_block(self, x, y, z, block_id):
        self.add_block_dictionary(x, y, z, block_id)
        self.add_block_model(x, y, z, block_id)

add_blockメソッドはブロックの設置機能であり、add_block_dictionaryメソッドと add_block_modelメソッドの2つを含みます。

    def add_block_dictionary(self, x, y, z, block_id):
        key = f'{floor(x)}_{floor(y)}_{floor(z)}'
        self.block_dictionary[key] = block_id

add_block_dictionaryメソッドは、インスタンス変数block_dictionary にブロックの情報を登録します。キーは位置情報 x_y_z を、値はブロックIDを保存します。インスタンス変数 block_dictionary のキーを調べて、その場所にブロックを置くことができるかどうか調べることができるようになります。

placeholder node
    def add_block_model(self, x, y, z, block_id):
        key = f'{floor(x)}_{floor(y)}_{floor(z)}'
        self.base.set(key, self.base.render.attachNewNode(PandaNode(key)))
        placeholder = self.base.get(key)
        placeholder.setPos(floor(x), floor(y), floor(z))
        block = self.base.loader.loadModel(f'models/{block_id}')
        block.reparentTo(placeholder)

add_block_modelメソッドは、画面上にブロックモデルを表示します。
モデルを直接 render に表示するのではなく、ノード(ツリー構造の分岐点)を挟んで表示する方法を用います(上図参照)。ノードの名前を「x_y_z」の形式としました。ブロックを削除するときは、ノードの名前を使って、ノードごと削除することができます。


        key = f'{floor(x)}_{floor(y)}_{floor(z)}'
        self.base.set(key, self.base.render.attachNewNode(PandaNode(key)))
        placeholder = self.base.get(key)

ここで、MCクラスで定義したgetメソッドsetメソッドが出てきます。
ノードを作成するとき、ノードの名前は、変数key = f'{floor(x)}_{floor(y)}_{floor(z)}' の形で与えられます。
インスタンス変数の名前変数で与えられたときは、setメソッド、getメソッドを使って、インスタンス変数の定義と取得を行うことができます。self.base.set() の第一引数がインスタンス変数の名前、第2引数が定義内容です。self.base.get() は第一引数にインスタンス変数の名前を入れることで、定義内容を取り出すことができます。

    def set_flat_world(self):
        ground_size = self.ground_size
        for i in range(ground_size):
            for j in range(ground_size):
                x = i - ground_size // 2
                y = j - ground_size // 2
                z = -1
                self.add_block(x, y, z, 'grass_block')

set_flat_worldメソッドはフラットワールドを作成します。与えられた変数ground_sizeの大きさで、高さ-1 に草ブロックを敷き詰めます。

ブロック破壊系のメソッドremove_xxx は後ほど説明いたします。

05_01_main.py

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


class Game(MC):
    def __init__(self):
        # MCを継承する
        MC.__init__(self)

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


game = Game()
game.run()
05_01_main.py
from src import MC

05_01_main.pyは、mc.pyを読み込んで、ゲームを実行することができます。__init__.py で初期化処理を行なっているため、form src import xxx でsrcディレクトリ内の全てのクラスにアクセスできます。MCクラスをインポートしました。
実行すると、サイズ128x128 のフラットワールドが表示されます。これがゲームの基本形であり、ここにプレイヤーやモブなどを乗せていくことでマイクラに近づけていくことができます。

ここまで来たら後は簡単、とはいきませんが、一つの山を越えることができました。ここからの改造は比較的簡単にできるからです。前回作ったお城のアップグレードバージョンをフラットワールドに作ってみましょう。

お城 ver.2 を作る

"""05_02_main.py"""

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

        # お城
        # 壁
        for i in range(60):
            if i % 3 == 0:
                for y in (-20, -17, 16, 19):
                    self.block.add_block(i - 30, y, 16, 'diamond_block')
                if i < 40:
                    for x in [-30, -25, 25, 29]:
                        self.block.add_block(x, i - 20, 16, 'diamond_block')
            for j in range(40):
                for k in range(16):
                    if 0 <= i <= 3 or 56 <= i <= 59 or 0 <= j <= 3 or 36 <= j <= 39:
                        self.block.add_block(i - 30, j - 20, k, 'gold_block')

        # 台座
        for i in range(18):
            for j in range(14):
                for k in range(12):
                    self.block.add_block(i - 9, j - 7, k, 'emerald_block')

        # 尖塔
        for x0, y0 in ((0, 0), (-28, -18), (-28, 18), (28, 18), (28, -18)):
            if x0 == 0:
                shift_z = 12
            else:
                shift_z = 0
            for i in range(13):
                for j in range(13):
                    # シリンダー
                    for k in range(20):
                        if 5**2 < (i - 6)**2 + (j - 6)**2 <= 6**2:
                            self.block.add_block(x0 + i - 6, y0 + j - 6, k + shift_z, 'bricks')

                    # コーン(円錐)
                    for k in range(12):
                        r = 6 - k / 2
                        if ((r - 1) ** 2 < (i - 6) ** 2 + (j - 6) ** 2 <= r ** 2) or (i == 6 and j == 6 and k == 11):
                            self.block.add_block(x0 + i - 6, y0 + j - 6, k + 20 + shift_z, 'blue_wool')

            # サイン波
            for i in range(6):
                for j in range(3):
                    x = i
                    y = sin(pi * x / 6) * 2
                    z = j
                    self.block.add_block(x0 + x, y0 + y, z + 32 + shift_z, 'red_wool')
05_02_main.py

前回作ったお城を ver.2 にアップグレードしてみました。壁を厚くして天井にダイアモンドの飾りブロックを乗せました。中央の塔は台座に乗せて、Z方向に移動しました。カッコ良くなりましたね。

05_02_main.py  の説明をします。
前回作成した 04_05_castle.py と似ていますが、ブロックを置く部分のコードが Blockクラスの add_block()メソッドに変更されています。Blockクラスを作成したおかげで、少しコードの見通しが良くなりました。

さて、このままではお城に入れないので、壁に穴を開けたいです。Blockクラスで定義した破壊系のメソッドで、壁の中央に大門を作ります。ついでに、5つの塔に窓を追加してみましょう。

壁をくり抜いて大門を作る

"""05_03_main.py"""

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

        # 大門
        for i in range(6):
            for j in range(40):
                for k in range(9):
                    self.block.remove_block(i - 3, j - 20, k)

        # 尖塔の窓
        for x0, y0 in ((0, 0), (-28, -18), (-28, 18), (28, 18), (28, -18)):
            if x0 == 0:
                shift_z = 12
            else:
                shift_z = 0
            for i in range(80):
                for j in range(1, 4):
                    h = j * 5
                    self.block.remove_block(x0, y0 + i - 40, h + shift_z)
                    self.block.remove_block(x0, y0 + i - 40, h + shift_z + 1)
                    self.block.remove_block(x0 + i - 40, y0, h + shift_z)
                    self.block.remove_block(x0 + i - 40, y0, h + shift_z + 1)
05_03_main.py

ブロックを削除することで壁に大門を付けて、棟に窓を付けました。本物のお城にかなり近づけることができました。破壊系のメソッドを使いこなすことができれば、建築した後で穴をあけることで、より複雑な建築ができるようになります。

"""src/block.py"""

Blockクラスの破壊系のメソッド

    def remove_block_dictionary(self, x, y, z):
        key = f'{floor(x)}_{floor(y)}_{floor(z)}'
        if key in self.block_dictionary:
            del self.block_dictionary[key]

    def remove_block_model(self, x, y, z):
        key = f'{floor(x)}_{floor(y)}_{floor(z)}'
        placeholder = self.base.get(key)
        if placeholder:
            placeholder.removeNode()

    def remove_block(self, x, y, z):
        self.remove_block_dictionary(x, y, z)
        self.remove_block_model(x, y, z)

Blockクラスの破壊系のメソッドを再掲しました。remove_blockメソッドがブロックの破壊を操作します。remove_block_dictionaryメソッドとremove_block_modelメソッドの2つを含みます。

remove_block_dictionaryメソッドは、インスタンス変数block_dictionary からブロック情報を消去します。条件式 if key in self.block_dictionary: により、その場所にブロック情報が存在する時のみ、del self.block_dictionary[key]  により情報を取り除きます。

removeNode

 remove_block_modelメソッドは、画面上からブロックモデルを消去します。placeholder = self.base.get(key) により、変数key の名前をもつノードが取得できますが、存在しない時は None が返されます。条件式 if placeholder: によりノードが存在した時のみ、removeNodeメソッドによりノードごとブロックモデルが消されます。

今回はクラスを設計して、ブロックの設置と削除ができるようになりました。次回はプレイヤーをPanda3Dの世界に導入します。お楽しみに。


前の記事
Pythonでマイクラを作る ④プログラミングで自動建築
次の記事
Pythonでマイクラを作る ⑥プレイヤーを動かす

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


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