Utiliser les événements


  • Moddeurs confirmés Rédacteurs Administrateurs

    Lors du développement de la version 2.0 du mod Buildcraft, SpaceToad a voulu ajouter du pétrole. Or, pour ajouter un fluide, il faut modifier la classe du seau de Minecraft, ce qui aurait causé une incompatibilité avec les autres mods voulant ajouter un fluide. Pour corriger ce problème, SpaceToad a eu l'idée de créer une API qui modifie les classes de Minecraft et permettrait aux mods utilisant cette API d'ajouter des fluides sans modifier les classes de Minecraft. C'est ainsi que Forge a été créé. En effet, la fonctionnalité pour les seaux était la première de Forge (le 4ème commit sur github)
    Depuis, un système d’événement a été mis en place de façon plus optimisée. Les événements font donc partie des fonctionnalités les plus importantes de Forge, puisqu'ils permettent de faire des choses qui serait normalement impossibles sans modifier les classes de Minecraft. De plus, dans les versions récentes, ils sont présents presque partout.

    Une liste des différents événements est disponible à cette adresse (liste non-exhaustive, car elle n'est pas mise à jour systématiquement).

    Sommaire du tutoriel

    Pré-requis

    Principe de fonctionnement

    Le principe des événements est simple : à un endroit du code de Minecraft, Forge va déclencher un événement associé. Les mods vont pouvoir souscrire à ces événements à l'aide d'une méthode, qui sera donc appelée lors du déclenchement de ce dernier, ce qui permet alors au mod d'effectuer diverses actions. Certains événements peuvent être annulés et, dans ce cas, la logique de Minecraft associée ne sera pas effectuée.

    schema-event.jpg

    Les événements disposent d'un système de priorité, un mod écoutant un événement en EventPriority.HIGHEST verra sa méthode appelée avant celle d'un mod ne précisant rien (NORMAL par défaut). Les priorités sont les suivantes :

        HIGHEST, // la première a être exécuté
        HIGH,
        NORMAL,
        LOW,
        LOWEST; // la dernière a être exécuté
    

    Note

    Il existe aussi plusieurs bus. MinecraftForge.EVENT_BUS est le bus sur lequel la presque totalité des événements sont déclenchés, c'est principalement sur celui qu'il faudra souscrire. Il existe également FMLJavaModLoadingContext.get().getModEventBus() qui est le bus spécifique au mod Java. Il est utilisé pour tout ce qui touche au registre (événements d'enregistrement d'entité, de bloc, d'item, etc.).

    Souscrire à un événement

    La souscription à un événement passera toujours par une méthode prenant en argument l'événement en question (et aucun autre argument). Cette méthode peut avoir n'importe quel nom, seul le premier paramètre a son importance :

    public void onEntityDeath(LivingDeathEvent event) {
        // ok
    }
    
    public void myMethod(LivingDeathEvent event) {
        // ok
    }
    
    public void onEntityDeath(LivingDeathEvent event, EntityPlayer player) {
        // MAUVAIS ! Seul un événement peut être mis en argument !
    }
    

    Ensuite, en fonction de si votre méthode est statique ou non, 2 ou 3 options se présentent pour enregistrer la méthode. La première permet d'enregistrer les méthodes une par une, tandis que les deux autres indiquent qu'une classe entière contient des méthodes d'événements.

    Méthode statique

    L'enregistrement de fonction au cas par cas se fait en appelant la méthode addListener du bus voulu, suivie de la priorité (optionnelle) et de la méthode sous la forme NomDeLaClasse::nomDeLaMethode :

    MinecraftForge.EVENT_BUS.addListener(EventPriority.HIGH, ModTutorial::onEntityDeath);
    

    Et la méthode d'événement associée :

    public static void onEntityDeath(LivingDeathEvent event) {
         // instructions ...
    }
    

    La seconde façon permet d'enregistrer une classe entière contenant des événements :

    MinecraftForge.EVENT_BUS.register(ModTutorialEvents.class);
    

    Grace à cet enregistrement, Forge va chercher toutes les fonctions d'événements de la classe ModTutorialEvents. Encore faut-il lui indiquer lesquels il s'agit. Pour cela il faut utiliser l'annotation @SubscribeEvent :

        @SubscribeEvent
        public static void onEntityDeath(LivingDeathEvent event) {
            // instructions ...
        }
    
        @SubscribeEvent(priority = EventPriority.HIGHEST) // on peut également préciser la priorité comme cela
        public static void onEntityAttack(LivingAttackEvent event) {
            // instructions ...
        }
    

    Mais dans certains cas, nous n'avons pas la possibilité d'utiliser addListener ni register car certains événements sont appelés avant même que la classe principale du mod soit construite. On peut alors utiliser l'annotation de classe @EventBusSubscriber :

    package dev.mff.modtutorial;
    
    import net.minecraftforge.event.entity.living.LivingDeathEvent;
    import net.minecraftforge.eventbus.api.EventPriority;
    import net.minecraftforge.eventbus.api.SubscribeEvent;
    import net.minecraftforge.fml.common.Mod.EventBusSubscriber;
    
    @EventBusSubscriber(modid = ModTutorial.MOD_ID)
    public class ModTutorialEvents {
    
        @SubscribeEvent(priority = EventPriority.HIGHEST)
        public static void onEntityDeath(LivingDeathEvent event) {
            // instructions ...
        }
    }
    

    Celle-ci a exactement le même effet qu'utiliser la fonction register sur une classe. La syntaxe peut simplement être plus pratique et permet d'enregistrer une classe d'événement dès sa découverte par Forge et donc avant l'instanciation de la classe principale.

    Par défaut, @EventBusSubscriber enregistre avec le bus de Forge.EVENT_BUS, mais on peut également le changer (nécessaire pour l'enregistrement de bloc, item, entité, etc) :

    @EventBusSubscriber(modid = ModTutorial.MOD_ID, bus = Bus.MOD)
    

    On peut aussi cibler une distribution (client ou serveur dédié, par défaut ce sont les deux). Cela peut se montrer utile pour les événements étant présents que sur une distribution, par exemple les événements de rendu qui ne sont que sur le client :

    @EventBusSubscriber(modid = ModTutorial.MOD_ID, value = Dist.CLIENT)
    public class ModTutorialEvents {
    
        @SubscribeEvent
        public static void onOverlay(RenderGameOverlayEvent.Text event) {
            event.getLeft().add("Test d'overlay");
        }
    }
    

    Méthode non statique

    Comme avant, on peut enregistrer les méthodes une par une. Seule la façon de préciser la méthode diffère : au lieu d'indiquer la classe, on met l'instance de la classe :

    MinecraftForge.EVENT_BUS.addListener(EventPriority.HIGH, this::onEntityDeath);
    

    Et la méthode d'événement associée, qui devra donc se trouver dans la même classe :

    public void onEntityDeath(LivingDeathEvent event) {
         // instructions ...
    }
    

    La deuxième méthode passe par la fonction register et, à nouveau, seul l'argument diffère :

    MinecraftForge.EVENT_BUS.register(new ModTutorialEvents());
    

    Les méthodes d'événements devront également avoir l'annotation @SubscribeEvent comme vu précédemment. Pensez, par contre, à ne pas mettre le mot clé static dans ce cas.

    Naturellement, il n'y a pas d'équivalent à EventBusSubscriber pour les méthodes non statiques, Forge ne sachant pas comment instancier votre classe contenant les événements.

    Exemples d'utilisation

    Les possibilités offertes par les différents événements étant trop nombreuses, il n'est pas possible de toutes les présenter dans un tutoriel. Afin que ce tutoriel ne soit pas trop abstrait et uniquement théorique, je vous propose quelques exemples d'utilisations d'événements.

    Modifier le comportement des explosions

    Forge propose deux événements en rapport avec les explosions, ExplosionEvent.Start déclenché avant l'explosion ce qui permet de l'annuler et ExplosionEvent.Detonate qui est déclenché après avoir calculé les dégâts et avant de les annuler. Pour cet exemple le second sera utilisé, le but ici étant de modifier le comportement de l'explosion en excluant un bloc (les lampes) et les creepers.

        public void onExplosion(ExplosionEvent.Detonate event) {
            event.getAffectedBlocks().removeIf(pos -> event.getWorld().getBlockState(pos).getBlock() == Blocks.REDSTONE_LAMP);
            event.getAffectedEntities().removeIf(e -> e instanceof EntityCreeper);
        }
    

    L'événement nous permet d'accéder à la liste des blocs (liste de BlockPos) et des entités affectés et avec la fonction removeIfdes listes de Java 8 on peut facilement retirer un élément s'il respecte une condition.
    On peut trouver plein d'autres utilités à cet événement, comme logger la liste des blocs détruits, empêcher la destruction de blocs dans une zone donnée, etc.

    Note

    Cet événement a été enregistré en utilisant MinecraftForge.EVENT_BUS.addListener(EventPriority.HIGH, this::onExplosion);

    Modifier la croissance des plantes

    Certains événements ont un résultat, ils sont remarquables grâce à l'annotation @HasResult. C'est par exemple le cas de BlockEvent.CropGrowEvent.Pre. La javadoc nous indique qu'un résultat DEFAULT garde le comportement normal, ALLOW force la croissance de la plante et DENY l'empêche.
    Rendons la croissance des cactus impossible hormis dans le désert :

    	public void onCropGrow(BlockEvent.CropGrowEvent.Pre event) {
    	    if (event.getState().getBlock() == Blocks.CACTUS && event.getWorld().getBiome(event.getPos()) != Biomes.DESERT) {
    	        event.setResult(Result.DENY);
    	    }
    	}
    

    (L'événement dans cet exemple est enregistré comme le précédent, d'où l'absence de l'annotation @SubscribeEvent).

    Texte sur le HUD et message de chat

    Certains événements ne sont que présents sur le client. Ils sont facilement identifiables par le fait qu'ils se trouvent en général dans le package net.minecraftforge.client.event. Ils concernent souvent le rendu, l'affichage d'éléments ou la gestion des entrées clavier / souris.

    RenderGameOverlayEvent, par exemple est un événement client qui est déclenché lors du rendu du HUD de Minecraft et dispose de plusieurs sous-classes (.Pre déclenché avant le rendu de l'overlay, .Post pour après et .Text pour l'affichage de texte). Ici, il va être utilisé pour afficher un texte en haut à gauche du HUD.

    Afin de vous montrer un exemple d'événement annulé, je vous propose aussi un exemple d'utilisation de ClientChatReceivedEvent, qui est déclenché lorsque le client reçoit un message de chat, juste avant de l'afficher. L'exemple qui suit sert à empêcher l'affichage du message de connexion d'un joueur au serveur.

    package dev.mff.modtutorial;
    
    import net.minecraft.util.text.TextComponentTranslation;
    import net.minecraftforge.api.distmarker.Dist;
    import net.minecraftforge.api.distmarker.OnlyIn;
    import net.minecraftforge.client.event.ClientChatReceivedEvent;
    import net.minecraftforge.client.event.RenderGameOverlayEvent;
    import net.minecraftforge.client.event.RenderGameOverlayEvent.ElementType;
    import net.minecraftforge.eventbus.api.SubscribeEvent;
    import net.minecraftforge.fml.common.Mod.EventBusSubscriber;
    
    @EventBusSubscriber(modid = ModTutorial.MOD_ID, value = Dist.CLIENT)
    @OnlyIn(Dist.CLIENT)
    public class ModTutorialClientEvents {
    
        @SubscribeEvent
        public static void onOverlay(RenderGameOverlayEvent.Text event) {
            event.getLeft().add("MFF");
        }
    
        @SubscribeEvent
        public static void onMessage(ClientChatReceivedEvent event) {
            if (event.getMessage() instanceof TextComponentTranslation) { // si le message est un texte de traduction
                TextComponentTranslation translation = (TextComponentTranslation) event.getMessage();
                if (translation.getKey().equals("multiplayer.player.joined")) { // et qu'il s'agit du message de connexion
                    event.setCanceled(true); // on annule, le message ne s'affichera donc pas dans le chat
                }
            }
        }
    }
    

    Note

    Pour savoir si un événement peut être annulé, il suffit de regarder s'il a l'annotation @Cancelable. Appeler la fonction event.setCanceled sur un événement qui ne peut pas être annulé causera un crash.

    Résultat

    2019-04-16_21.56.13.png

    Les cactus ne poussent plus, la TNT ne détruit plus les lampes et MFF s'affiche en haut à gauche. Tout est bon !

    Les différentes modifications du code sont retrouvables sur le commit Github suivant : https://github.com/MinecraftForgeFrance/mod-tutorial-1.13.2/commit/1a2e732cf4d9588737ff6467b53698a4b3f2ce8e

    Licence et attribution

    Creative Commons

    Ce tutoriel rédigé par @robin4002 corrigé par @BrokenSwing et @DiabolicaTrix et publié sur 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

    retour Sommaire des tutoriels


Log in to reply