Tutoriel pour apprendre à développer un jeu avec Kotlin, Gradle et TornadoFX

Application au jeu TicTacToe

Laurent Bernabé

Dans ce tutoriel, nous allons développer une application de TicTacToe graphique – sans intelligence artificielle – avec le langage Kotlin et la bibliothèque graphique TornadoFX. Par ailleurs nous allons gérer les dépendances avec l’outil Gradle. Celui-ci sera directement intégré dans l’EDI Intellij Idea, que nous allons utiliser.

Commentez

- Titre : Tutoriel pour apprendre à développer un jeu avec Kotlin, Gradle et TornadoFX

- Auteur : Laurent Bernabé

- Parution : 27 novembre 2020

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2020 Laurent Bernabé. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Présentation du jeu

- I-A. Les règles

- I-B. L'architecture de notre application

- II. Prérequis et présentation de l’environnement

- II-A. Prérequis

- II-B. L’environnement

- II-B-1. Kit Java SDK et Kit JavaFX

- II-B-2. Gradle

- II-B-3. Intellij Idea

- II-B-4. Kotlin/TornadoFX

- III. Mise en place du projet

- III-A. Création

- III-B. Gestion des dépendances

- III-B-1. Explication du script généré

- III-B-2. Premières adaptations du script

- III-B-3. Mise en application du script

- IV. Codage du TicTacToe

- IV-A. Ressources

- IV-B. Le code

- IV-B-1. Les importations

- IV-B-2. L'entrée de l'application

- IV-B-3. Classe logique du plateau

- IV-B-4. Définition graphique du plateau

- V. Empaquetage de l'application en JAR et exécution du programme

- V-A. Mise à jour du fichier build.gradle.kts

- V-B. Génération et exécution

- VI. Aller plus loin

- VII. Conclusion et remerciements

I - Présentation du jeu

I-A - Les règles

Le TicTacToe est un jeu à deux joueurs, et qui se joue avec une grille de 3x3 cases (qui est vide au départ du jeu). Les joueurs inscrivent des symboles, croix et ronds, à tour de rôle dans les différentes cases restées vides. Le premier joueur qui parvient à aligner trois de ses symboles en horizontal, vertical ou en diagonale a gagné.

I-B - L'architecture de notre application

C'est l'architecture de notre application illustrée avec un diagramme de classes UML.

Nous avons donc :

- une énumération CellValue, listant les différentes valeurs possibles pour les cases ;

- une classe métier BoardLogic, dont la responsabilité sera de gérer le cœur des fonctionnalités du plateau ;

- une classe métier BoardView, s'appuyant sur le framework TornadoFx pour définir l'apparence du plateau ;

- une classe d'entrée TicTacToeApp, s'appuyant également sur le framework TornafoFx, pour définir le point d'entrée de l'application.

La classe BoardLogic dispose de trois méthodes :

- reset() afin de réinitialiser l'état du plateau ;

- play() afin de permettre à un joueur de placer son pion sur une case, si toutefois la case est libre ;

- getPietAt() afin de récupérer le statut d'une case du plateau.

II - Prérequis et présentation de l’environnement

II-A - Prérequis

Afin de suivre ce tutoriel, il est recommandé d’avoir des notions :

- dans le langage Kotlin ;

- dans la bibliothèque graphique JavaFX ;

- de préférence, comprendre comment fonctionne un outil tel que Maven, voire Gradle qui sera utilisé dans ce tutoriel.

II-B - L’environnement

Notre environnement nous permettra de gérer le projet, en ne partant de quasiment rien. De plus, grâce au script Gradle, le projet pourra être recréé simplement, sans même avoir à télécharger manuellement les bibliothèques. Donc il suffira de transmettre le script à une personne tierce pour lui permettre de reconstruire le projet.

II-B-1 - Kit Java SDK et Kit JavaFX

Vous aurez besoin d’un kit Java SDK : mon choix s’est porté sur la version 8, mais rien ne vous empêche d’utiliser une version ultérieure. Par contre, ne serait-ce que pour que l’installation de JavaFX soit plus aisée, je vous recommande d’utiliser OpenJDK. À ce moment-là le kit JavaFX correspondant est tout simplement OpenJFX : OpenJFX 14 devrait fonctionner avec le OpenJDK 8.

