9 sept. 2013, 20:53

Dans ce tutoriel, nous allons apprendre à créer un container et un gui. D’autres tutoriels sont (seront pour l’instant) disponible afin de plus détailler chaque partie.

(Il y aura un tutoriel sur les container et les slots en détail, et un autre sur les gui qui sera plus généralisé, donc aussi sur les gui de type menu)

Comme expliqué dans l’introduction, il va y avoir plus de 5 classes, soyez bien attentif afin de ne pas vous perdre.

Le bloc

Pour commencer, il faut un bloc. Pour ça, rendez-vous sur le tutoriel d’un bloc basique, et des TileEntity.
Dans mon cas, je vais ajoutez le gui au metadata 3 du bloc tutoriel avec metadata. Je considère que vous un bloc avec un tileEntity prêt. Nous allons ajouter quelques méthode :

public boolean onBlockActivated(World world, int x, int y, int z, EntityPlayer player, int par6, float par7, float par8, float par9)
{
    if(world.getBlockMetadata(x, y, z) == 3)
    {
        FMLNetworkHandler.openGui(player, ModTutoriel.instance, 0, world, x, y, z);
        return true;
    }
    return false;
}

Je vous l’ai dit au dessus, je mets mon gui et mon container sur mon bloc avec metadata, sur le metadata 3, d’où la condition au dessus. Si vous n’avez pas de metadata, mettez le code openGui directement dans la méthode.
Ce code sert à ouvrir le gui. La méthode openGui prend déjà en compte si le joueur est accroupi ou pas, inutile d’ajouter une condition. Modtutoriel.instance est l’instance de mon mod, et 0 est l’id du gui, dans notre cas les id de gui ne nous servirons pas, vous pouvez toujours mettre 0, car nous allons utiliser le TileEntity dans le guiHandler, en revanche si vous souhaitez mettre plusieurs gui sur un même TiteEntity (genre des boutons qui ouvre un autre gui, ces id seront nécessaire, mais cela sera traité dans un autre tutoriel)

public void onBlockPlacedBy(World world, int x, int y, int z, EntityLivingBase living, ItemStack stack)
{
    TileEntity te = world.getBlockTileEntity(x, y, z);
    if(te != null && stack.getItemDamage() == 3 && te instanceof TileEntityBigChest && stack.hasDisplayName())
    {
        ((TileEntityBigChest)te).setCustomGuiName(stack.getDisplayName());
    }
}

TileEntity big chest est le nom de mon TileEntity, une fois de plus la condition stack.getItemDamage() == 3 sert à faire en sorte que le code ne soit que fait pour le metadata 3. Ce code sert à mettre un nom Custom dans le gui si le bloc à été renommé avec l’enclume. Pour l’instant vous allez avoir une erreur sur setCustomGuiName, c’est normal.

public void breakBlock(World world, int x, int y, int z, int side, int metadata)
{
    if(metadata == 3)
    {
        dropContainerItem(world, x, y, z);
    }
    super.breakBlock(world, x, y, z, side, metadata);
}

Comme avant, la condition vérifie que le metadata est bien 3, passez à la retirer si votre bloc n’a pas de metadata ou à changer la valeur si besoin. dropContainerItem est une fonction que nous allons créer pour droper le contenu du coffre :

