見出し画像

Pythonでマイクラを作る ⑰建築MODで村を作る

Pythonでマイクラを作る 第17回目です。今回は本格的な建築を行います。
本家マイクラでは、MODモッド(Modification)と言われる非公式の機能拡張プログラムがあります。プレイヤーや敵モブの見た目を変えたり、公式では存在しないアイテムを追加したり、便利な機能を追加したりすることができます。
マイクラクローンに建築MODを導入します。道路、街路樹、家を簡単に作れる機能を追加して、小さな村を作ります。では始めましょう。

ディレクトリ構造

# ディレクトリ構造
Documents/
  ├ pynecrafter/
  │  ├ fonts/
  │  │  
  │  ├ images/
  │  │  
  │  ├ textures/
  │  │  
  │  ├ models/
  │  │  
  │  ├ src/
  │  │  ├ __init__.py
  │  │  ├ block.py  # ブロック関連
  │  │  ├ player.py  # プレイヤー関連
  │  │  ├ player_model.py  # プレイヤーモデル関連
  │  │  ├ camera.py  # カメラ関連
  │  │  ├ target.py  # ターゲットブロック関連
  │  │  ├ user_interface.py  # インターフェース関連
  │  │  ├ utils.py  # ユーティリティー
  │  │  ├ inventory.py  # インベントリ
  │  │  ├ menu.py  # メニュー関連
  │  │  ├ architecture.py  # 建築MOD  # 追加
  │  │  ├ mc.py  # 統合クラス
  │  │  
  │  ├ 17_01_main.py  # 統合クラスをインポートしてゲームを起動する. #  追加

今回追加するのは2つのファイルです。
srcディレクトリ内に architecture.py を作成し、Architectureクラスを記述します。これが建築MOD になります。
17_01_mian.py は、建築MOD用のワールドを生成します。

建築MOD導入のための準備

"""src/__init__.py"""
from .block import Block
from .player import Player
from .user_interface import UserInterface
from .inventory import Inventory
from .menu import Menu
from .architecture import Architecture  # 追加
from .mc import MC

__init__.py は、srcパッケージの初期設定を記述します。Architectureクラスをインポートする行を追記します。MCクラスのインポートより前に記述してください。

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


class MC(ShowBase, UserInterface, Inventory, Menu, Architecture):  # 追記

MCクラスは全てのクラスを統合して、ゲームを起動するクラスです。MCクラスの引数に Architecure を追記すると、Architectureクラスを継承することができます。これで、Architectureクラスのメソッドが使えるようになりました。

"""src/block.py"""

追加

def add_block_if_not_exists(self, x, y, z, block_id):
    if not self.is_block_at(Point3(x, y, z)):
        self.add_block(x, y, z, block_id)

Blockクラスに、add_block_if_not_exsitsメソッドを追加します。このメソッドは、設置したい場所にブロックがないときに、ブロックを設置できるメソッドです。Architectureクラスのメソッドで使用します。

建築MOD(道路)

"""src/architecture.py"""
from random import randint
from panda3d.core import *


class Architecture:

    def make_road(self, length=128, width=9, initial_position=Point3(0, 0, 0), angle=0):
        for i in range(length):
            for j in range(width):
                if 0 < i % 10 < 5 and j == width // 2:
                    block_id = 'sand'
                else:
                    block_id = 'stone'
                for k in [0, 0.5]:
                    x = (i + k) * cos(radians(angle)) - (j + k) * sin(radians(angle))
                    y = (i + k) * sin(radians(angle)) + (j + k) * cos(radians(angle))
                    z = -1
                    position = initial_position + Point3(x, y, z)
                    self.block.add_block_if_not_exists(*position, block_id)

Architecureクラスを記述していきます。まずは道路を作ってみましょう。

Architectureクラスは初期化メソッド(def __init__(self) )は必要ないので省略できます。
make_roadメソッドは道路を自動建築するメソッドです。引数は、length(長さ)、width(幅)、initial_position(設置の基準位置)、そして angle(道路を伸ばす角度)を指定できます。
道路は stoneブロックで、白線は sandブロックで作ります。条件式 if 0 < i % 10 < 5 and j == width // 2: によって、白線部分を判定し、block_id を決定しています。
angle が 0 または90 の整数倍以外の時は、道路のブロックに隙間ができてしまいます。その隙間を埋めるために、for k in [0, 0.5]: の繰り返しを追加して、0.5単位でブロックの位置を決めて、add_block_if_not_existsメソッドでブロックを設置します。

