Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # この文書について
- へぼへぼ(Twitter @hevohevo)がソースコードを読んで、わかったことを書きなぐるページ。
- まちがっていたら、ご指摘&直接修正してください。
- # Minecraft Forge(#1180) Event サイクル。たぶんマイクラ1.7系共通
- - NOINIT("Uninitialized",null),
- - LOADING("Loading",null),
- - CONSTRUCTING("Constructing mods",FMLConstructionEvent.class),
- - PREINITIALIZATION("Pre-initializing mods", FMLPreInitializationEvent.class),
- - INITIALIZATION("Initializing mods", FMLInitializationEvent.class),
- - POSTINITIALIZATION("Post-initializing mods", FMLPostInitializationEvent.class),
- - AVAILABLE("Mod loading complete", FMLLoadCompleteEvent.class),
- - SERVER_ABOUT_TO_START("Server about to start", FMLServerAboutToStartEvent.class),
- - SERVER_STARTING("Server starting", FMLServerStartingEvent.class),
- - SERVER_STARTED("Server started", FMLServerStartedEvent.class),
- - SERVER_STOPPING("Server stopping", FMLServerStoppingEvent.class),
- - SERVER_STOPPED("Server stopped", FMLServerStoppedEvent.class),
- - ERRORED("Mod Loading errored",null)
- ## ゲーム本体の起動
- - NOINIT("Uninitialized",null),
- - LOADING("Loading",null),
- ## MOD群の初期化
- - CONSTRUCTING("Constructing mods",FMLConstructionEvent.class),
- - PREINITIALIZATION("Pre-initializing mods", FMLPreInitializationEvent.class),
- - INITIALIZATION("Initializing mods", FMLInitializationEvent.class),
- - POSTINITIALIZATION("Post-initializing mods", FMLPostInitializationEvent.class),
- - AVAILABLE("Mod loading complete", FMLLoadCompleteEvent.class),
- ## サーバースタート
- - SERVER_ABOUT_TO_START("Server about to start", FMLServerAboutToStartEvent.class),
- - SERVER_STARTING("Server starting", FMLServerStartingEvent.class),
- - SERVER_STARTED("Server started", FMLServerStartedEvent.class),
- ## サーバーストップ
- - SERVER_STOPPING("Server stopping", FMLServerStoppingEvent.class),
- - SERVER_STOPPED("Server stopped", FMLServerStoppedEvent.class),
- - ERRORED("Mod Loading errored",null);
- # 以下、modの初期化部分について重点的に調べる。つまりForgeModLoaderの部分
- # 各Modの情報は、ModContainerオブジェクトに格納される
- - cpw.mods.fml.common.ModContainer
- - Mod名とかMod依存関係とか
- - boolean registerBus(EventBus, LoadController)で、LoadControllerのEventBusに自身の初期化メソッドを追加
- # EventBus は、com.google.common.eventbus を使っている。
- - googleさんの提供するイベント駆動基盤
- - http://docs.guava-libraries.googlecode.com/git-history/v10.0/javadoc/com/google/common/eventbus/package-summary.html
- - 日本語で解説 http://cco.hatenablog.jp/entry/20111210/1323509513
- - ``@Subscribeアノテーションを使うことでevent handlerとなる公開メソッドを実装することができます。イベントである引数は必ず1つのみであることに注意が必要です。``
- - 日本語で解説 http://blog.kengo-toda.jp/entry/20111006/1317907239
- - ``リスナーはどのようなクラスでもよく、メソッドを@Subscribeアノテーションで修飾するだけで使えます。``
- - ``リスナーが@Subscribeで修飾されたメソッドを複数個持っている場合、これらが実行される順番は環境依存となります。``
- - 上記を読んで理解した内容
- - Busにあらかじめレシーバーを複数登録(bus.register(receiver))しておく。
- - レシーバーには@Subscribeをつけたメソッド(引数は1つのobject)を定義しておく。メソッド名は問わない。ただし複数あるとその実行順序は環境依存となるので注意
- - Busに対してメッセージをポスト(bus.post(object))すると、そのobjectのタイプに応じたレシーバーのSubscribeメソッドを実行
- - ただし、同じタイプのオブジェクトを受け取るようなレシーバーメソッドを複数登録してしまうと、その実行順序は環境依存
- -- 補足1:要するにオーバーロードに類する実装がリスナーを用いて出来るのだが、同じものを複数個登録する危険性については放置されたままの実装になっている。絶対に同じ型のリスナーを追加しないように注意が必要。(@n416
- -- ↑たとえば、Object型(別に特定のイベント型でもいいけど)を受け取るレシーバーを登録しておいてログ取りなんてできる。危険性を理解したうえでの応用方法はありそう(h2
- -- 補足2:また、Androidのイベントリスナーの実装とは異なりSubscribeはイベントを消化せずに下に流すような使い方は出来ない。(@n416 ←Androidすごい(h2
- - (感想)これは、シンプルで使いやすそう!
- # LoadController について
- - cpw.mods.fml.common.LoadController
- - イベント駆動部分の主体。以下private変数を一部抜粋
- - ```private masterChannel = new EventBus("FMLMainChannel");```、 LoadControllerが自身の中で@subscribeされているメソッドの駆動用チャンネル。EventBusで実装
- - ```private ImmutableMap eventChannels<modid, そのmodのEventBus>```、 各Modの初期化メソッド駆動用チャンネルを集めたもの。Mapで実装
- - LoadController内で@Subscribe定義されているもの(メッセージがポストされたらどちらかを実行)
- - ```public void buildModList(FMLLoadEvent event)```
- - 有効なModごとのEventBus作って、eventChannelに登録、各Modの状態リスト作成、リソースパックにModを登録(?)
- - ```public void propogateStateMessage(FMLEvent stateEvent)```
- - 外部からきたメッセージを振り分ける。"FMLPreInitializationEvent"なら上記のModList作成を行う。それ以外なら有効なModContainer全部にメッセージ伝達
- - 有効なModContainer全部にメッセージ伝達するメソッド ```private void sendEventToModContainer(FMLEvent stateEvent, ModContainer mc)```
- - Mod依存関係や、バージョンの解決などを行っている。
- # まとめ
- ## 階層型のイベント駆動になっており。興味深い。
- - 「LoadController」の挙動 (EventBusで実装されたイベント駆動)
- - "FMLPreInitializationEvent"受信: 有効なModリストを作ったり、有効ModごとにEventBus作ってMapに保存。そして↓を実行
- - それ以外受信: 有効なModContainer全てへメッセージを伝播(というイメージ)
- - 各「ModContainer」の挙動 (イメージ、実際はLoadControllerがeventChannelsを使って処理)
- - 何らかのメッセージを受信: 自身が依存しているModや必要バージョンなどを確認し、依存ModがまだERRORED状態なら自身もERROREDとなる
- - "Skipping event %s and marking errored mod %s since required dependency %s has errored"
- - 依存関係等問題なければ、自身の初期化用EventBusに対してメッセージを伝播。
- - 各「Modの初期化EventBus」の挙動 (EventBusで実装されたイベント駆動)
- - 何らかのメッセージを受信: メッセージにより反応するレシーバメソッドをすべて実行。つまり対応する初期化メソッドを全て実行。ただし、複数あるとき実行順は環境依存
- - 初期化失敗したModリストは、"private modState"に入る。外部から参照するには、```public void printModStates(StringBuilder ret)```を利用。
- ## つまりわかりやすく言うと
- - 100個のModがあったら、どのように初期化作業が進むか。
- - "FMLPreInitializationEvent"メッセージ来た=> 有効なModリストなどの作成後、100個のModにメッセージ送信。それぞれのModは対応する初期化メソッド実行
- - "FMLInitializationEvent"メッセージ来た=> 100個のModにメッセージ送信。それぞれのModは対応する初期化メソッド実行
- - "FMLPostInitializationEvent"メッセージ来た=> 100個のModにメッセージ送信。それぞれのModは対応する初期化メソッド実行
- ## 未解読、謎の部分
- - 依存関係により初期化失敗したModの後処理はどうなっているのか?
- - たぶん、"FMLPreInitializationEvent"などのメッセージを発信するエンジンのコードを読まないと・・・力尽きて今日は無理です。
- ## 感想
- - どんなModが追加されるのかわからないわけで、各Modの初期化を完全に正常に行うことをある程度あきらめた設計に思える。
- - つまり、「初期化せよ」というメッセージは送るが、後はそれぞれのModのメソッドにまかせた!という設計。
- ------------------------------------------------ここからまた、別のお話
- # マイクラがWorld内のブロックのデータをどのように取り扱っているかのメモ
- ## ブロックは、1種類につきゲーム内で1つしか存在しない
- - 独自追加したものも含めて、Block(を継承したクラス)はゲーム開始時に1種類1個ずつインスタンスが作成される。
- - マイクラ世界の個々のブロックはBlockクラスのインスタンスではなく、マッピングデータ。(そりゃそうだ、万単位のブロックが個別のインスタンスだとPCがチヌ)
- - (イメージ) 座標(1,1,1)と(1,1,2) にはBlockID3のブロック(1つのインスタンスを指す)が設置されているよ。
- - ```world.getBlock(x,y,z)```、 ワールドの座標をキーにBlockインスタンスを取り出す。取り出したオブジェクトは、同じ種類のブロックなら常に同一のもの。
- - だから、独自ブロックを追加したときに、クラス変数どころか、インスタンス変数も全ブロックで共有されることになる。(要調査:違うワールドではどうなる?)
- ##上記の内容を検証したサンプルコード
- - (基本知識)独自ブロックを追加するためには、Modの初期化メソッド中で最低限、以下を実行する
- 1. Blockクラスを継承(extends)した新しいクラスを作成。
- 2. new して、そのインスタンスオブジェクトを、一意な名前とともにGameRegistryに登録
- - 元とするMaterial(例:rock)を指定することでその性質をそのまま引き継いだブロックができる
- - 独自機能をつけたいなら@Override
- - 以下のコードでは、説明簡略化のためにテクスチャ関係は省略
- ```
- // このコードだけでは動きません
- // 初期化作業として以下が最低でも必要
- // Block blockSample = new BlockSample(); //インスタンス生成
- // GameRegistry.registerBlock(blockSample, "block_sample"); //それをゲームに登録
- // import は省略
- public class BlockSample extends Block {
- public static Integer varC=0; // クラス変数
- public Integer varI=0; // インスタンス変数
- public BlockSample() {
- super(Material.rock); // 基本的な性質は rockのものを使う
- this.setCreativeTab(CreativeTabs.tabBlock); //Creativeモードでブロックを取り出すとき、ブロックタブから取り出せるように
- }
- /**
- * Blockクラスにもとからあるメソッドを上書き @Override
- * このメソッドはブロックを右クリックすると呼び出される
- * 本プログラムは右クリックでカウンタの値が増える。ポイントは、ワールド中に設置した全てのブロックでカウンタ(2種類)が共有されていること。
- * なぜならば、どれも同じ BlockSampleインスタンスを参照しているからである。
- * ついでに、サーバー/クライアントでそれぞれ実行されるので、1クリックで2回呼び出されていること、
- * そしてさらに、サーバー/クライアント間でも共通のインスタンスを参照していることがわかる。(←シングルだからです。マルチなら違うはず)
- */
- @Override
- public boolean onBlockActivated(World world, int blockX, int blockY, int blockZ, EntityPlayer player, int hitSide, float par7, float par8, float par9) {
- BlockSample.varC++;
- this.varI++;
- System.out.println("BlockSample.onBlockActivated"+world);
- System.out.println(this.toString()); // 参照しているオブジェクトの情報表示
- System.out.println("Class var: "+BlockSample.varG); // こちらが共通なのは当然といえば当然の結果
- System.out.println("Instance var: "+this.varI); // 問題はこちら
- return super.onBlockActivated(world, blockX, blockY, blockZ, player, hitSide, par7, par8, par9);
- }
- }
- ```
- ## (補足) サーバー/クライアントごとのコードの書き分けについて
- - 基本的な考え方は以下を参照。
- - http://minecraftjp.info/modding/index.php/%E6%96%B0%E3%81%97%E3%81%84%E3%82%B3%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0%E8%A8%98%E6%B3%95%E3%81%A8Universal_Modding
- - なお、@SideOnly(Sides.CLIENT)アノテーションは、シングル環境ではうまく働かない模様。
- - ```if (!world.isRemote){}```による書き分けが必要?
- - 情報源。 http://forum.minecraftuser.jp/viewtopic.php?f=39&t=18801
- - サーバークライアント間での同期、通信方法については要調査。
- # ブロックごとに個別の情報を格納したいときにどうするか
- ## 小さなデータはMetaDataで
- - 小さなデータならMetaData(メタ値)、複雑なデータはTileEntityを使う。
- - 小さなデータなら、メタ値として情報を格納できる。たぶん容量は4bit
- - ```world.getBlockMetadata(x,y,z)``` 、座標をキーにメタ値取り出し。「ブロックをキー」にではないところに注意。
- - メタ値によって表示するテクスチャを変えることで、違うブロックのように見せかけることが可能。羊毛とかね。あとは原木の向きとか
- - ブロックが返すテクスチャを面によって切り替えるには、Blockを継承したクラスで、以下のメソッドを@Override
- - ```public IIcon getIcon(){};```
- - 引数のタイプによって2種類ある。ブロック設置時によばれるものと、プレイヤインベントリ内にあるときに呼ばれるもの
- - 以下、テクスチャについてのサンプルコード
- ```
- // ブロックをextendsしたクラス内で以下を@Override
- @SideOnly(Side.CLIENT) // サーバーサイドはテクスチャ処理いらない
- private IIcon topIcon;
- /*
- * テクスチャ画像をシステムに登録する部分
- */
- @Override
- @SideOnly(Side.CLIENT)
- public void registerBlockIcons(IIconRegister iconRegister)
- {
- // すでにブロックにデフォルトのテクスチャが設定されているときはそれを呼び出して登録
- // たとえば block.setBlockTextureName("sand") などすでにされているとき。
- // これで、親クラス(Block)でprotectede宣言されている blockIconフィールドが利用可能。
- super.registerBlockIcons(iconRegister);
- // 原木のテクスチャをセット
- this.topIcon = iconRegister.registerIcon("log_oak");
- }
- /*
- * プレイヤーインベントリ内と手に持っているブロックの描画に使われるテクスチャを返す
- */
- @Override
- @SideOnly(Side.CLIENT)
- public IIcon getIcon(int side, int meta){
- //描画面が上下ならデフォのテクスチャを使い、そうでなければ原木のテクスチャを使う
- // 方向(side)は、0:上面、1:下面、2:北面、3:南面、4:西面、5:東面 だが、可読性最悪なのでForgeDirectionを使うべし
- // int metaの値を使ってテクスチャをきりかえてもいい
- if (side == ForgeDirection.UP.ordinal() || side == ForgeDirection.DOWN.ordinal()) {
- return this.topIcon;
- }
- return this.blockIcon;
- }
- /*
- * ワールドにブロックを設置したときの描画に使われるテクスチャを返す
- */
- @Override
- @SideOnly(Side.CLIENT)
- public IIcon getIcon(IBlockAccess iBlockAccess, int x, int y, int z, int side){
- if (side == ForgeDirection.UP.ordinal() || side == ForgeDirection.DOWN.ordinal()) {
- return this.blockIcon;
- }else{
- return this.sideIcon;
- }
- /* その他のTips、テクスチャ切り替えの材料
- * int meta = iBlockAccess.getBlockMetadata(x, y, z); でブロックのメタ値が得られる
- * TileEntity tileEntity = iBlockAccess.getTileEntity(x, y, z); でタイルエンティティ
- */
- }
- ```
- ## より大きなデータはTileEntityで
- TileEntity はゲーム内でどのように取り扱われているか
- - たとえば個別にインベントリを持つブロックなどは、各ブロックごとに大量のデータを保持しなくてはならない。
- - あるBlockがTileEntityを持つとあらかじめ宣言(手順は後述)しておくことで、ワールドにブロックを設置するごとに、新しいTileEntityインスタンス生成&保存
- - イメージとしては、個別ブロックとTileEntityが直接結び付けられているようだが、実装としては、World座標ベースで保存&管理されている。以下は根拠。
- - ```world.getTileEntity(x,y,z)``` : 座標をキーに個別のEntityを取り出し。「ブロックをキー」にではないところに注意。
- # TileEntityをブロックと結びつける方法
- ## 最低限の手順
- - TileEntityの用意
- 1. TileEntityクラスを継承した独自のTileEntityクラスを作っておく
- - ```public class TileEntitySample extends TileEntity{}```
- - この中で独自機能や独自変数を定義しておく
- 2. GameRegistoryに一意な名前と一緒に登録して、これはTileEntityなんだよということをシステムに知らせる
- - ```GameRegistry.registerTileEntity(TileEntitySample.class, "TileEntitySample");```
- - ブロックの追加とは異なり```TileEntitySample.class```というクラスそのものを追加していることに注意。
- - Block側の用意
- 1. Blockクラスではなくそれを拡張したBlockContainerクラスを使って、新しいBlockを定義
- 2. BlockContainerクラスの中で、createnewTileEntityメソッドを@Overrideする。その中で結び付けたいTileEntityをnewして返すようにする。
- - ```public TileEntity createNewTileEntity(World world, int param) { return new TileEntitySample() }```
- これらの手順を踏むことで、システムは、ワールドにブロックが設置されるたびにそのBlockのcreateNewTileEntity()メソッドを呼び出し、
- 新しいTileEntityオブジェクトを得て、それをワールドデータに(位置座標をキーに)保存する。
- - (要検討事項)
- - createNewTileEntity()の2番目のint paramは何が入ってくるのか謎。
- - たぶんこの値によって、呼び出すTileEntityを変えるよう実装できるのかな?
- (回答-A.K.)ブロックのメタデータです。メタ別に変えることが出来ます。
- (h2)ありがとうございます。
- ## 以下、知らなくても別にいいけれど、知っておくと得した気分になれる情報
- - マイクラソースコード中では、BlockContainerは以下のように定義されている
- - ```public abstract class BlockContainer extends Block implements ITileEntityProvider{}```
- - ITileEntityProvider インタフェースは、```TileEntity createNewTileEntity(World world, int param)```だけが定義されている
- - つまり、TileEntityを取り扱うブロックは、原則としてBlockContainerクラスを使わなくてはならない(もちろんこのProviderインタフェースを独自実装したなら話は別)。
- - (注釈-A.K.)BlockクラスにcreateTileEntityメソッドがあり、そこから、createNewTileEntityメソッドが呼ばれている。
- - (注釈続き)つまり、TileEntityを実装するだけなら、Blockクラス継承でもよい。実際は、設置撤去の処理が必要なので、BlockContainerクラスを継承するのが良い。
- - (h2)ありがとうございます。BlockContainerから継承が無難なんですね。
- # 今回のまとめ
- - ブロックの実体そのものは、ゲーム内で1種類あたり1個しか存在しない
- - ゲーム内で特定の座標に設置されたブロックが何であるかという情報は、Worldごとに、座標をキーとしたマッピングデータで保存されている
- - ```world.getBlock(x,y,z)``` でそのブロックの実体を参照できる。
- - ブロックの実体は1種類あたり1個しか存在しないので、座標が違っても種類が同じなら、結局指すものは同じもの。
- - そのため、ワールドに設置したブロック1つずつに独自情報を保存するためには別の手段が必要
- - 手段1: MetaData(メタ値)によって小さなデータ保存(4bitかな?10進数で言う0-15)
- - 原木の向きや羊毛の色などはこの値によって管理されている
- - 独自データは、ブロックではなく、座標に結び付けられて保存されていることに注意。
- - ```world.getBlockMetaData(x,y,z)``` でその「座標」のメタデータを入手できる。
- - もし、ブロックマッピングデータとMetaDataマッピングデータに齟齬がでたら・・・ガクブル
- - 手段2: EntityDataによって様々なデータを保存
- - 内部インベントリを持つブロックなど、大量のデータを保存するときはこちらの手段を使う。
- - ``world.getTileEntity(x,y,z)``` 「座標」をキーに個別のEntityオブジェクトを入手できる。
- - もし、ブロックマッピングデータと(略
- # 感想(というか妄想みたいなもの)
- きっと、最初期のマイクラはデータ保存の方法としてBlockマッピングデータとMetaDataマッピングデータしか用意していなかったんじゃないかな?
- そしてバージョンアップするうちに、これじゃ領域が足りん!ということで、後付でTileEntityという保存手段を実装したんじゃないかな?
- (h2)最初期からTileEntityは存在したということなので、上記は間違いっぽい。
- # 次に何を調べるかまとめるか
- - マイクラ世界に存在するブロックとは別のオブジェクトであるEntity。それがどのようにデータ管理されているのか、そしてどうやってEntityを追加するのか。
- - マルチプレイヤー環境に対応できるMOD作成のために、
- - サーバー・クライアントでどのように処理を分けるのか。ロジック面と実装面の両方について。
- - もちろん具体的な実装方法も。サーバークライアント間の同期をどのように行うのかについても調べる。
- - 直方体のブロックにテクスチャを貼るだけではなく、独自レンダリングをどうやって行うのか。噂に聞くと、内容理解は相当ハードらしい。
- - 「独自のレンダリングはUVマッピングあたり抑えておけばできるけど、TileEntity使うならTileEntitySpecialRendererでModelを描画しちゃうのが楽」とのこと
- ------------------------- TileEntitySpecialRenderについて
- # TileEntitySpecialRenderについて
- - Knowledge of OpenGL is recommended for this tutorial to be useful. お、おう。
- - http://www.minecraftforge.net/wiki/Tile_entity_special_renderer
- - Tessellator
- - http://www.minecraftforge.net/wiki/Tessellator
- - What is the tessellator?
- - Basically the tessellator is a singleton convenience class to help the user interact with OpenGL more easily more simple ? "it draws stuff and things"
- - https://github.com/reginn/Tutorial-Renderer
- - OpenGLおよびグラフィックス系は本当にさっぱり。本を買って勉強すべきか。でもとりあえずサンプルコードのまま試してみよう。
- - 仕組みは理解した。問題はOpenGL計算部分
- # 最低限の準備
- - Blockの新しいRenderTypeIDを取得
- ```
- @SideOnly(Side.CLIENT)
- public static int blockColumnRenderID = RenderingRegistry.getNextAvailableRenderId();
- ```
- - Blockクラスをextendsした新しいブロック作成し、以下の3つのメソッドを@Override
- - public int getRenderType() { return (上記renderID) }
- - public boolean renderAsNormalBlock() { retrun false;} // 正方形ではない
- - public boolean isOpaqueCube() { return false; } // 透明部分がある
- - 具体的な描画を行うハンドラークラスを作る(ISimpleBlockRenderingHandlerインタフェースを実装)。4つのメソッドを@Override
- - public void renderInventoryBlock(Block block, int metadata, int modelId, RenderBlocks renderer) {謎の計算部分。要勉強} // インベントリ内での描画
- - public boolean renderWorldBlock(IBlockAccess world, int x, int y, int z, Block block, int modelId, RenderBlocks renderer) {} // world 設置時の描画。描画したらtrueをreturnさせる
- - public boolean shouldRender3DInInventory(int modelId) {} // インベントリで描画するか
- - public int getRenderId() { return (上記renderID) }
- ```
- @SideOnly(Side.CLIENT)
- public class BlockColumnRenderer implements ISimpleBlockRenderingHandler{}
- ```
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement