Блок со своим интерфейсом (1 часть)

Блок со своим интерфейсом (1 часть)

Нет прав для скачивания
Версия(и) Minecraft
1.12.2
Блок со своим интерфейсом
Привет всем. В этом туториале мы разберём создание собственного блока хранения с графическим интерфейсом с 3-мя слотами (после прочтения туториала - вы сможете создавать любое количество слотов в ваших интерфейсах)

1. Создание классов
Для начала создадим необходимые нам классы (в приведенных ниже класса обязательно скопируйте также и импорты, которые я специально оставил). Копируйте в таком порядке, в каком я их здесь расположил, иначе у вас могут возникнуть некоторые ошибки с импортами:
TileEntityStorage.java:
public class TileEntityStorage extends TileEntity
{
    //private ItemStack[] items = new ItemStack[15];
    public InventoryBasic basic;
 
    public TileEntityStorage()
    {
        basic = new InventoryBasic("invEntityStorage", false, 15);
    }

    public int getSizeInventory()
    {
        return basic.getSizeInventory();
    }

    public ItemStack getStackInSlot(int slot)
    {
        return basic.getStackInSlot(slot);
    }

    public ItemStack decrStackSize(int slot, int amount)
    {
        if (basic.getStackInSlot(slot) != null)
        {
            ItemStack itemstack;

            if (basic.getStackInSlot(slot).getCount() == amount)
            {
                itemstack = basic.getStackInSlot(slot);
                itemstack = null;
                markDirty();
                return itemstack;
            }
            else
            {
                ItemStack stack = basic.getStackInSlot(slot);
                itemstack = basic.getStackInSlot(slot).splitStack(amount);
                if (basic.getStackInSlot(slot).getCount() == 0) stack = null;
                markDirty();
                return itemstack;
            }
        } else {
            return null;
        }
    }

    public ItemStack getStackInSlotOnClosing(int slot)
    {
        if (basic.getStackInSlot(slot) != null)
        {
            ItemStack itemstack = basic.getStackInSlot(slot);
            itemstack = null;
            return itemstack;
        } else {
            return null;
        }
    }

    public void setInventorySlotContents(int slot, ItemStack stack)
    {
        ItemStack itemstack = basic.getStackInSlot(slot);
        itemstack = stack;
        if (stack != null && stack.getCount() > getInventoryStackLimit())
        {
            stack.setCount(getInventoryStackLimit());
        }

        markDirty();
    }

    public String getInventoryName()
    {
        return "container.storage";
    }

    public boolean hasCustomInventoryName()
    {
        return false;
    }

    @Override
    public void readFromNBT(NBTTagCompound nbt)
    {
        super.readFromNBT(nbt);
        NBTTagList list = nbt.getTagList("Items", Constants.NBT.TAG_COMPOUND);

        for (int i = 0; i < list.tagCount(); ++i)
        {
            NBTTagCompound comp = list.getCompoundTagAt(i);
            int j = comp.getByte("Slot") & 255;
         
            if (j >= 0 && j < getSizeInventory())
            {
                this.basic.setInventorySlotContents(j, new ItemStack(comp));
            }
        }
    }

    @Override
    public NBTTagCompound writeToNBT(NBTTagCompound nbt)
    {
        super.writeToNBT(nbt);
        NBTTagList list = new NBTTagList();

        for (int i = 0; i < getSizeInventory(); ++i)
        {
            if (getStackInSlot(i) != null)
            {
                NBTTagCompound comp = new NBTTagCompound();
                comp.setByte("Slot", (byte) i);
                basic.getStackInSlot(i).writeToNBT(comp);
                list.appendTag(comp);
            }
        }

        nbt.setTag("Items", list);
        return nbt;
    }

    public int getInventoryStackLimit()
    {
        return 64;
    }

    // Метод, который проверят: является ли тайл энтити нужным нам тайл энтити
    // и если нет, то метод возвращает false
    // если же является - то мы проверяем что расстояние между нашим хранилищем (тайл энтити)
    // не больше/равно 64 - и возвращаем true.
    public boolean isUseableByPlayer(EntityPlayer player)
    {
        return world.getTileEntity(pos) != this ? false : player.getDistanceSq(pos) <= 64.0D;
    }

    public void openInventory() {}

    public void closeInventory() {}

    public boolean isItemValidForSlot(int slot, ItemStack stack)
    {
        return true;
    }
}
BlockStorage.java:
public class BlockStorage extends BlockContainer
{
    private static final String name = "storage";

    private final Random rand = new Random();

