Версия(и) Minecraft
1.7.10, 1.12.2
Источник
https://shadowfacts.net/tutorials/forge-modding-112/tile-entities/
Доброго времени суток, уважаемые модмейкеры. В данной статье я предоставляю свой перевод гайда с сайта shadowfacts по работе с TileEntity (1.12.2). Исходный туториал значительно доработан и кроме того адаптирован для версии 1.7.10. Исходники разобранного примера на GitHub: 1.7.10 и 1.12.2. Приятного чтения.

Изначально туториал содержал метку "Перевод", однако оригинальное содержимое сильно переработано, описана синхронизация и дополнительно добавлена информация для версии игры 1.7.10. Всвязи с этим я позволил себе убрать метку, однако ссылку на исходную статью оставляю.

Tile Entities
Это первый туториал из серии статей о TileEntity.

В Майнкрафте класс Block используется для представления не просто единичного блока в мире, а блока как типа. Инстанс (экземпляр) Block содержит свойства для каждого экземпляра вашего блока, существующего в мире. Если мы хотим что бы наш блок содержал уникальные данные для каждого отдельно взятого экземпляра нам нужно использовать TileEntity.

Существует распространённый миф что TileEntity плохо влияет на производительность - это не так. Они могут негативно влиять на производительность если они реализованы не умело, как в прочем и любые другие объекты.

Тайлы бывают двух типов: обновляющиеся (ticking) и не обновляющиеся (non-ticking).
Обновляющиеся тайлы обновляются каждый игровой тик (обычно 20 раз в секунду). Они влияют на производительность интенсивнее и требуют аккуратной реализации. Не обновляющиеся тайлы существуют для простого хранения данных. Ну а теперь подробнее по реализации:

1.7.10

Все создаваемые тайлы обновляются автоматически, что значит метод updateEntity() вызывается каждый тик. Отключить обновление можно переопределив canUpdate() и вернув в нём false (true по умолчанию).

1.12.2

Класс TileEntity лишён реализации автоматического обновления по умолчанию. Если обновление необходимо, то реализуйте интерфейс ITickable, который добавит метод update().

Разделение: клиент и сервер

Сразу отмечу что изменение данных, хранимых в тайле должно происходить только на серверной стороне. Перед их изменением мы должны удостовериться, что действия происходят на сервере. Делаем мы это потому как в Майнкрафте клиент и сервер полностью разделены и некоторые методы вызываются для обеих сторон.

В многопользовательской игре множество клиентов подключены к одному серверу. В этом случае разделение между сторонами очевидно, но в одиночной игре всё немного сложнее. В многопользовательской игре сервер, к которому происходит подключение, отражает физический сервер и все отдельные подключенные клиенты являются физическими клиентами.

В одиночной игре клиент и сервер тоже разделены даже несмотря на то, что они исполняются на одном компьютере (на одной JVM, но в разных потоках). В одиночной игре клиент подключается к локальному, приватному серверу, функции которого схожи с физическим сервером. В этом случае серверный поток отражает логический сервер, а клиентский поток отражает логический клиент, так как обе логические стороны выполняются на одной физической стороне.

Поле World#isRemote используется для проверки стороны, на которой происходит выполнение (будь она логической или физической). Оно равно true для физического клиента в многопользовательской игре и для логического клиента в одиночной игре. Это поле равно false для физического сервера в многопользовательской игре и для логического сервера в одиночной.

Делая проверку в условии !World#isRemote, мы обеспечиваем уверенность что действия будут производится на сервере (физическом или логическом).

NBT формат

Хранение данных TileEntity между сессиями обеспечивается NBT. Данный формат используется для хранения данных в виде пар ключ-значение, которые легко сериализуется в байты и сохраняется на диск. Вы можете ознакомиться с классом NBTTagCompound для представления о типах данных, которые он может хранить. Ванильный код содержит множество хороших примеров по сохранению и чтению сложных структур данных.

1.7.10
В рамках статьи я покажу как создать простой тайл без обновления, предназначенный для простого хранения данных. В конце дополнительно описана организация синхронизации данных с клиентом при их изменении на сервере.

