Les capabilities
-
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.
Sommaire du tutoriel
- Pré-requis
- Comprendre les capabilities
- Créer une capability
- Attacher une capability à une classe
- Utiliser une capability
- Rendre votre capability persistante après la mort du joueur
- Synchroniser votre capability
- Résultat
- Licence et attribution
Pré-requis
- Utiliser les événements
- Communiquer entre le client et le serveur : le réseau et les paquets (nécessaire seulement si vous voulez synchroniser vos données entre le serveur et le client)
Comprendre les capabilities
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 deIItemHandler
, tandis que la capabilityCapabilityEnergy.ENERGY
permet d’accéder à une instance deIEnergyStorage
.
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 unICapabilityProvider
(fournisseur de capability, ça a du sens). Il existe plusieurs classes dans Minecraft qui implémentent cette interface, commeEntity
,Chunk
ou encoreItemStack
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 deICapabilityProvider
on peut récupérer une instance de l’interface associée à la capability.Créer une 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.
/** * Interface permettant de gérer la fatigue. La fatigue est représentée par un entier allant de 0 à 10 000, avec 10 000 * correspondant au maximum de fatigue. */ public interface IExhaustable { /** * Renvoie un nombre entre 0 et 10 000 * @return la fatigue */ int getExhaustion(); /** * Permet de définir la fatigue, celle-ci est un nombre entre 0 et 10 000 (bornes incluses). * Dans le cas d'une valeur passée supérieure à 10 000, celle-ci est est plafonnée automatiquement. * Dans le cas d'une valeur négative passée, celle-ci est compté comme nulle. * @param value La nouvelle valeur de la fatigue */ void setExhaustion(int value); /** * Réduit la fatigue. * @param value La quantité de fatigue à enlever */ default void reduceExhaustion(int value) { this.setExhaustion(this.getExhaustion() - value); } /** * Augmente la fatigue. * @param value La quantité de fatigue à ajouter */ 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 deCapability.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 avecCapabilityInject
, cette annotation permettra à Forge de changer la valeur de la variable une fois notre capability enregistrée via leCapabilityManager
.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’eventFMLCommonSetupEvent
:@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.
Attacher une capability à une classe
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émenteICapabilityProvider
qu’elle possède forcément votre capability (par exemple on imagine mal unItemStack
ê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 unLazyOptional
qui doit êtreempty()
si l’instance ne possède pas la capability, ou alorsof(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 viaAttachCapabilitiesEvent#addCapability(ResourceLocation, ICapabilityProvider)
. Comme vous pouvez le voir il nous faut là aussi une instance deICapabilityProvider
donc nous allons implémenter une classe comme si vous voulions lui donner notre capability puis nous en passerons une instance à cette méthode. LaResourceLocation
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émenterICapabilitySerializable
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 dansCapability#orEmpty
car c’est un comportement que la majorité des moddeurs et Forge utilisent.Pour ce qui est de
serializeNBT
etdeserializeNBT
, je redirige simplement les appels vers l’implémentation deCapability.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 classeCapabilityExhaustion
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énementAttachCapabilitiesEvent<ItemStack>
. Les type génériques valides sontEntity
,ItemStack
,Chunk
,TileEntity
etWorld
.Voilà, maintenant toutes les instance de
EntityLivingBase
côté serveur possèdent notre capability.Utiliser une 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 deLazyOptional
après avoir appeléICapabilityProvider#getCapability
puis je l’utilise pour effectuer un traitement. J’utilise la méthodeLazyOptional#ifPresent
. Si vous n’êtes pas à l’aise avec lesOptional
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); } // Operations : // 0 : valeur à ajouter à la valeur de base // valeurFinale = valeurBase + quantite // 1 : pourcentage de la valeur de base à ajouter // valeurFinale += valeurBase * quantite // 2 : pourcentage de la valeur finale à rajouter en plus // valeurFinale *= 1 + quantite 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.Rendre votre capability persistante après la mort du joueur
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 uneMap
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éthodePlayerEvent.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); } }); } }
Synchroniser votre capability
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); // setExhaustion peut être appelé trop tôt et que player.connection soit null 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 deIExhaustable
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 fonctionattachToEntities(AttachCapabilitiesEvent<Entity>)
. Suivant si l’entité est une instance deEntityPlayerMP
ou non, on va instancier l’une ou l’autre de nos implémentations deIExhausable
. Je vais aussi retirer la condition vérifiant que nous sommes côté serveur, cependant dans la classeExhaustionModificationsHandler
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.
Résultat
Les différentes modifications du code sont retrouvables sur le Github de MinecraftForgeFrance.
Vous pouvez vous référer aux différents commits :
- Créer une capability
- Attacher une capability à une classe
- Utiliser une capability
- Rendre votre capability persistante après la mort du joueur
- Synchroniser votre capability
Licence et attribution
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
-
Bonjour,
je me posais deux petites questions…- Est-ce que récupérer la capability d’un joueur prend beaucoup de ressources ? Je veux dire, on pourrait le faire tous les ticks sur un certain nombre de joueurs sans que cela n’ait de répercussion ?
- Dans la classe PlayerExhaustionHolder, tu récupères la capability sur le joueur afin de l’envoyer au client. Mais la capability du joueur c’est pas ‘this’ (si c’est effectivement un joueur côté serveur) ? Ne pourrait-on pas remplacer les lignes 18 à 22 par:
TutorialNetwork.CHANNEL.send(PacketDistributor.PLAYER.with(() -> this.player), new SyncExhaustionPacket(this));
(edit: J’ai fait une faute d’inattention, c’est corrigé)
? (oui le point d’interrogation est placé loin du reste de la phrase xD)
Merci du temps que vous consacrez à mes questions (et merci de ce tutoriel qui m’a été très utile !), -
-
-