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.
Commençons par créer un nouveau projet. Le didacticiel officiel du jeu Bare Flame fait un excellent travail en décrivant toutes les étapes pour le faire, alors suivez-le.
Une chose à ajouter : lorsque vous configurez le fichier pubspec.yaml
, 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. ( syntaxe caret )
Si vous avez suivi toutes les étapes, votre fichier main.dart
devrait ressembler à ceci :
import 'package:flame/game.dart'; import 'package:flutter/widgets.dart'; void main() { final game = FlameGame(); runApp(GameWidget(game: game)); }
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 le pack Arcade Platformer Assets de 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 le texture packer) 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.
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 Nearest Neighbour (hard edges)
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.
Avec des explications à l'avance, téléchargez les ressources que j'ai préparées ou préparez les vôtres et ajoutez-les au dossier assets/images
de votre projet.
Chaque fois que vous ajoutez de nouveaux éléments, vous devez les enregistrer dans le fichier pubspec.yaml
comme ceci :
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); } }
PlatformerGame
est la classe principale qui représente notre jeu, elle étend FlameGame
, la classe de jeu de base utilisée dans le moteur Flame. Ce qui à son tour étend Component
- 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 Component
a une méthode asynchrone onLoad
, qui est appelée lors de l'initialisation du composant. Habituellement, toute la logique de configuration des composants y va.
Enfin, nous avons importé notre fichier assets.dart
que nous avons créé précédemment et ajouté as Assets
pour déclarer explicitement d'où proviennent nos constantes d'assets. Et utilisé la méthode images.loadAll
pour charger tous les éléments répertoriés dans la liste SPRITES
dans le cache des images du jeu.
Ensuite, nous devons créer notre nouveau PlatformerGame
à partir de main.dart
. Modifiez le fichier comme suit :
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.
Créez un nouveau dossier lib/actors/
et un nouveau fichier theboy.dart
à l'intérieur. Ce sera le composant qui représente le personnage du joueur : le garçon.
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 SpriteAnimationComponent
qui est un composant utilisé pour les sprites animés et a un mixin HasGameRef
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.
Dans notre méthode onLoad
, nous créons une nouvelle SpriteAnimation
à partir de la feuille de sprite THE_BOY
que nous avons déclarée dans le fichier assets.dart
.
Ajoutons maintenant notre joueur au jeu ! Revenez au fichier game.dart
et ajoutez ce qui suit au bas de la méthode 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 !
Tout d'abord, nous devons ajouter la possibilité de contrôler The Boy à partir du clavier. Ajoutons le mixin HasKeyboardHandlerComponents
au fichier game.dart
.
class PlatformerGame extends FlameGame with HasKeyboardHandlerComponents
Revenons ensuite au mixin theboy.dart
et 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 onKeyEvent
qui permet d'écouter les entrées clavier :
@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 _horizontalDirection
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 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 render
Component's
, puis un nouvel état est calculé dans la méthode update
. Le paramètre dt
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 à 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 onLoad
comme ceci :
@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 update
et lancez le jeu.
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!
À la fin de chaque partie, j'ajouterai une liste de créateurs géniaux et de ressources dont j'ai appris.