Вспомогательные средства

Прежде чем мы создадим тайл, мы добавим класс, который упростит их создание в будущем.

В первую очередь создадим класс BlockTileEntity:
Java:
public abstract class BlockTileEntity<T extends TileEntity> extends BlockBase {

    public BlockTileEntity(String name, Material material, float hardness, float resistanse, SoundType soundType) {
  
        super(name, material, hardness, resistanse, soundType);
  
        GameRegistry.registerTileEntity(this.getTileEntityClass(), this.getUnlocalizedName());
    }

    public abstract Class<T> getTileEntityClass();

    public T getTileEntity(World world, int xPos, int yPos, int zPos) {
  
        return (T) world.getTileEntity(xPos, yPos, zPos);
    }

    @Override
    public boolean hasTileEntity(int metadata) {
  
        return true;
    }

    @Nullable
    @Override
    public abstract T createTileEntity(World world, int metadata);
}


Этот класс предоставляет нам следующее:
  • Он расширяет BlockBase, содержащий нашу удобную базовую реализацию Block
Java:
public class BlockBase extends Block {

    public BlockBase(String name, Material material, float hardness, float resistanse, SoundType soundType) {
  
        super(material);
  
        this.setBlockName(name);
        this.setHardness(hardness);
        this.setResistance(resistanse);
        this.setStepSound(soundType);
    }
}
  • Содержит обобщение, представляющее наш класс TileEntity. Он будет использован для создания вспомогательных средств для уменьшения количества необходимых приведений типов для получения экземпляра нашего тайла на определённой позиции и для уверенности в том, что созданный тайл имеет корректный тип.
  • Переопределяет hasTileEntity() из Block и возвращает true. Это даёт понять Майнкрафту что у нашего блока есть тайл и его нужно создать.
  • Содежит два абстрактных метода: 1) getTileEntityClass() - тут мы будем возвращать класс нашего TileEntity, что позволит ему быть зарегистрированным вместе с блоком, 2) createTileEntity() - более спицифичная версия стандартного метода из Block. Майнкрафт вызывает его каждый раз когда нужно создать новый экземпляр тайла, например когда мы устанавливаем наш блок.
  • Обеспечивает автоматическую регистрацию тайла при инициализации блока.

Блок

Теперь когда у нас есть удобная основа самое время создать блок.

Создадим BlockCounter, расширяющий BlockTileEntity.
Java:
public class BlockCounter extends BlockTileEntity<TileEntityCounter> {

    public BlockCounter(String name, Material material, float hardness, float resistanse, SoundType soundType) {
  
        super(name, material, hardness, resistanse, soundType);
  
        this.setHarvestLevel("pickaxe", 3);
        this.setBlockTextureName(TilesMain.MODID + ":" + "counter/counter");
    }

    @Override
    public boolean onBlockActivated(World world, int xPos, int yPos, int zPos, EntityPlayer player, int side, float hitX, float hitY, float hitZ) {
  
        if (!world.isRemote) {
      
            TileEntityCounter tileEntity = this.getTileEntity(world, xPos, yPos, zPos);
      
            switch (side) {
      
                case 0://Bottom
                    tileEntity.decrementCount();
                    break;
          
                case 1://Top
                    tileEntity.incrementCount();
                    break;
              
                default:
                    break;
            }
      
            player.addChatMessage(new ChatComponentTranslation("tile.counter.current", tileEntity.getCount()));
        }
  
        return true;
    }

    @Override
    public Class<TileEntityCounter> getTileEntityClass() {
  
        return TileEntityCounter.class;
    }

    @Override
    public TileEntityCounter createTileEntity(World world, int meta) {
  
        return new TileEntityCounter();
    }
}


Наш блок расширяет BlockTileEntity и содержит обобщение TileEntityCounter (который предстоит создать), так как этот тайл принадлежит этому блоку.

В конструкторе мы просто напросто вызываем суперконструктор и передаём туда все параметры, которые будем указывать при создании блока.

В методе getTileEntityClass() вернём TileEntityCounter.class (мы ещё создадим его).
Это позволит зарегистрировать его ассоциировав с именем блока.