II-B-2 - Gradle

L’outil Gradle (qui est similaire à Maven pour ceux qui connaissent) permet d’importer simplement toutes les dépendances du projet grâce à un script.

Ces dépendances peuvent être le langage à utiliser (il doit pouvoir être exécuté sur la machine virtuelle de Java), mais aussi l’ensemble des bibliothèques extérieures.

Il n’est pas nécessaire d’installer Gradle, car cela sera géré par l’EDI Intellij Idea.

II-B-3 - Intellij Idea

La version Community (gratuite) est largement suffisante dans le cadre de ce tutoriel. Lors de la configuration initiale, je vous recommande de laisser les options par défaut (hormis le thème, où vous pouvez choisir celui qui vous plait bien évidemment) : ainsi le plugin Gradle et le langage Kotlin seront intégrés. Toutefois, dans le cas contraire, vous pouvez installer Gradle et Kotlin en allant dans le menu Configure?>Plugins depuis la page d’accueil. (Si vous n’êtes pas dans la page d’accueil, il est possible d’y revenir en lançant le menu File->Close Project).

Dans la barre de recherche, tapez Gradle (ou Kotlin pour le plugin Kotlin), puis cliquez sur le bouton Installer dans la section détails dans la zone droite de la fenêtre des plugins. Veuillez noter que l’installation ne sera effective qu’après avoir redémarré Intellij Idea.

II-B-4 - Kotlin/TornadoFX

Ces deux dernières dépendances seront gérées par le script Gradle, une fois le projet créé, dans la section suivante.

III - Mise en place du projet

III-A - Création

Veuillez démarrer un nouveau projet depuis la page d’accueil.

Choisissez le type de projet Gradle dans la zone gauche, et sélectionnez l’option Kotlin/JVM dans la zone détails. Veuillez aussi à ce que l’option Kotlin DSL for build script soit cochée.

Dans l’écran suivant, entrez le nom du projet : TicTacToeFx, vous pourrez alors valider la création du projet.

III-B - Gestion des dépendances

C’est au niveau du fichier build.gradle.kts que nous définissons les dépendances. Le fichier a déjà été créé en même temps que le projet.

C’est aussi ici que nous allons décrire, le moment venu, quelle est la classe principale pour le JAR autoexécutable.

III-B-1 - Explication du script généré

Tout d'abord, le voici :

1. plugins {

2. java

3. kotlin("jvm") version "1.4.0"

4. }

6. group = "org.example"

7. version = "1.0-SNAPSHOT"

9. repositories {

10. mavenCentral()

11. }

13. dependencies {

14. implementation(kotlin("stdlib"))

15. testCompile("junit", "junit", "4.12")

16. }

- Lignes 1-4 : définitions des plugins utilisés. Ici, les plugins pour Java et Kotlin semblent donc logiques.

- Lignes 6 et 7 : ces informations sont davantage utiles dans le cadre de la création d’une bibliothèque. Ce qui ne nous empêche pas d’adapter au moins le groupe.

- Lignes 9-11 : où Gradle doit rechercher et télécharger les dépendances. La configuration mavenCentral() nous sera amplement suffisante ici. Mais nous aurions pu ajouter une ligne avec jCenter() afin de disposer d’un éventail de bibliothèques plus large.

- Lignes 13-16 : cette section est capitale. C’est ici que nous allons déclarer les bibliothèques à télécharger. Pour l’instant nous disposons de la bibliothèque standard de Kotlin et du framework de test Junit (dont nous n’aurons pas besoin dans ce tutoriel). En principe, les dépendances essentielles sont déclarées avec implementation suivi de l’identifiant de la dépendance.

III-B-2 - Premières adaptations du script

1. Remplacez le groupe par com.developpez et la version par 1.0.

2. Dans la section des dépendances, vous pouvez retirer Junit.

