提供: Minecraft Modding Wiki
移動先: 案内検索

この記事は"Minecraft Forge4.3x"を前提MODとしています。

TileEntityのNBT同期

 1.3以降, TileEntityのNBTはサーバーとクライアントで同期しなければならなくなった. これは1.2.5までのSMPの挙動そのものであり, これを解決するにはサーバーからクライアントにNBTの情報をパケットとして送信し, 同期を取る必要がある.

 なお, チェストのようにGUIとコンテナ(Container)を使い, スロットを経由するもの(すなわちアイテム)はコンテナのスーパークラスで同期処理が行われている. よって今回のサンプルではブロックの向きをNBTに保存し, 同期取りを行う.

ソースコード

  • TileEntitySampleCore.java
package mods.tileentitysample;

import net.minecraft.src.*;

import cpw.mods.fml.common.SidedProxy;

import cpw.mods.fml.common.Mod;

import cpw.mods.fml.common.network.NetworkMod;
import cpw.mods.fml.common.event.FMLInitializationEvent;

import cpw.mods.fml.common.registry.GameRegistry;
import cpw.mods.fml.common.registry.LanguageRegistry;

@Mod
(
	modid   = "TileEntitySampleCore",
	name    = "TileEntity Sample Core",
	version = "1.0.0"
)
@NetworkMod
(
	clientSideRequired = true,
	serverSideRequired = false,
	channels = "TileEntity",
	packetHandler = PacketHandler.class
)
public class TileEntitySampleCore
{
	@SidedProxy(clientSide = "mods.tileentitysample.client.ClientProxy", serverSide = "mods.tileentitysample.CommonProxy")
	public static CommonProxy proxy;
	
	public static Block blockTileEntityNoop;
	
	@Mod.Init
	public void init(FMLInitializationEvent event)
	{
		blockTileEntityNoop = (new BlockTileEntityNoop(3999, 0)).setBlockName("tileentitysample");
		GameRegistry.registerBlock(blockTileEntityNoop);
		
		GameRegistry.registerTileEntity(TileEntityNoop.class, "TileEntityNoop");
		
		LanguageRegistry.addName(blockTileEntityNoop, "TileEntity Noop Block");
		
	}
}
  • BlockTileEntityNoop.java
package mods.tileentitysample;

import net.minecraft.src.*;

public class BlockTileEntityNoop extends BlockContainer
{
	public BlockTileEntityNoop(int blockId, int terrainId)
	{
		super(blockId, Material.rock);
		this.blockIndexInTexture = terrainId;
		this.setCreativeTab(CreativeTabs.tabDecorations);
	}
	
	@Override
	public int getBlockTexture(IBlockAccess world, int x, int y, int z, int side)
	{
		TileEntityNoop tileEntityNoop = (TileEntityNoop)world.getBlockTileEntity(x, y, z);
		if (side == 1)
		{
			return  62;
		}
		if (side == 0)
		{
			return  62;
		}
		int facing = tileEntityNoop.getFacing();
		if (side != facing)
		{
			return  45;
		}
		else
		{
			return  44;
		}
	}
	
	@Override
	public int getBlockTextureFromSide(int side)
	{
		if (side == 1)
		{
			return  62;
		}
		else if (side == 0)
		{
			return  62;
		}
		else if (side == 4)
		{
			return  44;
		}
		else
		{
			return  45;
		}
	}
	
	@Override
	public boolean isOpaqueCube()
	{
		return false;
	}
	
	@Override
	public boolean renderAsNormalBlock()
	{
		return false;
	}
	
	@Override
	public TileEntity createNewTileEntity(World world)
	{
		return new TileEntityNoop();
	}
	
	@Override
	public void onBlockAdded(World world, int x, int y, int z)
	{
		super.onBlockAdded(world, x, y, z);
		this.setDefaultDirection(world, x, y, z);
	}
	
	private void setDefaultDirection(World world, int x, int y, int z)
	{
		TileEntityNoop tileEntityNoop = (TileEntityNoop)world.getBlockTileEntity(x, y, z);
		
		if (!world.isRemote)
		{
			int var5 = world.getBlockId(x, y, z - 1);
			int var6 = world.getBlockId(x, y, z + 1);
			int var7 = world.getBlockId(x - 1, y, z);
			int var8 = world.getBlockId(x + 1, y, z);
			byte var9 = 3;
			
			if (Block.opaqueCubeLookup[var5] && !Block.opaqueCubeLookup[var6])
			{
				var9 = 3;
			}
			
			if (Block.opaqueCubeLookup[var6] && !Block.opaqueCubeLookup[var5])
			{
				var9 = 2;
			}
			
			if (Block.opaqueCubeLookup[var7] && !Block.opaqueCubeLookup[var8])
			{
				var9 = 5;
			}
			
			if (Block.opaqueCubeLookup[var8] && !Block.opaqueCubeLookup[var7])
			{
				var9 = 4;
			}
			
			tileEntityNoop.setFacing(var9);
		}
	}

