J'ai toujours voulu faire des jeux vidéo. Ma toute première application Android qui m'a aidé à obtenir mon premier emploi était un jeu simple, créé avec des vues Android. Après cela, il y a eu de nombreuses tentatives pour créer un jeu plus élaboré à l'aide d'un moteur de jeu, mais toutes ont échoué par manque de temps ou par la complexité d'un framework. Mais quand j'ai entendu parler pour la première fois du moteur Flame, basé sur Flutter, j'ai été immédiatement attiré par sa simplicité et son support multiplateforme, alors j'ai décidé d'essayer de créer un jeu avec. Je voulais commencer par quelque chose de simple, mais toujours difficile, pour avoir une idée du moteur. Cette série d'articles est mon parcours d'apprentissage de Flame (et Flutter) et de création d'un jeu de plateforme de base. Je vais essayer de le rendre assez détaillé, il devrait donc être utile à tous ceux qui viennent de plonger leurs orteils dans Flame ou dans le développement de jeux en général. Au cours de 4 articles, je vais créer un jeu à défilement latéral 2D, qui comprend : Un personnage qui peut courir et sauter Une caméra qui suit le joueur Carte de niveau défilante, avec sol et plates-formes Fond de parallaxe Pièces que le joueur peut collecter et HUD qui affiche le nombre de pièces Écran de victoire Dans la première partie, nous allons créer un nouveau projet Flame, charger tous les actifs, ajouter un personnage joueur et lui apprendre à courir. Configuration du projet Commençons par créer un nouveau projet. Le didacticiel officiel fait un excellent travail en décrivant toutes les étapes pour le faire, alors suivez-le. du jeu Bare Flame Une chose à ajouter : lorsque vous configurez le fichier , vous pouvez mettre à jour les versions des bibliothèques avec les dernières disponibles, ou les laisser telles quelles, car le signe caret (^) avant une version garantira que votre application utilise la dernière non -version de rupture. ( ) pubspec.yaml syntaxe caret Si vous avez suivi toutes les étapes, votre fichier devrait ressembler à ceci : main.dart import 'package:flame/game.dart'; import 'package:flutter/widgets.dart'; void main() { final game = FlameGame(); runApp(GameWidget(game: game)); } Actifs Avant de continuer, nous devons préparer les ressources qui seront utilisées pour le jeu. Les actifs sont des images, des animations, des sons, etc. Pour les besoins de cette série, nous n'utiliserons que des images qui sont également appelées sprites dans le développement de jeux. Le moyen le plus simple de créer un niveau de plate-forme consiste à utiliser des cartes de tuiles et des sprites de tuiles. Cela signifie que le niveau est essentiellement une grille, où chaque cellule indique quel objet/sol/plate-forme elle représente. Plus tard, lorsque le jeu est en cours d'exécution, les informations de chaque cellule sont mappées sur le sprite de tuile correspondant. Les graphismes des jeux construits à l'aide de cette technique peuvent être très élaborés ou très simples. Par exemple, dans Super Mario bros, vous voyez que beaucoup d'éléments se répètent. C'est parce que, pour chaque tuile de sol dans la grille de jeu, il n'y a qu'une seule image de sol qui la représente. Nous suivrons la même approche et préparerons une seule image pour chaque objet statique que nous avons. Nous souhaitons également que certains objets, tels que le personnage du joueur et les pièces, soient animés. L'animation est généralement stockée sous la forme d'une série d'images fixes, chacune représentant une seule image. Lorsque l'animation est en cours de lecture, les images se succèdent, créant l'illusion de l'objet en mouvement. Maintenant, la question la plus importante est de savoir où obtenir les actifs. Bien sûr, vous pouvez les dessiner vous-même ou les commander à un artiste. De plus, de nombreux artistes géniaux ont contribué à l'open source. J'utiliserai de . le pack Arcade Platformer Assets GrafxKid En règle générale, les ressources d'image se présentent sous deux formes : des feuilles de sprites et des sprites uniques. Le premier est une grande image, contenant tous les éléments du jeu en un. Ensuite, les développeurs de jeux spécifient la position exacte du sprite requis et le moteur de jeu le découpe de la feuille. Pour ce jeu, j'utiliserai des sprites uniques (à l'exception des animations, il est plus facile de les conserver comme une seule image) car je n'ai pas besoin de tous les éléments fournis dans la feuille de sprites. Que vous créiez vous-même des sprites ou que vous les obteniez d'un artiste, vous devrez peut-être les découper pour les rendre plus adaptés au moteur de jeu. Vous pouvez utiliser des outils créés spécifiquement à cet effet (comme ou n'importe quel éditeur graphique. J'ai utilisé Adobe Photoshop, car, dans cette feuille de sprites, les sprites ont un espace inégal entre eux, ce qui rendait difficile l'extraction d'images par des outils automatiques, j'ai donc dû le faire manuellement. le texture packer) Vous voudrez peut-être aussi augmenter la taille des éléments, mais s'il ne s'agit pas d'une image vectorielle, l'image-objet résultante pourrait devenir floue. Une solution de contournement que j'ai trouvée qui fonctionne très bien pour le pixel art consiste à utiliser la méthode de redimensionnement dans Photoshop (ou l'interpolation définie sur Aucun dans Gimp). Mais si votre élément est plus détaillé, cela ne fonctionnera probablement pas. Nearest Neighbour (hard edges) Avec des explications à l'avance, téléchargez ou préparez les vôtres et ajoutez-les au dossier de votre projet. les ressources que j'ai préparées assets/images Chaque fois que vous ajoutez de nouveaux éléments, vous devez les enregistrer dans le fichier comme ceci : pubspec.yaml flutter: assets: - assets/images/ Et le conseil pour l'avenir : si vous mettez à jour des ressources déjà enregistrées, vous devez redémarrer le jeu pour voir les modifications. Maintenant, chargeons les actifs dans le jeu. J'aime avoir tous les noms d'actifs au même endroit, ce qui fonctionne très bien pour un petit jeu, car il est plus facile de garder une trace de tout et de le modifier si nécessaire. Alors, créons un nouveau fichier dans le répertoire : lib assets.dart const String THE_BOY = "theboy.png"; const String GROUND = "ground.png"; const String PLATFORM = "platform.png"; const String MIST = "mist.png"; const String CLOUDS = "clouds.png"; const String HILLS = "hills.png"; const String COIN = "coin.png"; const String HUD = "hud.png"; const List<String> SPRITES = [THE_BOY, GROUND, PLATFORM, MIST, CLOUDS, HILLS, COIN, HUD]; Et puis créez un autre fichier, qui contiendra toute la logique du jeu à l'avenir : game.dart import 'package:flame/game.dart'; import 'assets.dart' as Assets; class PlatformerGame extends FlameGame { @override Future<void> onLoad() async { await images.loadAll(Assets.SPRITES); } } est la classe principale qui représente notre jeu, elle étend , la classe de jeu de base utilisée dans le moteur Flame. Ce qui à son tour étend - le bloc de construction de base de Flame. Tout dans votre jeu, y compris les images, l'interface ou les effets sont des Composants. Chaque a une méthode asynchrone , qui est appelée lors de l'initialisation du composant. Habituellement, toute la logique de configuration des composants y va. PlatformerGame FlameGame Component Component onLoad Enfin, nous avons importé notre fichier que nous avons créé précédemment et ajouté pour déclarer explicitement d'où proviennent nos constantes d'assets. Et utilisé la méthode pour charger tous les éléments répertoriés dans la liste dans le cache des images du jeu. assets.dart as Assets images.loadAll SPRITES Ensuite, nous devons créer notre nouveau à partir de . Modifiez le fichier comme suit : PlatformerGame main.dart import 'package:flame/game.dart'; import 'package:flutter/widgets.dart'; import 'game.dart'; void main() { runApp( const GameWidget<PlatformerGame>.controlled( gameFactory: PlatformerGame.new, ), ); } Toute la préparation est terminée et la partie amusante commence. Ajout de personnage de joueur Créez un nouveau dossier et un nouveau fichier à l'intérieur. Ce sera le composant qui représente le personnage du joueur : le garçon. lib/actors/ theboy.dart import '../game.dart'; import '../assets.dart' as Assets; import 'package:flame/components.dart'; class TheBoy extends SpriteAnimationComponent with HasGameRef<PlatformerGame> { TheBoy({ required super.position, // Position on the screen }) : super( size: Vector2.all(48), // Size of the component anchor: Anchor.bottomCenter // ); @override Future<void> onLoad() async { animation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.THE_BOY), SpriteAnimationData.sequenced( amount: 1, // For now we only need idle animation, so we load only 1 frame textureSize: Vector2.all(20), // Size of a single sprite in the sprite sheet stepTime: 0.12, // Time between frames, since it's a single frame not that important ), ); } } La classe étend qui est un composant utilisé pour les sprites animés et a un mixin qui nous permet de référencer l'objet du jeu pour charger des images à partir du cache du jeu ou obtenir des variables globales plus tard. SpriteAnimationComponent HasGameRef Dans notre méthode , nous créons une nouvelle à partir de la feuille de sprite que nous avons déclarée dans le fichier . onLoad SpriteAnimation THE_BOY assets.dart Ajoutons maintenant notre joueur au jeu ! Revenez au fichier et ajoutez ce qui suit au bas de la méthode : game.dart onLoad final theBoy = TheBoy(position: Vector2(size.x / 2, size.y / 2)); add(theBoy); Si vous lancez le jeu maintenant, nous devrions pouvoir rencontrer The Boy ! Mouvement du joueur Tout d'abord, nous devons ajouter la possibilité de contrôler The Boy à partir du clavier. Ajoutons le mixin au fichier . HasKeyboardHandlerComponents game.dart class PlatformerGame extends FlameGame with HasKeyboardHandlerComponents Revenons ensuite au mixin et : theboy.dart KeyboardHandler class TheBoy extends SpriteAnimationComponent with KeyboardHandler, HasGameRef<PlatformerGame> Ensuite, ajoutez de nouvelles variables de classe au composant : TheBoy final double _moveSpeed = 300; // Max player's move speed int _horizontalDirection = 0; // Current direction the player is facing final Vector2 _velocity = Vector2.zero(); // Current player's speed Enfin, redéfinissons la méthode qui permet d'écouter les entrées clavier : onKeyEvent @override bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) { _horizontalDirection = 0; _horizontalDirection += (keysPressed.contains(LogicalKeyboardKey.keyA) || keysPressed.contains(LogicalKeyboardKey.arrowLeft)) ? -1 : 0; _horizontalDirection += (keysPressed.contains(LogicalKeyboardKey.keyD) || keysPressed.contains(LogicalKeyboardKey.arrowRight)) ? 1 : 0; return true; } Maintenant vaut 1 si le joueur se déplace vers la droite, -1 si le joueur se déplace vers la gauche et 0 si le joueur ne bouge pas. Cependant, nous ne pouvons pas encore le voir à l'écran, car la position du joueur n'a pas encore changé. Corrigeons cela en ajoutant la méthode . _horizontalDirection update Maintenant, je dois expliquer ce qu'est la boucle de jeu. Fondamentalement, cela signifie que le jeu est exécuté dans une boucle sans fin. À chaque itération, l'état actuel est rendu dans la méthode , puis un nouvel état est calculé dans la méthode . Le paramètre dans la signature de la méthode est le temps en millisecondes depuis la dernière mise à jour de l'état. Dans cet esprit, ajoutez ce qui suit à : render Component's update dt theboy.dart @override void update(double dt) { super.update(dt); _velocity.x = _horizontalDirection * _moveSpeed; position += _velocity * dt; } Pour chaque cycle de boucle de jeu, nous mettons à jour la vitesse horizontale, en utilisant la direction actuelle et la vitesse maximale. Ensuite, nous modifions la position du sprite avec la valeur mise à jour multipliée par . dt Pourquoi avons-nous besoin de la dernière partie ? Eh bien, si vous mettez à jour la position avec juste la vitesse, le sprite s'envolera dans l'espace. Mais pouvons-nous simplement utiliser la plus petite valeur de vitesse, vous demanderez-vous ? Nous pouvons, mais la façon dont le joueur se déplace sera différente avec un taux d'images par seconde (FPS) différent. Le nombre d'images (ou de boucles de jeu) par seconde dépend des performances du jeu et du matériel sur lequel il est exécuté. Plus les performances de l'appareil sont bonnes, plus le FPS est élevé et plus le joueur se déplace rapidement. Afin d'éviter cela, nous faisons dépendre la vitesse du temps écoulé depuis la dernière image. De cette façon, le sprite se déplacera de la même manière sur n'importe quel FPS. D'accord, si nous lançons le jeu maintenant, nous devrions voir ceci : Génial, faisons maintenant demi-tour au garçon lorsqu'il se dirige vers la gauche. Ajoutez ceci au bas de la méthode : update if ((_horizontalDirection < 0 && scale.x > 0) || (_horizontalDirection > 0 && scale.x < 0)) { flipHorizontally(); } Logique assez simple : on vérifie si la direction courante (la flèche sur laquelle l'utilisateur appuie) est différente de la direction du sprite, puis on retourne le sprite le long de l'axe horizontal. Maintenant, ajoutons également une animation en cours d'exécution. Définissez d'abord deux nouvelles variables de classe : late final SpriteAnimation _runAnimation; late final SpriteAnimation _idleAnimation; Ensuite, mettez à jour comme ceci : onLoad @override Future<void> onLoad() async { _idleAnimation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.THE_BOY), SpriteAnimationData.sequenced( amount: 1, textureSize: Vector2.all(20), stepTime: 0.12, ), ); _runAnimation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.THE_BOY), SpriteAnimationData.sequenced( amount: 4, textureSize: Vector2.all(20), stepTime: 0.12, ), ); animation = _idleAnimation; } Ici, nous avons extrait l'animation inactive précédemment ajoutée à la variable de classe et défini une nouvelle variable d'animation d'exécution. Ensuite, ajoutons une nouvelle méthode : updateAnimation void updateAnimation() { if (_horizontalDirection == 0) { animation = _idleAnimation; } else { animation = _runAnimation; } } Et enfin, invoquez cette méthode au bas de la méthode et lancez le jeu. update Conclusion Voilà pour la première partie. Nous avons appris comment configurer un jeu Flame, où trouver des ressources, comment les charger dans votre jeu et comment créer un personnage animé génial et le faire bouger en fonction des entrées au clavier. Le code de cette partie se trouve . sur mon github Dans le prochain article, j'expliquerai comment créer un niveau de jeu à l'aide de Tiled, comment contrôler la caméra Flame et ajouter un arrière-plan de parallaxe. Restez à l'écoute! Ressources À la fin de chaque partie, j'ajouterai une liste de créateurs géniaux et de ressources dont j'ai appris. Actifs de la plate-forme Arcade de GrafxKid https://opengameart.org/content/arcade-platformer-assets Série de développement de jeux DevKage Flame : https://youtu.be/mSPalRqZQS8 Chaîne Craig Oda https://youtu.be/hwQpBuZoV9s Tutoriel du jeu Ember Quest https://github.com/flame-engine/flame/blob/main/doc/tutorials/platformer/platformer.md Documentation Flame Engine https://docs.flame-engine.org/1.6.0/flame/flame.html