3. Il nous faut maintenant ajouter la dépendance vers TornadoFX. Sur la page de ![TornadoFX] https://tornadofx.io/, il y a notamment un badge mavenCental, avec le dernier numéro de version. En cliquant dessus, on arrive sur la page Maven Central liée aux artéfacts de TornadoFX. Celui qui nous intéresse est simplement tornadofx. Donc nous avons pour id no.tornado, pour nom tornadofx et pour numéro de version 1.7.20 (lors de la rédaction). Il nous faut donc ajouter la dépendance implementation " no.tornado:tornadofx:1.7.20 ".

4. Enfin, ajouter le code suivant en fin de fichier, afin que le code soit entièrement interprété pour la version 8 de Java (ou la version que vous utilisez) :

1. tasks.withType<KotlinCompile> {

2. kotlinOptions.jvmTarget = "1.8"

3. }

Et l’importation suivante en début de fichier :

1. import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

III-B-3 - Mise en application du script

Il faut maintenant dire à Gradle de prendre en compte les modifications. Pour ce faire, il suffit de cliquer sur le bouton Local changes, dans la zone éditeur.

IV - Codage du TicTacToe

IV-A - Ressources

Tout d’abord, nous allons simplement utiliser des images pour les pièces des deux joueurs. Le site [FlatIcon] https://www.flaticon.com/ propose gratuitement de nombreuses images vectorielles, pour peu que l’on crédite les auteurs.

Nous allons utiliser [une image pour la croix] https://www.flaticon.com/free-icon/close_1828665?term=cross&page=1&position=2, et [une image pour le cercle] https://www.flaticon.com/free-icon/rec_808569?term=circle&page=1&position=5 (n’oubliez pas de changer la couleur par défaut, à l’aide du bouton Edit Color). Nous prendrons la version png, taille 24px. Sauvegardez-les dans le dossier src/main/resources du projet.

Prenez également soin de les renommer cross.png et circle.png, respectivement.

IV-B - Le code

Voici le code, que nous allons analyser. Mise en garde toutefois, l'application ne vérifie pas s'il y a un gagnant.

1. package com.developpez.tictactoe

3. import javafx.geometry.Insets

4. import javafx.scene.control.Button

5. import javafx.scene.image.Image

6. import javafx.scene.image.ImageView

7. import tornadofx.*

8. import java.lang.IllegalStateException

10. class TicTacToeApp : App(BoardView::class)

12. sealed class Piece {

14. }

16. enum class CellValue {

17. Empty, Cross, Circle

18. }

20. class BoardLogic {

21. // Les valeurs des différentes cases : 1re dimension = rangée.

22. var cellsValues: Array<Array<CellValue>> = Array(3) { Array(3) { CellValue.Empty } }

24. // Efface le plateau

25. fun reset() {

26. cellsValues = Array(3) { Array(3) { CellValue.Empty } };

27. }

29. // Joue la pièce demandée à l'endroit demandé, si la case est vide.

30. // Retourne true si la pièce a pu être jouée, false sinon.

31. fun play(row: Int, col: Int, value: CellValue): Boolean {

32. if (cellsValues[row][col] == CellValue.Empty) {

33. cellsValues[row][col] = value;

34. return true

35. }

36. return false

37. }

39. // Simple getter.

40. fun getPieceAt(row: Int, col: Int): CellValue = cellsValues[row][col]

41. }

43. data class CellClickedEvent(val row: Int, val col: Int) : FXEvent()

45. class BoardView : View("Tic Tac Toe") {

46. var currentPlayer: CellValue = CellValue.Cross

47. val boardLogic = BoardLogic()

49. // Attention ! Cette ligne doit figurer avant le block init !

50. override val root = gridpane {

51. repeat(3) { rowIndex ->

52. row {

53. repeat(3) { colIndex ->

54. button {

55. gridpaneConstraints {

56. margin = Insets(2.0)

57. setPrefSize(100.0, 100.0)

58. }

59. action {

60. fire(CellClickedEvent(rowIndex, colIndex))

61. }

62. }

63. }

64. }

65. }

66. }

68. init {

69. boardLogic.reset()

70. // Réaction à l'évènement click sur le plateau

71. subscribe<CellClickedEvent> {

72. if (boardLogic.getPieceAt(it.row, it.col) == CellValue.Empty) {

73. boardLogic.play(it.row, it.col, currentPlayer)

74. refreshContent()

75. toggleCurrentPlayer()

76. }

77. }

79. refreshContent()

80. }

82. // Change le joueur au trait

83. private fun toggleCurrentPlayer() {

84. currentPlayer = when (currentPlayer) {

85. CellValue.Cross -> CellValue.Circle

86. CellValue.Circle -> CellValue.Cross

87. else -> throw IllegalStateException("Current player is not defined.")

88. }

89. }

91. // Rafraichit l'affichage du plateau

92. private fun refreshContent() {

93. repeat(3) { row ->

94. repeat(3) { col ->

95. val source = root.children[3 * row + col] as Button

96. when (boardLogic.getPieceAt(row, col)) {

97. CellValue.Cross -> source.graphic = ImageView(Image("/cross.png"))

98. CellValue.Circle -> source.graphic = ImageView(Image("/circle.png"))

99. else -> {

100. }

101. }

102. }

103. }

104. }

106. }

