見出し画像

Pythonでマイクラを作る ⑱マインクラフトにワールドを転送する

Pythonでマイクラを作る 第18回目です。今回は本家マインクラフトとの連携を実装します。マイクラクローン(Pynecrafter)で作成した建築を本家マイクラに転送することができるようになります(トップ画像)。

まず本家マイクラにMOD(RemoteControllerMod)を導入して、マイクラクローンからコマンド(命令)を送信できるようにします。次に、マイクラクローン側にワールド転送プログラムを書いていきます。

RemoteControllerMod

本家マインクラフトとの連携の準備として、MODを導入したマインクラフト(Java Edition)を用意しなくてはなりません。MOD の導入については、Naohiro2g様の minecraft_remote を参照させていただきます。

minecraft_remote

今回は、マインクラフト(Java Edition)バージョン 1.12.2 を対象とします。必要なファイルは、2つです。リンク先からダウンロードしてください。
Forge 1.12.2
RemoteControllerMod-1.12.2 v0.02

Forge のダウンロード

forge-1.12.2

Forge はMODを導入するための基本プログラムです。多くのMODは、Forge を土台とすることで、インストールして使用することができます。
「Download Recommended」はお薦めバージョンなので、こちらを使用します。

froge install

ダウンロードしたファイルをダブルクリックして、Forgeのインストールを開始します。「Install client」が選ばれていることを確認して、「OK」ボタンをクリックしてください。あとは自動でインストール処理が進みます。
「Successfully installed client profile ….」と表示されたら、インストールは完了です。

MODのダウンロード

remote controller mod

MODのリンク先から、「Minecraft 1.12」の方を選んで、「ダウンロード」ボタンをクリックします。「ダウンロード」フォルダーに「RemoteController-0.02.jar」がダウンロードされていることを確認します。

次に、マインクラフトの設定を行います。

マインクラフトの設定

Minecraft settings 01


マインクラフト(Java Edition)を起動します。「起動構成」タブから「新規作成」ボタンをクリックしてください。

Minecraft settings 02

起動構成の編集画面が表示されます。
ゲームの名前は任意ですが、「1.12.2-forge」としました。
バージョンは、「release 1.12.2-forge-xxx」を選びます(先ほど、Forgeをインストールしたので、この項目が選べるようになっているのです)。
ゲームディレクトリも任意ですが、ゲームごとに別のディレクトリを作ることをお勧めします。今回は「ドキュメント」ディレクトリに「Minecraft」ディレクトリを作り、全てのゲームをここで管理することにしました。その中にゲームの名前と同じ「1.12.2-forge」というディレクトリを作って、そこをゲームディレクトリとして指定します。
全ての設定が終わったら、「保存」ボタンで保存します。

MODのインストール

minecraft play

マインクラフトの「プレイ」タブから、プレイ画面を表示します。
ゲームの名前「1.12.2-forge」を選んで、「プレイ」ボタンをクリックします。
ゲームが起動したら、「Quit Game」ボタンからゲームをそのまま終了します。次に、必要なMODをインストールします。

# ディレクトリ構造
Documents/
  ├ Minecraft/
  │  ├ 1.12.2-forge/
  │  │  ├ config/
  │  │  ├ logs/
  │  │  ├ mods/
  │  │  │  ├ RemoteController-0.02.jar
  │  │  │
  │  │  ├ resourcepacks/
  │  │  ├ saves/
  │  │  ├ xxx
  │  │  ├ 
  │  │  ├ 

「ドキュメント」ディレクトリ→「Minecraft」ディレクトリ→「1.12.2-forge」ディレクトリを開きます。この中に「mods」ディレクトリが作成されているので、「RemoteController-0.02.jar」ファイルを配置します。
これでMODのインストール準備は完了です。もう一度、マインクラフトを起動して、ゲーム名「1.12.2-forge」を「プレイ」します。

minecraft mods installed

MODがインストールされたマインクラフトが起動します。左下を見ると、Forge と MOD がインストールされていることが確認できます。
次に、ワールドを作成します。

ワールドの作成

create new world

ゲームが起動したら、「Singleplayer」ボタンから、ワールド選択画面に入ります。「Create New World」ボタンから、ワールドの作成を開始します。
ワールド名は任意で「New World」のままで進めます。Game Mode は「Creative」を選びます。「More World Options…」ボタンをクリックします。

Superflat

面倒な平地化作業を省略するために、平面のみのワールド「Superflat」を選びます。「Create New World」ボタンから、新しいワールドを作成します。

New World

ゲームが開始します。余計なブロックのない「スーパーフラット」ワールドが作成できました。これで準備は完了です。「エスケープ」キーからGame Menu画面に入って、「Save and Quit to Title」ボタンをクリックします。「Quit Game」ボタンで、マインクラフトを終了します。

