Le système de capability ajouté par Forge est relativement simple et terriblement efficace. Il permet en effet d’exposer des interfaces aux autres mods. Par exemple Forge a créé des capabilities pour différentes choses, il met à disposition des capabilities pour jouer avec les liquides, les items ou encore l’énergie (entre autres). Mais vous pouvez vous aussi exposer votre propre capability ou l’implémentation d’une capability déjà existante.
C’est donc ce que nous allons voir dans ce tutoriel. Vous apprendrez à utiliser les capabilities déjà exposées par Forge, à créer votre propre capability et à l’exposer.
Avant de commencer à créer ou utiliser des capabilities il est important de comprendre comment celles-ci fonctionnent. Une capability est un objet permettant d’accéder à l’instance d’une interface pour un objet donné. Par exemple la capability CapabilityItemHandler.ITEM_HANDLER_CAPABILITY
permet d’accéder à une instance de IItemHandler
, tandis que la capability CapabilityEnergy.ENERGY
permet d’accéder à une instance de IEnergyStorage
.
Comme vous vous en doutez sûrement, vous ne pouvez cependant pas récupérer ces instances depuis n’importe quel objet, il faut en effet que l’objet soit un ICapabilityProvider
(fournisseur de capability, ça a du sens). Il existe plusieurs classes dans Minecraft qui implémentent cette interface, comme Entity
, Chunk
ou encore ItemStack
par exemple. On peut cependant implémenter cette interface nous même dans nos propres classes.
Donc pour résumer, les capabilities permettant d’exposer des interfaces. Muni d’une capability et d’une instance de ICapabilityProvider
on peut récupérer une instance de l’interface associée à la capability.
Voyons comment créer notre propre capability. Si vous avez correctement lu ce qui a été dit plus haut, une capability permet d’exposer une interface, nous allons donc faire notre interface.
Personnellement je veux implémenter un système d’épuisement dans le jeu, voici donc l’interface que je crée.
| |
| |
| |
| |
| public interface IExhaustable |
| { |
| |
| |
| |
| |
| |
| int getExhaustion(); |
| |
| |
| |
| |
| |
| |
| |
| void setExhaustion(int value); |
| |
| |
| |
| |
| |
| default void reduceExhaustion(int value) { |
| this.setExhaustion(this.getExhaustion() - value); |
| } |
| |
| |
| |
| |
| |
| default void increaseExhaustion(int value) { |
| this.setExhaustion(this.getExhaustion() + value); |
| } |
| } |
C’est une interface tout ce qu’il y a de plus simple.
La seconde étape est de créer une implémentation par défaut de notre capability. Encore une fois cela va être très simple, à l’image de notre interface :
| public class ExhaustionHolder implements IExhaustable |
| { |
| private int exhaustion = 0; |
| |
| @Override |
| public int getExhaustion() |
| { |
| return this.exhaustion; |
| } |
| |
| @Override |
| public void setExhaustion(int value) |
| { |
| this.exhaustion = clamp(value); |
| } |
| |
| private int clamp(int value) { |
| if(value > 10000) return 10000; |
| if(value < 0) return 0; |
| return value; |
| } |
| } |
Et la dernière chose dont nous avons besoin est une implémentation de Capability.IStorage
, celle-ci permet d’enregistrer les données relatives à notre interface dans des tags NBT afin de la sauvegarder (qu’on ne perde pas nos données quand quitte le monde). Nous allons faire ceci tout de suite et encore une fois c’est pas une implémentation compliquée.
| public static class DefaultExhaustionStorage implements Capability.IStorage<IExhaustable> { |
| |
| @Nullable |
| @Override |
| public INBTBase writeNBT(Capability<IExhaustable> capability, IExhaustable instance, EnumFacing side) |
| { |
| return new NBTTagInt(instance.getExhaustion()); |
| } |
| |
| @Override |
| public void readNBT(Capability<IExhaustable> capability, IExhaustable instance, EnumFacing side, INBTBase nbt) |
| { |
| instance.setExhaustion(((NBTTagInt)nbt).getInt()); |
| } |
| } |
Maintenant je vais créer une classe que je vais appeler CapabilityExhaustion
dans laquelle je vais rajouter une fonction permettant d’enregistrer ma capability :
| public static void register() |
| { |
| CapabilityManager.INSTANCE.register(IExhaustable.class, new DefaultExhaustionStorage(), ExhaustionHolder::new); |
| } |
Pour enregistrer une capability on doit donc appeler CapabilityManager#register
en passant l’interface que l’on veut exposer, notre implémentation de Capability.IStorage
ainsi que l’implémentation par défaut de notre interface.
Dans cette même classe je vais ajouter une variable statique qui contiendra l’instance de notre capability (celle permettant de récupérer l’instance de notre interface via une instance de ICapabilityProvider
, relisez la permière partie si vous êtes perdus).
| @CapabilityInject(IExhaustable.class) |
| public static final Capability<IExhaustable> EXHAUSTION_CAPABILITY = null; |
On crée donc une variable de type Capability
que l’on annote avec CapabilityInject
, cette annotation permettra à Forge de changer la valeur de la variable une fois notre capability enregistrée via le CapabilityManager
.
On va maintenant appeler notre fonction register
sinon notre capability ne sera jamais enregistrée. Pour cela je me rend dans la classe principale de mon mod et j’appelle ma fonction dans l’event FMLCommonSetupEvent
:
| @Mod(ModTutorial.MOD_ID) |
| public class ModTutorial { |
| |
| |
| |
| public ModTutorial() { |
| FMLJavaModLoadingContext.get().getModEventBus().addListener(this::setup); |
| |
| } |
| |
| private void setup(final FMLCommonSetupEvent event) { |
| |
| CapabilityExhaustion.register(); |
| } |
| |
| |
| } |
Voilà, nous avons créer notre capability et nous l’avons enregistrée. Cependant nous ne l’utilisons pas, et elle ne sert pas à grand chose. Rendez-vous dans la suite du tutoriel.
Afin qu’on puisse récupérer une capability depuis l’instance d’une classe, il faut que celle-ci implémente l’interface ICapabilityProvider
. Cette interface possède une méthode à implémenter (et une autre dont l’implémentation par défaut est suffisante donc nous nous en occuperont pas). La méthode de cette interface permet donc de récupérer une capability, cependant il faut savoir que ce n’est pas parce qu’une classe implémente ICapabilityProvider
qu’elle possède forcément votre capability (par exemple on imagine mal un ItemStack
être fatigué, il ne possèdera donc sûrement pas notre capability). Afin d’être sûr que vous soyez parfaitement conscient de ce que vous faites quand vous manipulez les capabilities, Forge demande de retourner un LazyOptional
qui doit être empty()
si l’instance ne possède pas la capability, ou alors of(instance)
où instance
est l’instance de l’interface associée à la capability demandée.
Dans le cas d’une classe qui nous appartient il est très simple d’implémenter cette interface mais imaginons que je veuille ajoute ma capability aux entités, je n’ai bien-heureusement pas la possibilité de modifier le code source de cette classe. Comme vous pouvez vous en douter Forge a ajouté un event pour pouvoir faire ce genre de choses. Cet event est AttachCapabilitiesEvent
, et nous ajoutons notre capability via AttachCapabilitiesEvent#addCapability(ResourceLocation, ICapabilityProvider)
. Comme vous pouvez le voir il nous faut là aussi une instance de ICapabilityProvider
donc nous allons implémenter une classe comme si vous voulions lui donner notre capability puis nous en passerons une instance à cette méthode. La ResourceLocation
est une clé permettant d’identifier de manière unique votre capability (elle sert pour la sauvegarde).
Nous allons donc créer une implémentation de ICapabilityProvider
pour notre capability, et plus précisement nous allons implémenter ICapabilitySerializable
afin que Forge sache qu’il faut sauvegarder notre capability.
Voici donc notre implémentation :
| public class PlayerExhaustionWrapper implements ICapabilitySerializable<INBTBase> |
| { |
| private IExhaustable holder = CapabilityExhaustion.EXHAUSTION_CAPABILITY.getDefaultInstance(); |
| private final LazyOptional<IExhaustable> lazyOptional = LazyOptional.of(() -> this.holder); |
| |
| @Nonnull |
| @Override |
| public <T> LazyOptional<T> getCapability(@Nonnull Capability<T> cap, @Nullable EnumFacing side) |
| { |
| return CapabilityExhaustion.EXHAUSTION_CAPABILITY.orEmpty(cap, lazyOptional); |
| } |
| |
| @Override |
| public INBTBase serializeNBT() |
| { |
| return CapabilityExhaustion.EXHAUSTION_CAPABILITY.writeNBT(this.holder, null); |
| } |
| |
| @Override |
| public void deserializeNBT(INBTBase nbt) |
| { |
| CapabilityExhaustion.EXHAUSTION_CAPABILITY.readNBT(holder, null, nbt); |
| } |
| } |
Il ne devrait rien à avoir de surprenant, je crée une instance de mon interface (celle par défaut) puis lorsque quelqu’un appelle getCapability
en passant en paramètre ma capability (CapabilityExhaustion.EXHAUSTION_CAPABILITY
) je retourne cette instance. Ce mécanisque de vérification est déjà fait pour nous dans Capability#orEmpty
car c’est un comportement que la majorité des moddeurs et Forge utilisent.
Pour ce qui est de serializeNBT
et deserializeNBT
, je redirige simplement les appels vers l’implémentation de Capability.IStorage
que j’ai founi lors de l’enregistrement de ma capability.
On peut donc maintenant fournir une instance de cette implémentation à l’event AttachCapabilitiesEvent
. Donc ma classe CapabilityExhaustion
que j’annote préalablement de @Mod.EventBusSubscriber(modid = ModTutorial.MOD_ID)
, j’ajoute la variable suivante :
public static final ResourceLocation CAP_KEY = new ResourceLocation(ModTutorial.MOD_ID, "exhaustion");
Ainsi que l’event pour attacher ma capability aux entités (je veux que les entités subissent la fatigue) :
| @SubscribeEvent |
| public static void attachToEntities(AttachCapabilitiesEvent<Entity> event) |
| { |
| if(event.getObject() instanceof EntityLivingBase && !event.getObject().world.isRemote) |
| { |
| PlayerExhaustionWrapper wrapper = new PlayerExhaustionWrapper(); |
| event.addCapability(CAP_KEY, wrapper); |
| } |
| } |
Ici ma condition permet de vérifier que l’entité est une entité vivante et aussi que je sois côté serveur.
Important
Ne faites pas d’opération trop lourde pendant cet event car il est appelé à l’instantiation de chaque entité (dans notre cas), ce qui correspond à un nombre d’appels assez conséquent.
Note
Si nous avions voulu attacher notre capability à un ItemStack
nous souscrit à l’événement AttachCapabilitiesEvent<ItemStack>
. Les type génériques valides sont Entity
, ItemStack
, Chunk
, TileEntity
et World
.
Voilà, maintenant toutes les instance de EntityLivingBase
côté serveur possèdent notre capability.
Nous allons donc maintenant apprendre à utiliser une capability et plus précisément notre capability mais gardez bien en tête que les méthodes à appeler pour utiliser d’autre capabilities sont exactement les mêmes que celles que nous allons voir. En réalité il n’y a qu’une seule méthode à utiliser : ICapabilityProvider#getCapability
.
Je vais mettre ici beaucoup de code dans lequel je récupére une instance de LazyOptional
après avoir appelé ICapabilityProvider#getCapability
puis je l’utilise pour effectuer un traitement. J’utilise la méthode LazyOptional#ifPresent
. Si vous n’êtes pas à l’aise avec les Optional
je vous invite à consulter la documentation de Java.
| @Mod.EventBusSubscriber(modid = ModTutorial.MOD_ID) |
| public class ExhaustionModificationsHandler |
| { |
| private static final UUID MODIFIER_ID = UUID.fromString("73c800ed-7da9-4a6f-8fe1-b106096269f6"); |
| |
| @SubscribeEvent |
| public static void onLivingJump(LivingEvent.LivingJumpEvent event) |
| { |
| event.getEntity().getCapability(CapabilityExhaustion.EXHAUSTION_CAPABILITY) |
| .ifPresent(cap -> cap.increaseExhaustion(50)); |
| } |
| |
| @SubscribeEvent |
| public static void onLivingTick(LivingEvent.LivingUpdateEvent event) |
| { |
| event.getEntity().getCapability(CapabilityExhaustion.EXHAUSTION_CAPABILITY) |
| .ifPresent(cap -> { |
| cap.reduceExhaustion(2); |
| if(event.getEntity().motionX == 0 && event.getEntity().motionY == 0 && event.getEntity().motionZ ==0) |
| { |
| cap.reduceExhaustion(4); |
| } |
| if(event.getEntity().isSprinting()) |
| { |
| cap.increaseExhaustion(3); |
| } |
| if(event.getEntity().isSwimming()) |
| { |
| cap.increaseExhaustion(3); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| AttributeModifier modifier = new AttributeModifier( |
| MODIFIER_ID, |
| "exhaustion", |
| - cap.getExhaustion() / 10_000f, |
| 2 |
| ); |
| |
| updateModifierFor(EntityLivingBase.SWIM_SPEED, modifier, event.getEntityLiving()); |
| updateModifierFor(SharedMonsterAttributes.MOVEMENT_SPEED, modifier, event.getEntityLiving()); |
| updateModifierFor(SharedMonsterAttributes.ATTACK_SPEED, modifier, event.getEntityLiving()); |
| }); |
| } |
| |
| @SubscribeEvent |
| public static void onLivingEat(LivingEntityUseItemEvent event) |
| { |
| event.getEntity().getCapability(CapabilityExhaustion.EXHAUSTION_CAPABILITY).ifPresent(capa -> { |
| if(event.getItem().getItem() instanceof ItemFood) |
| { |
| ItemFood food = (ItemFood) event.getItem().getItem(); |
| capa.reduceExhaustion(food.getUseDuration(event.getItem())); |
| } |
| }); |
| } |
| |
| @SubscribeEvent |
| public static void onLivingAttack(LivingAttackEvent event) |
| { |
| event.getEntity().getCapability(CapabilityExhaustion.EXHAUSTION_CAPABILITY).ifPresent(capa -> capa.increaseExhaustion(50)); |
| } |
| |
| private static void updateModifierFor(IAttribute attribute, AttributeModifier modifier, EntityLivingBase entity) |
| { |
| IAttributeInstance instance = entity.getAttribute(attribute); |
| if(instance.hasModifier(modifier)) |
| { |
| instance.removeModifier(modifier); |
| } |
| |
| if(entity instanceof EntityPlayerMP) { |
| EntityPlayerMP player = (EntityPlayerMP) entity; |
| if(player.isCreative() || player.isSpectator()) return; |
| } |
| |
| instance.applyModifier(modifier); |
| } |
| } |
À chaque fois je récupère le LazyOptional
et si il contient une instance de mon interface j’effectue un traitement avec. Pour résumer le code, quand le joueur effectue certaines actions (courir, sauter, nager, prendre des dégâts) il accumule de la fatigue, quand il mange il perd de la fatigue et il perd aussi de la fatigue au cours du temps. Plus le joueur est fatigué, plus il est lent.
Si vous avez déjà un peu testé le code que l’on a fait jusqu’à maintenant vous vous êtes peut-être rendu compte que si vous mourrez, vous ne conservez pas la fatigue que vous avez accumulée avant votre mort. Dans le cas de la fatigue ce n’est pas très grave mais il peut arriver que vous vouliez garder le même état.
Pour cela nous allons un peu modifier le contenu de notre classe CapabilityExhaustion
, je vais ajouter une Map
qui me permettra de stocker l’instance de mon interface (celle associée à ma capability) telle qu’elle était avant le respawn du joueur afin de pouvoir la restorer par la suite :
private static final Map<Entity, IExhaustable> INVALIDATED_CAPS = new WeakHashMap<>();
Attention
Il est important d’avoir une WeakHashMap
ici afin de ne pas créer de fuite de mémoire.
Ensuite dans l’event pour attacher ma capability aux entités, je stock l’instance de mon interface dans le cas où l’entité est un joueur (seuls le joueurs meurent est respawn) :
| @SubscribeEvent |
| public static void attachToEntities(AttachCapabilitiesEvent<Entity> event) |
| { |
| if(event.getObject() instanceof EntityLivingBase && !event.getObject().world.isRemote) |
| { |
| PlayerExhaustionWrapper wrapper = new PlayerExhaustionWrapper(); |
| event.addCapability(CAP_KEY, wrapper); |
| if(event.getObject() instanceof EntityPlayer) |
| { |
| event.addListener(() -> wrapper.getCapability(CapabilityExhaustion.EXHAUSTION_CAPABILITY).ifPresent(cap -> INVALIDATED_CAPS.put(event.getObject(), cap))); |
| } |
| } |
| } |
Puis je souscrit à l’event PlayerEvent.Clone
qui est appelé au respawn du joueur (après sa mort ou après avoir passé un portail). Pour savoir si le joueur vient de mourir on a la méthode PlayerEvent.Clone#isWasDeath
. Si le joueur était mort on récupère donc les données de l’ancienne instance de notre interface et on les passe à la nouvelle instance de notre capability :
| @SubscribeEvent |
| public static void copyCapabilities(PlayerEvent.Clone event) |
| { |
| if(event.isWasDeath()) |
| { |
| event.getEntityPlayer().getCapability(CapabilityExhaustion.EXHAUSTION_CAPABILITY).ifPresent(newCapa -> { |
| if(INVALIDATED_CAPS.containsKey(event.getOriginal())) |
| { |
| INBTBase nbt = CapabilityExhaustion.EXHAUSTION_CAPABILITY.writeNBT(INVALIDATED_CAPS.get(event.getOriginal()), null); |
| CapabilityExhaustion.EXHAUSTION_CAPABILITY.readNBT(newCapa, null, nbt); |
| } |
| }); |
| } |
| } |
Lorsque que vous créez une capability, les données relatives à celle-ci ne sont pas synchronisées automatiquement entre le client et le serveur. Dans notre cas, la capability que j’ai créé n’est :
-
Premièrement : pas attachée à aux entités côté client car j’ai mis la condition !event.getObject().world.isRemote
qui permet de s’assurer que l’on se trouve côté serveur.
-
Deuxièmement : pas synchronisée avec le client. En effet celui-ci n’a aucun moyen de connaitre son état de fatigue actuel, cela pose un problème si nous voulons par exemple afficher celui-ci sur un GUI.
Nous allons donc voir comment synchroniser notre capability avec le client concerné afin de pouvoir y accéder côté client. Je ne vais vous montrer ici que le paquet que je vais utiliser car vous avez bien sûr les pré-requis.
Pour commencer je vais donc créer un simple paquet qui aura pour rôle de transporter la valeur de la fatigue du serveur vers le client. À sa réception je change la valeur de la fatigue sur le client :
| public class SyncExhaustionPacket |
| { |
| private int exhaustion; |
| |
| public SyncExhaustionPacket(IExhaustable instance) { |
| this.exhaustion = instance.getExhaustion(); |
| } |
| |
| public SyncExhaustionPacket(int exhaustion) |
| { |
| this.exhaustion = exhaustion; |
| } |
| |
| public static void encode(SyncExhaustionPacket pck, PacketBuffer buf) |
| { |
| buf.writeInt(pck.exhaustion); |
| } |
| |
| public static SyncExhaustionPacket decode(PacketBuffer buf) |
| { |
| return new SyncExhaustionPacket(buf.readInt()); |
| } |
| |
| public static void handle(SyncExhaustionPacket pck, Supplier<NetworkEvent.Context> ctxSupplier) |
| { |
| if(ctxSupplier.get().getDirection().getReceptionSide() == LogicalSide.CLIENT) |
| ctxSupplier.get().enqueueWork(() -> handleClientUpdate(pck)); |
| ctxSupplier.get().setPacketHandled(true); |
| } |
| |
| @OnlyIn(Dist.CLIENT) |
| private static void handleClientUpdate(SyncExhaustionPacket pck) |
| { |
| Minecraft.getInstance().player.getCapability(CapabilityExhaustion.EXHAUSTION_CAPABILITY) |
| .ifPresent(capa -> capa.setExhaustion(pck.exhaustion)); |
| } |
| |
| } |
On veut pouvoir envoyer ce paquet dès que la valeur de la fatigue change. C’est pourquoi je vais créer une implémentation dédiée pour les joueurs côté serveur afin qu’elles prennent en paramètre un joueur :
| public class PlayerExhaustionHolder extends ExhaustionHolder |
| { |
| |
| private EntityPlayerMP player; |
| |
| public PlayerExhaustionHolder(EntityPlayerMP player) |
| { |
| this.player = player; |
| } |
| |
| @Override |
| public void setExhaustion(int value) |
| { |
| super.setExhaustion(value); |
| |
| if (player.connection != null) |
| { |
| player.getCapability(CapabilityExhaustion.EXHAUSTION_CAPABILITY) |
| .ifPresent(capa -> TutorialNetwork.CHANNEL.send( |
| PacketDistributor.PLAYER.with(() -> this.player), |
| new SyncExhaustionPacket(capa)) |
| ); |
| } |
| } |
| } |
A chaque fois que la valeur change, on envoie un paquet au client concerné. On va aussi modifier un peu l’implémentation dans notre classe PlayerExhaustionWrapper
afin de pouvoir renseigner l’instance de IExhaustable
voulue :
| public class PlayerExhaustionWrapper implements ICapabilitySerializable<INBTBase> |
| { |
| |
| private IExhaustable holder; |
| |
| |
| public PlayerExhaustionWrapper(IExhaustable holder) { |
| this.holder = holder; |
| } |
| |
| |
| |
| } |
Et puis on modifie ce qui se trouve dans l’événement pour attacher notre capability aux entités. Rendez-vous donc dans le classe CapabilityExhaustion
à la fonction attachToEntities(AttachCapabilitiesEvent<Entity>)
. Suivant si l’entité est une instance de EntityPlayerMP
ou non, on va instancier l’une ou l’autre de nos implémentations de IExhausable
. Je vais aussi retirer la condition vérifiant que nous sommes côté serveur, cependant dans la classe ExhaustionModificationsHandler
je vérifierai bien à chaque fois que je sois sur le serveur afin de modifier ma valeur seulement côté serveur puis de la synchroniser :
| @SubscribeEvent |
| public static void attachToEntities(AttachCapabilitiesEvent<Entity> event) |
| { |
| if(event.getObject() instanceof EntityLivingBase) |
| { |
| IExhaustable holder; |
| if(event.getObject() instanceof EntityPlayerMP) |
| { |
| holder = new PlayerExhaustionHolder((EntityPlayerMP)event.getObject()); |
| } |
| else |
| { |
| holder = CapabilityExhaustion.EXHAUSTION_CAPABILITY.getDefaultInstance(); |
| } |
| |
| PlayerExhaustionWrapper wrapper = new PlayerExhaustionWrapper(holder); |
| event.addCapability(CAP_KEY, wrapper); |
| |
| if(event.getObject() instanceof EntityPlayer) |
| { |
| event.addListener(() -> wrapper.getCapability(CapabilityExhaustion.EXHAUSTION_CAPABILITY).ifPresent(cap -> INVALIDATED_CAPS.put(event.getObject(), cap))); |
| } |
| } |
| } |
Et voilà, tout devrait fonctionner. Pour vérifier cela je met un point d’arrêt au niveau de l’arrivé du paquet sur le client et je vois que je reçois bien la valeur de la fatigue :

La mise à jour se bien fait sur le client.
Les différentes modifications du code sont retrouvables sur le Github de MinecraftForgeFrance.
Vous pouvez vous référer aux différents commits :

Ce tutoriel rédigé par @BrokenSwing 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
Sommaire des tutoriels