Minecraft1.21.1 Mod開発#8:基礎編⑥(ブロックの向きが変わるようにしよう!)

基礎

こんにちは!
初めましての人は初めまして。
___です。

Minecraft(マイクラ)のMODを実際に作りながら、読者の皆さんにも一緒に作ってもらえるような、実践型連載の第八弾!

前回、ブロックの登録ができましたが、問題が1つ残っていました。
それは「プレイヤーの置く向きが変わっても、ブロックの向きが変わらない」ことです。
かまどは結構な問題です!

ということで今回の目標は

— ブロックの向きを変えられるようにしよう! —

前回作った鋼鉄のかまどを、向きを変えて置けるようにしましょう!

それでは、
Let’s Minecraft Mod Life!

前提

まず前提として、マイクラのブロック描画の順番を知る必要があります。
簡単に書くとこんな感じです。

【ワールドに置かれたブロック】
↓
① blockstates
「このブロックの状態(向きや条件)に応じたモデルファイルはどれ?」を案内する標識
↓
② models/block
「3Dの形(立方体)と、どの画像を使うか」の設計図
↓
③ textures/block
「どのテクスチャを張るか」という実際の画像

ワールドに置かれたブロックに対して、実際に置かれている状態がどんな状態なのか、まずはシステムが理解する必要があります。
それに合わせてモデルを選択し、テクスチャを張っている、という流れなのです。

この時重要になるのが「blockstates」です。
この中に入っているファイルが「今このブロックの状態に対応したモデルはどれ?」というのを認識しているわけです。

では現在の「blockstates」はどうなっているのか。
「blockstates」フォルダの中の「steel_furnace.json」を見てみましょう。

{
  "variants": {
    "": {
      "model": "practicemymod:block/steel_furnace"
    }
  }
}

これの中身を見ていきましょう。

{
  "variants": {

  ... 中略 ...

  }
}

「variants」
これはバリエーション設定をここで書くよ!という目印です。
ブロックが置かれた向き(東西南北)や状態(オン/オフなど)によって表示を変えるための設定場所です。
つまりこの中に書かれた設定に基づいてモデルが選択される、ということです。

ではその中身を見ていきましょう。

"": {
  "model": "practicemymod:block/steel_furnace"
}

「””」
これは「どんな時も(常に)」という意味です。
「どんな時でもこの中に書かれたモデルを選択してね」ということです。
そして今は「model」として「practicemymod:block/steel_furnace」を選んでいるので、常にそれが読み込まれる、ということです。

つまり現在の設定では、「どんな時でも同じモデルをそのまま出力する」という設定になっているのです。
なので、置く向きが変わっても常に同じ方向を向いていたということ。
裏を返せば、ここの設定次第では方向を変えられるということです!

「blockstates」だけで向きが変わる?

先ほどまでの話だと、「blockstatesを変えれば向きが変わる」という感じがしますね。

しかし!答えはNoです。変わりません。
なぜでしょうか。

それは、現在のままではそもそも向きを変えるための基準やきっかけ(ユーザーの向きなど)の情報が取得できていないので、「向きを変える」ということ自体が「blockstates」に届かないということです。

そこでそれらの情報を取得してから「blockstates」を読み込んで情報を処理できるように、新しく処理を追加しなければなりません!

必要なシステムは「共通化」する

これから作るのは「プレイヤーの置く向きによって、ブロックの向きも変わる」ための処理です。

ここで「じゃぁ今は鋼鉄のかまどを作っているからそれ用に作ればいいや」でいいのでしょうか?
もしそうしたら、今度「鋼鉄の作業台」など、他の向きが変わるブロックが出てきたときにどうでしょう。
その都度「鋼鉄の作業台用のファイル」「他のブロック用のファイル」と増やしていくと、今後ブロックが大量になったとき、とても管理しきれません。

そこで「同じ処理を他でも使える場合には、固有のファイルではなく、共通化した1つのファイル」にします。
そうすることで、同じ処理は何度もブロックごとに書いたりしなくて済みます。
何かの修正も1か所で済みます。

この「共通化」の考え方はとてもプログラミングっぽい考え方ですが、何かと便利になるので大切にしましょう。

なので今回も「共通で使える、向きが変わるブロックのための処理」というものを作っていきます。

「RotatableBlock.java」の作成

今回は共通の処理「RotatableBlock.java」を作っていきます。
まずは「RotatableBlock.java」自体を作りましょう。
ファイル階層は以下。