"""17_01_main.py"""
from math import *
from random import randint, choice
from panda3d.core import *
from src import MC


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

        # プレイヤーの位置を変更
        self.player.position = Point3(5, -30, 0)

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

        # 道路
        self.make_road(initial_position=Point3(-64, 0, 0))
        self.make_road(length=64, initial_position=Point3(9, -32, 0), angle=90)
        self.make_road(length=32, initial_position=Point3(9, 30, 0), angle=45)
        self.make_road(length=32, initial_position=Point3(9, 35, 0), angle=135)


game = Game()
game.run()
道路を建築

17_01_main.py は村を生成するプログラムを書いていきます。
make_roadメソッドで4本の道路を建築します。X軸と平行な道路、Y軸に平行な道路、そしてY字路を作る 45度傾いた道路2本です。

建築MOD(街路樹)

"""src/architecture.py"""

メソッドを追加

    def make_tree(self, initial_position=Point3(0, 0, 0), log_block_id='oak_log'):
        x, y, z = initial_position
        tree_height = randint(6, 8)
        leaf_block_id = 'oak_leaves'
        for i in range(tree_height):
            if i < 3:
                self.block.add_block_if_not_exists(x, y, z + i, log_block_id)
            elif i == tree_height - 1:
                self.block.add_block_if_not_exists(x, y, z + i, leaf_block_id)
            else:
                for j in range(3):
                    for k in range(3):
                        self.block.add_block_if_not_exists(x - 1 + j, y - 1 + k, z + i, leaf_block_id)

Architectureクラスに make_treeメソッドを追加します。
make_treeメソッドは街路樹を自動で建築します。引数は initial_position(街路樹を設置する基準点)、log_block(幹ブロックのID)を指定します。
randomモジュールの randintメソッドはランダムな整数を返すメソッドです。街路樹にランダム性を持たせたいため、randint(6, 8)により高さを 6 ~ 8 にばらつきを持たせました。
変数i で条件分岐させて、幹の部分、葉の部分に分けて街路樹のブロックを設置していきます。

"""17_01_main.py"""

追記

        # 街路樹
        for i in range(-101, 100, 10):
            for j in [-1, 9]:
                self.make_tree(initial_position=Point3(i, j, 0))
街路樹を植える

17_01_main.py に街路樹を建築するコードを追加します。
X軸と平行な道路の両脇に街路樹を 10メートル間隔で設置しました。range関数の第3引数(=10)を指定すると、10 個飛ばしで数を数え上げてくれます。高さにバリエーションのある街路樹 42本を植えることができました。

建築MOD(家)

"""src/architecture.py"""

メソッドを追加

    def make_house(self, initial_position=Point3(0, 0, 0), w=10, d=12, h=12,
              roof_block_id='red_wool', wall_block_id='white_wool', pillar_block_id='brown_wool'):
        x, y, z = initial_position
        half_w = int((w + 2) / 2)
        # 屋根の高さを計算する
        heights = []
        for i in range(w + 2):
            if i < half_w:
                if w % 2 == 0:
                    heights.append(h - half_w + i)
                else:
                    heights.append(h - half_w + i - 1)
            else:
                heights.append(h + half_w - i - 1)
        print(heights)

        # 家を建築する
        for i in range(w + 2):
            for j in range(d):
                roof_height = heights[i]
                if 0 < i < w + 1:  # 家の部分
                    for k in range(roof_height):
                        if (i == 1 or i == w) or (j == 0 or j == d - 1):  # 壁の部分
                            if (i == 1 and j == 0) or (i == 1 and j == d - 1) or \
                                    (i == w and j == 0) or (i == w and j == d - 1):  # 四隅の柱部分
                                block_id = pillar_block_id
                            elif heights[i] >= h - 1 and (k == 3 or k == 4):  # 窓部分
                                block_id = 'glass'
                            else:
                                block_id = wall_block_id
                            self.block.add_block_if_not_exists(i + x, j + y, k + z, block_id)
                    else:
                        self.block.add_block_if_not_exists(i + x, j + y, k + z + 1, roof_block_id)
                else:  # 屋根の張り出し部分
                    self.block.add_block_if_not_exists(i + x, j + y, heights[1] + z - 1, roof_block_id)

