読者です 読者をやめる 読者になる 読者になる

Minecraftとタートルと僕

PCゲームMinecraftのMOD「ComputerCraft」の情報を集めたニッチなブログです。

マイクラModdingの基礎知識(BlockとMetaDataとTileEntityの関係について)

この記事の目的

この記事は、Java初心者かつModding初心者であるhevohevoが、ネット上に落ちている情報を拾い読みしたり、公開されているソースコードを読んで、自分なりに理解したことをまとめたものです。

当然ながら、間違いが含まれている可能性が高いです。記事を100%信用してはいけません。

この記事はつまり、間違っているであろう自分の知識をわざと公言することで、心優しい識者の方から教えをいただけないだろうかという期待を元に公開しています。

識者の方は、やさしくご指摘いただけると助かります。

はじめに

以下の内容は、Minecraft1.7.10およびForge#1180を対象にしています。それ以前のバージョンでは異なる可能性があるのでご注意ください。

また、マルチプレイヤー環境ではなくシングルプレイヤー環境を想定しています。マルチプレイヤー環境独自のmoddingなど、そこまでまだ理解が進んでいません。

以下の記述中で、アルファベット大文字からはじまる単語はJavaのクラス名を指します(Block、TileEntityなど)。あるクラスから、newして作ったオブジェクトをここではインスタンスと呼びます。

ブロックは1種類につき1個しか存在しない

マイクラ世界は数え切れないほど多くのブロックから成り立っています。

ゲームをやったことがある人なら、経験的に石ブロックを一番多くみかけることでしょう。次によく見るのは土ブロックでしょうか?

実は、何もない空間は全て透明の空気ブロックで埋め尽くされています。地表面が高度63だとすると、高度限界は256なので、差し引き193個の空気ブロックが存在することになります。圧倒的ですね。

では、ゲーム内でもっとも多く存在するブロックは空気ブロックなのでしょうか?

いえ実は、「システム的には」違います。

システム的には(内部実装としては)、ブロックは1種類につき1個しか存在しません。

厳密に言うと、Blockクラス(およびそれをextendsした独自クラス)のインスタンスは、1種類につき1個しか作成(new)されません。

たとえば石ブロックのインスタンスは、ゲーム開始時に1個だけ作成されて、GameRegistryに記憶されます。

後は原則として、それを使いまわすのです。

たとえば、ワールド上のあらゆるところで石ブロックを見かけます。狭い範囲でもおそらく数万、数十万という単位で石ブロックは存在するように思われます。

仮に、その石ブロック1つ1つがインスタンスとして存在しているとしたらどうでしょうか、newを数万回実行する? そんなバカな。PCがチンでしまいます。

実はワールド上のどの座標にどのブロックがあるかという情報は、マッピングデータとしてゲーム内で管理されているのです。

具体的には、ワールドオブジェクト(Worldクラスのインスタンス)ごとに、座標(x,y,z)とGameRegistryに保存されているブロックインスタンスを結びつける形で情報を記録しています。

そのため、とあるワールドのとある座標にどんなブロックがあるかを調べるには、以下のようにコードを書けばよいのです。

Block myBlock = world.getBlock(x,y,z);

ここで注意したいのは、同じ種類のブロックであれば、マッピングデータ上では同じブロックインスタンスを指し示すということです。

つまり、異なる座標であったとしても、そこに存在するブロックが同じ種類のブロックであれば、その指し示すブロックインスタンスは同じなのです。

つまり以下のように。

Block myBlock1 = world.getBlock(x1,y1,z1);
Block myBlock2 = world.getBlock(x2,y2,z2);

if(myBlock1 == myBlock2){
  System.out.println("同じインスタンス!");
}

ここで、おやっと思いませんでした?

もしそうだとするならば、 同じ種類のブロックであったとしても、原木や階段のように向きが違ったり、羊毛のように色が違ったり、チェストやカマドのように中に異なるものが詰まっているブロックはどうやって表現されているのでしょうか。