    public BlockStorage()
    {
        super(Material.WOOD);
        this.setRegistryName(name);
        this.setUnlocalizedName(name);
        // Помещаем блок во вкладку "Разное"
        this.setCreativeTab(CreativeTabs.MISC);
    }

    @Override
    public boolean onBlockActivated(World worldIn, BlockPos pos, IBlockState state, EntityPlayer playerIn, EnumHand hand, EnumFacing facing, float hitX, float hitY, float hitZ)
    {
        // Проверка на сервер
        if(worldIn.isRemote) return true;

        TileEntity te = worldIn.getTileEntity(pos);
        if (te != null && te instanceof TileEntityStorage)
        {
            playerIn.openGui(Main.INSTANCE, 0, worldIn, pos.getX(), pos.getY(), pos.getZ());
            return true;
        }
        return false;
    }

    @Override
    public void breakBlock(World worldIn, BlockPos pos, IBlockState state)
    {
        // Проверка на сервер
        if (worldIn.isRemote) return;

        ArrayList drops = new ArrayList();

        TileEntity teRaw = worldIn.getTileEntity(pos);

        if (teRaw != null && teRaw instanceof TileEntityStorage)
        {
            TileEntityStorage te = (TileEntityStorage) teRaw;
            for (int i = 0; i < te.getSizeInventory(); i++)
            {
                ItemStack stack = te.getStackInSlot(i);
                if (stack != null) drops.add(stack.copy());
            }
        }
        for (int i = 0; i < drops.size(); i++)
        {
            EntityItem item = new EntityItem(worldIn, pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, (ItemStack) drops.get(i));
            item.setVelocity((rand.nextDouble() - 0.5) * 0.25, rand.nextDouble() * 0.5 * 0.25, (rand.nextDouble() - 0.5) * 0.25);
            worldIn.spawnEntity(item);
            // Обновление выхода редстоуна для компараторов
            // (это если кто-нибудь захочет отследить зависимость силы редстоуна от кол-ва предметов в хранилище)
            worldIn.updateComparatorOutputLevel(pos, this);
        }
    }

    public TileEntity createNewTileEntity(World world, int par2)
    {
        return new TileEntityStorage();
    }
}
ContainerStorage.java:
public class ContainerStorage extends Container
{
    private TileEntityStorage te;
 
    private int slotID = 0;
 
    public ContainerStorage(TileEntityStorage te, EntityPlayer player)
    {
        this.te = te;
     
        addSlotToContainer(new Slot(te.basic, slotID++, 44, 17));
        addSlotToContainer(new Slot(te.basic, slotID++, 80, 35));
        addSlotToContainer(new Slot(te.basic, slotID++, 116, 53));
     
        // Инвентарь
        for (int i = 0; i < 3; i++)
        {
            for (int j = 0; j < 9; j++)
            {
                addSlotToContainer(new Slot(player.inventory, j + i * 9 + 9, 8 + j * 18, 84 + i * 18));
            }
        }
     
        // Хотбар
        for (int i = 0; i < 9; i++)
        {
            addSlotToContainer(new Slot(player.inventory, i, 8 + i * 18, 142));
        }
    }
 
    @Override
    public ItemStack transferStackInSlot(EntityPlayer player, int slotRaw)
    {
        ItemStack stack = ItemStack.EMPTY;
        Slot slot = inventorySlots.get(slotRaw);
     
        if (slot != null && slot.getHasStack())
        {
            ItemStack stackInSlot = slot.getStack();
            stack = stackInSlot.copy();
         
           // С помощью кода ниже мы предотвращаем краш при перетаскивании предметов нажатием Shift
            if (slotRaw < 3)
            {
                if (!mergeItemStack(stackInSlot, 3, inventorySlots.size(), true))
                {
                    return ItemStack.EMPTY;
                }
            }
            else if (!mergeItemStack(stackInSlot, 0, 3, false))
            {
                return ItemStack.EMPTY;
            }
         
            if (stackInSlot.isEmpty())
            {
                slot.putStack(ItemStack.EMPTY);
            }
         
            else
            {
                slot.onSlotChanged();
            }
        }
        return stack;
    }

    @Override
    public boolean canInteractWith(EntityPlayer player)
    {
        return te.isUseableByPlayer(player);
    }
}
GuiHandler.java:
import guteemon.bwioit.block.ContainerStorage;

public class GuiHandler implements IGuiHandler
{
    @Override
    public Object getServerGuiElement(int ID, EntityPlayer player, World world, int x, int y, int z)
    {
        TileEntity te = world.getTileEntity(new BlockPos(x, y, z));

        if (te != null)
        {
            if (ID == 0)
            {
                return new ContainerStorage((TileEntityStorage)te, player);
            }
        }
        return null;
    }