src/
 └─ main/
     └─ java/
         └─ com/
             └─ mypractice/
                 └─ mymod/
                     └─ blockclass/ ← 新しく作るフォルダ
                     |  └─ RotatableBlock.java ← 今回作るJavaファイル
                     ├─ AddExistingCreativeModeTab.java
                     ├─ Config.java
                     ├─ ModBlocks.java
                     ├─ ModCreativeModeTabs.java
                     ├─ ModItems.java
                     ├─ PracticeMyMod.java
                     └─ PracticeMyModClient.java

その中に以下を書きましょう。
(コピペする場合には、一番頭の一文の「mypractice.mymod」の部分は自分のフォルダ階層に合わせて変更してください)

package com.mypractice.mymod.blockclass;// ← mypractice.mymodは自分のフォルダ階層に合わせてください。

import net.minecraft.core.Direction;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.HorizontalDirectionalBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.EnumProperty;

/**
 * 設置時にプレイヤーの方向を向く(東西南北の向きを持つ)汎用ブロッククラス
 */
public class RotatableBlock extends Block {
    // 方角(東西南北)のプロパティを定義
    public static final EnumProperty<Direction> FACING = HorizontalDirectionalBlock.FACING;

    public RotatableBlock(Properties properties) {
        super(properties);
        // デフォルトの向きを「北(NORTH)」に設定
        this.registerDefaultState(this.stateDefinition.any().setValue(FACING, Direction.NORTH));
    }

    // 設置したプレイヤーの反対(=正面が自分を向く)を設定
    @Override
    public BlockState getStateForPlacement(BlockPlaceContext context) {
        return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite());
    }

    // ブロックの状態定義に FACING を追加
    @Override
    protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
        builder.add(FACING);
    }
}

これで回転するための情報の取得はできるようになります!

※補足※
これ以降はこのコードの解説になります。
とりあえず進めたい方は読まずにここまで進んでも大丈夫です。

コードを1つずつ分解しながら見ていきましょう。
まずは最初の部分。

package com.mypractice.mymod.blockclass;

この部分は「package」から始まるもので、「このファイル自身がどこにいるのか」という住所(フォルダ階層)を定義する役割があります。

Java言語の特徴として、最初にファイル自身が「どのフォルダに入っているか」を一番上の行に書くルールがあります。
筆者の場合は「com.mypractice.mymod」です。
もし自分が作成したプロジェクトのフォルダ(パッケージ)構成が違うのであれば、それに合わせて書き換える必要があります。
ここが間違っているとJavaの方で「ファイルが見つからない!」と赤い波線(エラー)が出てしまうので注意です!

次は上から順にいくと「import」文ですが、これは各一文ごとにそれぞれ、マイクラやNeoForgeが用意してくれている「便利な道具(機能)」の呼び出しをしています。

Javaは最初からすべての機能が使えるわけではありません。
「これからマイクラの『Item』という仕組みや、NeoForgeの『DeferredRegister』という登録機能を使いたい!」と事前に宣言(import)しています。
他のファイルで書いた処理をこのファイルでも使えるように借りてきているイメージです。

なので今回は、それぞれを使うタイミングでお知らせしようと思います。

public class RotatableBlock extends Block {

... 中略 ...

}

「public」から文章が始まっている「クラスの定義」の書き方です。
今回の場合は「RotatableBlock」という名前のブロック登録時に使えるクラスを作る枠組みを定義しています。
この定義をするときの先頭が「public」じゃないと、他のファイルからでも使えるようにならないので、今回のように後々別のファイル「ModBlocks.java」で使いたいとなる場合には、「public」である必要があります。

そしてそのクラスの根幹として、既存のマイクラの設定も付け加えよう、ということで「extends Block」が入っています。
この時に既存のマイクラの情報を取得しないといけないので、インポート文が必要です。

import net.minecraft.world.level.block.Block;

それでは今回の「RotatableBlock」では呼び出された時に何をするのか。
その中身を見ていきましょう。

// 方角(東西南北)のプロパティを定義
public static final EnumProperty<Direction> FACING = HorizontalDirectionalBlock.FACING;

ここでは何をしているのかというと、ブロックに「東西南北(NORTH, SOUTH, WEST, EAST)」の向き情報を持たせるための変数(プロパティ)を宣言しています。

さらに分解して見ていきましょう。
(コードがバッと書かれると、いったいどこが区切りなのか分かりずらいので)

public static final

まずここが1つ目の区切りです。
この部分の意味は「一度保存したら変わらない定数を定義するよ!」という意味です。
(ここだけでそんな意味があります)

この部分の書き方はJava言語の仕様です。
Java言語では、簡単に言うと以下のような意味を持ちます。

  • public:別のファイルから読んでも使える(共有される)ものとして定義する
  • static:これがあるとずっと保存される
  • final:これがあると定数という意味になる