Veuillez copier le code dans un fichier Launcher.kt, dans le dossier src/main/kotlin.

IV-B-1 - Les importations

Lignes 3-8 : vous pouvez constater que la plupart des importations sont des importations JavaFX standards. TornadoFX ne remplace pas JavaFX : TornadoFX tend simplement à en simplifier l'utilisation dans le langage Kotlin. Il faut également importer différents éléments du paquetage tornadofx.

IV-B-2 - L'entrée de l'application

Ligne 10. Nous définissons une sous-classe de la classe tornadofx.App. Par conséquent, il n'y a pas obligation de créer une fonction main.

Remarquez aussi que le constructeur de la classe App prend en paramètre la référence class de la classe Board : classe personnelle qui définit la vue de notre application. Nous allons l'analyser plus loin.

IV-B-3 - Classe logique du plateau

Lignes 20 à 41, nous définissons la classe BoardLogic qui permet de gérer les fonctionnalités du plateau. Cette classe ne présente pas de difficulté particulière.

IV-B-4 - Définition graphique du plateau

Ligne 43 nous définissons un évènement personnalisé : il hérite de la classe tornadofx.FXEvent.

Lignes 45 à 104, nous avons la définition graphique du plateau. Elle utilise différentes fonctions de TorndadoFX : gridpane, row, button, gridpaneConstraints, action. Et la syntaxe paraît assez intuitive. Évidemment, il ne faut pas hésiter à se rendre sur la documentation officielle de TornadoFX afin de connaître les différents composants et layouts disponibles.

Pour définir l'apparence graphique du composant, il faut redéfinir la valeur root (lignes 50 à 66). Notez que cette définition doit être située avant tout bloc init qui doit faire référence à la propriété root.children. En effet, on se retrouverait avec une valeur root nulle sinon.

Remarquez comment j'ai pu tout simplement insérer du code dans la définition : notamment la fonction repeat de Kotlin pour éviter de dupliquer bêtement les mêmes éléments.

L'élément action (lignes 59 à 61) permet ici de définir le gestionnaire d'évènement click pour le bouton. Il émet (par le biais de la fonction tornadofx.Component.fire) un objet de type CellClickedEvent, que nous avons nous-mêmes défini (ligne 43).

Lignes 68 à 80, nous définissons un bloc init, afin de définir les actions à entreprendre dès que l'évènement CellClickedEvent est émis. Cela est fait par l'intermédiaire de la fonction tornadofx.Component.subscribe.

Ce bloc appelle notamment notre méthode personnelle refreshContent, définie en lignes 92 à 104. Le tableau root.children étant à une seule dimension, il faut effectuer un petit calcul (ligne 95) pour retrouver les différents boutons. Pour changer l'image d'un bouton, il suffit d'attribuer une nouvelle instance de javafx.scene.image.ImageView à sa propriété graphic (lignes 97 et 98). Remarquez également qu'il est facile d'y faire référence (et cela fonctionnera même une fois que l'application sera empaquetée en JAR) grâce au simple fait d'avoir mis les images dans le dossier resources (toujours lignes 97 et 98).

V - Empaquetage de l'application en JAR et exécution du programme

Nous voilà bientôt prêts à créer le fichier JAR autoexécutable. Mais avant, il faut que le script Gradle indique que l'on souhaite disposer de cette possibilité.