В createTileEntity() мы возвращаем новый экземпляр класса TileEntityCounter.

Ну и в конце концов в onBlockActivated(), вызываемый при правом клике по блоку мы делаем следующее:
  • Проверяем, что действия производятся на сервере (это очень важно при работе с TileEntity!).
  • Достаём экземпляр TileEntityCounter.
  • Если игрок кликнул по нижней стороне - уменьшаем значение счётчика.
  • Если клик по верхней стороне - увеличиваем значение.
  • Отправляем сообщение в чат, содержащие значение счётчика.

Создание тайла для блока

Теперь когда у нас есть блок, мы должны создать тайл для него.

Создадим класс TileEntityCounter:
Java:
public class TileEntityCounter extends TileEntity {

    private int count;

    public int getCount() {
  
        return this.count;
    }

    public void incrementCount() {
  
        this.count++;
  
        this.markDirty();
    }

    public void decrementCount() {
  
        this.count--;
  
        this.markDirty();
    }

    @Override
    public boolean canUpdate() {
  
        return false;
    }

    @Override
    public void writeToNBT(NBTTagCompound tagCompound) {
  
        tagCompound.setInteger("count", this.count);
  
        super.writeToNBT(tagCompound);
    }

    @Override
    public void readFromNBT(NBTTagCompound tagCompound) {
  
        this.count = tagCompound.getInteger("count");
  
        super.readFromNBT(tagCompound);
    }
}


Содержимое весьма нехитрое:
  • Класс расширяет стандартный TileEntity
  • Содержит приватное поле count, содержащее значение счётчика.
  • Переопределяет writeToNBT() и readFromNBT() что бы наши данные были сохранены и загружены с диска.
  • Предоставляет методы getCount(), incrementCount(), decrementCount() для работы с полем count.
Кроме того, в методах, изменяющих значение счётчика (count) происходит вызов markDirty() из TileEntity. Он даёт игре понять что данные были изменены с момента последнего сохранения и их нужно перезаписать.

В нашем случае мы сохраняем значение поля count как целочисленный тип Integer с ключём в виде строки "count" в NBT в методе writeToNBT() и читаем его в readFromNBT().

Регистрация

Тайл нашего блока будет зарегистрирован автоматически при инициализации самого блока. Блок регистрируем в отдельном классе для удобства. Свой класс для блоков я назвал BlocksRegistry:
Java:
public class BlocksRegistry {

    public static final Block
    COUNTER = new BlockCounter("counter", Material.rock, 5.0F, 5.0F, Block.soundTypeStone).setCreativeTab(TilesMain.TILES);
  
    public static final Block[] BLOCKS = new Block[] {
          
            COUNTER
    };
  
    public static void register() {
      
        registerBlocks(BLOCKS);
    }
  
    private static void registerBlocks(Block... blocks) {
      
        for (int i = 0; i < blocks.length; i++) {

            GameRegistry.registerBlock(blocks[i], blocks[i].getUnlocalizedName());
        }
    }
}


Вам нужно только инициализировать блок и добавить его в массив. Не забудьте указать нужную креативную вкладку для блока (я указал свою).

В главном классе вашего мода нужно вызвать метод BlocksRegistry#register() во время преинициализации.
Java:
    @EventHandler
    public void preInit(FMLPreInitializationEvent event) {         
      
        BlocksRegistry.register();
    }


Финал

Теперь, когда все сделано, запускаем игру и достаем наш блок-счётчик из вкладки "блоки".
Установив его, кликните ПКМ по верхней стороне блока - счётчик увеличит значение и в чате появится сообщение. Приписка "Server" добавлена специально, так как сообщение отправляется с сервера. Клиент тем временем понятия не имеет о значении счётчика.
2018-03-30_18.35.15.png

Нажав ПКМ по нижней стороне несколько раз вы увидите это:
2018-03-30_18.35.27.png

Клик ПКМ по другим сторонам выведет в чат сообщение с текущим значением счётчика.

Синхронизация