Architectureクラスに、家を自動建築する make_houseメソッドを追記します。
make_houseメソッドの引数は、initial_position(建築の基準位置)、w, d, h(横width、奥行きdepth、高さheight)、roof_block_id, wall_block_id,pillar_block_id(屋根、壁、柱のブロックID)の7つです。引数を変更することで、さまざまなバリエーションの家を作ることができます。


        # 屋根の高さを計算する
        heights = []
        for i in range(w + 2):
            if i < half_w:
                if w % 2 == 0:
                    heights.append(h - half_w + i)
                else:
                    heights.append(h - half_w + i - 1)
            else:
                heights.append(h + half_w - i - 1)

マイクラ建築をされたことがある方はわかると思いますが、三角屋根の家は作るのが面倒です。家の幅が偶数のときは一番上のブロックが2つ並びます。家の幅が奇数のときは一番上のブロックが1つです。
上記のコードが横方向の位置による屋根ブロックの高さを計算する部分です。
例えば、横8高さ12のときは、heights = [7, 8, 9, 10, 11, 11, 10, 9, 8, 7] になります。横9高さ12のときは、heights = [6, 7, 8, 9, 10, 11, 10, 9, 8, 7, 6] になります。最大高さが 12 で左右対称の屋根ブロックの高さを計算できました。


        # 家を建築する
        for i in range(w + 2):
            for j in range(d):
                roof_height = heights[i]
                if 0 < i < w + 1:  # 家の部分
                    for k in range(roof_height):
                        if (i == 1 or i == w) or (j == 0 or j == d - 1):  # 壁の部分
                            if (i == 1 and j == 0) or (i == 1 and j == d - 1) or \
                                    (i == w and j == 0) or (i == w and j == d - 1):  # 四隅の柱部分
                                block_id = pillar_block_id
                            elif heights[i] >= h - 1 and (k == 3 or k == 4):  # 窓部分
                                block_id = 'glass'
                            else:
                                block_id = wall_block_id
                            self.block.add_block_if_not_exists(i + x, j + y, k + z, block_id)
                    else:
                        self.block.add_block_if_not_exists(i + x, j + y, k + z + 1, roof_block_id)
                else:  # 屋根の張り出し部分
                    self.block.add_block_if_not_exists(i + x, j + y, heights[1] + z - 1, roof_block_id)

上記コードが実際にブロックを設置する部分です。条件式がたくさん含まれますが、ブロックを置くかどうか、どのブロックを置くかを判定しています。コメントで判定する条件について書いてありますので、コードと照らし合わせて読み取ってください。

"""17_01_main.py"""

追記

        # 家
        roof_colors = ['red', 'blue', 'green', 'pink', 'gray']
        for i in range(-51, 40, 30):
            for j in [-21, 30]:
                w = randint(8, 10)
                d = randint(8, 10)
                h = randint(8, 10)
                x = i + (21 - w) // 2
                if j == -21:
                    y = j
                else:
                    y = j - d
                roof_block_id = choice(roof_colors) + '_wool'
                self.make_house(initial_position=Point3(x, y, 0), w=w, d=d, h=h, roof_block_id=roof_block_id)
家の建築

17_01_main.py に家を建築するコードを追記します。X軸に平行に家を8軒建築しました。randintメソッドを使って、家のサイズはバリエーションを持しています。屋根の色は、赤、青、緑、ピンク、グレイの中からランダムに選ばれます。
これで村は完成です。

プレイヤーの空中浮揚を実装

作成した村を上から見下ろすために、プレイヤーの空中浮揚を実装します。矢印上キーを押すとプレイヤーを垂直に上に移動させ、矢印下で下に移動させます。

"""src/player.py"""


class Player(PlayerModel, Camera, Target):# コンストラクタ
    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
        self.is_on_ground = True  # 地面に接している
        self.is_flying = False  # 空中に浮かんでいる   # 追加

Playerクラスを修正します。空中に浮かんでいるか判定するために、インスタンス変数is_flyng を追加します。

"""src/player.py"""

        # キー操作を保存
        self.key_map = {
            'w': 0,
            'a': 0,
            's': 0,
            'd': 0,
            'space': 0,
            'arrow_up': 0,   # 追加
            'arrow_down': 0,   # 追加
        }

        # ユーザーのキー操作
        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)
        base.accept('space', self.update_key_map, ["space", 1])
        base.accept('space-up', self.update_key_map, ["space", 0])
        base.accept('arrow_up', self.update_key_map, ["arrow_up", 1])   # 追加
        base.accept('arrow_up-up', self.update_key_map, ["arrow_up", 0])   # 追加
        base.accept('arrow_down', self.update_key_map, ["arrow_down", 1])   # 追加
        base.accept('arrow_down-up', self.update_key_map, ["arrow_down", 0])   # 追加