    @Override
    @SideOnly(Side.CLIENT)
    public Object getClientGuiElement(int ID, EntityPlayer player, World world, int x, int y, int z)
    {
        TileEntity te = world.getTileEntity(new BlockPos(x, y, z));

        if (te != null)
        {
            if (ID == 0)
            {
                return new GuiStorage((TileEntityStorage)te, player);
            }
        }
        return null;
    }
}
GuiStorage.java:
import guteemon.bwioit.block.ContainerStorage;

public class GuiStorage extends GuiContainer
{
    // Расположение текстур вашего интерфейса
    private ResourceLocation texture = new ResourceLocation(Main.MODID, "textures/gui/container/mystorage.png");

    private InventoryPlayer inventory;
    private TileEntityStorage te;

    public GuiStorage(TileEntityStorage te, EntityPlayer player)
    {
        super(new ContainerStorage(te, player));
        inventory = player.inventory;
        this.te = te;
    }

    @Override
    protected void drawGuiContainerBackgroundLayer(float par1, int par2, int par3)
    {
        // Рисуем текстуру интерфейса
        Minecraft.getMinecraft().renderEngine.bindTexture(texture);
     
        GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
     
        int x = (width - xSize) / 2;
        int y = (height - ySize) / 2;
     
        drawTexturedModalRect(x, y, 0, 0, xSize, ySize);
    }

    @Override
    protected void drawGuiContainerForegroundLayer(int par1, int par2)
    {
        // Берём название интерфейса (которое выводиться сверху) из lang файлов нашего мода
        fontRenderer.drawString(I18n.format(te.getInventoryName()), (xSize / 2) - (fontRenderer.getStringWidth(I18n.format(te.getInventoryName())) / 2), 6, 4210752, false);
     
        // Берётся название стандартного инвентаря, т.е. если оно изменится, то и у нас тоже
        fontRenderer.drawString(I18n.format(inventory.getName()), 8, ySize - 96 + 2, 4210752);
    }
}

1. Разбор полётов
Итак, классы мы создали, как же теперь нам разобраться - что за что отвечает? Сейчас всё подробно расскажу. Начнём с класса GuiStorage, вот он:
GuiStorage.java:
public class GuiStorage extends GuiContainer
{
    // Расположение текстур вашего интерфейса
    private ResourceLocation texture = new ResourceLocation(Main.MODID, "textures/gui/container/mystorage.png");

    private InventoryPlayer inventory;
    private TileEntityStorage te;

    public GuiStorage(TileEntityStorage te, EntityPlayer player)
    {
        super(new ContainerStorage(te, player));
        inventory = player.inventory;
        this.te = te;
    }

    @Override
    protected void drawGuiContainerBackgroundLayer(float par1, int par2, int par3)
    {
        // Рисуем текстуру интерфейса
        Minecraft.getMinecraft().renderEngine.bindTexture(texture);
     
        GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
     
        int x = (width - xSize) / 2;
        int y = (height - ySize) / 2;
     
        drawTexturedModalRect(x, y, 0, 0, xSize, ySize);
    }

    @Override
    protected void drawGuiContainerForegroundLayer(int par1, int par2)
    {
        // Берём название интерфейса (которое выводиться сверху) из lang файлов нашего мода
        fontRenderer.drawString(I18n.format(te.getInventoryName()), (xSize / 2) - (fontRenderer.getStringWidth(I18n.format(te.getInventoryName())) / 2), 6, 4210752, false);
     
        // Берётся название стандартного инвентаря, т.е. если оно изменится, то и у нас тоже
        fontRenderer.drawString(I18n.format(inventory.getName()), 8, ySize - 96 + 2, 4210752);
    }
}
Для начала нам нужно нарисовать свою текстуру для "фона" интерфейса. Да, его нужно рисовать самому, т.к. на самом деле текстура интерфейса это всего-лишь изображение, а координаты расположения ячеек на нём указываются отдельно в коде в нужных местах на изображении. (При рисовании своей текстуры вы можете воспользоваться шаблоном ванильной текстуры, которую я приложу ниже. Вы можете удалять/добавлять ячейки на ней в каком угодно месте и порядке, особенно это очень легко делать по пикселям, например, в фотошопе)

Ванильный сундук:
shulker_box.png

Итак, когда вы нарисовали текстуру - можно приступать к расположению самих ячеек на ней. Вот класс контейнера, который за это отвечает:
ContainerStorage.java:
public class ContainerStorage extends Container
{
    private TileEntityStorage te;
  
