![見出し画像](https://assets.st-note.com/production/uploads/images/83576341/rectangle_large_type_2_3355ae48f38bee2622f3de89766c1b83.png?width=1200)
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()
![](https://assets.st-note.com/img/1658935132172-MVRyRWcwSX.png?width=1200)
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))
![](https://assets.st-note.com/img/1658936011944-ZVKUc34dbm.png?width=1200)
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)
![](https://assets.st-note.com/img/1659040038305-3dwdXGesnj.png?width=1200)
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でマイクラを作る ⑱マインクラフトにワールドを転送する
その他のタイトルはこちら
この記事が気に入ったらサポートをしてみませんか?