mcpiライブラリのインストール

pipコマンドで「mcpi」ライブラリをインストールします。
Windowsの場合は、PowerShell を開いて、以下のコマンドを実行します。

$ pip install mcpi

Mac の場合は、ターミナルを開いて、以下のコマンドを実行します。

$ pip3 install mcpi

次は、マイクラクローン(Pynecrafter)側の操作になります。前回作成した村のワールドを本家マイクラに転送するプログラムを作成します。

ワールド転送プログラム

"""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 .connect_to_mcpi import ConnectToMCPI  # 追記
from .mc import MC

__init__.py はパッケージの初期化を行います。connect_to_mcpiモジュールから「ConnectToMCPIクラス」をインポートします。

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


class MC(ShowBase, UserInterface, Inventory, Menu, Architecture, ConnectToMCPI):  # 修正
    def __init__(self, ground_size=128, mode='normal'):
        self.mode = mode
        self.ground_size = ground_size
        # ShowBaseを継承する
        ShowBase.__init__(self)
        self.font = self.loader.loadFont('fonts/PixelMplus12-Regular.ttf')
        UserInterface.__init__(self)
        Inventory.__init__(self)
        Menu.__init__(self)
        if self.mode == 'mcpi':  # 追記
            ConnectToMCPI.__init__(self)  # 追記

以下略

MCクラスは、 全てのクラスをまとめて、ゲームを起動するクラスです。
MCクラスの引数に「ConnectToMCPIクラス」を追加して、継承します。そして、インスタンス変数mode が 'mcpi' のとき、ConnectToMCPIクラスの初期化メソッドを実行し、ワールドの転送機能が使えるようにします。

"""src/connect_to_mcpi.py"""
from mcpi.minecraft import Minecraft


class ConnectToMCPI():
    # connection port for MCPI
    PORT_MC = 4711

    # block IDs for MCPI  ## see block.py
    AIR = 0
    STONE = 1
    GRASS_BLOCK = 2
    DIRT = 3
    COBBLESTONE = 4
    OAK_PLANKS = 5
    SAPLING = 6
    BEDROCK = 7
    WATER_FLOWING = 8
    WATER = WATER_FLOWING
    WATER_STATIONARY = 9
    LAVA_FLOWING = 10
    LAVA = LAVA_FLOWING
    LAVA_STATIONARY = 11
    SAND = 12
    GRAVEL = 13
    GOLD_ORE = 14
    IRON_ORE = 15
    COAL_ORE = 16
    OAK_LOG = 17
    OAK_LEAVES = 18
    GLASS = 20
    LAPIS_ORE = 21
    LAPIS_BLOCK = 22
    SANDSTONE = 24
    BED = 26
    RAIL_POWERED = 27
    RAIL_DETECTOR = 28
    COBWEB = 30
    GRASS_TALL = 31
    DEAD_BUSH = 32
    WHITE_WOOL = 35
    ORANGE_WOOL = (35, 1)
    MAGENTA_WOOL = (35, 2)
    LIGHT_BLUE_WOOL = (35, 3)
    YELLOW_WOOL = (35, 4)
    LIME_WOOL = (35, 5)
    PINK_WOOL = (35, 6)
    GRAY_WOOL = (35, 7)
    LIGHT_GRAY_WOOL = (35, 8)
    CYAN_WOOL = (35, 9)
    PURPLE_WOOL = (35, 10)
    BLUE_WOOL = (35, 11)
    BROWN_WOOL = (35, 12)
    GREEN_WOOL = (35, 13)
    RED_WOOL = (35, 14)
    BLACK_WOOL = (35, 15)
    FLOWER_YELLOW = 37
    FLOWER_CYAN = 38
    MUSHROOM_BROWN = 39
    MUSHROOM_RED = 40
    GOLD_BLOCK = 41
    IRON_BLOCK = 42
    STONE_SLAB_DOUBLE = 43
    STONE_SLAB = 44
    BRICK_BLOCK = 45
    TNT = 46
    BOOKSHELF = 47
    MOSS_STONE = 48
    OBSIDIAN = 49
    TORCH = 50
    FIRE = 51
    STAIRS_WOOD = 53
    CHEST = 54
    DIAMOND_ORE = 56
    DIAMOND_BLOCK = 57
    CRAFTING_TABLE = 58
    FARMLAND = 60
    FURNACE = 61
    BURNING_FURNACE = 62
    SIGN_STANDING = 63
    DOOR_WOOD = 64
    LADDER = 65
    RAIL = 66
    STAIRS_COBBLESTONE = 67
    SIGN_WALL = 68
    DOOR_IRON = 71
    REDSTONE_ORE = 73
    TORCH_REDSTONE = 76
    SNOW = 78
    ICE = 79
    SNOW_BLOCK = 80
    CACTUS = 81
    CLAY = 82
    SUGAR_CANE = 83
    FENCE = 85
    PUMPKIN = 86
    NETHERRACK = 87
    SOUL_SAND = 88
    GLOWSTONE_BLOCK = 89
    LIT_PUMPKIN = 91
    STAINED_GLASS = 95
    BEDROCK_INVISIBLE = 95
    TRAPDOOR = 96
    STONE_BRICK = 98
    GLASS_PANE = 102
    MELON = 103
    FENCE_GATE = 107
    STAIRS_BRICK = 108
    STAIRS_STONE_BRICK = 109
    MYCELIUM = 110
    NETHER_BRICK = 112
    FENCE_NETHER_BRICK = 113
    STAIRS_NETHER_BRICK = 114
    END_STONE = 121
    WOODEN_SLAB = 126
    STAIRS_SANDSTONE = 128
    EMERALD_ORE = 129
    RAIL_ACTIVATOR = 157
    LEAVES2 = 161
    TRAPDOOR_IRON = 167
    FENCE_SPRUCE = 188
    FENCE_BIRCH = 189
    FENCE_JUNGLE = 190
    FENCE_DARK_OAK = 191
    FENCE_ACACIA = 192
    DOOR_SPRUCE = 193
    DOOR_BIRCH = 194
    DOOR_JUNGLE = 195
    DOOR_ACACIA = 196
    DOOR_DARK_OAK = 197
    GLOWING_OBSIDIAN = 246
    NETHER_REACTOR_CORE = 247
    PISTON = 33
    JACK_O_LANTERN = 91

    def __init__(self):
        print('connect to mcpi')
        self.mc = Minecraft.create(port=ConnectToMCPI.PORT_MC)
        self.base_position = self.mc.player.getPos()

        self.accept('m', self.transport_blocks_to_mcpi)
        self.accept('n', self.clear_all_blocks)

    def transport_blocks_to_mcpi(self):
        self.mc.postToChat('transport_blocks_to_mcpi')
        player_position = self.player.position
        self.mc.player.setPos(
            self.base_position.x - player_position.x,
            self.base_position.y + player_position.z,
            self.base_position.z + player_position.y
        )
        for key, block_id in self.block.block_dictionary.items():
            x, y, z = [int(value) for value in key.split('_')]
            block_id_mcpi = self.get(block_id.upper()) or 1
            if isinstance(block_id_mcpi, int):
                self.mc.setBlock(
                    self.base_position.x - x,
                    self.base_position.y + z,
                    self.base_position.z + y,
                    block_id_mcpi
                )
            else:  # colored wool
                self.mc.setBlock(
                    self.base_position.x - x,
                    self.base_position.y + z,
                    self.base_position.z + y,
                    *block_id_mcpi
                )

    def clear_all_blocks(self):
        self.mc.postToChat('clear_all_blocks')
        base_position = self.mc.player.getPos()
        self.mc.setBlocks(
            base_position.x - 100,
            base_position.y,
            base_position.z - 100,
            base_position.x + 100,
            base_position.y + 100,
            base_position.z + 100,
            0  # AIR
        )

        self.mc.setBlocks(
            base_position.x - 100,
            base_position.y - 1,
            base_position.z - 100,
            base_position.x + 100,
            base_position.y - 2,
            base_position.z + 100,
            2  # GRASS
        )
    

srcディレクトリの中に「connect_to_mcpi.py」ファイルを作成し、ワールド転送プログラムを書いていきます。

ConnectToMCPIクラスは、マイクラクローンと本家マイクラをつなぎ、各種コマンドで本家マイクラを操作できるようにするクラスです。
1行目で、mcpi.minecraftモジュールから、Minecraftクラスをインポートします。

このクラスには、クラス変数をたくさん設定しなければなりません。PORT_MC はポート番号を指定しています。MOD導入済みのマイクラはコマンドを受けつけるサーバーを開いていますが、そのサーバーに接続するための番号が「4171」番になります。
クラス変数AIR = 0 以降が、ブロックIDの指定です。マイクラクローンでは、ブロックID は「stone」など、文字列で表されます。一方、マイクラ(バージョン1.12.2)では、数字で表されます。これらのクラス変数を設定することで、ブロックIDの変換ができるようにします。

初期化メソッド(__init__)を設定します。
はじめに、Minecraft.createメソッドで、マイクラクローンと本家マイクラの接続を開始します。引数port は ConnectToMCPI.PORT_MC(4171)番を指定します。
次に、mc.player.getPosメソッドで本家マイクラ側のプレイヤーの位置を取得して、インスタンス変数base_position に代入します。ブロックやプレイヤーは、base_position を位置の基準として配置していくことになります。

acceptメソッドで、ユーザーのキー操作をゲーム側で受け取れるようにします。「M」キーで、マイクラクローン側の建築を本家マイクラに転送するコマンドを送信します(transport_blocks_to_mcpiメソッド)。「N」キーで、本家マイクラの建築を全て削除して、元のスーパーフラットワールドに戻るコマンドを送信します(clear_all_blocksコマンド)。

transport_blocks_to_mcpiメソッド

transport_blocks_to_mcpiメソッドは、マイクラクローン側で設置したブロックを全て本家マイクラにコピーできます。
for文を使って、マイクラクローンに設置したブロックを本家マイクラに一つずつ設置するコマンド(mc.setBlockメソッド)を送信します。インスタンス変数 block.block_dictionary からキーと値を取得します。
キーはブロックの位置情報を含みます。「_(アンダーバー)」で分割して、ブロックの座標(x, y, z)を得ることができます。
値はブロックIDです。getメソッドで、クラス変数にアクセスして、「文字列」→「数値」の変換を行い、本家マイクラのブロックIDを得ることができます。ここで注意してほしいのは、本家マイクラの羊毛ブロックの IDは (35, 1) のように2つの数字で指定する必要があることです。色によって、2つ目のIDの値が変わります。条件式「if isinstance(block_id_mcpi, int):」により場合分けをして、ブロックID がタプルであったときは、「*block_id_mcpi」のように「*(アスタリスク)」をつけて展開して指定するようにします。

座標系の違い

本家マイクラとマイクラクローン(Panda3D)の座標変換が必要です。上図の左がマインクラフト、右が Panda3Dの座標系です。

# ブロックを設置する部分のコード

                self.mc.setBlock(
                    self.base_position.x - x,
                    self.base_position.y + z,
                    self.base_position.z + y,
                    block_id_mcpi
                )

したがってマイクラ座標とPanda3D座標の変換は x → -x、y → z、z → y、とすることで、正しい方向でワールドが転送できます。
(ちなみに本家マイクラでは、X座標が東、Z座標が南と方角が設定されています。豆知識)

clear_all_blocksメソッド

clear_all_blocksメソッドは、本家マイクラのワールドを初期化します。建築したブロックを全て削除してやり直したい時に使用します。mc.setBlocksメソッドは、範囲を指定してブロックを設置できます。プレイヤーを中心に縦横200、高さ100にAIR(空気)ブロックを設置して、範囲内のブロックを全て消すことができます。それから高さ-1に200x200のGRASS_BLOCK(草)ブロックを設置して地面を復元します。

転送するワールドを作成する

"""18_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='mcpi')

        # プレイヤーの位置を変更
        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)

        # 壁
        for i in range(4):
            for j in range(3):
                if i == 0:
                    self.block.add_block(i, 0, j, 'stone')
                else:
                    self.block.add_block(i, 0, j, 'gold_block')

        # 道路
        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)

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

        # 家
        count = 0
        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'
                roof_block_id = roof_colors[count % len(roof_colors)] + '_wool'
                self.make_house(initial_position=Point3(x, y, 0), w=w, d=d, h=h, roof_block_id=roof_block_id)
                count += 1


