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, les Proxysont 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 et var.
    • son type est plus particulier. CommonProxy? signifie que le type de cette variable peut être soit CommonProxy, soit null. C'est une différence d'avec Java, dans lesquels n'importe quel type peut recevoir la variable null. 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 recevoir null).
    • 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 type Logger? et de la valeur null 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énible NullPointerException.
    • 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() de builder est appelé. C'est à ce moment là que le constructeur ModBuilder()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 et ServerSideProxy. 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, ce ModBuilder ?

    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.png

    Le 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 de thisdans la fonction en second paramètre. Ainsi, dans cette fonction, lorsque l'on fait setMaxStackSize(1), on fait un appel à cette méthode sur this, 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 de getX() et setX() 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'une Texture à cet Item. 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 de CommonProxy qui n'est implémentée qu'au niveau de ClientProxy. 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() de CommonProxy est déclarée open, elle pourrait donc être surchargée. Contrairement à Java où tout peut être surchargé par défaut et où il faut préciser final lorsque l'on veut restreindre les surcharges, en Kotlin, tout est final par défaut (et donc non spécifié). La surcharge est une opération doublement volontaire. Doublement, car dans ClientSideProxy, on peut voir que la fonction est déclarée override. 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 si proxy est null, c'est ici qu'il doit lancer une NullPointerException. Sinon, il peut considérer que la valeur n'est pas null, et donc du type réel CommonProxy, et non CommonProxy?.

    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 de proxy.

    • 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 jamais null.

    • proxy est utilisé pour appeler la méthode associateTextureToItem.

    • au passage, je place l'item dans le CreativeTabs.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 un CreativeTabs. Ç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 nomme this, 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, thisest 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 est associateTexture(itemName), sans ce proxy 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

    Retour vers le sommaire des tutoriels



  • 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 ?


  • Moddeurs confirmés Rédacteurs Administrateurs

    kotlin est un langage utilisant la JVM.
    Scala est aussi un langage utilisant la JVM.


Log in to reply