答えは、「それは別データとして保持している」です。

ブロック個別のデータをどうやって保存・管理しているのか

別データとは言いますが、ブロックはその保存したいデータの大きさによって2つの保存方法が用意されています。

それはMetaData(メタ値)という形でデータを直接保存する方法と、TileEntityを利用して間接的にデータを保持・管理する方法です。

ともに、ワールドごとに座標をキーとして保存します。ブロックではありません。「座標をキー」にです。

たとえば、とあるワールドのとある座標に(繰り返しますがブロックではありませんよ)保存されているMetaDataを取り出したいのならば、以下のコードを書きます。

(ここではメタ値をint型として取り出しています)

int myMetaData = world.getBlockMetadata(x,y,z);

また、とあるワールドのとある座標に(繰り返しますがブロックでは・・・略)保存されているTileEntityは次のように取り出します。

(TileEntityオブジェクトが得られます)

TileEntity myTileEntity = world.getTileEntity(x,y,z);

それではこの2種類の方法、どのような違いがあり、どのように使い分ければよいのでしょうか。

MetaDataの特徴

まず、とても小さなデータはMetaData(メタ値)として保存できます。

MetaDataは容量が4bit(10進数で言うと0-15)しかありませんが、処理が高速かつ取り扱いが簡単です。

たとえばゲーム内では、羊毛の色はMetaDataによって変えています。

ワールドに色違いの羊毛ブロックが複数置いてあるとき、world.getBlock(x,y,z)によって得られるブロックインスタンスはどの羊毛ブロックも共通です。

しかし、world.getBlockMetaData(x,y,z)によって得られるメタ値はそれぞれ色によって異なるのです。

ゲーム内でとある羊毛ブロックを視界に入れたとき、システムはそのブロックと同じ座標に保存されているMetaDataを自動的に参照し、そのMetaDataの違いによって表示するテクスチャを変えているのです*1

またたとえば、Modderが設置方向が重要となる新しいブロックを追加するとしましょう(参考としたソースコード)。

まるでバニラのカマドのように、設置するとプレイヤーキャラクタと向き合う形でブロックの正面方向が決定され、その正面方向だけ他の方向とは異なるテクスチャが使われるケースです。

この正面方向はワールド内のどの方向*2であるかという情報を保存するのに、MetaDataを使うことができます。

たとえば、ブロックをワールドに設置したときにプレイヤーの向いている方向を元にブロックの正面がどちらかという情報をMetaDataに記録させます。

具体的には、ブロック設置したときに必ず呼び出されるonBlockPlacedBy()メソッドを@Overrrideして、この中でworld.setBlockMetadataWithNotify()メソッドを使ってMetaDataを記録します。

あとは、画面表示のときに呼び出されるgetIcon()メソッド*3を@Overrideして、その中でMetaDataを呼び出しつつその値によって返すテクスチャ(IIconクラス)を切り替える処理を記述すればよいのです。

MetaDataはint型なので保存できるデータ量が限られてしまうという欠点がありますが、実値なので呼び出しが高速であり、ゲームを再開したときなど画面描写が早いという利点があります。

たとえば様々な色の羊毛ブロックが視界内に大量に設置されていても、そこまで負荷は大きくないですよね。

TileEntityの特徴

実値であるMetaDataに対して、TileEntityはクラスなので、いくらでも好きなデータを保持することができます。

さらに言うならば、様々なメソッドがクラス内で定義されておりそれらを@Overrideすることで新しい機能を付け加えることも可能ですし、保存データを用いた新しい処理メソッドを追加することもできます。

その意味でTileEntityは、ブロックのデータ保存方法という範疇を軽く超えており、ブロックのデータ管理基盤とも言うべき特殊なクラスです。

いまいちイメージがつかめない人には、以下のたとえ話はどうでしょう。

  • Block+MetaData=羊毛ブロックの色を変える程度の能力。
  • Block+TileEntity=工業Modの機械のように、中にデータを保持するだけでなくそれらデータを使った特殊な機能を実装する程度の能力。