protected void dropContainerItem(World world, int x, int y, int z)
{
    TileEntityBigChest bigchest = (TileEntityBigChest)world.getBlockTileEntity(x, y, z);

    if (bigchest != null)
    {
        for (int slotId = 0; slotId < bigchest.getSizeInventory(); slotId++)
        {
            ItemStack stack = bigchest.getStackInSlot(slotId);

            if (stack != null)
            {
                float f = world.rand.nextFloat() * 0.8F + 0.1F;
                float f1 = world.rand.nextFloat() * 0.8F + 0.1F;
                EntityItem entityitem;

                for (float f2 = world.rand.nextFloat() * 0.8F + 0.1F; stack.stackSize > 0; world.spawnEntityInWorld(entityitem))
                {
                    int k1 = world.rand.nextInt(21) + 10;

                    if (k1 > stack.stackSize)
                    {
                        k1 = stack.stackSize;
                    }

                    stack.stackSize -= k1;
                    entityitem = new EntityItem(world, (double)((float)x + f), (double)((float)y + f1), (double)((float)z + f2), new ItemStack(stack.itemID, k1, stack.getItemDamage()));
                    float f3 = 0.05F;
                    entityitem.motionX = (double)((float)world.rand.nextGaussian() * f3);
                    entityitem.motionY = (double)((float)world.rand.nextGaussian() * f3 + 0.2F);
                    entityitem.motionZ = (double)((float)world.rand.nextGaussian() * f3);

                    if (stack.hasTagCompound())
                    {
                        entityitem.getEntityItem().setTagCompound((NBTTagCompound)stack.getTagCompound().copy());
                    }
                }
            }
        }
    }
}

C’est la même fonction que le coffre de minecraft, vous aurez aussi une erreur dessus.
C’est fini pour le bloc, on passe au tile entity.

Le Tile Entity

Commencez par ajoutez ceci à la suite de la déclaration de la classe, juste après le extends :

implements IInventory

Vous allez avoir une erreur sur la classe, add missing method. Cliquez dessus, et la de nombreuse méthode vont être créées, nous allons les compléter.
Déclarer les ItemStack est un String (juste après l’accolade de la classe) :

    private ItemStack[] inventory = new ItemStack[72];
    private String customName;

72 est le nombre de slot que va avoir mon container, faite attention à cette variable, si vous créez plus de slot que de la taille de ce tableau, le jeu va crasher avec un ArrayIndexOutOfBoundException.

Ensuite, nous allons ajoutez les deux méthodes pour les tag NBT :

public void readFromNBT(NBTTagCompound nbttag)
{
    super.readFromNBT(nbttag);
    NBTTagList nbttaglist = nbttag.getTagList("Items");
    this.inventory = new ItemStack[this.getSizeInventory()];

    if (nbttag.hasKey("CustomName"))
    {
        this.customName = nbttag.getString("CustomName");
    }

    for (int i = 0; i < nbttaglist.tagCount(); i++)
    {
        NBTTagCompound nbttagcompound1 = (NBTTagCompound)nbttaglist.tagAt(i);
        int j = nbttagcompound1.getByte("Slot");

        if (j >= 0 && j < this.inventory.length)
        {
            this.inventory[j] = ItemStack.loadItemStackFromNBT(nbttagcompound1);
        }
    }
}

public void writeToNBT(NBTTagCompound nbttag)
{
    super.writeToNBT(nbttag);
    NBTTagList nbttaglist = new NBTTagList();

    for (int i = 0; i < this.inventory.length; i++)
    {
        if (this.inventory* != null)
        {
            NBTTagCompound nbttagcompound1 = new NBTTagCompound();
            nbttagcompound1.setByte("Slot", (byte)i);
            this.inventory*.writeToNBT(nbttagcompound1);
            nbttaglist.appendTag(nbttagcompound1);
        }
    }

    nbttag.setTag("Items", nbttaglist);

    if (this.isInvNameLocalized())
    {
        nbttag.setString("CustomName", this.customName);
    }
}

Ces deux fonctions enregistre et charge les ItemStack ainsi que le nom custom si il y en a un.
Maintenant nous allons modifier les méthodes de l’interface IInventory :

    @Override
    public int getSizeInventory()
    {
        return inventory.length;
    }

Cette méthode donne la taille de l’inventaire, le return doit donc donner la taille du tableau d’ItemStack.

    @Override
    public ItemStack getStackInSlot(int slotId)
    {
        return inventory[slotId];
    }