Зачастую игровому клиенту требуется иметь информацию, которая хранится в тайле. TileEntity содержит встроенную систему синхронизации. Чтобы синхронизировать данные тайла требуется использовать пакетную систему. Конечно можно использовать свою собственную, но тайлы уже содержат реализованную ванильную систему синхронизации.

Что бы воспользоваться ей, требуется переопределить несколько методов:
Java:
    @Override
     public Packet getDescriptionPacket() {
  
        NBTTagCompound tagCompound = new NBTTagCompound();
  
        this.writeToNBT(tagCompound);//Можно записать всё, или отдельную информацию.
  
        return new S35PacketUpdateTileEntity(this.xCoord, this.yCoord, this.zCoord, 3, tagCompound);
     }
    
     @Override
     public void onDataPacket(NetworkManager networkManager, S35PacketUpdateTileEntity packet) {
   
         NBTTagCompound tagCompound = packet.func_148857_g();
   
         this.readFromNBT(tagCompound);
     }


Это обеспечит синхронизацию при изменении данных. Для демонстрации я создал версию счётчика, который запоминает имя владельца и выводит его в чат на клиентской стороне при взаимодействии с блоком. Его вы сможете найти в моём репозитории, смотрите классы BlockCounterPersonal и TileEntityCounterPersonal.

Ну вот и всё.

1.12.2
В рамках статьи я покажу как создать простой тайл без обновления, предназначенный для простого хранения данных. В конце дополнительно описана организация синхронизации данных с клиентом при их изменении на сервере.

Вспомогательные средства

Прежде чем мы создадим тайл, мы добавим класс, который упростит их создание в будущем.

В первую очередь создадим класс BlockTileEntity:
Java:
public abstract class BlockTileEntity<T extends TileEntity> extends BlockBase {

    public BlockTileEntity(String name, Material material, float hardness, float resistanse, SoundType soundType) {

        super(name, material, hardness, resistanse, soundType);

        GameRegistry.registerTileEntity(this.getTileEntityClass(), this.getRegistryName().toString());
    }

    public abstract Class<T> getTileEntityClass();

    public T getTileEntity(IBlockAccess world, BlockPos position) {

        return (T) world.getTileEntity(position);
    }

    @Override
    public boolean hasTileEntity(IBlockState blockState) {

        return true;
    }

    @Nullable
    @Override
    public abstract T createTileEntity(World world, IBlockState blockState);
}


Этот класс предоставляет нам следующее:
  • Он расширяет BlockBase, содержащий нашу удобную базовую реализацию Block
Java:
public class BlockBase extends Block {

    public BlockBase(String name, Material material, float hardness, float resistanse, SoundType soundType) {

        super(material);

        this.setRegistryName(name);
        this.setUnlocalizedName(name);
        this.setHardness(hardness);
        this.setResistance(resistanse);
        this.setSoundType(soundType);
    }
}
  • Содержит обобщение, представляющее наш класс TileEntity. Он будет использован для создания вспомогательных средств для уменьшения количества необходимых приведений типов для получения экземпляра нашего тайла на определённой позиции и для уверенности в том, что созданный тайл имеет корректный тип.
  • Переопределяет hasTileEntity() из Block и возвращает true. Это даёт понять Майнкрафту что у нашего блока есть тайл и его нужно создать.
  • Содежит два абстрактных метода: 1) getTileEntityClass() - тут мы будем возвращать класс нашего TileEntity, что позволит ему быть зарегистрированным вместе с блоком, 2) createTileEntity() - более спицифичная версия стандартного метода из Block. Майнкрафт вызывает его каждый раз когда нужно создать новый экземпляр тайла, например когда мы устанавливаем наш блок.
  • Обеспечивает автоматическую регистрацию тайла при инициализации блока.

Блок

Теперь когда у нас есть удобная основа самое время создать блок.

Создадим BlockCounter, расширяющий BlockTileEntity.
Java:
public class BlockCounter extends BlockTileEntity<TileEntityCounter> {

    public BlockCounter(String name, Material material, float hardness, float resistanse, SoundType soundType) {

        super(name, material, hardness, resistanse, soundType);

        this.setHarvestLevel("pickaxe", 3);       
    }

