Créer un item simple avec Kotlin
-
Sommaire
Introduction
Dans ce tutoriel, je vous propose d’explorer la création d’un item simple, sur le modèle du tutoriel équivalent en Java, mais en utilisant Kotlin. Il est préférable, mais pas indispensable, de savoir créer un item en 1.9 en utilisant Java.
La démarche de création du code utilisée ici est différente de celle utilisée pour le tutoriel sur la création d’un simple item, pour un résultat similaire.
Pré-requis
Code
ModBuilder
Dans le tutoriel précédent, nous avions crée la classe de base d’un Mod, en Kotlin. La responsabilité de la classe de base est de « bootstraper » le Mod. C’est le point d’entrée pour FML qui repère le Mod grâce à l’annotation Mod et y envoie des événements concernant le démarrage et l’initialisation du Mod.
L’initialisation peut inclure la récupération de l’instance de Logger ainsi que la création du proxy. Dans cette version Kotlin, cette classe va servir de frontière entre le monde Java et le monde Kotlin, car s’ils sont interopérables, ces deux langages ont leurs particularités.
Voici le contenu du fichier KotlinBasedMod.kt une fois modifié. Les explications suivent.
package org.puupuu.kotlinbasedmod import net.minecraftforge.fml.common.Mod import net.minecraftforge.fml.common.Mod.EventHandler import net.minecraftforge.fml.common.SidedProxy import net.minecraftforge.fml.common.event.FMLInitializationEvent import net.minecraftforge.fml.common.event.FMLPreInitializationEvent import org.apache.logging.log4j.Logger @Mod(modid = KotlinBasedMod.MODID, version = KotlinBasedMod.VERSION, modLanguageAdapter = "io.drakon.forge.kotlin.KotlinAdapter") object KotlinBasedMod { const val MODID = "kotlinbasedmod" const val VERSION = "0.1" @SidedProxy(clientSide = "org.puupuu.kotlinbasedmod.ClientSideProxy", serverSide = "org.puupuu.kotlinbasedmod.ServerSideProxy") private var proxy: CommonProxy? = null private lateinit var logger: Logger private val builder by lazy { ModBuilder() } @EventHandler fun preInit(event: FMLPreInitializationEvent) { logger = event.modLog builder.preInit() } @EventHandler fun init(event: FMLInitializationEvent) { } } open class CommonProxy class ClientSideProxy : CommonProxy() class ServerSideProxy : CommonProxy()
Les nouveautés à expliquer par rapport à la version vide de cette classe sont :
- tout comme dans la version Java, FML va injecter dans une variable annotée par
@SidedProxy
les instances des proxy spécifiés. Pour rappel, lesProxy
sont des classes qui permettent de différencier les appels à certaines fonctions en fonction de leur appel : depuis un client, ou depuis un serveur. - proxy est une variable privée, nous avons déjà vu les annotations
private
etvar
. - son type est plus particulier.
CommonProxy?
signifie que le type de cette variable peut être soitCommonProxy
, soitnull
. C’est une différence d’avec Java, dans lesquels n’importe quel type peut recevoir la variablenull
. Sans le point d’interrogation, le compilateur Kotlin s’assure autant qu’il le peut de la non nullité de la variable (si du code Java fait de l’injection de dépendance comme ici, Kotlin ne peut pas grand chose, c’est donc pour cela que l’on lui spécifie que la variable peut recevoirnull
). logger
a un peu changé depuis la fois précédente. Le mot clé[lateinit
a été ajouté et signifie que pour le moment,logger
n’a pas de valeur. C’est le constructeur qui donnera cette valeur. La différence avec l’utilisation d’un typeLogger?
et de la valeurnull
est la suivante : si un accès en lecture est faite àlogger
avant son initialisation, l’exception levée indiqué que la variable n’a pas été initialisée, plutôt qu’un toujours pénibleNullPointerException
.- enfin, une valeur builder est initialisé de manière « paresseuse » (
lazy
) à travers une fonction de délégation. Cette valeur est en faite une propriété de la classe, elle se comporte à la fois comme une fonction dont le corps serait{ ModBuilder() }
et un cache qui retiendrait cette valeur au passage. - Dans la fonction
preInit()
,logger
est initialisé - Puis la méthode
preInit()
debuilder
est appelé. C’est à ce moment là que le constructeurModBuilder()
sera appelé, et nous verrons plus tard pourquoi. Car après tout,ModBuilder
est le titre de la section. - À la fin du fichier, trois classes vides sont déclarées :
CommonProxy
,ClientSideProxy
etServerSideProxy
. L’héritage entre classe ne nécessite pas le mot cléextends
, les deux-points suffisent.CommonProxy
est accompagnée du mot clé «open
» précisant que c’est une classe dont les méthodes pourront être surchargées. - Les classes Proxy iront plus tard dans les propres fichiers et seront implémentées, mais pour le moment, elles peuvent se trouver
Mais du coup, ceModBuilder
?
La classe de base du mode, je le disais, à la responsabilité de faire la transition entre le monde Java et le monde Kotlin. Cependant, même pour un Mod en Java, cette classe a déjà la responsabilité d’être l’ancre entre FML et le Mod. C’est assez de responsabilité pour une classe.
ModBuilder
est une classe dont la responsabilité est de créer les éléments nécessaires au Mod.Et à ce stade, elle ressemble à ceci, dans le fichier
ModBuilder.kt
:package org.puupuu.kotlinbasedmod class ModBuilder() { fun preInit() { } }
Création de l’Item
Je vous laisse revenir sur la création des fichiers json de l’item dans le tutoriel Java correspondant.
En résumé, les fichiers suivants doivent être créés :
src/main/resources/assets/kotlinbasedmod/lang/en_US.lang
src/main/resources/assets/kotlinbasedmod/models/item/kotlinbasedmod_item.json
src/main/resources/assets/kotlinbasedmod/textures/items/kotlinebasedmod_item.pngLe fichier en_US.lang contient:
item.kotlinbasedmod:kotlinbasedmod_item.name=Strange Tool
Le fichier kotlinbasedmod_item.json contient :
{ "parent": "item/generated", "textures": { "layer0": "kotlinbasedmod:items/kotlinbasedmod_item" } }
Revenons à Kotlin. La création d’un Item en Forge 1.9 se fait en plusieurs étapes :
- construction d’une instance d’Item (ou d’une sous-classe d’Item), éventuellement en précisant des propriétés de cet Item
- un appel à setRegistryName() sur l’instance, afin de fournir un identifiant auprès de Forge
- un appel à setUnlocalizedName() sur l’instance, afin de fournir un identifiant pour la localisation
- un appel à GameRegistry.register() afin d’enregistrer l’objet crée.
- l’association d’une Texture à l’Item.
Voilà ce que cela donnerait en l’écrivant tel quel, mis à part la dernier étape.
var item = Item() item.setMaxStackSize(1) item.setRegistryName(KotlinBasedMod.MODID, "kotlinbasedmod_item") item.setUnlocalizedName(item.getRegistryName().toString()) GameRegistry.register(item)
Ou éventuellement comme ça
var item = Item().setMaxStackSize(1) .setRegistryName(KotlinBasedMod.MODID, "kotlinbasedmod_item") item.setUnlocalizedName(item.getRegistryName().toString())
Aux points-virgules près, c’est ce qui aurait pu être écrit en Java. Transformons ça un peu plus en Kotlin.
with(Item()) { setMaxStackSize(1) setRegistryName(KotlinBasedMod.MODID, "kotlinbasedmod_item") unlocalizedName = registryName.toString() GameRegistry.register(this) }
Comme souvent, le code est un peu plus léger que la version Java.
with()
est une fonction qui prend deux paramètres. Un objet, et une fonction. En Kotlin, une fonction en dernier paramètre peut être écrite après les parenthèses, comme un bloc de code, ce qui donne une syntaxe plus aérée et plutôt sympathique.- le premier argument est le résultat du constructeur d’
Item
, donc une instance d’Item
. - La fonction
with()
donne ensuite à ce premier paramètre le nom et la fonction dethis
dans la fonction en second paramètre. Ainsi, dans cette fonction, lorsque l’on faitsetMaxStackSize(1)
, on fait un appel à cette méthode surthis
, et donc sur l’instance d’item
tout juste créée. - l’accès à
unlocalizedName
peut paraître plus étrange. Kotlin simplifie l’écriture des accès aux propriétés Java. Toute paire degetX()
etsetX()
est vue par Kotlin comme une propriété et peut être accédée plus simplement avec une syntaxe de variable membre.
Au passage, on peut voir que ce qui était presque bien dans la concision de la seconde écriture version Java par chaînage des appels, car l’API de FML est bien faite, est un peu ruinée par le dernier appel. La nécessité d’accéder à
getRegistryName()
nécessite d’avoir l’instance d’item
disponible. Ce qui casse le chaînage des appels.Je trouve l’écriture Kotlin nettement plus élégante.
Association de la texture
La dernière étape de la création de l’
Item
est l’association d’uneTexture
à cetItem
. Comme cette association n’intervient qu’au niveau du Client et non du Serveur, c’est ici qu’interviennent les proxys. Le tutoriel Java sur la création des item propose une fonction vide au niveau deCommonProxy
qui n’est implémentée qu’au niveau deClientProxy
. Gardons ce principe.Et puisque les proxy vont recevoir du code, je déplace les trois dans un fichier
Proxy.kt
.package org.puupuu.kotlinbasedmod import net.minecraft.client.renderer.block.model.ModelResourceLocation import net.minecraft.item.Item import net.minecraftforge.client.model.ModelLoader open class CommonProxy { open fun associateTextureToItem(item: Item, name: String) { } } class ServerSideProxy : CommonProxy() class ClientSideProxy : CommonProxy() { override fun associateTextureToItem(item: Item, name: String) { super.associateTextureToItem(item, name) val completeResourceName = KotlinBasedMod.MODID + ":" + name ModelLoader.setCustomModelResourceLocation( item, 0, ModelResourceLocation(completeResourceName, "inventory")) } }
- la fonction
associateTextureToItem()
deCommonProxy
est déclaréeopen
, elle pourrait donc être surchargée. Contrairement à Java où tout peut être surchargé par défaut et où il faut préciserfinal
lorsque l’on veut restreindre les surcharges, en Kotlin, tout estfinal
par défaut (et donc non spécifié). La surcharge est une opération doublement volontaire. Doublement, car dansClientSideProxy
, on peut voir que la fonction est déclaréeoverride
. La volonté ici est d’être solide face au changement d’interface et de s’éviter de longues sessions à debugger des appels entre objets. ServerSideProxy
est toujours aussi vide.ClientSideProxy
implémente l’appel àModelLoader()
, qui est la méthode 1.9 pour associer la ressource de texture à l’objet.- Au passage, on remarquera que la concaténation de chaînes de caractères se fait avec l’opérateur +.
C’est bien beau tout ça, mais
ModBuilder
n’a pas accès à une instance de Proxy. Et c’est pour cela que j’avais spécifié sa création de manière « lazy », au moment du premier accès àbuilder
, le proxy a été injecté par FML, et je peux le passer en paramètre du constructeur :private val builder by lazy { ModBuilder(proxy!!) }
- les deux points d’exclamations après
proxy
indiquent à Kotlin que siproxy
estnull
, c’est ici qu’il doit lancer uneNullPointerException
. Sinon, il peut considérer que la valeur n’est pasnull
, et donc du type réelCommonProxy
, et nonCommonProxy?
.
Et côté ModBuilder, la nouvelle version se retrouve comme ceci:
package org.puupuu.kotlinbasedmod import net.minecraft.creativetab.CreativeTabs import net.minecraft.item.Item import net.minecraftforge.fml.common.registry.GameRegistry class ModBuilder(val proxy: CommonProxy) { fun preInit() { val item = with(Item()) { setMaxStackSize(1) setRegistryName(KotlinBasedMod.MODID, "kotlinbasedmod_item") unlocalizedName = registryName.toString() setCreativeTab(CreativeTabs.tabTools) GameRegistry.register(this) } proxy.associateTextureToItem(item, "kotlinbasedmod_item") } }
-
où l’on retrouve le
proxy
passé en paramètre du constructeur par défaut. Le mot-cléval
indique que l’on en fait aussi une variable membre du nom deproxy
. -
on peut remarquer que le type de proxy est à présent
CommonProxy
, et Kotlin nous assure que, tant que cette valeur reste dans le monde Kotlin, elle ne sera jamaisnull
. -
proxy
est utilisé pour appeler la méthodeassociateTextureToItem
. -
au passage, je place l’
item
dans leCreativeTabs.tabTools
, histoire de le retrouver. -
Voir les modifications des Proxy sur GitHub (et la correction du nom de l’asset, oups)
Extension
Lorsque l’on écrit un programme, tout signe de duplication est un mauvais signe. Hors en l’état, si je veux créer un second Item, je vais devoir répéter toute la séquence qui relève plus de la technique que de la sémantique. Mélanger les deux, c’est aussi un mauvais signe. En séparant la sémantique de la technique, j’ai une chance de ne pas n’avoir que des parties très localisées à réécrire au prochain changement d’API de Forge et/ou de Minecraft.
Ce que je voudrais écrire, c’est ça :
class ModBuilder(val proxy: CommonProxy) { fun preInit() { val itemName = "kotlinbasedmod_item" with(Item()) { register(itemName) associateTexture(proxy, itemName) setCreativeTab(CreativeTabs.tabTools) } } }
Autrement dit : je crée un
Item
, je l’enregistre, je lui associe une texture et je le mets dans unCreativeTabs
. Ça, c’est ce que je veux faire. Le comment je le fais, je veux le mettre ailleurs.Mais
Item()
est une classe de Minecraft, à moins de patcher Minecraft, je ne peux pas la changer. Heureusement, Kotlin offre un concept de méthode d’extension. Une méthode d’extension est une fonction avec une syntaxe particulière qui peut recevra une instance de l’objet associé. D’un point de vue appel, c’est comme si on appelait une méthode de la classe. D’un point de vue implémentation de la fonction, bien entendue, et même si l’instance se nommethis
, nous n’avons accès qu’aux parties publiques de la classe.fun Item.register(name: String) { setRegistryName(KotlinBasedMod.MODID, name) unlocalizedName = registryName.toString() GameRegistry.register(this) } fun Item.associateTexture(safeProxy: CommonProxy, name: String) { safeProxy.associateTextureToItem(this, name) }
- la déclaration de la méthode d’extention se fait en précisant la classe que l’on veut étendre avant le nom de la méthode, séparé par un point.
- à l’intérieur de la fonction,
this
est l’objet sur lequel a été invoquée la méthode. - le passage de
proxy
àassociateTexture()
est inélégant, ce que l’on voudrait vraiment écrire estassociateTexture(itemName)
, sans ceproxy
qui est un détail d’implémentation. Lorsque j’aurai trouvé mieux, si je trouve, je modifierai ce tutoriel. - en continuant le Mod, ces méthodes d’extension ne resteraient pas dans ce fichier.
Les méthodes d’extentions font partie des apports syntaxiques vraiment agréables et puissant de Kotlin. Elles permettent un code plus concis et mieux agencé.
Résultat
Crédits
Rédaction :
- Mokona78
Correction :
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 - tout comme dans la version Java, FML va injecter dans une variable annotée par
-
Kotlin à l’aire de plus en plus intéressant, surtout avec les méthode d’extension qui ont l’aire très pratiques !
-
Mais enfaite c’est quoi kotlin et les possibilités ?
-
kotlin est un langage utilisant la JVM.
Scala est aussi un langage utilisant la JVM.