Une fonction pour récupérer un ItemStack sur un slot précis.

    @Override
    public ItemStack decrStackSize(int slotId, int quantity)
    {
        if (this.inventory[slotId] != null)
        {
            ItemStack itemstack;

            if (this.inventory[slotId].stackSize <= quantity)
            {
                itemstack = this.inventory[slotId];
                this.inventory[slotId] = null;
                this.onInventoryChanged();
                return itemstack;
            }
            else
            {
                itemstack = this.inventory[slotId].splitStack(quantity);

                if (this.inventory[slotId].stackSize == 0)
                {
                    this.inventory[slotId] = null;
                }

                this.onInventoryChanged();
                return itemstack;
            }
        }
        else
        {
            return null;
        }
    }

    @Override
    public ItemStack getStackInSlotOnClosing(int slotId)
    {
        if (this.inventory[slotId] != null)
        {
            ItemStack itemstack = this.inventory[slotId];
            this.inventory[slotId] = null;
            return itemstack;
        }
        else
        {
            return null;
        }
    }

    @Override
    public void setInventorySlotContents(int slotId, ItemStack stack)
    {
        this.inventory[slotId] = stack;

        if (stack != null && stack.stackSize > this.getInventoryStackLimit())
        {
            stack.stackSize = this.getInventoryStackLimit();
        }

        this.onInventoryChanged();
    }

Ces trois fonction servent à retirer des ItemStack dans l’inventaire, en ajouter, et à récupérer un ItemStack lorsque vous sortez la souris du container (pour mettre à jour ce qui a été prit ou ajouté).

    @Override
    public String getInvName()
    {
        return this.isInvNameLocalized() ? this.customName : "container.bigchest";
    }

    @Override
    public boolean isInvNameLocalized()
    {
        return this.customName != null && this.customName.length() > 0;
    }

    public void setCustomGuiName(String name)
    {
        this.customName = name;
    }

Les trois fonction pour le nom custom. La première ramène sur le nom du container, si il a un nom custom, il utilise le String customName, sinon il utilise le nom container.bigchest (qui sera localisé grâce à une fonction dans le gui). C’est donc le nom non localisé, sans mon fichier en_US.lang je vais devoir mettre container.bigchest=Big Chest
isInvNameLocalized sert à vérifier si le bloc à un nom custom ou pas, donc si le String customName n’est pas null et que sa taille est supérieur à 0.
La troisième méthode sert à définir le nom custom.

    @Override
    public int getInventoryStackLimit()
    {
        return 64;
    }

La taille maximum des ItemStack dans le container (pas plus que 64).

    @Override
    public boolean isUseableByPlayer(EntityPlayer player)
    {
        return worldObj.getBlockTileEntity(xCoord, yCoord, zCoord) == this && player.getDistanceSq(xCoord + 0.5, yCoord + 0.5, zCoord + 0.5) < 64;
    }

Pour savoir si le joueur peut ouvrir le gui (qu’il est pas trop loin)

    @Override
    public void openChest()
    {

    }

    @Override
    public void closeChest()
    {

    }

Peut être utile pour une animation, mais ce ne sera pas traité dans ce sujet.

    @Override
    public boolean isItemValidForSlot(int slotId, ItemStack stack)
    {
        return true;
    }

Pour savoir si un ItemStack spécifique peut aller dans un slot donné. (ici tous les ItemStack peuvent aller dans n’importe quel slot, le tutoriel sur le four est un bon exemple d’utilisation de cette méthode)

Le GuiHandler

Nous allons commencer par enregistrer le guiHandler, dans la classe principale à la suite des enregistrements de TileEntity, ajoutez :

NetworkRegistry.instance().registerGuiHandler(this.instance, new GuiHandlerTutorial());

Créez le GuiHandler. Dans la méthode getServerGuiElement, ajoutez :

