Une boussole
-
Sommaire
Introduction
Bonjour à tous, dans ce tutoriel je vais vous apprendre à faire une boussole qui pointe vers une position donnée.
Ce code peut avoir plusieurs utilités : avoir une boussole qui pointe vers un autre joueur dont la position est connue, vers un biome, un minerai, un spawner, …
Je n’utiliserai pour ce code que les coordonnées en X et Z, on pourrait le faire également avec X et Y, ou Z et Y, mais je ne vois pas l’intérêt d’utiliser les coordonnées Y.
On pourrait également utiliser les trois coordonnées, il suffit juste de faire une boussole en 3D également, mais on perd une partie de réalisme après.Pré-requis
- Créer un item simple
- Trigonométrie dans un triangle rectangle - Je n’ai pas lu ce cours, mais il me semble contenir le bagage nécessaire pour ce tutoriel.
- Cercle trigonométrique - Idem pour celui-ci.
Code
Pour la suite, nous allons avoir besoin de créer un objet. Je vais l’appeler ItemTutorialCompass, et donc créer une classe appropriée.
ItemCustomCompass, la classe de la boussole :
Vous devriez avoir une classe similaire, pour démarrer :
package fr.minecraftforgefrance.tutorial.items; import net.minecraft.creativetab.CreativeTabs; import net.minecraft.item.Item; public class ItemTutorialCompass extends Item { public static final String NAME = "tutorial_compass"; public ItemTutorialCompass() { super(); TutorialItems.setItemName(this, NAME); setCreativeTab(CreativeTabs.TOOLS); setMaxStackSize(1); } }
Tout ce que nous avons à faire, c’est déterminer l’angle de l’aiguille de la boussole, il faut bien lui dire dans quel sens elle va tourner. Vous allez donc ajouter, à la suite de setMaxStackSize(1), ceci :
addPropertyOverride(new ResourceLocation("angle"), new IItemPropertyGetter() {});
Cette ligne signifie que nous allons écraser la propriété “angle” déjà existante, avec un nouvel IItemPropertyGetter, que nous allons définir entre crochets.
Déjà, ici, vous devriez avoir plusieurs erreurs. Importez net.minecraft.util.ResourceLocation; et net.minecraft.item.IItemPropertyGetter;
IItemPropertyGetter() devrait être encore souligné en rouge, vous indiquant une erreur. Ajoutez les méthodes non implémentées. Pour la suite de ce tutoriel, sauf indication contraire, vous resterez entre les deux crochets, pour définir notre IItemPropertyGetter.Nous allons juste modifer quelques parties du code généré.
- Ajoutez
@SideOnly(Side.CLIENT)
au-dessus de
@Override
World worldin
devient
World world
EntityLivingBase entityIn
devient
EntityLivingBase entityLiving
À vous d’utiliser ou non ces modifications, mais elles seront présentes dans la suite du code.
Voici le code que vous devriez avoir, avec des noms différents :
package fr.minecraftforgefrance.tutorial.items; import net.minecraft.creativetab.CreativeTabs; import net.minecraft.entity.EntityLivingBase; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.util.ResourceLocation; import net.minecraft.world.World; import net.minecraft.item.IItemPropertyGetter; public class ItemTutorialCompass extends Item { public static final String NAME = "tutorial_compass"; public ItemTutorialCompass() { super(); TutorialItems.setItemName(this, NAME); setCreativeTab(CreativeTabs.TOOLS); setMaxStackSize(1); addPropertyOverride(new ResourceLocation("angle"), new IItemPropertyGetter() { @SideOnly(Side.CLIENT) @Override public float apply(ItemStack stack, World world, EntityLivingBase entityLiving) { // TODO Auto-generated method stub return 0; } }); } }
La méthode apply sera la plus importante, c’est elle qui va retourner la valeur et donc diriger notre aiguille. Actuellement, elle ne renvoie que 0, laissant l’aiguille fixe, peu importe ce qu’il se passe.
Vous pouvez retirer le commentaire généré automatiquement. Ensuite, juste au-dessus de la fonction apply, ajoutez ceci :@SideOnly(Side.CLIENT) double rotation; @SideOnly(Side.CLIENT) double rota; @SideOnly(Side.CLIENT) long lastUpdateTick;
Ces trois variables nous serviront par la suite.
Placez-vous maintenant dans la fonction apply.
if (entityLiving == null && !stack.isOnItemFrame()) return 0.0F;
Cette ligne dit que, si l’objet n’est pas dans l’inventaire d’une entité vivante (soit, en gros, un joueur) et qu’il n’est pas sur un tableau (ItemFrame), on renvoie l’angle 0.0F (lorsque l’aiguille pointe vers le haut). Par exemple, lorsque l’item est au sol, sous forme de drop.
Ensuite, nous savons que la boussole est soit dans un inventaire, soit sur un itemframe. Nous allons vérifier ça avec la ligne qui suit.
final boolean entityExists = entityLiving != null;
Cette variable vaudra true si elle est à l’intérieur d’un inventaire, false si elle est dans un itemframe.
Nous allons utiliser, pour la suite, une Entity qui sera soit l’itemframe, soit le joueur.
final Entity entity = (Entity) (entityExists ? entityLiving : stack.getItemFrame());
Une condition basique, avec un cast vers la classe Entity. Littéralement, “si l’entité est un joueur, prend la valeur de entityLiving, sinon, prend l’itemframe dans lequel la boussole est contenue”.
if (world == null) world = entity.world;
Nous vérifions ici que le monde soit bien existant. Sinon, on prend le monde de l’entité. Vous pouvez d’ailleurs, grâce à ça, faire des modifications à la fonction pour vous assurer que la boussole se trouve bien dans le monde que vous souhaitez. Je ne vous montre ici qu’une boussole basique, mais vous pouvez l’améliorer à souhait !
Voici la fonction que vous devriez avoir actuellement :
@SideOnly(Side.CLIENT) @Override public float apply(ItemStack stack, World world, EntityLivingBase entityLiving) { if (entityLiving == null && !stack.isOnItemFrame()) return 0.0F; final boolean entityExists = entityLiving != null; final Entity entity = (Entity) (entityExists ? entityLiving : stack.getItemFrame()); if (world == null) world = entity.world; return 0; }
double rotation = entityExists ? (double) entity.rotationyaw : getFrameRotation((EntityItemFrame) entity); rotation %= 360.0D;
Nous créons une nouvelle variable rotation (différente de celle créée précédemment, nous l’utiliserons dans une autre fonction) que nous initialisons à la direction dans laquelle le joueur regarde, ou la direction dans laquelle l’itemframe est tourné.
La variable yaw n’a aucune limite, si le joueur tourne sur lui-même, elle peut dépasser 360. Nous faisons donc un modulo 360 pour avoir la valeur avec un angle en degrés comprise entre -360 et 359 (360 = 0). Nous avons maintenant l’angle dans lequel le joueur regarde, en degrés.Créez la fonction getFrameRotation, comme ceci :
@SideOnly(Side.CLIENT) private double getFrameRotation(EntityItemFrame itemFrame) { return (double) MathHelper.clampAngle(180 + itemFrame.facingDirection.getHorizontalIndex() * 90); }
clampAngle renvoie un angle dans l’intervalle [-180;180[ avec la valeur passée en paramètre. Ici, nous prenons la valeur de l’orientation de l’itemframe, qui est une valeur entre 0 et 3, dans l’ordre S-W-N-E (Sud Ouest Nord Est), et on la multiplie par 90 pour avoir un angle en degrés. On ajoute 180 pour avoir l’ordre N-E-S-W (Nord Est Sud Ouest), et on passe le résultat en paramètre. La fonction getFrameRotation donne donc l’orientation de l’itemframe en degrés, entre -180 et 179.
Retournez maintenant dans la fonction apply et ajoutez cette ligne :
double adjusted = Math.PI - ((rotation - 90.0D) * (2 * Math.PI / 360) - getAngle(world, entity, stack));
On va essayer de comprendre ce qui se trouve ici. En premier, nous avons Math.PI. Si vous vous souvenez vos cours sur les cercles trigonométriques, PI est situé sur l’axe des abscisses, en -1. Donc on peut considérer que l’aiguille pointe vers PI lorsqu’elle pointer vers l’ouest. Nous avons donc adjusted selon l’ouest.
Ce qui se trouve entre parenthèses se trouve selon l’est, donc au final, faire OUEST - EST va donner NORD. adjusted est donc exprimé en fonction du nord.
(rotation - 90.0D) change l’angle que nous avons défini avant en utilisant l’est et non le nord, car la fonction getAngle va renvoyer un angle suivant l’axe EST.Pour des raisons de clarté, j’ai choisi d’écrire (2* Math.PI / 360), pour vous montrer que nous ne faisons qu’une simple conversion DEG -> RAD. Vous pouvez remplacer cette valeur par 0.01745329238474369D, une approximation suffisamment précise pour être utilisée à la place de la valeur exacte. Vous gagnerez légèrement en performances. adjusted est donc la différence entre les deux angles, en radians.
Nous allons maintenant créer la fonction getAngle().
@SideOnly(Side.CLIENT) private double getAngle(World world, Entity entity, ItemStack stack) { return Math.atan2((double) blockZ - entity.posZ, (double) blockX - entity.posX); }
Ce code ne nécessite pas réellement d’explications, c’est de la trigonométrie basique. Je vous conseille de prendre un papier et un crayon, de faire deux axes X et Z, et de regarder comment faire la trigonométrie, si vous ne comprenez pas réellement.
La fonction arctan est définie comme ça :double atan2(double y, double x)
Je vous cache les détails de l’implémentation, ils ne sont pas utiles, vous devez juste comprendre que le y est le premier argument, et le x le second. Ici, notre axe Z fait office d’axe y.
Je vous laisse retourner à l’endroit où nous avons déclaré nos variables rotation, rota et lastUpdateTick, et je vous laisse ajouter ces deux déclarations :
double blockX = 0; double blockZ = 0;
La boussole pointera vers le point (0,y,0).
Vous pouvez changer ces deux variables selon l’endroit vers lequel vous souhaitez que la boussole pointe.if (entityExists) adjusted = wobble(world, adjusted);
Cette ligne permet de donner un effet de tremblement lorsque le joueur tourne sur lui-même. Si vous faites attention, l’aiguille tremble lorsque vous tournez rapidement sur vous-même. C’est la fonction wobble qui va s’en occuper.
@SideOnly(Side.CLIENT) private double wobble(World world, double amount) { if (world.getTotalWorldTime() != lastUpdateTick) { lastUpdateTick = world.getTotalWorldTime(); double d0 = amount - rotation; d0 %= Math.PI * 2D; d0 = MathHelper.clamp(d0, -1.0D, 1.0D); rota += d0 * 0.1D; rota *= 0.8D; rotation += rota; } return rotation; }
Voici la fonction wobble. Ne vous inquiétez pas, nous allons la décortiquer.
if (world.getTotalWorldTime() != lastUpdateTick) {
Cette condition permet de ne pas mettre à jour l’aiguille plusieurs fois par tick.
lastUpdateTick = world.getTotalWorldTime();
Cette ligne change le dernier tick de mise à jour, pour éviter de mettre à jour l’aiguille plusieurs fois par tick.
double d0 = amount - rotation;
Cette fois, nous utilisons la variable rotation que nous avons définie en début de tutoriel.
d0 est donc la différence entre la rotation actuelle et celle voulue.d0 %= Math.PI * 2D;
Modulo 2PI, un tour complet.
d0 = MathHelper.clamp(d0, -1.0D, 1.0D);
On clamp la valeur entre -1.0 et 1.0, pour limiter la vitesse de rotation à 1 radian.
rota += d0 * 0.1D; rota *= 0.8D;
On ajoute 10% du résultat et on baisse le nouveau résultat par 20%. Plus l’angle à parcourir est grand, plus la vitesse est importante.
rotation += rota;
On ajoute le résultat.
Maintenant, retournons dans la fonction apply. Nous en avons presque terminé avec elle, il suffit juste de retourner la bonne valeur.
Nous allons tout d’abord prendre le nombre de tours à faire :final float f = (float) (adjusted / (Math.PI * 2D));
Puis retourner le résultat, appartenant à [0; 1[
return MathHelper.positiveModulo(f, 1.0F);
Félicitations, vous avez une boussole qui tourne ! Cependant, il nous reste à configurer les textures, pour que le jeu sache comment afficher la boussole.
Vous devriez avoir un code ressemblant à celui-ci :
package fr.minecraftforgefrance.tutorial.items; import net.minecraft.creativetab.CreativeTabs; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.item.EntityItemFrame; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.util.ResourceLocation; import net.minecraft.util.math.MathHelper; import net.minecraft.world.World; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; import net.minecraft.item.IItemPropertyGetter; public class ItemTutorialCompass extends Item { public static final String NAME = "tutorial_compass"; public ItemTutorialCompass() { super(); TutorialItems.setItemName(this, NAME); setCreativeTab(CreativeTabs.TOOLS); setMaxStackSize(1); addPropertyOverride(new ResourceLocation("angle"), new IItemPropertyGetter() { @SideOnly(Side.CLIENT) double rotation; @SideOnly(Side.CLIENT) double rota; @SideOnly(Side.CLIENT) long lastUpdateTick; double blockX = 0; double blockZ = 0; @SideOnly(Side.CLIENT) @Override public float apply(ItemStack stack, World world, EntityLivingBase entityLiving) { if (entityLiving == null && !stack.isOnItemFrame()) return 0.0F; final boolean entityExists = entityLiving != null; final Entity entity = (Entity) (entityExists ? entityLiving : stack.getItemFrame()); if (world == null) world = entity.world; double rotation = entityExists ? (double) entity.rotationYaw : getFrameRotation((EntityItemFrame) entity); rotation %= 360.0D; double adjusted = Math.PI - ((rotation - 90.0D) * 0.01745329238474369D - getAngle(world, entity, stack)); if (entityExists) adjusted = wobble(world, adjusted); final float f = (float) (adjusted / (Math.PI * 2D)); return MathHelper.positiveModulo(f, 1.0F); } @SideOnly(Side.CLIENT) private double getFrameRotation(EntityItemFrame itemFrame) { return (double) MathHelper.clampAngle(180 + itemFrame.facingDirection.getHorizontalIndex() * 90); } @SideOnly(Side.CLIENT) private double getAngle(World world, Entity entity, ItemStack stack) { return Math.atan2((double) blockZ - entity.posZ, (double) blockX - entity.posX); } @SideOnly(Side.CLIENT) private double wobble(World world, double amount) { if (world.getTotalWorldTime() != lastUpdateTick) { lastUpdateTick = world.getTotalWorldTime(); double d0 = amount - rotation; d0 %= Math.PI * 2D; d0 = MathHelper.clamp(d0, -1.0D, 1.0D); rota += d0 * 0.1D; rota *= 0.8D; rotation += rota; } return rotation; } }); } }
Les textures et modèles :
Bien, nous avons une boussole fonctionnelle, certes. Mais maintenant, nous devons faire MIEUX. Une boussole fonctionnelle avec une texture. Parce que bon, avoir la boussole qui pointe quelque part c’est bien, mais voir vers où elle pointe, c’est mieux, non ?
Dans la suite, nom_de_la_texture devra être remplacé par le nom de votre texture.
Pour la suite, je vais utiliser les fichiers basiques que Minecraft nous donne, mais on va légèrement les modifier. Vous aurez besoin, au total, de 32 .json et 32 .png, nommés comme ceci :
nom_de_la_texture.json
Ce sera notre .json de base, et nous en avons 31 autres, nommées
nom_de_la_texture_xx.json
Avec xx un nombre entre 00 et 31, en omettant le numéro 16.
Pour les .png, vous allez avoir 32 fichiers .png nommés comme ceci :
nom_de_la_texture_xx.png
Avec xx un nombre entre 00 et 31, 16 inclus.
Pour les .json, je vais vous donner le contenu de nom_de_la_texture.json et nom_de_la_texture00.json, les autres .json seront créés en suivant le même format.
{ "parent": "item/generated", "textures": { "layer0": "modid:items/nom_de_la_texture_16" }, "overrides": [ { "predicate": { "angle": 0.000000 }, "model": "modid:item/nom_de_la_texture" }, { "predicate": { "angle": 0.015625 }, "model": "modid:item/nom_de_la_texture_17" }, { "predicate": { "angle": 0.046875 }, "model": "modid:item/nom_de_la_texture_18" }, { "predicate": { "angle": 0.078125 }, "model": "modid:item/nom_de_la_texture_19" }, { "predicate": { "angle": 0.109375 }, "model": "modid:item/nom_de_la_texture_20" }, { "predicate": { "angle": 0.140625 }, "model": "modid:item/nom_de_la_texture_21" }, { "predicate": { "angle": 0.171875 }, "model": "modid:item/nom_de_la_texture_22" }, { "predicate": { "angle": 0.203125 }, "model": "modid:item/nom_de_la_texture_23" }, { "predicate": { "angle": 0.234375 }, "model": "modid:item/nom_de_la_texture_24" }, { "predicate": { "angle": 0.265625 }, "model": "modid:item/nom_de_la_texture_25" }, { "predicate": { "angle": 0.296875 }, "model": "modid:item/nom_de_la_texture_26" }, { "predicate": { "angle": 0.328125 }, "model": "modid:item/nom_de_la_texture_27" }, { "predicate": { "angle": 0.359375 }, "model": "modid:item/nom_de_la_texture_28" }, { "predicate": { "angle": 0.390625 }, "model": "modid:item/nom_de_la_texture_29" }, { "predicate": { "angle": 0.421875 }, "model": "modid:item/nom_de_la_texture_30" }, { "predicate": { "angle": 0.453125 }, "model": "modid:item/nom_de_la_texture_31" }, { "predicate": { "angle": 0.484375 }, "model": "modid:item/nom_de_la_texture_00" }, { "predicate": { "angle": 0.515625 }, "model": "modid:item/nom_de_la_texture_01" }, { "predicate": { "angle": 0.546875 }, "model": "modid:item/nom_de_la_texture_02" }, { "predicate": { "angle": 0.578125 }, "model": "modid:item/nom_de_la_texture_03" }, { "predicate": { "angle": 0.609375 }, "model": "modid:item/nom_de_la_texture_04" }, { "predicate": { "angle": 0.640625 }, "model": "modid:item/nom_de_la_texture_05" }, { "predicate": { "angle": 0.671875 }, "model": "modid:item/nom_de_la_texture_06" }, { "predicate": { "angle": 0.703125 }, "model": "modid:item/nom_de_la_texture_07" }, { "predicate": { "angle": 0.734375 }, "model": "modid:item/nom_de_la_texture_08" }, { "predicate": { "angle": 0.765625 }, "model": "modid:item/nom_de_la_texture_09" }, { "predicate": { "angle": 0.796875 }, "model": "modid:item/nom_de_la_texture_10" }, { "predicate": { "angle": 0.828125 }, "model": "modid:item/nom_de_la_texture_11" }, { "predicate": { "angle": 0.859375 }, "model": "modid:item/nom_de_la_texture_12" }, { "predicate": { "angle": 0.890625 }, "model": "modid:item/nom_de_la_texture_13" }, { "predicate": { "angle": 0.921875 }, "model": "modid:item/nom_de_la_texture_14" }, { "predicate": { "angle": 0.953125 }, "model": "modid:item/nom_de_la_texture_15" }, { "predicate": { "angle": 0.984375 }, "model": "modid:item/nom_de_la_texture" } ] }
Pensez également à remplacer modid par votre modid.
Pour chacun de vos fichiers nom_de_votre_texture_xx.json, utilisez ce code, en remplaçant le nombre par celui écrit dans le nom de fichier.
{ "parent": "item/generated", "textures": { "layer0": "items/compass_00" } }
Je vais légèrement expliquer, même si le code est relativement simple.
Le .json principal fait que, par défaut, la texture utilisée est la numéro 16, d’où l’absence de .json avec le numéro 16. Selon l’angle donné par la fonction précédemment définie, on va appeler le .json correspondant, affichant la texture correcte. La texture est en fait identique, avec juste la flèche à l’intérieur qui change de position.Vous pouvez désormais lancer le jeu, vous rapprocher du point (0, y, 0) et vous verrez la boussole bouger !
Résultat
Crédits
Rédaction :
Correction :
Aide à la compréhension du code
Un énorme merci à eux, ils m’ont énormément aidé pour ce tutoriel.
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