    private int slotID = 0;
  
    public ContainerStorage(TileEntityStorage te, EntityPlayer player)
    {
        this.te = te;
      
        addSlotToContainer(new Slot(te.basic, slotID++, 44, 17));
        addSlotToContainer(new Slot(te.basic, slotID++, 80, 35));
        addSlotToContainer(new Slot(te.basic, slotID++, 116, 53));
      
        // Инвентарь
        for (int i = 0; i < 3; i++)
        {
            for (int j = 0; j < 9; j++)
            {
                addSlotToContainer(new Slot(player.inventory, j + i * 9 + 9, 8 + j * 18, 84 + i * 18));
            }
        }
      
        // Хотбар
        for (int i = 0; i < 9; i++)
        {
            addSlotToContainer(new Slot(player.inventory, i, 8 + i * 18, 142));
        }
    }
  
    @Override
    public ItemStack transferStackInSlot(EntityPlayer player, int slotRaw)
    {
        ItemStack stack = ItemStack.EMPTY;
        Slot slot = inventorySlots.get(slotRaw);
      
        if (slot != null && slot.getHasStack())
        {
            ItemStack stackInSlot = slot.getStack();
            stack = stackInSlot.copy();
          
            // С помощью кода ниже мы предотвращаем краш при перетаскивании предметов нажатием Shift
            if (slotRaw < 3)
            {
                if (!mergeItemStack(stackInSlot, 3, inventorySlots.size(), true))
                {
                    return ItemStack.EMPTY;
                }
            }
            else if (!mergeItemStack(stackInSlot, 0, 3, false))
            {
                return ItemStack.EMPTY;
            }
          
            if (stackInSlot.isEmpty())
            {
                slot.putStack(ItemStack.EMPTY);
            }
          
            else
            {
                slot.onSlotChanged();
            }
        }
        return stack;
    }

    @Override
    public boolean canInteractWith(EntityPlayer player)
    {
        return te.isUseableByPlayer(player);
    }
}
Вам нужно указать места для ячеек (слотов). Если вам необходимо хранилище по типу сундука или выбрасывателя, где ячейки расположени одним прямоугольником/квадратом, вы можете воспользоваться циклом для упрощённого их обозначения:
Java:
// Значение 3 - это высота ячеек (можете изменить на любое другое под вашу текстуру)
for (int i = 0; i < 3; i++)
{
    // Значение 5 - это ширина ячеек
    for (int j = 0; j < 5; j++)
    {
        addSlotToContainer(new Slot(te.basic, slotID++, 44 + j * 18, 17 + i * 18));
    }
}
С помощью цикла мы вызываем addSlotToContainer для каждой ячейки на нашей текстуре интерфейса. Вы спросите: Как же оно подсчитывает расстояние для каждой из них на изображении? Всё просто. Если вы откроете фотошоп, то можно будет линейкой измерить расстояние от одного до другого пикселя.
чтобы сменить режим линейки, нужно нажать Ctrl + R для появления шкалы слева и сверху, после чего нажать ЛКМ на ней и выбрать режим -> по пикселям
Первая ячейка в нашем цикле это левая в верхнем углу, т.к. отсчёт начинается с левой стороны и соответственно с вершины текстуры. Вот как это выглядит:
for1primer.png

Ровно 44 пикселя до первой ячейки. Как вы могли заметить в цикле у нас присутствовало данное значение - оно задаёт начальное расстояние до самого ряда с ячейками.
addSlotToContainer(new Slot(te.basic, slotID++, 44 + j * 18, 17 + i * 18));
Далее каждый раз цикл до определенного числа умножает число 18 (ширина одной ячейки) на числа 0, 1, 2, 3, 4 (Думаю не стоит объяснять работу цикла) и складывает с расстоянием до первой ячейки. Таким образом, каждая ячейка добавляется в наш контейнер поочерёдно.
Например, для второй ячейки наш цикл применяет вот эту формулу 44 + 1 * 18
результат которой соответствует этому размеру до ячейки:
for2primer.png

Далее в цикле создается второй цикл, который описывает уже позиции координаты yPosition
Вот пример:
for3primer.png

Ровно 35 пикселов до 2 ячейки из трёх начиная сверху.
Данное число также присутствует в нашей формуле для вычисления позиции также второй ячейки (уже из трёх, цикл от 0 до 2):
17 + 1 * 18
Получим тоже число 35.