TileEntity te = world.getBlockTileEntity(x, y, z);
if(te instanceof TileEntityBigChest)
{
    return new ContainerBigChest(player.inventory, (TileEntityBigChest)te);
}
return null;

Comme dit plus haut, nous passons par un condition avec le TileEntity. Si le TileEntity est d’instance TileEntityBigChest, il retourne sur le ContainerBigChest, sinon il retourne null.
Même chose pour le client, mais avec le gui (getClientGuiElement)

TileEntity te = world.getBlockTileEntity(x, y, z);
if(te instanceof TileEntityBigChest)
{
    return new GuiBigChest(player.inventory, (TileEntityBigChest)te);
}
return null;

Le container

package tutoriel.common;

import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.InventoryPlayer;
import net.minecraft.inventory.Container;
import net.minecraft.inventory.Slot;
import net.minecraft.item.ItemStack;

public class ContainerBigChest extends Container
{
    private TileEntityBigChest tileEntity;

    public ContainerBigChest(InventoryPlayer playerInventory, TileEntityBigChest teChest)
    {
        this.tileEntity = teChest;

        for(int i = 0; i < 6; i++)
        {
            for(int j = 0; j < 9; j++)
            {
                this.addSlotToContainer(new Slot(teChest, j + i * 9, 8 + j * 18, 18 + i * 18));
            }
        }
        this.bindPlayerInventory(playerInventory);
    }

    private void bindPlayerInventory(InventoryPlayer playerInventory)
    {
        int i;
        for(i = 0; i < 3; i++)
        {
            for(int j = 0; j < 9; j++)
            {
                this.addSlotToContainer(new Slot(playerInventory, j + i * 9 + 9, 8 + j * 18, 103 + i * 18 + 37));
            }
        }

        for(i = 0; i < 9; i++)
        {
            this.addSlotToContainer(new Slot(playerInventory, i, 8 + i * 18, 161 + 37));
        }
    }

    @Override
    public boolean canInteractWith(EntityPlayer player)
    {
        return tileEntity.isUseableByPlayer(player);
    }

    public ItemStack transferStackInSlot(EntityPlayer player, int slotId)
    {
        ItemStack itemstack = null;
        Slot slot = (Slot)this.inventorySlots.get(slotId);

        if(slot != null && slot.getHasStack())
        {
            ItemStack itemstack1 = slot.getStack();
            itemstack = itemstack1.copy();

            if(slotId < 9)
            {
                if(!this.mergeItemStack(itemstack1, 9, this.inventorySlots.size(), true))
                {
                    return null;
                }
            }
            else if(!this.mergeItemStack(itemstack1, 0, 9, false))
            {
                return null;
            }

            if(itemstack1.stackSize == 0)
            {
                slot.putStack((ItemStack)null);
            }
            else
            {
                slot.onSlotChanged();
            }
        }
        return itemstack;
    }
}

Dans le constructeur, on initialise le TileEntity, puis les slots du TileEntity ainsi que l’inventaire du joueur (this.bindPlayerInventory(playerInventory); et la fonction qui va avec)

La fonction addSlotToContainer ajoute les slots, le constructeur de la classe slot est :
new Slot(l’inventaire (donc notre TileEntity comme il est implémenté IInventory), l’id du slot (pensez à ne pas dépasser la taille du tableau d’ItemStack du TileEntity), position X, position Y)
La boucle for et les quelques calcules sert à simplifier le code. Les valeurs 18 correspondent à l’espace entre chaque slot (un slot fait 16x16, donc 18 pour laisser deux pixels entre eux)
Pour ajouter vos slot, je vous conseil fortement de lancer Minecraft en mode “Debug”, c’est le scarabée à côté de run :

Ainsi vous aurez juste besoin de fermer et de ré-ouvrir le gui en jeu pour voir les changements, plutôt que de relancer le jeu à chaque fois.
canInteractWith est la fonction pour savoir si le joueur est assez proche pour ouvrir le container (utilise avec la fonction dans le TileEntity)
Et transferStackInSlot est une méthode pour faire passer les items dans les slots. Le container du four est à nouveau un bon exemple pour faire quelque chose de plus complexe.