(補足)なお、TileEntityを使うときには、Blockクラスを使うよりもBlockクラスを拡張したBlockContainerクラスを使ったほうが機能がいろいろとそろっているので楽です。つまり、BlockContainer+TileEntityの組み合わせ。

TileEntityがどのくらい高機能か例を挙げると、以下のような処理ができるデータおよびメソッドがTileEntityクラスに標準で含まれています。ね?高機能でしょう?

  • Blockの座標データを保持(xCood, yCood, zCood)
  • 保持されているBlock座標データを使って、MetaDataを呼び出せる(getBlockMetadata())
  • 自分の座標から特定座標までの距離を計算できる(getDistanceFrom(x,y,z))
  • などなど。以上は標準で実装されている機能のごく一部です。

しかもこれは、標準のTileEntityクラスのお話です。

TileEntityをextendsして新しいMyTileEntityクラスを定義するなら、なんでもやりたい放題ですね!

とはいえ欠点が無いわけではありません。

高機能であるがゆえにいくつかの問題も抱えています。

ここでは初心者の僕程度でも気づくことができた、明確な欠点を2つ挙げましょう

欠点1)高機能なため重い

MetaDataは実値なので、素直にGameRegistoryの中に値が保存されています。ゲーム再開時にも保存された値を呼び出すだけなので、羊毛などの個別ブロックの描写もさほど負荷はかかりません。

しかしTileEntityは違います。ワールドの位置情報でマッピングされる形で、個別のTileEntity(およびそれをextendsしたクラス)インスタンスが保持されています。

たとえば、プレイヤーの直近に100個の機械ブロックがあってそれが全部稼働中ならば、それら100個のTileEntityインスタンスがメモリ中に保持されるわけですね。ヒィッ!*4

そりゃ、1チャンク内に工業Mod機械をぎっしり詰め込んだら重いはずですわ。

ついでなので、ここでTileEntityインスタンスのライフサイクルを簡単に記述しておきましょう。

  1. 新しくブロックをワールドに設置したときに、その設置ブロックのTileEntityインスタンスが新しく生成(new)される。
    • Block*5createNewTileEntity()メソッドが呼び出される。
    • TileEntityを使いたいときにはあらかじめこのメソッドを@OverrideしてTileEntityのインスタンスを返すようにしておかなければならない。
  2. 生成されたTileEntityインスタンスは、ワールド上に座標をキーにマッピングされる。
  3. 設置ブロックとそのTileEntityインスタンスに記述された内容ごとにいろいろな処理。
  4. ゲーム終了時に、TileEntityの一部データのみがセーブファイルに保存される
    • なおBlockのマッピングデータやMetaDataなどは自動でセーブファイルに保存される。
  5. ゲーム再開時、Blockごとに新しくTileEntityインスタンスを生成、セーブファイルから保存されたデータを取り出してTileEntityインスタンスにセット。
  6. 2へと進む。

(なお、TileEntityのデータがセーブされるタイミングは他にもあるかもしれないので、識者のつっこみ待ち。たとえばMekanismの機械は撤去しても内部データを保持しているので、撤去時に特殊な処理を行っているのかな?)

欠点2)独自に拡張可能 → どのデータを実際にセーブするかは自分で書いて

TileEntityのライフサイクルの中で、「ゲーム終了時に、TileEntityの一部データのみがセーブファイルに保存される」と説明しました。

具体的に言うと、TileEntityインスタンスの中でも、座標情報しかファイルにセーブされません。

そのため、機械ブロック作業中にゲームを再起動すると、中の処理が中に入れた材料ごとリセット・・・これはひどい。

明示的にどのデータを保存するか記述しましょう。記述する方法はいくつかあるようです。

(以下、識者のつっこみ待ち。NBTを直接読み書きするような低レベルのメソッドではなくもっと高レベルのメソッドで書けよ!等の突込みが欲しいのです。また、マルチプレイヤー環境で動くようなModを実装するときのpacket関連をフックするような書き方があるんじゃないかなぁと個人的には疑っています。このソースコードをみてもっと勉強しろ等の叱咤でもOKです。お願いします)