game = Game()
game.run()

18_01_main.py は自動で村を建築するプログラムです。
「MC.__init__(self, ground_size=256, mode='mcpi')」により、MCクラスの初期化メソッドを実行しますが、引数mode を 'mcpi' に指定します。そうすることで、本家マイクラとの接続が実行されます。村を作成する部分は、17_01_main.py とほぼ同じです。屋根の色のランダム要素を無くして、1件ずつ色が変わるように変更しました。

ワールドの転送を実行する

ワールドの転送

では、ワールドの転送実験を開始します。
まず、本家マイクラ「1.12.2-forge」を起動します。先ほど作成した「New World」でゲームを開始します。「/」キーを押すと、マイクラ画面を開いたまま別のウインドウに移れるので便利です。覚えておきましょう。
次に、マイクラクローン(Pynecrafter)を実行します。18_01_main.py を実行して、村の自動建築を行います。「M」キーを押すと、本家マイクラにワールドが転送されました。「N」キーで全ての建築を削除できることも確認しておいてください。

今回は、マイクラクローンと本家マイクラの連携について実験しました。「RemoteControllerMod」と「mcpi」ライブラリを使うと、簡単にワールドの転送が行えました。マイクラクローン側で試行錯誤して作ったワールドを本家マイクラに転送するなど、建築作業の効率化にお役立てください。

次回は、ゲームに音楽(ミュージック)を導入します。BGMとプレイヤーの動作音(ブロックの破壊音など)を鳴らすことができるようになります。お楽しみに。


前の記事
Pythonでマイクラを作る ⑰建築MODで村を作る
次の記事
Pythonでマイクラを作る ⑲サウンドを実装する

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


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