    @Override
    public boolean onBlockActivated(World world, BlockPos position, IBlockState blockState, EntityPlayer player, EnumHand hand, EnumFacing side, float hitX, float hitY, float hitZ) {

        if (!world.isRemote) {
    
            TileEntityCounter tileEntity = this.getTileEntity(world, position);
    
            switch (side) {
    
                case DOWN:
                    tileEntity.decrementCount();
                    break;
            
                case UP:
                    tileEntity.incrementCount();
                    break;
            
                default:
                    break;
            }
    
            player.sendMessage(new TextComponentTranslation("tile.counter.current", tileEntity.getCount()));
        }

        return true;
    }

    @Override
    public Class<TileEntityCounter> getTileEntityClass() {

        return TileEntityCounter.class;
    }

    @Override
    public TileEntityCounter createTileEntity(World world, IBlockState blockState) {

        return new TileEntityCounter();
    }
}


Наш блок расширяет BlockTileEntity и содержит обобщение TileEntityCounter (который предстоит создать), так как этот тайл принадлежит этому блоку.

В конструкторе мы просто напросто вызываем суперконструктор и передаём туда все параметры, которые будем указывать при создании блока.

В методе getTileEntityClass() вернём TileEntityCounter.class (мы ещё создадим его).
Это позволит зарегистрировать его ассоциировав с именем блока.

В createTileEntity() мы возвращаем новый экземпляр класса TileEntityCounter.

Ну и в конце концов в onBlockActivated(), вызываемый при правом клике по блоку мы делаем следующее:
  • Проверяем, что действия производятся на сервере (это очень важно при работе с TileEntity!).
  • Достаём экземпляр TileEntityCounter.
  • Если игрок кликнул по нижней стороне - уменьшаем значение счётчика.
  • Если клик по верхней стороне - увеличиваем значение.
  • Отправляем сообщение в чат, содержащие значение счётчика.

Создание тайла для блока

Теперь когда у нас есть блок, мы должны создать тайл для него.

Создадим класс TileEntityCounter:
Java:
public class TileEntityCounter extends TileEntity {

    private int count;

    public int getCount() {

        return this.count;
    }

    public void incrementCount() {

        this.count++;

        this.markDirty();
    }

    public void decrementCount() {

        this.count--;

        this.markDirty();
    }

    @Override
    public NBTTagCompound writeToNBT(NBTTagCompound tagCompound) {

        tagCompound.setInteger("count", this.count);

        return super.writeToNBT(tagCompound);
    }

    @Override
    public void readFromNBT(NBTTagCompound tagCompound) {

        this.count = tagCompound.getInteger("count");

        super.readFromNBT(tagCompound);
    }
}


Содержимое весьма нехитрое:
  • Класс расширяет стандартный TileEntity
  • Содержит приватное поле count, содержащее значение счётчика.
  • Переопределяет writeToNBT() и readFromNBT() что бы наши данные были сохранены и загружены с диска.
  • Предоставляет методы getCount(), incrementCount(), decrementCount() для работы с полем count.
Кроме того, в методах, изменяющих значение счётчика (count) происходит вызов markDirty() из TileEntity. Он даёт игре понять что данные были изменены с момента последнего сохранения и их нужно перезаписать.

В нашем случае мы сохраняем значение поля count как целочисленный тип Integer с ключём в виде строки "count" в NBT в методе writeToNBT() и читаем его в readFromNBT().

Регистрация

Тайл нашего блока будет зарегистрирован автоматически при инициализации самого блока. Блок регистрируем в отдельном классе BlocksRegistry:
Java:
@Mod.EventBusSubscriber(modid = TilesMain.MODID)
public class BlocksRegistry {

    public static final Block
    COUNTER = new BlockCounter("counter", Material.ROCK, 5.0F, 5.0F, SoundType.STONE).setCreativeTab(TilesMain.TILES);

    public static final Block[] BLOCKS = new Block[] {
    
            COUNTER
    };

    @SubscribeEvent
    public static void registerBlocks(RegistryEvent.Register<Block> event) {

        event.getRegistry().registerAll(BLOCKS);
    }

