Animer un rendu TESR


  • Administrateurs

    youtubeCe tutoriel est également disponible en vidéo.

    Sommaire

    Introduction

    Dans ce tutoriel nous allons apprendre à animer un rendu TESR. Je vais plus exactement expliquer comment animer les portes du placard que j'ai créé précédemment, mais le même principe peut être utilisé pour faire une animation en fonction d'autres facteurs (clic droit, animation en continu) et pas forcément en fonction de l'ouverture d'un container. Il n'est donc pas forcément nécessaire d'avoir un container et un gui pour animer un rendu TESR. Il sera indiqué dans le tutoriel lorsque les fonctions sont spécifiques à l’ouverture/fermeture d'un container.

    Pré-requis

    Code

    Modification de l'entité de bloc :

    L'animation va se faire en fonction de variables qui se trouveront dans l'entité de bloc. Dans mon cas précis comme je veux animer l'ouverture du placard, je vais devoir modifier la valeur d'une variable lorsqu'un joueur ouvre ou ferme le placard. Nous allons commencer par ajouter ces quatre variables :

        public float lidAngle;
        public float prevLidAngle;
        public int numPlayersUsing;
        private int ticksSinceSync;
    

    La première va correspondre à l'angle actuel de la portière. La seconde variable va être l'angle au tick précédent, numPlayersUsing va être le nombre de joueurs ayant ouvert le placard (pour éviter les doubles d'animation, il ne faut pas que la portière s'ouvre alors qu'un autre joueur l'a déjà ouverte), et la dernière variable (ticksSinceSync) va servir pour exécuter le code à un nombre donné de tick et non à chaque tick (ce qui causera une charge assez lourde).
    Dans le cas où vous souhaitez faire une animation en continu, une seule variable est suffisante (celle qui sera la valeur de l'angle ou la valeur en translation).

    Ensuite dans la fonction openInventory (dans le cas de l'animation d'un container pour l'ouverture / la fermeture seulement), ajoutez ceci :

        @Override
        public void openInventory()
        {
            if(this.numPlayersUsing < 0) // au cas où numPlayersUsing aurait prit une valeur inférieure à 0 à cause d'un problème de synchro ou autre
            {
                this.numPlayersUsing = 0; // on remet à 0
            }
    
            ++this.numPlayersUsing; // on augmente de 1 le nombre de joueurs ayant ouvert l'inventaire du bloc
            this.worldObj.addBlockEvent(this.xCoord, this.yCoord, this.zCoord, this.getBlockType(), 1, this.numPlayersUsing); // on ajoute un événement de bloc, il va servir pour la sync client / serveur
            this.worldObj.notifyBlocksOfNeighborChange(this.xCoord, this.yCoord, this.zCoord, this.getBlockType()); // on prévient le bloc d'un changement
            this.worldObj.notifyBlocksOfNeighborChange(this.xCoord, this.yCoord - 1, this.zCoord, this.getBlockType()); // on prévient le bloc d'en dessous d'un changement
        }
    

    Ensuite on fait l'inverse pour la fonction closeInventory (à nouveau cela ne concerne que l'ouverture / fermeture du container) :

        @Override
        public void closeInventory()
        {
            --this.numPlayersUsing;
            this.worldObj.addBlockEvent(this.xCoord, this.yCoord, this.zCoord, this.getBlockType(), 1, this.numPlayersUsing);
            this.worldObj.notifyBlocksOfNeighborChange(this.xCoord, this.yCoord, this.zCoord, this.getBlockType());
            this.worldObj.notifyBlocksOfNeighborChange(this.xCoord, this.yCoord - 1, this.zCoord, this.getBlockType());
        }
    

    Maintenant nous allons ajouter la fonction updateEntity. C'est dans cette fonction que nous allons changer la valeur de lidAngle et ensuite dans le TileEntitySpecialRenderer nous allons faire le rendu en fonction de cette valeur. Si vous voulez animer en contenu votre rendu, il suffit de mettre ceci :

        @Override
        public void updateEntity()
        {
            this.lidAngle += 0.1F;
            if(this.lidAngle >= 1.0F)
            {
                    this.lidAngle  = 0.0F;
            }
        }
    

    Ainsi la variable lidAngle va prendre une valeur allant de 0 à 1 successivement. Il suffira de changer la valeur après le += pour changer la vitesse.
    Pour animer en fonction de l'ouverture / la fermeture du container, le code est un peu plus complexe. Voici un code tiré du coffre de Minecraft que j'ai légèrement modifié et commenté :

        @Override
        public void updateEntity()
        {
            ++this.ticksSinceSync; // augmente de 1 la variable ticksSinceSync
            float f;
    
            // la condition sert à vérifier côté serveur seulement, si le nombre de joueur utilisant le coffre n'est pas de 0, à exécuter le code en dessous tous les environs 200 secondes. 
            // l’intérêt d'additionner les coordonnées du bloc à ticksSinceSync puis faire une division euclidienne (donc on récupère le reste) est d'éviter que tous les coffres du monde exécutent le code en même temps
            // il y aura un petit temps entre l'exécution de chaque coffre, le but étant d'éviter les lags car le code ci-dessous est assez lourd.
    
            if(!this.worldObj.isRemote && this.numPlayersUsing != 0 && (this.ticksSinceSync + this.xCoord + this.yCoord + this.zCoord) % 200 == 0)
            {
                this.numPlayersUsing = 0; // on remet sur 0 le nombre de joueurs qui utilisent le coffre
                f = 5.0F;
                // ci-dessous, on obtient la liste de tous les joueurs étant à moins de/à  5 blocs du coffre
                List list = this.worldObj.getEntitiesWithinAABB(EntityPlayer.class, AxisAlignedBB.getBoundingBox((double)((float)this.xCoord - f), (double)((float)this.yCoord - f), (double)((float)this.zCoord - f), (double)((float)(this.xCoord + 1) + f), (double)((float)(this.yCoord + 1) + f), (double)((float)(this.zCoord + 1) + f)));
                Iterator iterator = list.iterator();
    
                while(iterator.hasNext())
                {
                    EntityPlayer entityplayer = (EntityPlayer)iterator.next();
    
                    if(entityplayer.openContainer instanceof ContainerCupboard) // si le container ouvert par le joueur est le ContainerCupboard (remplacé ici par votre container)
                    {
                        IInventory iinventory = ((ContainerCupboard)entityplayer.openContainer).getTileTuto(); // on get l'inventaire depuis le getter que nous avons ajouté dans le tutoriel sur les container
    
                        if(iinventory == this) // si l'inventaire est ceci (donc ce tile entity)
                        {
                            ++this.numPlayersUsing; // on augmente de 1
                        }
                    }
                }
            }
            //le but de toute cette condition (ci-dessus) est de recompter le nombre de joueurs ayant ouvert le coffre 
            // (au cas où un joueur ferme le coffre de façon imprévue, par exemple en crashant ou en perdant la connexion)
    
            this.prevLidAngle = this.lidAngle; // l'angle précédent prend la valeur de l'angle actuel
            f = 0.1F;
    
            if(this.numPlayersUsing > 0 && this.lidAngle == 0.0F) // si le nombre de joueurs ayant ouvert le coffre est supérieur à 0 et que l'angle est 0 (donc que c'est fermé)
            {
                // on joue le son de l'ouverture
                this.worldObj.playSoundEffect(this.xCoord + 0.5D, this.yCoord + 0.5D, this.zCoord + 0.5D, "random.chestopen", 0.5F, this.worldObj.rand.nextFloat() * 0.1F + 0.9F);
            }
    
            // si le nombre de joueurs est 0 et que l'angle est plus grand que 0 (placard ouvert) ou que le nombre de joueurs est supérieur à 0 et que le placard n'est pas entièrement ouvert
            // (en gros, si les portières doivent bouger)
            if(this.numPlayersUsing == 0 && this.lidAngle > 0.0F || this.numPlayersUsing > 0 && this.lidAngle < 1.0F)
            {
                float f1 = this.lidAngle; // f1 prend la valeur de l'angle avant modification
    
                if(this.numPlayersUsing > 0) // si le nombre de joueur est supérieur à 0
                {
                    this.lidAngle += f; // on augmente la taille de l'angle (donc on ouvre plus)
                }
                else
                {
                    this.lidAngle -= f; // sinon on diminue (donc on ferme)
                }
    
                if(this.lidAngle > 1.0F) // si l'angle est plus grand que 1
                {
                    this.lidAngle = 1.0F; // on le remet sur 1
                }
    
                float f2 = 0.5F;
    
                if(this.lidAngle < f2 && f1 >= f2) // si l'angle actuel est plus petit que 0,5 et que l'angle avant modification est plus grand que 0,5
                {
                    // on joue le son de la fermeture. En gros il sera joué environ vers la moitié du chemin de la portière.
                    this.worldObj.playSoundEffect(this.xCoord + 0.5D, this.yCoord + 0.5D, this.zCoord + 0.5D, "random.chestclosed", 0.5F, this.worldObj.rand.nextFloat() * 0.1F + 0.9F);
                }
    
                if(this.lidAngle < 0.0F) // si l'angle est plus petit que 0
                {
                    this.lidAngle = 0.0F; // on le remet sur 0.
                }
            }
        }
    

    Avec ce code lidAngle va passer de 0F à 1.0F quand un joueur va ouvrir le container (et que personne ne l'avait ouvert avant) et lidAngle va passe de 1.0F à 0F quand le dernier joueur va le fermer. Il gère aussi le son comme expliqué dans les commentaires du code.

    Il ne reste plus qu’à indiquer que faire des événement de bloc que nous envoyons dans les fonctions open et close inventory. Pour cela ajoutez cette fonction, toujours dans votre tile entity :

        @Override
        public boolean receiveClientEvent(int id, int value)
        {
            if(id == 1)
            {
                this.numPlayersUsing = value;
                return true;
            }
            return super.receiveClientEvent(id, value);
        }
    

    l'id est 1 car dans close/openInventory nous avons mis 1 (this.worldObj.addBlockEvent(this.xCoord, this.yCoord, this.zCoord, this.getBlockType(), 1, this.numPlayersUsing);).

    Modification du bloc :

    *Ce qui se trouve dans cette partie concerne uniquement l'animation en fonction de l'ouverture / la fermeture d'un container.

    *Dans le tutoriel sur les entités de bloc, nous vous avons dit d'utiliser la fonction hasTileEntity et createTileEntity pour ajouter une entité de bloc à votre bloc. Si vous avez déjà un peu regardé le code de Minecraft, vous avez sûrement constaté que Minecraft utilise la classe BlockContainer. Nous n'avons pas utilisé cette classe dans les tutoriels car Forge ajoute déjà des méthodes pour les entités de bloc (cela date de l'époque où la classe BlockContainer ne permettait pas de mettre une entité de bloc différente en fonction du metadata). Cependant la classe BlockContainer contient une version modifiée de la fonction onBlockEventReceived que nous n'avons pas du-coup.
    Il faut donc ajouter cette fonction dans la classe de votre bloc :

        public boolean onBlockEventReceived(World world, int x, int y, int z, int id, int value)
        {
            TileEntity tileentity = world.getTileEntity(x, y, z);
            return tileentity != null ? tileentity.receiveClientEvent(id, value) : false;
        }
    

    Si vous ne le faites pas, la fonction receiveClientEvent de votre entité de bloc ne sera jamais appelée et donc les événements de bloc ne fonctionneront pas.

    Animation du TESR :

    Maintenant que nous avons tout préparé, nous allons enfin pouvoir nous attaquer au rendu. Allez dans la classe de votre TileEntitySpecialRenderer. Vous devrez avoir une fonction nommé renderTileEntity<votre nom ici>At semblable à celle-ci :

        private void renderTileEntityTutorielAt(TileEntityTutoriel tile, double x, double y, double z, float partialRenderTick)
        {
            GL11.glPushMatrix();
            GL11.glTranslated(x + 0.5D, y + 1.5D, z + 0.5D);
            GL11.glRotatef(180F, 0.0F, 0.0F, 1.0F);
            GL11.glRotatef((90F * tile.getDirection()) + 180F, 0.0F, 1.0F, 0.0F); // vous n'avez pas forcément cette ligne, cela dépend si votre bloc est orientable ou pas
            this.bindTexture(texture);
    
            model.renderAll();
            GL11.glPopMatrix();
        }
    

    Nous allons avant la fonction model.renderAll(); jouez avec les différentes parties du modèle. Tapez model. vous devrez voir apparaître les différentes parties :

    Chacune de ces parties ont trois variables : rotateAngleX, rotateAngleY et rotateAngleZ qui correspondent aux rotations sur chacun de ces axes.
    Ce sont ces variables que l'on va modifier en fonction de la valeur de tile.lidAngle (la variable que nous avons créée dans l'entité de bloc).
    Leurs valeurs sont exprimées en radian. 0 radian correspond à rien, 2π radian à un cercle entier (donc on revient au point de départ, 0 radian et 2π radian sont confondus). π radian correspond à la moitié et π/2 radian à un quart.
    Dans mon cas je vais faire varier l'angle de rotation en Y, et je veux que la portière fasse un quart de tour. Comme tile.lidAngle varie de 0 à 1, il me suffit de le multiplier par π/2. Petit rappel pour les rotations :

    Et voilà ma fonction après modification :

        private void renderTileEntityTutorielAt(TileEntityTutoriel tile, double x, double y, double z, float partialRenderTick)
        {
            GL11.glPushMatrix();
            GL11.glTranslated(x + 0.5D, y + 1.5D, z + 0.5D);
            GL11.glRotatef(180F, 0.0F, 0.0F, 1.0F);
            GL11.glRotatef((90F * tile.getDirection()) + 180F, 0.0F, 1.0F, 0.0F);
            this.bindTexture(texture);
            float f1 = tile.prevLidAngle;
            model.doorLeft.rotateAngleY = -(f1 * (float)Math.PI / 2.0F);
            model.leftHandle.rotateAngleY = -(f1 * (float)Math.PI / 2.0F);
            model.doorRight.rotateAngleY = (f1 * (float)Math.PI / 2.0F);
            model.rightHandle.rotateAngleY = (f1 * (float)Math.PI / 2.0F);
            model.renderAll();
            GL11.glPopMatrix();
        }
    

    Bon maintenant si on compare avec le coffre on verra une petite différence, vous constaterez que le coffre a un effet d’accélération. Pour avoir ce même effet il suffit de faire comme ceci :

        private void renderTileEntityTutorielAt(TileEntityTutoriel tile, double x, double y, double z, float partialRenderTick)
        {
            GL11.glPushMatrix();
            GL11.glTranslated(x + 0.5D, y + 1.5D, z + 0.5D);
            GL11.glRotatef(180F, 0.0F, 0.0F, 1.0F);
            GL11.glRotatef((90F * tile.getDirection()) + 180F, 0.0F, 1.0F, 0.0F);
            this.bindTexture(texture);
            float f1 = tile.prevLidAngle + (tile.lidAngle - tile.prevLidAngle) * partialRenderTick;
            f1 = 1.0F - f1; //f1 va aller de 1 à 0 quand elle va de 0 à 1 (inverse le sens de variation)
            f1 = 1.0F - f1 * f1 * f1; // à nouveau, sauf que là on retire f1 au cube. Du-coup le déplacement va être lent au début, puis va accélérer (il suffit de comparer les variations de y=x et de y=x^3 entre 0 et 1 pour constater)
            model.doorLeft.rotateAngleY = -(f1 * (float)Math.PI / 2.0F);
            model.leftHandle.rotateAngleY = -(f1 * (float)Math.PI / 2.0F);
            model.doorRight.rotateAngleY = (f1 * (float)Math.PI / 2.0F);
            model.rightHandle.rotateAngleY = (f1 * (float)Math.PI / 2.0F);
            model.renderAll();
            GL11.glPopMatrix();
        }
    

    Et voilà, votre animation devrait fonctionner !

    Pour ceux qui souhaiteraient faire une animation en translation, c'est un peu plus complexe. En effet il n'y a pas de model.nomDeLaPartie.translationX = ...
    Le plus simple est de retirer de la fonction renderAll le morceau que vous voulez faire bouger et de la rendre à part dans un GL11.glTranslate.
    Exemple :

        private void renderTileEntityTutorielAt(TileEntityTutoriel tile, double x, double y, double z, float partialRenderTick)
        {
            GL11.glPushMatrix();
            GL11.glTranslated(x + 0.5D, y + 1.5D, z + 0.5D);
            GL11.glRotatef(180F, 0.0F, 0.0F, 1.0F);
            GL11.glRotatef((90F * tile.getDirection()) + 180F, 0.0F, 1.0F, 0.0F);
            this.bindTexture(texture);
            GL11.glPushMatrix();
            GL11.glTranslatef(tile.translationValue, 0.0F, 0.0F);
            model.backPlate.render(0.0625F);
            GL11.glPopMatrix();
            model.renderAll();
            GL11.glPopMatrix();
        }
    

    Dans le code du modèle, pensez bien à retirer <nomdumorceau>.render(0.0625F); de la fonction renderAll !

    Résultat

    Voir le commit sur github
    Le commit sur github montre clairement où ont été placés les fichiers, ainsi que ce qui a été ajouté et retiré dans le fichier.

    En vidéo

    Youtube Video

    Crédits

    Rédaction :

    Correction :

    Creative Commons
    Ce tutoriel de Minecraft Forge France est mis à disposition selon les termes de la licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International

    retourRetour vers le sommaire des tutoriels