21 déc. 2013, 21:22

Introduction

Dans l’introduction du tutoriel sur les rendus TESR, il a été dit que le TESR a le désavantage d’être rendu à chaque tick. C’est en effet un désavantage car le rendu est lourd, mais c’est aussi un avantage car il permet de faire des rendus animés !
Pour ce tutoriel j’ai préparé deux blocs en metadata sur la sculpture précédemment créée dans le tutoriel sur les rendus de blocs TESR.

Voici les deux blocs en question :
Bloc à animer
À gauche, un placard, dont l’objectif va être d’animer les deux portes lorsqu’on ouvre et ferme le gui du bloc (car il s’agit bien sur d’un container que vous pouvez faire en suivant ce tutoriel) et à droite, une sorte de machine, l’objectif va être de faire pivoter la barre (le truc qui ressemble vaguement à un levier) à gauche lorsqu’on fait un clic sur la gauche du bloc, et inversement avec la droite. Le deuxième objectif avec ce bloc va être de faire tourner la partie noire (l’hélice + l’axe) plus ou moins vite en fonction du levier. Et pour ajouter de la difficulté/complexité, les deux blocs sont orientables.

Attention au placement de l’offset, je rappelle que c’est lui qui défini le point de rotation, s’il est mal placé la rotation ne fonctionnera pas correctement. Si vous souhaitez voir mes modèles (fait avec techne) :
0_1528886982815_Cupboard.tcn
0_1528886993322_Machine.tcn

Prérequis

Rotation des portes du placard

Nous allons commencer par le placard. Allez dans la classe de votre tile entity, vous avez surement déjà remarqué qu’il existe les méthodes openChest et closeChest. Nous allons en effet les utiliser. Mais déclarez d’abord dans votre tile entity les 4 variables suivantes :

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

lidAngle est l’angle de rotation actuel, prevLidAngle l’angle précédent, numUsingPlayers le nombre de joueurs qui utilisent le container, et ticksSinceSync le nombre de tick qui se sont écoulés depuis la dernière vérification. Nous allons définir et utiliser ces variables plus tard, pour l’instant elles ne fond rien.

Dans la méthode openChest() ajoutez :

        if(this.numUsingPlayers < 0)
        {
            this.numUsingPlayers = 0;
        }

        ++this.numUsingPlayers;
        this.worldObj.addBlockEvent(this.xCoord, this.yCoord, this.zCoord, this.getBlockType().blockID, 1, this.numUsingPlayers);

La condition sert à vérifier que le nombre de joueurs n’est pas négatif (on sait jamais, ça peut arriver), si c’est le cas la valeur est remise à 0.
Ensuite, on augmente le nombre de joueurs qui utilisent le coffre et on va envoyer un event pour signaler que le nombre de joueurs ayant ouvert le coffre à changé.
Dans la méthode closeChest() ajoutez :

        –this.numUsingPlayers;
        this.worldObj.addBlockEvent(this.xCoord, this.yCoord, this.zCoord, this.getBlockType().blockID, 1, this.numUsingPlayers);

Même chose qu’avant sauf qu’on diminue le nombre de joueurs.

Maintenant il faut récupérer l’évènement de bloc. Dans la classe de votre bloc, ajoutez ce code :

    public boolean onBlockEventReceived(World world, int x, int y, int z, int eventId, int eventValue)
    {
        super.onBlockEventReceived(world, x, y, z, eventId, eventValue);
        TileEntity tileentity = world.getBlockTileEntity(x, y, z);
        return tileentity != null ? tileentity.receiveClientEvent(eventId, eventValue) : false;
    }

Ce code va lancer la fonction receiveClientEvent dans le tile entity, donc retournez dans la classe de votre tile entity et ajoutez cette méthode :

    public boolean receiveClientEvent(int eventId, int eventValue)
    {
        if(eventId == 1)
        {
            this.numUsingPlayers = eventValue;
            return true;
        }
        else
        {
            return super.receiveClientEvent(eventId, eventValue);
        }
    }