	@Override
	public void onBlockPlacedBy(World world, int x, int y, int z, EntityLiving entityliving)
	{
		int playerFacing = MathHelper.floor_double((double)((entityliving.rotationYaw * 4F) / 360F) + 0.5D) & 3;
		
		byte facing = 0;
		if (playerFacing == 0)
		{
			facing = 2;
		}
		if (playerFacing == 1)
		{
			facing = 5;
		}
		if (playerFacing == 2)
		{
			facing = 3;
		}
		if (playerFacing == 3)
		{
			facing = 4;
		}
		
		TileEntity tileEntity = world.getBlockTileEntity(x, y, z);
		if (tileEntity != null && tileEntity instanceof TileEntityNoop)
		{
			((TileEntityNoop)tileEntity).setFacing(facing);
			world.markBlockNeedsUpdate(x, y, z);
		}
	}
	
}
  • TileEntityNoop.java
package mods.tileentitysample;

import net.minecraft.src.*;

public class TileEntityNoop extends TileEntity
{
	private byte facing;
	
	public byte getFacing()
	{
		return this.facing;
	}
	
	public void setFacing(byte facing)
	{
		this.facing = facing;
	}
	
	@Override
	public void readFromNBT(NBTTagCompound nbttagcompound)
	{
		super.readFromNBT(nbttagcompound);
		facing = nbttagcompound.getByte("facing");
	}
	
	@Override
	public void writeToNBT(NBTTagCompound nbttagcompound)
	{
		super.writeToNBT(nbttagcompound);
		nbttagcompound.setByte("facing", facing);
	}
	
	@Override
	public Packet getDescriptionPacket()
	{
		return PacketHandler.getPacket(this);
	}
	
}
  • CommonProxy.java
package mods.tileentitysample;

import net.minecraft.src.*;

public class CommonProxy
{
	public World getClientWorld()
	{
		return null;
	}
}
  • PacketHandler.java
package mods.tileentitysample;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;

import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteStreams;

import net.minecraft.src.*;

import cpw.mods.fml.common.network.Player;
import cpw.mods.fml.common.network.IPacketHandler;

public class PacketHandler implements IPacketHandler
{
	@Override
	public void onPacketData(NetworkManager network, Packet250CustomPayload packet, Player player)
	{
		if (packet.channel.equals("TileEntity"))
		{
			ByteArrayDataInput data = ByteStreams.newDataInput(packet.data);
			int x, y, z;
			byte facing;
			try
			{
				x = data.readInt();
				y = data.readInt();
				z = data.readInt();

				facing = data.readByte();
				
				World world = TileEntitySampleCore.proxy.getClientWorld();
				TileEntity tileEntity = world.getBlockTileEntity(x, y, z);
				
				if (tileEntity instanceof TileEntityNoop)
				{
					TileEntityNoop tileEntityNoop = (TileEntityNoop)tileEntity;
					tileEntityNoop.setFacing(facing);
				}
			} 
			catch (Exception e)
			{
				e.printStackTrace();
			}
		}
	}
	
	public static Packet getPacket(TileEntityNoop tileEntityNoop)
	{
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		DataOutputStream dos = new DataOutputStream(bos);
		
		int x = tileEntityNoop.xCoord;
		int y = tileEntityNoop.yCoord;
		int z = tileEntityNoop.zCoord;
		byte facing = tileEntityNoop.getFacing();
		
		try
		{
			dos.writeInt(x);
			dos.writeInt(y);
			dos.writeInt(z);
			dos.writeByte(facing);
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
		
		Packet250CustomPayload packet = new Packet250CustomPayload();
		packet.channel = "TileEntity";
		packet.data    = bos.toByteArray();
		packet.length  = bos.size();
		packet.isChunkDataPacket = true;
		
		return packet;
	}
}
  • ClientProxy.java
package mods.tileentitysample.client;

import net.minecraft.src.*;

import cpw.mods.fml.client.FMLClientHandler;

import cpw.mods.fml.common.Side;
import cpw.mods.fml.common.asm.SideOnly;

import mods.tileentitysample.CommonProxy;

@SideOnly(Side.CLIENT)
public class ClientProxy extends CommonProxy
{
	@Override
	public World getClientWorld()
	{
		return FMLClientHandler.instance().getClient().theWorld;
	}
}

解説