    @SubscribeEvent
    public static void registerItemBlocks(RegistryEvent.Register<Item> event) {

        event.getRegistry().registerAll(getItemBlocks(BLOCKS));
    }

    @SubscribeEvent
    @SideOnly(Side.CLIENT)
    public static void registerModels(ModelRegistryEvent event) {

        setRenderForAll(BLOCKS);
    }

    private static Item[] getItemBlocks(Block... blocks) {

        Item[] items = new Item[blocks.length];

        for (int i = 0; i < blocks.length; i++) {
    
            items[i] = new ItemBlock(blocks[i]).setRegistryName(blocks[i].getRegistryName());
        }

        return items;
    }

    @SideOnly(Side.CLIENT)
    private static void setRenderForAll(Block... blocks) {

        for (int i = 0; i < blocks.length; i++) {
    
            ModelLoader.setCustomModelResourceLocation(Item.getItemFromBlock(blocks[i]), 0, new ModelResourceLocation(blocks[i].getRegistryName(), "inventory"));
        }
    }
}


Вам нужно только инициализировать блок и добавить его в массив, об остальном класс позаботится.
Не забудьте указать нужную креативную вкладку для блока (я указал свою). Эвенты регистрации Forge будут загружены вместе с модом блогодаря аннотации @Mod.EventBusSubscriber(modid = Main.MODID)

Финал

Теперь, когда все сделано, запускаем игру и достаем наш блок-счётчик из вкладки "блоки".
Установив его, кликните ПКМ по верхней стороне блока - счётчик увеличит значение и в чате появится сообщение. Приписка "Server" добавлена специально, так как сообщение отправляется с сервера. Клиент тем временем понятия не имеет о значении счётчика.
2018-03-30_17.26.54.png

Нажав ПКМ по нижней стороне несколько раз вы увидите это:
2018-03-30_17.27.59.png

Клик ПКМ по другим сторонам выведет в чат сообщение с текущим значением счётчика.

Синхронизация

Зачастую игровому клиенту требуется иметь информацию, которая хранится в тайле. TileEntity содержит встроенную систему синхронизации. Чтобы синхронизировать данные тайла требуется использовать пакетную систему. Конечно можно использовать свою собственную, но тайлы уже содержат реализованную ванильную систему синхронизации.

Что бы воспользоваться ей, требуется переопределить несколько методов:
Java:
    @Override
    @Nullable
    public SPacketUpdateTileEntity getUpdatePacket() {
 
        return new SPacketUpdateTileEntity(this.pos, 3, this.getUpdateTag());
    }

    @Override
    public NBTTagCompound getUpdateTag() {
 
        return this.writeToNBT(new NBTTagCompound());//Запись данных для синхронизации, можно выбрать нужные либо всё (как тут).
    }

    @Override
    public void onDataPacket(NetworkManager networkManager, SPacketUpdateTileEntity packet) {
 
        super.onDataPacket(networkManager, packet);
 
        this.handleUpdateTag(packet.getNbtCompound());
    }


Это обеспечит синхронизацию при изменении данных. Для демонстрации я создал версию счётчика, который запоминает имя владельца и выводит его в чат на клиентской стороне при взаимодействии с блоком. Его вы сможете найти в моём репозитории, смотрите классы BlockCounterPersonal и TileEntityCounterPersonal.

На этом всё, если есть вопросы - не стесняйтесь и задавайте их в обсуждении.
Автор
AustereTony
Просмотры
6,553
Первый выпуск
Обновление
Оценка
5.00 звёзд 6 оценок

Другие ресурсы пользователя AustereTony

Последние обновления

  1. Обновление #1

    Туториал (перевод) переработан и дописан для 1.7.10.

Последние рецензии

Благодарю за отличный перевод/туториал! Все чрезвычайно понятно и разжевано до мелочей.
Спасибо, теперь я понимаю как это работает xD
Отличный перевод, разобрался!
Отличный перевод, я сам пользуюсь тем самым сайтом, а теперь намного проще без гугл переводчика
Очень хорошое оформление, да и использование обобщений возможно натолкнет новичков на изучение базы программирования)
Отличный перевод. Даже я разобрался)
Сверху