Maintenant le client et le serveur sont synchronisés, le nombre de joueurs ayant ouvert le coffre sera le même sur le WorldClient et le WorldServer.
Le problème, c’est que les méthodes openChest() et closeChest() ne sont jamais exécutées, il faut donc les appeler. Allez dans la classe de votre container.
On sait que la classe du container est instanciée à chaque fois qu’on l’ouvre, donc dans le constructeur de cette classe, ajoutez :

    te.openChest();

te étant l’instance de mon tileEntity (donc le nom qui se trouve après l’argument de votre tile entity dans le constructeur). Le constructeur ressemble donc à ça :

    public ContainerCupboard(InventoryPlayer playerInv, TileEntityCupboard te)
    {
        this.tileEntity = te;
        te.openChest();

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

Toujours dans cette même classe, ajoutez la méthode suivante :

    public void onContainerClosed(EntityPlayer player)
    {
        super.onContainerClosed(player);
        tileEntity.closeChest();
    }

Voila, maintenant les méthodes openChest() et closeChest() seront exécutées.

Retournez dans la classe de votre tile entity, et ajoutez la méthode suivante :

    public void updateEntity()
    {
        super.updateEntity();
        ++this.ticksSinceSync;

        if(!this.worldObj.isRemote && this.numUsingPlayers != 0 && (this.ticksSinceSync + this.xCoord + this.yCoord + this.zCoord) % 200 == 0)
        {
            this.numUsingPlayers = 0;
            List list = this.worldObj.getEntitiesWithinAABB(EntityPlayer.class, AxisAlignedBB.getAABBPool().getAABB(this.xCoord - 5, this.yCoord - 5, this.zCoord - 5, this.xCoord + 6, this.yCoord + 6, this.zCoord + 6));
            Iterator iterator = list.iterator();

            while(iterator.hasNext())
            {
                EntityPlayer entityplayer = (EntityPlayer)iterator.next();

                if(entityplayer.openContainer instanceof ContainerCupboard)
                {
                    ++this.numUsingPlayers;
                }
            }
        }

        this.prevLidAngle = this.lidAngle;

        if(this.numUsingPlayers > 0 && this.lidAngle == 0.0F)
        {
            this.worldObj.playSoundEffect(((double)this.xCoord + 0.5), (double)this.yCoord + 0.5D, (double)this.zCoord + 0.5D, "random.chestopen", 0.5F, this.worldObj.rand.nextFloat() * 0.1F + 0.9F);
        }

        if(this.numUsingPlayers == 0 && this.lidAngle > 0.0F || this.numUsingPlayers > 0 && this.lidAngle < 1.0F)
        {
            float f1 = this.lidAngle;

            if(this.numUsingPlayers > 0)
            {
                this.lidAngle += 0.1F;
            }
            else
            {
                this.lidAngle -= 0.1F;
            }

            if(this.lidAngle > 1.0F)
            {
                this.lidAngle = 1.0F;
            }

            float f2 = 0.5F;

            if(this.lidAngle < f2 && f1 >= f2)
            {
                this.worldObj.playSoundEffect((double)this.xCoord + 0.5D, (double)this.yCoord + 0.5D, (double)this.zCoord + 0.5D, "random.chestclosed", 0.5F, this.worldObj.rand.nextFloat() * 0.1F + 0.9F);
            }

            if(this.lidAngle < 0.0F)
            {
                this.lidAngle = 0.0F;
            }
        }
    }

Il s’agit de la même que celle du coffre, un peu simplifier sans les codes qui vérifient s’il y a deux coffres l’un à côté de l’autre.
Petite explication : Si le monde n’est pas distant (donc monde serveur) que le nombre de joueur n’est pas égale à 0, et que le que résultat de la division euclidienne de l’addition de toutes les coordonnées + le temps de tick par 200 est égale à 0 (ce code permet de ne pas vérifier à chaque tick, car si c’était le cas le code en dessous surchargerai le jeu et causerai des lags) alors on crée une liste de tous les joueurs sur un rayon de 5 blocs autour du bloc. Si le joueur a ouvert le bloc, on l’ajoute, augmentant le nombre de joueurs qui ont ouvert le coffre.
Le reste du code sert à jouer le son et à calculer l’angle.

Pour finir, allez dans le code de votre TileEntitySpecialRenderer. Dans la méthode renderTileEntityCupboardeAt ajoutez avant le “this.model.render(0.0625F);” une condition qui vérifie que le TileEntityCupboard n’est pas null (dans le cas du rendu dans l’inventaire il sera null, donc il faut mettre un “null check” pour ne pas avoir un crash avec un NullPointerException). Si vous avez suivi le bonus pour rendre votre bloc orientable, vous en avez déjà un.

    if(te != null)
    {
    }

Dans cette condition, ajoutez le code suivant :

    float angle = te.prevLidAngle + (te.lidAngle - te.prevLidAngle) * tick;
    angle = 1.0F - angle;
    angle = 1.0F - angle * angle * angle;
    this.model.doorLeft.rotateAngleY = (angle * (float)Math.PI / 2.0F);
    this.model.doorRight.rotateAngleY = -(angle * (float)Math.PI / 2.0F);

Les trois premières lignes vont nous donner une variable angle qui correspond à ce dont nous avons besoin. Une fois de plus, ce code vient du coffre. Ensuite je modifie l’angle de rotation Y (car mes portières sont sur l’axe Y, le coffre tourne sur l’axe X) en fonction de l’angle et de p/2. Ceux qui ont fait de la trigonométrie savent pourquoi on utilise p/2, pour ceux qui en n’ont pas fait, c’est tout simplement car p/2 correspond à 1/4 du périmètre d’un cercle (et que nous voulons faire une rotation de 1/4 de cercle). C’est quelque chose qui se voit en fin seconde, si vous être curieux : http://www.methodemaths.fr/cercle_trigo.php

Vous avez pu constatez que dans un cas j’utilise une valeur négative (droite) et dans l’autre une valeur positive (gauche), la raison est que les deux portières s’ouvrent à l’opposé. Pour ceux qui n’aurait pas compris doorLeft et doorRight sont les noms de mes morceaux de modèle.

Pour les réglages d’angles, je vous conseils d’utiliser le debug plutôt que le run normal, ainsi vous pouvez modifier et voir les changements en direct sans relancer le jeu à chaque fois.
Lancer en debug

Mon renderTileEntityCupboard ressemble maintenant à ça :

    public void renderTileEntityCupboardeAt(TileEntityCupboard te, double x, double y, double z, float tick)
    {
        GL11.glPushMatrix();
        GL11.glTranslated(x + 0.5F, y + 1.5F, z + 0.5F);
        this.bindTexture(textureLocation);
        GL11.glRotatef(180F, 0.0F, 0.0F, 1.0F);
        if(te != null)
        {
            GL11.glRotatef(90F * te.getDirection(), 0.0F, 1.0F, 0.0F);
            float angle = te.prevLidAngle + (te.lidAngle - te.prevLidAngle) * tick;
            angle = 1.0F - angle;
            angle = 1.0F - angle * angle * angle;
            this.model.doorLeft.rotateAngleY = (angle * (float)Math.PI / 2.0F);
            this.model.doorRight.rotateAngleY = -(angle * (float)Math.PI / 2.0F);
        }
        GL11.glRotatef(90F, 0.0F, 1.0F, 0.0F);
        this.model.render(0.0625F);
        GL11.glPopMatrix();
    }

Et maintenant il ne me reste plus qu’à lancer mon jeu, et voilà le résultat :
Porte ouverte Porte semi-ouverte
Voir le commit sur github

Animation de la machine

=> Cette partie n’a finalement jamais été redigé …

Voir le commit sur github