Le gui

package tutoriel.client;

import net.minecraft.client.gui.inventory.GuiContainer;
import net.minecraft.client.resources.I18n;
import net.minecraft.entity.player.InventoryPlayer;
import net.minecraft.inventory.IInventory;
import net.minecraft.util.ResourceLocation;

import org.lwjgl.opengl.GL11;

import tutoriel.common.ContainerBigChest;
import tutoriel.common.TileEntityBigChest;

public class GuiBigChest extends GuiContainer
{
    public static ResourceLocation texture = new ResourceLocation("modtutoriel", "textures/gui/container/bigChest.png");
    private TileEntityBigChest bigChest;
    private IInventory playerInventory;

    public GuiBigChest(InventoryPlayer inventory, TileEntityBigChest tileEntity)
    {
        super(new ContainerBigChest(inventory, tileEntity));
        this.bigChest = tileEntity;
        this.playerInventory = inventory;
        this.ySize = 230;
    }

    protected void drawGuiContainerForegroundLayer(int par1, int par2)
    {
        this.fontRenderer.drawString(this.playerInventory.isInvNameLocalized() ? this.playerInventory.getInvName() : I18n.getString(this.playerInventory.getInvName()), 8, 129, 0);
        this.fontRenderer.drawString(this.bigChest.isInvNameLocalized() ? this.bigChest.getInvName() : I18n.getString(this.bigChest.getInvName()), 8, 7, 0);
    }

    @Override
    protected void drawGuiContainerBackgroundLayer(float f, int i, int j)
    {
        GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
        this.mc.getTextureManager().bindTexture(texture);
        int x = (this.width - this.xSize) / 2;
        int y = (this.height - this.ySize) / 2;
        this.drawTexturedModalRect(x, y, 0, 0, this.xSize, this.ySize);
    }
}

Dans l’ordre, je commence par déclarer une localisation de ressource : new ResourceLocation(“votre mod id”, “le chemin de la texture après assets/modid/”) C’est la même chose que pour les mobs.
Ensuite je déclare le TileEntity est l’inventaire du joueur, ils sont initialisés dans le constructeur. D’ailleurs vous pouvez voir que je déclare aussi le Container, en effet même si il est géré par le serveur, le client l’utilise aussi pour connaître le nombre de slot et leurs emplacements. J’ai également modifier la taille y du gui, vous pouvez aussi modifier la taille x selon vos besoin.

La méthode drawGuiContainerForegroundLayer est utilisé pour les textes et autres truc à mettre en premier plan, comme je vous l’avais expliqué lorsque nous avons fait le TileEntity, les méthodes isInvNameLocalized, et getName sont utilisés ici, et comme vous pouvez le voir, si un nom custom à été entré, il n’est pas localisé (c’est I18n.getString ajoute le nom au fichier de lang). Les paramètres de la fonction sont :
this.fontRenderer.drawString(texte, position x, position y, couleur)
Une fois de plus le debug est très pratique pour placer le texte au bon endroit, utilisez-le. Pour la couleur, c’est un int, petit rappel, en java pour les couleurs c’est : rouge * 65536 + bleu * 256 + vert. Ou alors utilisez new Color(rouge, bleu, vert).getRGB();

La méthode drawGuiContainerBackgroundLayer va servir à faire apparaitre notre texture. (Background = fond, donc c’est logique, on va pas mettre la texture par dessus les textes ^^)

Le reste (affichage des items, etc …) et déjà géré dans la classe GuiContainer, et comme notre classe est extends GuiContainer, tout est déjà fait 🙂

Rendu final et finition

Pensez à ajouter dans les fichiers de lang le nom du gui. Votre gui et votre container est désormais opérationnel !

Rendu final sur Github