さぁ分からんぞ?
ということで解説。
(「public」は一つ前での解説とほぼ一緒なので飛ばします)

「static」
これは「これがあるとずっと保存される」と書きました。
そもそも変数や定数は保存されるものでは?と思うかもしれません。
しかし保存されるタイミングや保存される期間などが違うのです。

static なしstatic あり
初期化のタイミングインスタンスが生成されるたびに初期化最初にクラスがロードされたときに一度だけ初期化
データの保持インスタンスが破棄されるとメモリから解放プログラムが終了するまでメモリに保持
アクセス方法インスタンスを通してアクセスクラスを通じて直接アクセス
内容の変更現在のインスタンスやクラスからしか変更不可他のインスタンスやクラスからでも変更可能

いきなり出てきた「インスタンス」という言葉。
ネット検索の言葉を借りれば「インスタンスとは、クラスという「設計図」をベースにメモリ上に生成された「実体(オブジェクト)」のこと」だそうです。

まぁなんのこっちゃ、という話ですが、これを説明しだすと今度はJavaという言語そのものの解説になってしまうんですよね・・・。
ということで今回は割愛。

要するに
「static」が無いと、この「RotatableBlock」というクラスが呼び出されるたびに毎回初期化の処理が動いて大変だよね?一回保存したらもうそれを使いまわそう!だって今回保存するデータは「東西南北の方向」だから、ゲーム中に変わることは無いからね!
こんな感じ。

逆にクラスが呼び出されるたびにちゃんと初期化してほしい場合には、これを付けちゃいけない!ということになりますね。

「final」
じゃぁ今度はこれがどういう意味か。
これは言葉の通り「ここを宣言の最後(final)にする!」という目印です。
本来は変数はプログラム上で書き換わることの多い値ですが、これをしておくことで、一回目以降は変更はしない、つまり一回しか値を決めない、つまり一定の値を保持する「定数」になる、ということです。

では次の部分。

EnumProperty<Direction>

これはマイクラでブロックの「向き」を管理・保持するために使われるプロパティを宣言するための物です。
「ここで作った定数は、マイクラの「向き」のデータを扱うものだよ!」とプログラムに教えてあげているのです。

この時に使っている「EnumProperty」はマイクラの方で用意された物なので、これも読み込みが必要です。なのでインポート文があります。

import net.minecraft.world.level.block.state.properties.EnumProperty;

さらに「」もマイクラの基盤として登録されているものなのでインポート文が必要です。

import net.minecraft.core.Direction;

さて、次です。

FACING

この部分は、今回の定数の名前を決めています。
これが無いとプログラムは「なんて名前で保存すればいいの?」となってしまうので必須です。

 = HorizontalDirectionalBlock.FACING;

「=」の右側です。
これは「=」の左側で宣言された定数の値を何にするか、その内容を決めています。

ここでマイクラ既存の「HorizontalDirectionalBlock.FACING」という共通の定数を再利用しています。
これを使うことで、水平4方向のデータを簡単に扱えるようになります。
ただしこれを使うにはマイクラ既存の物を呼び出す必要がありますね。

ということでインポート文で以下を定義しているわけです。

import net.minecraft.world.level.block.HorizontalDirectionalBlock;

クラス内で1つ目の処理の解説が終わりました。
次に進みましょう。

public RotatableBlock(Properties properties) {
    super(properties);
    // デフォルトの向きを「北(NORTH)」に設定
    this.registerDefaultState(this.stateDefinition.any().setValue(FACING, Direction.NORTH));
}

この部分では、ブロックが生成された瞬間に、デフォルトの向きを「北(NORTH)」として登録しています。
また1つずつ見ていきましょう。

public RotatableBlock(Properties properties)

この部分は、関数の定義です。
「public」が付いているので定義ですね。
そしてRotatableBlockという名前が呼ばれたら、その中の情報を「properties」という変数を作って格納してから、中の処理をしてね!ということです。

では中の処理はどんなものか。

super(properties);

これは親クラス(Block)に硬さや音などの設定を渡します。
この後「ModBlocks.java」からこれを読みだしたときに渡される情報を、マイクラの方に繋いであげる役割があります。

this.registerDefaultState(this.stateDefinition.any().setValue(FACING, Direction.NORTH));

これは簡単に言うと「ここ(this)で読み込んだものの情報を、もし向きがまだ決まっていない状態なら、とりあえず北向きにしておいてね」ということです。
つまり何も決まっていなかった場合の対応として、初期値を北向きにしておく、ということです。