V-A - Mise à jour du fichier build.gradle.kts

- Ajoutez l'import suivant au début du script :

1. import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar

- Dans la section plugins, ajoutez la ligne id("com.github.johnrengelman.shadow") version "6.0.0".

- Dans la section repositories, ajoutez le dépôt jcenter().

- Ajoutez également la configuration du plugin ShadowJar.

1. tasks {

2. named<ShadowJar>("shadowJar") {

3. archiveBaseName.set("TicTacToe")

4. mergeServiceFiles()

5. manifest {

6. attributes(mapOf("Main-Class" to "com.developpez.tictactoe.TicTacToeApp"))

7. }

8. }

9. }

Remarquez d'ailleurs la définition de la classe principale.

- Enfin, nous spécifions que la tâche build doit également construire le JAR :

1. tasks {

2. build {

3. dependsOn(shadowJar)

4. }

5. }

Vous devriez donc avoir un script similaire à celui-ci :

1. import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

2. import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar

4. plugins {

5. java

6. kotlin("jvm") version "1.4.0"

7. id("com.github.johnrengelman.shadow") version "6.0.0"

8. }

10. group = "com.developpez"

11. version = "1.0"

13. repositories {

14. mavenCentral()

15. jcenter()

16. }

18. dependencies {

19. implementation(kotlin("stdlib"))

20. implementation("no.tornado:tornadofx:1.7.20")

21. }

23. tasks.withType<KotlinCompile> {

24. kotlinOptions.jvmTarget = "1.8"

25. }

27. tasks {

28. named<ShadowJar>("shadowJar") {

29. archiveBaseName.set("TicTacToe")

30. mergeServiceFiles()

31. manifest {

32. attributes(mapOf("Main-Class" to "com.developpez.tictactoe.TicTacToeApp"))

33. }

34. }

35. }

37. tasks {

38. build {

39. dependsOn(shadowJar)

40. }

41. }

N'oubliez pas d'appliquer les changements à l'aide du bouton Local Gradle changes.

V-B - Génération et exécution

D'abord, nous allons lancer la tâche build de Gradle. (Notez également que vous pouvez nettoyer le projet auparavant avec la tâche clean.)

Pour cela, cliquez sur le bouton d'accès rapide à Gradle. Voici comment cela se présente dans ma configuration.

Les tâches qui nous intéressent (clean et build) se situent dans le nœud Tasks->build. Il suffit de double-cliquer sur la tâche souhaitée. Lancez donc la tâche build.

Si tout se passe bien, le JAR se situe dans le dossier build/libs.

Vous pouvez alors démarrer le terminal de l'EDI.

Saisissez alors la commande suivante :

1. java -jar build/libs/TicTacToe-1.0-all.jar

Cela devrait vous permettre de démarrer votre nouveau programme. Par contre, pour les utilisateurs Linux dont le bureau est GTK3 (en tous cas, c'est sur cette configuration que j'ai rencontré le souci) : il existe un bug qui fait crasher l'application directement. Après avoir un peu recherché, il me semble que cela soit surtout lié à OpenJDK 8.

Quoi qu'il en soit, voici la solution sur laquelle je suis arrivé. Dans la commande d'exécution, il suffirait d'ajouter l'option java jdk.gtk.version=2. Ce qui nous donne :

1. java -Djdk.gtk.version=2 -jar build/libs/TicTacToe-1.0-all.jar

VI - Aller plus loin

Le site officiel de TornadoFX, et le guide officiel – les deux étant en anglais – peuvent vous permettre d'en savoir davantage sur les fonctionnalités de TornadoFX. Il existe aussi une chaîne Slack permettant de demander de l'aide.

N'hésitez pas aussi à consulter les ressources JavaFX disponibles sur le portail developpez.

Enfin, ceux qui le souhaitent peuvent essayer de gérer la détection d'un gagnant en guise d'exercice (voire la mise en évidence de la ligne gagnante : par exemple avec des images supplémentaires).

VII - Conclusion et remerciements

J'espère que ce tutoriel vous a plu.

Je tiens à remercier Mickaël Baron, pour sa relecture technique.

Je tiens également à remercier ClaudeLELOUP, pour sa relecture orthographique.