Если же у вас произвольное расположение слотов по интерфейсу (как в моём случае), то вот способ, как добавлять по одной ячейке. Для начала я приложу текстуру своего интерфейса (незнаю зачем, но я нарисовал свой ник на ней, однако если вам пригодится данная текстура можете её просто замазать)
mystorage.png


Расположить в ней ячейки нужным образом в коде можно следующим образом:
Java:
addSlotToContainer(new Slot(te.basic, slotID++, xPosition, yPosition));
addSlotToContainer(new Slot(te.basic, slotID++, xPosition, yPosition));
addSlotToContainer(new Slot(te.basic, slotID++, xPosition, yPosition));

В этот раз нам придёться самим считать пиксели до наших ячеек, так как мы хотим произвольное их расположение по инвентарю. Для начала посчитаем количество пикселей до первой ячейки:
myPrimer1.png

И запишем в метод добавления первой ячейки
addSlotToContainer(new Slot(te.basic, slotID++, 44, yPosition));

Далее рассчитаем количество пикселей начиная с вершины:
myPrimer2.png

Получилось 17 пикселей. Запишем это в метод:
addSlotToContainer(new Slot(te.basic, slotID++, 44, 17));

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

Добавляем вторую:
myPrimer3.png

myPrimer4.png

addSlotToContainer(new Slot(te.basic, slotID++, 80, 35));

А теперь последнюю, третью:
myPrimer5.png

myPrimer6.png

addSlotToContainer(new Slot(te.basic, slotID++, 116, 53));

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

Не забудьте зарегистрировать класс GuiHandler в CommonProxy в init'e с помощью этого кода:
NetworkRegistry.INSTANCE.registerGuiHandler(Main.INSTANCE, new GuiHandler());

1. Готовый блок
А вот так выглядит уже готовый блок, интерфейс для которого мы сами создали:
screenshot.266.jpg
screenshot.267.jpg


Если у вас возникли проблемы с тем, что вашего блока нет в креативных вкладках и вы не можете выдать его себе с помощью команды /give player modid:block, то вы наверняка не зарегистрировали/у вас нету класса регистрации самого блока. Так как это не относится к теме создания интерфейса для блока, то я поместил эту часть в спойлер, поскольку вы уже должны знать, как создавать и регистрировать свой блок.
Собственно, вот класс регистрации вашего блока с интерфейсом, для того, чтобы вы смогли проверить его работу:
BlocksRegister.java:
public class BlocksRegister
{
    public static Block BLOCK_STORAGE = new BlockStorage();

    public static void register()
    {
        setRegister(BLOCK_STORAGE);
    }

    @SideOnly(Side.CLIENT)
    public static void registerRender()
    {
        setRender(BLOCK_STORAGE);
    }

    private static void setRegister(Block block)
    {
        ForgeRegistries.BLOCKS.register(block);
        ForgeRegistries.ITEMS.register(new ItemBlock(block).setRegistryName(block.getRegistryName()));
    }

    @SideOnly(Side.CLIENT)
    private static void setRender(Block block)
    {
        Minecraft.getMinecraft().getRenderItem().getItemModelMesher().register(Item.getItemFromBlock(block), 0, new ModelResourceLocation(block.getRegistryName(), "inventory"));
    }
}
А также регистрация в ClientProxy (init):
BlocksRegister.registerRender();
И в CommonProxy (preInit):
BlocksRegister.register();

На этом туториал подошёл к концу, надеюсь я вам помог и вы сделали то, что хотели. Во 2 части я буду рассказывать о том, как создать блок печки со своим интерфейсом (+рецепты), где уже не будут затрагиваться темы 1 части (например, создания фона интерфейса) и мы будем создавать уже более продвинутые вещи. А также я добавил ссылку на скачивание готового рабочего мода, если у вас всё-таки что-то не получиться в своём. Достаточно нажать на кнопку "Скачать" в верхнем правом углу.

P.S. Да, рисовал я сам, так что возможны некоторые косяки... А также если вы найдёте в тексте или в коде ошибки (или у вас есть советы по оформлению и улучшению дизайна туториала), то пожалуйста, сообщите мне в каком месте, чтобы я смог исправить их.
Автор
sk9zist :l
Скачивания
14
Просмотры
4,187
Первый выпуск
Обновление
Оценка
1.50 звёзд 2 оценок

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

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

Тема не раскрыта. Руководство по тупое копирование готового кода без его разбора. Основная часть посвящена обучению работы с линейкой в фотошопе. Достаточно было одного абзаца про это.
Уже есть такие гайды
sk9zist :l
sk9zist :l
Хм, где?
Сверху