 TileEntity自体の説明や, ブロックの向きに関する説明はここでは省略する. これらの解説はカスタムパケットとは関係がない.

TileEntitySampleCore

@NetworkMod
(
	clientSideRequired = true,
	serverSideRequired = false,
	channels = "TileEntity",
	packetHandler = PacketHandler.class
)

channelsはパケットで使うチャンネル名, packetHandlerは後述するIPacketHandlerを実装したクラス. この指定がないとカスタムパケットは使われない.

TileEntityNoop

@Override
public Packet getDescriptionPacket()
{
	return PacketHandler.getPacket(this);
}

適切なタイミングで呼ばれ, パケットを返すメソッド. このとき返すパケットはサーバーからクライアントへのパケットである. 今回は後述するPacketHandlerクラスでカスタムパケットの生成を行っている. なお適切なタイミングとはワールド保存時, ワールドログイン時, プレイヤーが右クリックしたときなどである.

PacketHandler

カスタムパケットの読み込み

public class PacketHandler implements IPacketHandler

カスタムパケットを読み込むためのハンドラを作るために, IPacketHandlerインタフェースを実装する.

@Override
public void onPacketData(NetworkManager network, Packet250CustomPayload packet, Player player)
{
	if (packet.channel.equals("TileEntity"))
	{
		ByteArrayDataInput data = ByteStreams.newDataInput(packet.data);
		int x, y, z;
		byte facing;
		try
		{
			x = data.readInt();
			y = data.readInt();
			z = data.readInt();

			facing = data.readByte();
			
			World world = TileEntitySampleCore.proxy.getClientWorld();
			TileEntity tileEntity = world.getBlockTileEntity(x, y, z);
			
			if (tileEntity instanceof TileEntityNoop)
			{
				TileEntityNoop tileEntityNoop = (TileEntityNoop)tileEntity;
				tileEntityNoop.setFacing(facing);
			}
		} 
		catch (Exception e)
		{
			e.printStackTrace();
		}
	}
}

IPacketHandlerで実装するメソッドの実装例. 引数のPacket250CustomPayload packetが送られてくるパケット. 詳細解説は後述.

if (packet.channel.equals("TileEntity"))

@NetworkModで指定したチャンネルのチェック. 複数のパケットを使う場合, このチャンネルを増やしてパケットを判別してもよいし, パケット自体にパケットインデックスなどをつけてもよい.

ByteArrayDataInput data = ByteStreams.newDataInput(packet.data);

Packet.dataはbyte配列なので, そのままではint型などは扱いづらい. そこでDataInputStreamかByteArrayDataInputを使ってバイトストリームとして扱う.

x = data.readInt();
y = data.readInt();
z = data.readInt();

facing = data.readByte();

x, y, zはworldからTileEntityを取得するために必要な情報. 今回同期するデータは一番下のfacing.

World world = TileEntitySampleCore.proxy.getClientWorld();
TileEntity tileEntity = world.getBlockTileEntity(x, y, z);

if (tileEntity instanceof TileEntityNoop)
{
	TileEntityNoop tileEntityNoop = (TileEntityNoop)tileEntity;
	tileEntityNoop.setFacing(facing);
}

プロキシを利用してクライアントのワールドを取得し, 該当座標にあるTileEntityを取得する. 取得したTileEntityのセッタに同期したいデータを渡してデータの同期を行っている. なおここではnullチェックを省いている.

カスタムパケットの生成

public static Packet getPacket(TileEntityNoop tileEntityNoop)
{
	ByteArrayOutputStream bos = new ByteArrayOutputStream();
	DataOutputStream dos = new DataOutputStream(bos);
	
	int x = tileEntityNoop.xCoord;
	int y = tileEntityNoop.yCoord;
	int z = tileEntityNoop.zCoord;
	byte facing = tileEntityNoop.getFacing();
	
	try
	{
		dos.writeInt(x);
		dos.writeInt(y);
		dos.writeInt(z);
		dos.writeByte(facing);
	}
	catch (Exception e)
	{
		e.printStackTrace();
	}
	
	Packet250CustomPayload packet = new Packet250CustomPayload();
	packet.channel = "TileEntity";
	packet.data    = bos.toByteArray();
	packet.length  = bos.size();
	packet.isChunkDataPacket = true;
	
	return packet;
}

引数のTileEntityで同期したいデータのパケットを生成するメソッド. 使うパケットはPacket250CustomPayload.

ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);

packet.dataに書き込むためのバイト配列と, そのストリームを生成.

dos.writeInt(x);
dos.writeInt(y);
dos.writeInt(z);
dos.writeByte(facing);

バイト配列にデータを書き込む. x, y, zは先述した通りTileEntityの生成に必要なデータ. 同期したいデータ自体はfacing.

Packet250CustomPayload packet = new Packet250CustomPayload();
packet.channel = "TileEntity";
packet.data    = bos.toByteArray();
packet.length  = bos.size();
packet.isChunkDataPacket = true;

return packet;

新しいパケットを生成し, パケットにチャンネルを設定する. このチャンネル名は先述した@NetworkModで指定したもの. dataに先ほど作成したバイト配列を, lengthにそのサイズを設定している.

実際の挙動

 かまどと同じテクスチャの無機能ブロックを追加し, そのブロックの向きを同期しているサンプルである. バニラのかまどはメタデータを利用して向きを保存している. この場合カスタムパケットは必要ないことに注意しよう. カスタムパケットはあくまでバニラで同期されないデータを同期するための処理である.