ユーザーのキー操作を有効にするために、インスタンス変数key_map に 'arrow_up' と 'arrow_down' の2つのキーを追加します。acceptメソッドで、矢印上キーを押すと key_map['arrow_up'] を 1 に、矢印下で key_map['arrow_down'] を 1 に変更するようにします。

"""src/player.py"""

    def update_velocity(self):
        key_map = self.key_map

        if self.is_on_ground or self.is_flying:  # 修正
            if key_map['w'] or key_map['a'] or key_map['s'] or key_map['d']:
                heading = self.direction.x
                if key_map['w'] and key_map['a']:
                    angle = 135
                elif key_map['a'] and key_map['s']:
                    angle = 225
                elif key_map['s'] and key_map['d']:
                    angle = 315
                elif key_map['d'] and key_map['w']:
                    angle = 45
                elif key_map['w']:
                    angle = 90
                elif key_map['a']:
                    angle = 180
                elif key_map['s']:
                    angle = 270
                else:  # key_map['d']
                    angle = 0
                self.velocity = \
                    Vec3(
                        cos(radians(angle + heading)),
                        sin(radians(angle + heading)),
                        0
                    ) * Player.speed
            else:
                self.velocity = Vec3(0, 0, 0)

            if key_map['space']:
                self.is_on_ground = False
                self.is_flying = False  # 追記
                self.velocity.setZ(Player.jump_speed)

update_velocityメソッドを修正します。
条件式 if self.is_on_ground or self.is_flying: により、空中浮揚しているときも WASDキーで平行移動できるようにします。そしてスペースキーを押してジャンプすると空中浮揚が無効化されるように、self.is_flying = False を追加します。

"""src/player.py"""

    def update_position(self):
        self.update_velocity()
        dt = globalClock.getDt()
        self.position = self.position + self.velocity * dt

        floor_height = self.base.block.get_floor_height() + 1

        # # ジャンプ中の位置情報を保存
        # self.record_jump_positions_with_time(dt, floor_height)

追記ここから
        key_map = self.key_map
        if key_map['arrow_up']:
            self.is_on_ground = False
            self.is_flying = True
            self.position.setZ(self.position.getZ() + Player.jump_speed * dt)
        elif key_map['arrow_down']:
            self.position.setZ(self.position.getZ() - Player.jump_speed * dt)
            if self.position.z <= floor_height:
                self.position.z = floor_height
                self.is_on_ground = True
                self.is_flying = False
追記ここまで

        if not self.is_flying:  # 追記
            if not self.is_on_ground:
                if self.position.z <= floor_height:
                    self.position.z = floor_height
                    self.is_on_ground = True
                else:
                    self.velocity.setZ(self.velocity.getZ() - Player.gravity_force * dt)
            else:
                if floor_height < self.position.z:
                    self.is_on_ground = False
                    self.velocity.setZ(-Player.gravity_force * dt)

update_positionメソッドを修正します。
key_map のキー'arrow_up' を読み取って、ユーザーが矢印上を押していると判定したら、インスタンス変数is_on_ground = False、is_flying = True に変更して空中浮揚中であることを保存します。プレイヤーのZ方向の位置を上に移動させます。
key_map のキー'arrow_down' を読み取って、ユーザーが矢印下を押していると判定したら、プレイヤーのZ方向の位置を下に移動させます。プレイヤー位置が地面より下に来たとき着地したと判定し、インスタンス変数is_on_ground = True、is_flying = False に変更して空中浮揚中が終わったことを保存します。
最後、条件式 if not self.is_flying: を追記して、空中浮揚中は重力の影響を受けなくしてコードは完成です。

17_01_main.py を実行して、村ワールドを起動します。矢印上、下キーで空中浮揚できることを確認してください。村を上から見下ろして楽しんでください。

今回は建築MOD を作成しました。読者の皆さんはこのMOD をさらに拡張して、ビルや井戸、公園などを自動生成できるコードを追加してみてください。面白い村ができたら報告してくださいね。
次回は本家マイクラとの連携を考えます。今回作成した村ワールドを本家マイクラにコピーする方法を学びます。お楽しみに。


前の記事
Pythonでマイクラを作る ⑯レンダリングの高速化
次の記事
Pythonでマイクラを作る ⑱マインクラフトにワールドを転送する

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


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