では次に行きましょう。

// 設置したプレイヤーの反対(=正面が自分を向く)を設定
@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
    return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite());
}

ここでは「プレイヤーがワールドにブロックを置いた瞬間に呼び出され、プレイヤーの向きに合わせてブロックの向きを決める」処理をしています。

@Override

これはJava言語の書き方です。
下で書く処理が正しく動いてくれるかを判定してくれるようなものです。
正しく動かない場合にはエラーを出してくれます。

ということは(機能的にですが)これは記載が無くてもOKではあります。
しかし実際問題、この部分がどんな処理をしていて、どんなエラーが出そうかを教えてくれる目印として、入れることがほとんどのようです。

public BlockState getStateForPlacement(BlockPlaceContext context) {

まぁここは言わずもがなになってきましたかね。
関数の定義です。

return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite());

これについては「この関数が呼ばれた時に、プレイヤーの方向の逆方向を教える(返す)」役割があります。

return

これが最初にあると、この後の部分の結果を元のこの関数を読み出した場所に教えてあげてね、という意味になります。

this.defaultBlockState().setValue(

この部分で、今回のデフォルトのブロック情報に、この後書く内容をセット(setValue)してね、ということを書いています。
そして

FACING, context.getHorizontalDirection().getOpposite()

このブロックのプロパティの「FACING」を「context.getHorizontalDirection().getOpposite()」にしてね、ということになります。
この時「context.getHorizontalDirection()」だと、「プレイヤーが見ている方向」のままになります。
そこに「.getOpposite()」を付けることで、反転させることができます!便利機能ですね。

さて、この部分で必要だったのは以下です。

import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.block.state.BlockState;

さてお次はこちら。

// ブロックの状態定義に FACING を追加
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
    builder.add(FACING);
}

この部分でやっていることは、ここで初めて「このブロックが「FACING(向き)という状態(BlockState)を持つブロックですよ」とマイクラのシステムに登録」しています。
なので今までのことは準備だったというわけです。

ここで使われているのは以下です。

import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;

頭から見ていきましょう。

protected void

この「protected」は「public」と似たような仲間です。
これの後に書かれたものを定義するよ!ということです。
しかし「public」との大きな違いは、それへのアクセス権限です。
「public」の場合には他のクラスやファイルからでも読み込めたのに対して、「protected」はアクセスできる範囲を「自分自身のクラス」と「それを継承した子クラス(サブクラス)」に限定します。
つまり限られたアクセス権限なのです。

ではそのあとの「void」は?
これは「戻り値が無いよ」という宣言です。
戻り値とは、ここで定義されたものが呼び出された時に、その呼び出し元に返す(教える)値のことです。
それが無いということは、処理をするだけだよ、ということです。

createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {

この部分で定義しています。
では中で何をするのかというと、

builder.add(FACING);

これだけです。
まぁ短いですが、やってることも短いので。
これは「FACING」という向きの状態の情報を追加(add)するよ、と書いてあります。

さて、以上がコードのざっくりとした解説になります。
筆者も専門家ではないので、説明がニュアンスで進めているところはあしからず・・・。
ということで、これ以降はまたブロックの向きを変えられるようにする方法に戻ります。

ブロックの読み出し方の変更

先ほど作った「RotatableBlock.java」ですが、実はこれは、今までのように「自分のMod名.java」のファイルに「読み込んでね!」という処理を書くわけではありません。

ではどこで読み込むのか。
それは「ブロック自体を登録するとき」です!
まずは現在の「鋼鉄のかまど」を登録している「ModBlocks.java」の部分を見てみましょう。

// 鋼鉄のかまど(Steel Furnace)
public static final DeferredBlock<Block> STEEL_FURNACE = BLOCKS.register("steel_furnace",
    () -> new Block(BlockBehaviour.Properties.of()
        .strength(5.0F, 6.0F) // 硬さと爆発耐性
        .sound(SoundType.METAL) // 設置・歩行時の音(金属音)
        .requiresCorrectToolForDrops() // 適正ツール(ツルハシ等)が必要
    )
);

ここです。
このとき、ブロックの登録のために「new Block(BlockBehaviour.Properties.of() …」という感じで処理が書かれていると思います。
この中の「Block」に注目。
実は同じファイルの最初の方の「import」文の中にこんな記述があります。

import net.minecraft.world.level.block.Block;

この処理が何を読み込んでいるのかというと、これも実は「ブロックの状態」を処理するための物を読み込んでいます!
内容は先ほど作った「RotatableBlock.java」のようなファイルなのです!
それを使って、「新しく(new)既定のブロック(Block)を作ってね」ということをしていたのです!

なので同じように「新しく(new)方向の変わるブロック(RptatableBlock)を作ってね」と指示してあげればいいのです!

ということでまずはそのための「RotatableBlock.java」の読み込みをしましょう。
「ModBlocks.java」ファイル内の最初の方「import」がたくさんあるところに、以下のように追記しましょう。
(ここでも「mypractice.mymod」の部分は、先ほど作った「blockclass」フォルダのある階層に修正してください)

import com.mypractice.mymod.blockclass.RotatableBlock;

入力すると最初は黄色い波線が付いてちょっと色が薄くなりますが問題ありません。

次に以下の部分を探し、

// 鋼鉄のかまど(Steel Furnace)
public static final DeferredBlock<Block> STEEL_FURNACE = BLOCKS.register("steel_furnace",
    () -> new Block(BlockBehaviour.Properties.of()
        .strength(5.0F, 6.0F) // 硬さと爆発耐性
        .sound(SoundType.METAL) // 設置・歩行時の音(金属音)
        .requiresCorrectToolForDrops() // 適正ツール(ツルハシ等)が必要
    )
);

下のように修正しましょう。

// 鋼鉄のかまど(Steel Furnace)
public static final DeferredBlock<Block> STEEL_FURNACE = BLOCKS.register("steel_furnace",
    () -> new RotatableBlock(BlockBehaviour.Properties.of()
        .strength(5.0F, 6.0F) // 硬さと爆発耐性
        .sound(SoundType.METAL) // 設置・歩行時の音(金属音)
        .requiresCorrectToolForDrops() // 適正ツール(ツルハシ等)が必要
    )
);

これでブロックを登録するときに自動的に「RptatableBlock.java」が動いてくれます!

「blockstates」の編集

次はいよいよ本命の「blockstates」の中のファイルの編集です。

まずは今回変更する「steel_furnace.json」ファイルの中身を見てみましょう。

{
  "variants": {
    "": {
      "model": "practicemymod:block/steel_furnace"
    }
  }
}

今回はこれを、以下のように変更します。
(これも4か所の「practicemymod」は自分のModのIDにしましょう)

{
  "variants": {
    "facing=north": { "model": "practicemymod:block/steel_furnace" },
    "facing=south": { "model": "practicemymod:block/steel_furnace", "y": 180 },
    "facing=west":  { "model": "practicemymod:block/steel_furnace", "y": 270 },
    "facing=east":  { "model": "practicemymod:block/steel_furnace", "y": 90 }
  }
}

変更前の「variants」の中身です。

"": { "model": "practicemymod:block/steel_furnace" }

変更後の中身です。

"facing=north": { "model": "practicemymod:block/steel_furnace" },
"facing=south": { "model": "practicemymod:block/steel_furnace", "y": 180 },
"facing=west":  { "model": "practicemymod:block/steel_furnace", "y": 270 },
"facing=east":  { "model": "practicemymod:block/steel_furnace", "y": 90 }

こうして並べてみると、違いが分かりやすいかなと。

変更前は「””」の時、つまり「どんな時も」でした。
変更後は「”facing=north”」の時、という区切りができていますね。

ちなみにここで使われている「facing」ですが、これが「RotatableBlock.java」の中で方角を取得してきていたものになります。
それを使って判定をしているわけです。
「RotatableBlock.java」の中で北を基準にしているので、「facing=north(つまり北を向いているとき)」は「{ “model”: “practicemymod:block/steel_furnace” },」であり、方向を変えずにモデルを出力しているわけです。
そういう感じで4つの方角に合わせてモデルの角度を変えているのです。

さて、ここまで出来たらテスト起動してみましょう。
ブロックを、自分を中心に4方向へ置いてみてください。
しっかり方向が全部自分の方を向いて置けたらばっちりです!
(中心に向かって置けていればOK)

おわりに&次回予告

さて、いかがだったでしょうか。

これで無事に「置いたときに常に自分の方を正面に向いて置かれる」ができるようになりました!
これでよりちゃんとブロックを置いている感が出ますね!

これでブロックの登録もなんとなく区切りはついたような気がします。
次回は一体何をしようか・・・。
せっかくかまどを作ったので、右クリックしたらかまどの画面(UI)が出るようにしてもいいかもしれませんね。
他にも、とりあえず残してある「Example Item」をちょっと改変してみるとか・・・。

まぁ未定ってことですね!()

今回は少し短めでしたが、いかがだったでしょうか?
今回は以上になります。
ありがとうございました。
次回もお楽しみに!

コメント