提供: Minecraft Modding Wiki
移動先: 案内検索
(Teleporterについて追記、Entity#changeDimension利用は不具合のもとなので、PlayerList#transferEntityToWorld利用を書いたら消す。)
(PlayerListについて加筆。不具合のあるEntity#changeDimension利用コードを削除。)
152行目: 152行目:
  
 
=== エンティティを転送する ===
 
=== エンティティを転送する ===
Entityの別のディメンションへの転送は、Entity#dimensionを変更したのち、出発地のディメンションに対応するWorldServerからEntityを削除、座標を変更してTeleporterを呼び出し、目的地のディメンションに対応するWorldServerインスタンスを渡してEntityのクローンを作成し、クローンを目的地に強制スポーンさせることで実現しています(Entity#changeDimensionの操作)。
+
Entityの別のディメンションへの転送方法について解説します。バニラ+Forge環境で利用可能な転送用メソッドは二種類あります。
 +
 
 +
一つ目の「net.minecraft.entity.Entity#changeDimension」は、ネザーポータル、エンドポータルによるディメンション間移動に使用されています。これは、まずEntity#dimensionを変更したのち、出発地のディメンションに対応するWorldServerからEntityを削除、座標を変更してTeleporterを呼び出し、目的地のディメンションに対応するWorldServerインスタンスを渡してEntityのクローンを作成し、クローンを目的地に強制スポーンさせることでディメンション間移動を実現しています。
  
 
クローンのEntity#dimensionはコンストラクタでWorldServerから取得されることになり、最初の代入は無駄にも思えますが、ポータルを生成するTeleporterに渡されるのは最初の(出発地側の)Entityであるため、Teleporter内ではコンストラクタに渡される目的地のWorldServerのディメンションIDとEntity#dimensionは一致することになります(バニラでは転送先はWorldServerから取って判定しているのだが)。
 
クローンのEntity#dimensionはコンストラクタでWorldServerから取得されることになり、最初の代入は無駄にも思えますが、ポータルを生成するTeleporterに渡されるのは最初の(出発地側の)Entityであるため、Teleporter内ではコンストラクタに渡される目的地のWorldServerのディメンションIDとEntity#dimensionは一致することになります(バニラでは転送先はWorldServerから取って判定しているのだが)。
  
===== PlayerList#transferEntityToWorldを使用する手法 =====
+
二つ目の「net.minecraft.server.management.PlayerList#transferEntityToWorld」は、「net.minecraft.entity.player.EntityPlayerMP」で再実装された「#changeDimension」から「PlayerList#changePlayerDimension」を経由して呼ばれています。
(未検証)
+
 
 +
<small>
 +
バニラの「PlayerList#changePlayerDimension」は、ForgeによってTeleporter引数を追加したメソッド「#transferPlayerToDimension」に処理を移されています([https://github.com/MinecraftForge/MinecraftForge/blob/1.11.x/patches/minecraft/net/minecraft/server/management/PlayerList.java.patch#L151 GitHub/MinecraftForge/PlayerList.java.patch#L151])。「#transferPlayerToDimension」は、出発地のWorldServerからEntityPlayerMPを削除したのち、「#transferEntityToWorld」を呼び出しています。
 +
</small>
 +
 
 +
<small>
 +
バニラの「PlayerList#transferEntityToWorld」は、ForgeによってTeleporter引数を追加した同名メソッドに処理を移されています([https://github.com/MinecraftForge/MinecraftForge/blob/1.11.x/patches/minecraft/net/minecraft/server/management/PlayerList.java.patch#L179 GitHub/MinecraftForge/PlayerList.java.patch#L179])。
 +
</small>
  
===== 強引にEntity#changeDimensionを使用する手法 =====
+
「PlayerList#transferEntityToWorld」のおおよその処理は「Entity#changeDimension」と同じですが、新しくEntityのインスタンスを作り直さず、「Entity#setWorld」を呼んで出発地のWorldに属するEntityインスタンスをそのまま目的地のWorldで再利用します。
# AccessTransformerを使って、「private final」な「WorldServer#worldTeleporter」、SRG名「field_85177_Q」のアクセス修飾子を「public」にする(下ソース参照、net.minecraftforge.fml.relauncher.ReflectionHelperでも可)。
 
# 転送時、元々のWorldServer#worldTeleporterを退避して、好きなTeleporterインスタンスを代入。Entity#changeDimensionを呼んでEntityを移動させ、WorldServer#worldTeleporterを復元する。
 
  
META-INF/<modid>_at.cfg
+
また注意すべき点として、「PlayerList#transferEntityToWorld」は、出発地のWorldからEntityを削除しません。自分で出発地のWorldに対して「World#removeEntityDangerously」を呼んで、World及びChunkからEntityを削除する必要があります。
<source lang="text">
 
public-f net.minecraft.world.WorldServer field_85177_Q #worldTeleporter non-final
 
</source>
 
  
2の例。ただし以下の点で不完全。
+
===== 例:「触れるとディメンションを移動するブロック」を追加する =====
* エンド以外から転送する場合、ネザーへの転送では座標が1/8倍、オーバーワールドへの転送では座標が8倍される。
 
** オーバーワールド-ネザー間では望ましいが、追加ディメンションからオーバーワールドに帰還するときTeleporterに渡される座標が狂う。
 
  
 +
BlockTeleporter.java
 
<source lang="java" line>
 
<source lang="java" line>
public static <T extends Entity> void transferEntity(World worldIn, BlockPos portalPos, T entityIn,
+
package samplemod.block;
                                                    int destDimension, Function<WorldServer, Teleporter> funcTeleporter,
 
                                                    Consumer<T> callback) {
 
    entityIn.setPortal(portalPos);
 
    entityIn.timeUntilPortal = 10; // この2行は不要?
 
  
    if (!worldIn.isRemote && !entityIn.isDead) {
+
import net.minecraft.block.Block;
        MinecraftServer server = entityIn.getServer();
+
import net.minecraft.block.material.Material;
        WorldServer destServer = server.worldServerForDimension(destDimension);
+
import net.minecraft.block.state.IBlockState;
 +
import net.minecraft.entity.Entity;
 +
import net.minecraft.entity.player.EntityPlayerMP;
 +
import net.minecraft.server.MinecraftServer;
 +
import net.minecraft.server.management.PlayerList;
 +
import net.minecraft.util.math.AxisAlignedBB;
 +
import net.minecraft.util.math.BlockPos;
 +
import net.minecraft.world.*;
 +
import samplemod.SampleMod;
 +
import samplemod.world.sample.TeleporterSample;
  
        Teleporter teleporter = funcTeleporter.apply(destServer);
+
import javax.annotation.Nullable;
        Teleporter cache = destServer.worldTeleporter; // Access Transformer | private final -> public
 
  
        destServer.worldTeleporter = teleporter;
+
public class BlockTeleporter extends Block {
        T clone = (T) entityIn.changeDimension(destDimension);
 
        callback.accept(clone); // 座標設定等
 
  
         destServer.worldTeleporter = cache;
+
    public BlockTeleporter() {
 +
         super(Material.ROCK);
 +
    }
  
 +
    @Nullable
 +
    @Override
 +
    public AxisAlignedBB getCollisionBoundingBox(IBlockState blockState, IBlockAccess worldIn, BlockPos pos) {
 +
        return FULL_BLOCK_AABB.expandXyz(-.05d);
 +
    }
 +
 +
    @Override
 +
    public void onEntityCollidedWithBlock(World worldIn, BlockPos pos, IBlockState state, Entity entityIn) {
 +
        MinecraftServer server = worldIn.getMinecraftServer();
 +
        if (server != null) {
 +
            PlayerList playerList = server.getPlayerList();
 +
            int dest = entityIn.dimension == DimensionType.OVERWORLD.getId() ? SampleMod.SAMPLE_DIMENSION.getId() : DimensionType.OVERWORLD.getId();
 +
 +
            Teleporter teleporter = new TeleporterSample(server.worldServerForDimension(dest));
 +
 +
            if (entityIn instanceof EntityPlayerMP) {
 +
                playerList.transferPlayerToDimension((EntityPlayerMP) entityIn, dest, teleporter);
 +
            }
 +
            else {
 +
                int origin = entityIn.dimension;
 +
                entityIn.dimension = dest;
 +
                worldIn.removeEntityDangerously(entityIn);
 +
 +
                entityIn.isDead = false;
 +
 +
                playerList.transferEntityToWorld(entityIn, origin, server.worldServerForDimension(origin), server.worldServerForDimension(dest), teleporter);
 +
            }
 +
        }
 
     }
 
     }
 
}
 
}
 +
 
</source>
 
</source>

2017年3月26日 (日) 15:01時点における版

この記事は"Minecraft Forge Universal 13.19.0.xxx~"を前提MODとしています。

Stone pickaxe.png
中級者向けのチュートリアルです。
C world.png
Worldに関係のあるチュートリアルです。

ディメンションの追加

ディメンションを生成する

DimensionType

「net.minecraft.world.DimensionType」は、ディメンションを登録する列挙型です。

ForgeがDimensionTypeに列挙子を追加するメソッド「DimensionType#register」を用意してくれているので、これを使います。

  1. 「net.minecraftforge.common.DimensionManager#getNextFreeDimId」から、使用可能なディメンションID(int)を取得します。
  2. 「DimensionType#register」を呼び出し、「DimensionType」型の戻り値を得ます。これが追加された列挙子で、WorldProviderで使います。
    • 第一引数(String):ディメンションの名前(e.g. Overworld、Nether、The End)
    • 第二引数(String):ディメンションのセーブデータ等で使う後置詞(e.g. _nether、_end)
    • 第三引数(int):1で取得したID
    • 第四引数(Class<? extends WorldProvider>):WorldProviderのクラス(下で解説)
    • 第五引数(boolean):ディメンションをアンロードしないかどうか
  3. 「DimensionManager#registerDimension」を呼び出し、こちらにもディメンションを登録します。

WorldProvider

「net.minecraft.world.WorldProvider」は、ワールドの振る舞いを決定する抽象クラスです。

  1. 「WorldProvider」のサブクラスを作成します。
  2. コンストラクタから「WorldProvider#init」が呼ばれるので、WorldProviderのフィールド(下表参照)を好きなように変更してください。
  3. 「DimensionType」の項で得た、このWorldProviderに対応する「DimensionType」列挙子を「getDimensionType」から返します。
フィールド名 影響 参考
doesWaterVaporize 水の蒸発の有無
  • net.minecraft.item.ItemBucket
  • net.minecraft.block.BlockDynamicLiquid
  • net.minecraft.block.BlockIce
hasNoSky 地図の描画、スポーン座標
  • net.minecraft.world.WorldProvider#getActualHeight
  • net.minecraft.item.ItemMap
hasSkyLight 日光の有無
  • net.minecraft.world.chunk.Chunk
biomeProvider バイオームの決定 下で解説


メソッド名 影響 参考
calculateCelestialAngle 昼夜の移り変わり 0fで真昼(エンド)、.5fで真夜中(ネザー)
  • net.minecraft.world.World#getCelestialAngleRadians
  • net.minecraft.item.ItemClock
  • net.minecraft.block.BlockDaylightDetector
  • net.minecraft.client.renderer.RenderGlobal#renderSky
  • net.minecraft.village.VillageSiege
createChunkGenerator チャンクの地形等、生成手法の決定 net.minecraft.world.chunk.IChunkGeneratorを実装したクラスのインスタンスを作成して返す。詳しくは下で解説。
canRespawnHere リスポーンの可否
isSurfaceWorld 空の描画の有無
getVoidFogYFactor 奈落の霧(Japan Wiki)の開始Y座標
getHorizon 地平線の高さ
doesXZShowFog 霧の描画の有無
  • net.minecraft.client.renderer.EntityRenderer#setupFog
getWelcomeMessage ディメンション入場時のメッセージ?(未使用、Forge)
getDepartMessage ディメンション退場時のメッセージ?(未使用、Forge)

BiomeProvider

「net.minecraft.world.biome.BiomeProvider」は、バイオームを決定するクラスです。

サブクラス「net.minecraft.world.biome.BiomeProviderSingle」は、単一バイオームのディメンションを作成するのに使うことができます(ネザー、エンドで使用)。

オーバーワールドでは、「net.minecraft.world.gen.layer.GenLayer#initializeAllBiomeGenerators」、「net.minecraft.world.biome.BiomeProvider#getModdedBiomeGenerators」を通じて得られた「GenLayer」の「#getInts」からバイオームIDを取得しています。

IChunkGenerator

「net.minecraft.world.chunk.IChunkGenerator」は、チャンクの地形や構造物を生成するインターフェースです。

  1. 「IChunkGenerator」を実装したクラスを作成します。コンストラクタでWorldProviderからWorldのインスタンスを受け取ってください。チャンクのインスタンス化に使います。
  2. 「#provideChunk」を実装します。第一引数はチャンクのX座標、第二引数はチャンクのZ座標です。
    1. 「net.minecraft.world.chunk.ChunkPrimer」をインスタンス化します。
    2. 上のWorld、ChunkPrimerを使って「net.minecraft.world.chunk.Chunk」をインスタンス化します。
    3. Chunk#setBlockStateなどを使って、上のChunkに地形を生成します。
    4. Chunkにバイオームを割り当てます。
      • net.minecraft.world.chunk.ChunkProviderHellの手法
      1. Chunk#getBiomeArrayの戻り値としてバイオーム割り当てに使用するバイオームIDのbyte配列を得ます。
      2. 上のWorldからBiomeProviderを取得し、BiomeProvider#getBiomesから、チャンク座標に対応するバイオームのBiome配列を得ます。
      3. byte配列のindexに対応するバイオームIDをBiome配列から取り出して代入します(配列はオブジェクトなので、Chunkが持つ配列が変更されます)。
メソッド名 操作 参考
provideChunk チャンクのインスタンス化、地形生成
populate 湖、鉱石、ダンジョン等の生成
generateStructures 構造物の追加生成(海底遺跡で使用)
getPossibleCreatures EntityCreatureの自然スポーン設定
  • 通常スポーン
    • Biome#getSpawnableListから取得
  • ダンジョン内等の特殊スポーン
    • 好きなリストを返す
  • net.minecraft.world.WorldServer#getSpawnListEntryForTypeAt
  • net.minecraft.world.WorldEntitySpawner
getStrongholdGen ダンジョンを生成するブロック座標を返す。なければnull。 オーバーワールドの要塞を見つけるエンダーアイで使用。
  • net.minecraft.world.WorldServer#findNearestStructure
  • net.minecraft.item.ItemEnderEye
recreateStructures ダンジョン等MapGenBaseのサブクラスのインスタンスがあればMapGenBase#generateを呼ぶ。

チャンクをファイルから読み込む時に呼ばれる模様。ブロック生成以外の構造物のデータを読み込む?

  • net.minecraft.world.gen.ChunkProviderServer#loadChunkFromFile
  • net.minecraftforge.common.chunkio.ChunkIOProvider#syncCallback
  • net.minecraft.world.gen.MapGenBase#generate
  • net.minecraft.world.gen.structure#MapGenStructure#recursiveGenerate


ポータルを生成する

Teleporter

「net.minecraft.world.Teleporter」は移動するEntityの現在座標をもとにポータルを生成し、ポータル位置にEntityを移動させるクラスです。

EntityがDimension移動するのに使われる「net.minecraft.entity.Entity#changeDimension」では、「net.minecraft.world.WorldServer#getDefaultTeleporter」を呼び出し、Teleporter型の「WorldServer#worldTeleporter」を得ています。

また、Entityがエンド以外からネザーへ転送される時はTeleporter呼び出し前に座標が1/8倍され、エンド以外からオーバーワールドへ転送されるときは8倍されます(PlayerList#transferEntityToWorldではForgeによりデッドコード化されているため、座標は変更されない)。

  1. 「Teleporter」のサブクラスを作成します。コンストラクタで目的地のWorldServerを受け取ってください。
  2. 「makePortal」、「placeInExistingPortal」を再実装します。「placeInPortal」はエンドへの転送時の足場生成を変更する場合のみ、再実装するとよいでしょう。
メソッド名 操作
makePortal バニラでは、ネザーポータルの生成位置決定と生成を行う。
placeInPortal PlayerList#transferEntityToWorldから呼ばれる。バニラでは、エンド以外への転送の場合、placeInExistingPortalを呼び出し、falseが返されたらmakePortalを呼び、もう一度placeInExistingPortalを呼び出している。つまり、ポータルの存在の有無によってmakePortalが呼ばれるかどうかが変わり、makePortalで適切にポータルを生成していれば、既存のポータルまたは新しいポータルで転送できる。エンドへの転送の場合、足場となる黒曜石を生成している。
placeInExistingPortal Entity#changeDimensionから呼ばれる。バニラでは、既存の「ポータルブロック」(Blocks.PORTAL)を探索し、見つからない場合falseを返す。見つかった場合、Entityをポータル座標に移動させてtrueを返す。また、キャッシュされていないポータル(座標)のキャッシュもここで行っている。
removeStalePortalLocations worldTimeをもとに、古いポータル(座標)のキャッシュを削除する。

エンティティを転送する

Entityの別のディメンションへの転送方法について解説します。バニラ+Forge環境で利用可能な転送用メソッドは二種類あります。

一つ目の「net.minecraft.entity.Entity#changeDimension」は、ネザーポータル、エンドポータルによるディメンション間移動に使用されています。これは、まずEntity#dimensionを変更したのち、出発地のディメンションに対応するWorldServerからEntityを削除、座標を変更してTeleporterを呼び出し、目的地のディメンションに対応するWorldServerインスタンスを渡してEntityのクローンを作成し、クローンを目的地に強制スポーンさせることでディメンション間移動を実現しています。

クローンのEntity#dimensionはコンストラクタでWorldServerから取得されることになり、最初の代入は無駄にも思えますが、ポータルを生成するTeleporterに渡されるのは最初の(出発地側の)Entityであるため、Teleporter内ではコンストラクタに渡される目的地のWorldServerのディメンションIDとEntity#dimensionは一致することになります(バニラでは転送先はWorldServerから取って判定しているのだが)。

二つ目の「net.minecraft.server.management.PlayerList#transferEntityToWorld」は、「net.minecraft.entity.player.EntityPlayerMP」で再実装された「#changeDimension」から「PlayerList#changePlayerDimension」を経由して呼ばれています。

バニラの「PlayerList#changePlayerDimension」は、ForgeによってTeleporter引数を追加したメソッド「#transferPlayerToDimension」に処理を移されています(GitHub/MinecraftForge/PlayerList.java.patch#L151)。「#transferPlayerToDimension」は、出発地のWorldServerからEntityPlayerMPを削除したのち、「#transferEntityToWorld」を呼び出しています。

バニラの「PlayerList#transferEntityToWorld」は、ForgeによってTeleporter引数を追加した同名メソッドに処理を移されています(GitHub/MinecraftForge/PlayerList.java.patch#L179)。

「PlayerList#transferEntityToWorld」のおおよその処理は「Entity#changeDimension」と同じですが、新しくEntityのインスタンスを作り直さず、「Entity#setWorld」を呼んで出発地のWorldに属するEntityインスタンスをそのまま目的地のWorldで再利用します。

また注意すべき点として、「PlayerList#transferEntityToWorld」は、出発地のWorldからEntityを削除しません。自分で出発地のWorldに対して「World#removeEntityDangerously」を呼んで、World及びChunkからEntityを削除する必要があります。

例:「触れるとディメンションを移動するブロック」を追加する

BlockTeleporter.java

package samplemod.block;

import net.minecraft.block.Block;
import net.minecraft.block.material.Material;
import net.minecraft.block.state.IBlockState;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.management.PlayerList;
import net.minecraft.util.math.AxisAlignedBB;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.*;
import samplemod.SampleMod;
import samplemod.world.sample.TeleporterSample;

import javax.annotation.Nullable;

public class BlockTeleporter extends Block {

    public BlockTeleporter() {
        super(Material.ROCK);
    }

    @Nullable
    @Override
    public AxisAlignedBB getCollisionBoundingBox(IBlockState blockState, IBlockAccess worldIn, BlockPos pos) {
        return FULL_BLOCK_AABB.expandXyz(-.05d);
    }

    @Override
    public void onEntityCollidedWithBlock(World worldIn, BlockPos pos, IBlockState state, Entity entityIn) {
        MinecraftServer server = worldIn.getMinecraftServer();
        if (server != null) {
            PlayerList playerList = server.getPlayerList();
            int dest = entityIn.dimension == DimensionType.OVERWORLD.getId() ? SampleMod.SAMPLE_DIMENSION.getId() : DimensionType.OVERWORLD.getId();

            Teleporter teleporter = new TeleporterSample(server.worldServerForDimension(dest));

            if (entityIn instanceof EntityPlayerMP) {
                playerList.transferPlayerToDimension((EntityPlayerMP) entityIn, dest, teleporter);
            }
            else {
                int origin = entityIn.dimension;
                entityIn.dimension = dest;
                worldIn.removeEntityDangerously(entityIn);

                entityIn.isDead = false;

                playerList.transferEntityToWorld(entityIn, origin, server.worldServerForDimension(origin), server.worldServerForDimension(dest), teleporter);
            }
        }
    }
}