一番シンプルなのは、TileEntityクラスのreadFromNBT()メソッドとwriteToNBT()メソッドを@Overrideすることだと思います。

実際、デフォルトの状態では、ここで座標(とブロックID?)くらしか読み書きしていないのですよね。

ここで、内部インベントリの状態ですとか、作業の進行状況などを明示的に読み書きしてあげれば、ゲーム再開時でもTileEntityの状態を中断時の状態に戻すことができるはずです。

ちなみに、NBTとは何だ! という質問には、「僕も不勉強なもので・・・」としか答えられません。今わかっていることは以下くらい。

  • NBTとは
  • 上記の参考ページを読んでの理解
    • Minecraftにおけるデータ保存方式。原則として、NBTTagCompoundをハンドルとして、データをTagと対にして保管。
    • NBTに保存したデータは、セーブファイルとして保存される(?)
    • データタイプ(String等)は厳密に管理されており、データタイプごとに保管メソッド、読み込みメソッドを変えなくてはならない。
    • (上記とは別ソースですが)文字列データには、容量の上限はないのではという噂。

ここまでのまとめ

  • Block(をextendsした独自Block)クラスのインスタンスはゲーム内に1種類につき1つしか存在しない
  • ワールド内の特定座標にブロックが設置されているという情報は、位置をキーとして上記インスタンスをマッピングする形で保存されている
  • そのため、Blockという単位(枠組み)では、ブロックに個別情報を保存できない。
    • ブロックの個別情報を保存する方法として、MetaDataとして保存する方法と、TileEntityを経由して任意のデータを保存する方法の2つがある
  • MetaDataとは
    • ワールド上に設置された個別のブロックに結び付けられたブロックデータ保存形式
    • 保存できるデータ量は4bitと小さい。でもお手軽で読み込みも早い
    • Blockクラスから直接利用できる。
    • http://www.minecraftforge.net/wiki/Metadata_Based_Subblocks
  • TileEntity とは、
    • ワールド上に設置された個別のブロックに結び付けられたブロックデータ管理基盤。
    • どんなデータでも保存できる。ついでにそれらデータを取り扱うメソッドも追加できる。
    • 通常は、BlockクラスをextendsしたBlockContainerクラスから利用する(楽だし)
    • ブロックを新しく設置するたびにTileEntityは新しく生成され、ワールドに位置をキーとして保存される
  • Blockマッピングデータとは異なりTileEntityマッピングデータには個別のTileEntityインスタンスが保存されているので、インスタンス内に独自データを自由に保存できる。
    • ただし、ブロック再設置、ゲーム再起動などでインスタンスは再生成されるので、残しておきたいデータはNBTを読み書きするメソッドを@Overrideして明示的に保存するよう記述しなくてはならない

次回について

次は、ItemとItemContainerとDamgeValueとNBTの関係についてまとめたいところ。

なお、ItemBlockについては以下のページがとてもわかりやすくまとめており参考になるので、ここでまとめる必要はないですよね。

 個人的なメモ帳

http://codeshare.io/4KSk6

*1:もしかしたらテクスチャ切り替えではなく色乗算で変えているかも?ここは識者の指摘待ち

*2:ゲーム内では、上:0、下:1、北:2、南:3、西:4、東:5のようにint型で表現されていますが、わかりづらいので ForgeDirection.NORTHのようなヘルパークラスを使うことが多いようです。

*3:ワールドに設置したときに呼ばれるものと、インベントリ内にあるItemBlockを表示するときに呼ばれるものの2種類があることに注意

*4:これは特殊な負荷軽減処理を行わない場合のお話で、もしかすると負荷軽減のための特殊な機能があるかもしれず、初心者の僕ではそこまでの知識がないので識者の突っ込み待ち

*5:一般的にはBlockContainerクラスを使う