paint-brush
Développement de jeux MVP avec Flutter et Flameby@leobit
408
408

Développement de jeux MVP avec Flutter et Flame

Leobit28m2024/06/06
Read on Terminal Reader

Les MVP (produits minimum viables) sont de plus en plus populaires dans l'industrie du jeu vidéo. Avec MVP, vous pouvez créer une application avec des fonctionnalités de base à court terme et avec un budget limité. Avec l'aide de Flame, un moteur de jeu robuste et open source construit sur Flutter, vous pouvez créer de superbes jeux 2D.
featured image - Développement de jeux MVP avec Flutter et Flame
Leobit HackerNoon profile picture
0-item

Seules 2 startups sur 5 sont rentables, selon une récente enquête . Un MVP (produit minimum viable) augmente considérablement les chances de rentabilité d'une startup car il permet à ces entreprises de recueillir les premiers commentaires des utilisateurs sans dépenser la totalité du budget dans une application dotée de fonctionnalités complètes.


Avec MVP, vous pouvez créer une application avec des fonctionnalités de base à court terme et avec un budget limité, recueillir les commentaires des utilisateurs et continuer à développer la solution avec votre équipe de développement en fonction de ces commentaires.


Les MVP sont de plus en plus populaires dans l’industrie du jeu vidéo. Aujourd'hui, nous allons explorer les tenants et les aboutissants du développement rapide de jeux MVP avec Flutter et Flame, une combinaison stellaire pour créer des produits multiplateformes minimum viables.

Pourquoi choisir Flutter et Flame ?

Flutter, une plate-forme sécurisée et riche en fonctionnalités pour le développement multiplateforme , a pris d'assaut le monde des applications mobiles et sa portée s'étend bien au-delà de l'interface utilisateur. Avec l'aide de Flame, un moteur de jeu robuste et open source construit sur Flutter , vous pouvez créer de superbes jeux 2D qui fonctionnent parfaitement sur les appareils Android, iOS, Web et de bureau.


Flutter est également devenu une solution populaire pour créer des MVP de jeux en raison de ses fonctionnalités intégrées qui facilitent le développement rapide de solutions présentant les fonctionnalités de base sur différents appareils. En particulier, divers avantages et fonctions intégrées de Flutter permettent :


  • Créez un produit avec une base de code partagée pour différentes plates-formes, notamment Android et iOS, ce qui est beaucoup plus rapide et rentable que la création d'applications natives distinctes pour différentes plates-formes. Il existe également certaines pratiques pour créer des applications Web Flutter avec la même base de code.


  • Créez des interfaces utilisateur flexibles avec des widgets prédéfinis et des animations par défaut, ce qui augmente la vitesse de développement, l'un des facteurs les plus critiques en termes de développement MVP.


  • Flutter offre une fonctionnalité de rechargement à chaud qui permet aux développeurs de visualiser simultanément les modifications apportées au code dans l'application qui apparaissent à l'écran, garantissant ainsi une plus grande flexibilité dans le développement de MVP. Cette fonctionnalité rend l'itération et l'expérimentation beaucoup plus simples, permettant aux développeurs d'essayer rapidement différents mécanismes et visuels.


  • Le développement d'un produit minimum viable implique généralement un nombre minimum de ressources, et Flutter satisfait pleinement à cette exigence, car l'intégration par défaut de Flutter avec Firebase réduit considérablement la complexité de la programmation côté serveur.


Flutter ne consomme pas beaucoup de ressources informatiques et facilite la configuration simple d'applications multiplateformes.


L'application MVP basée sur la combinaison Flutter et Flame est une solution fiable mais relativement simple à développer. Il se compile directement en code natif, garantissant un gameplay et une réactivité fluides. Vous pouvez développer votre jeu MVP une seule fois et le déployer sur différentes plates-formes, économisant ainsi du temps et des ressources. Flutter et Flame gèrent les différences de plate-forme sous le capot.


De plus, les deux technologies disposent de communautés dynamiques avec une documentation complète, des didacticiels et des exemples de code. Cela signifie que vous ne serez jamais à court de réponse ou d'inspiration.

Alors, que peut faire la flamme ?

Flame fournit un ensemble complet d'outils pour créer des fonctionnalités de jeu MVP à court terme et sans dépenser trop de ressources. Ce framework de modélisation multiplateforme offre des outils pour un large éventail de cas d'utilisation différents :


  • Sprites et animations : vous pouvez créer rapidement vos sprites ou les utiliser à partir de diverses bibliothèques en ligne. Flame prend également en charge l'animation squelettique, ce qui vous permet de créer des animations plus complexes et plus réalistes.


  • Détection de collision : il dispose d'un système de détection de collision intégré qui facilite la création de jeux avec votre propre physique. Vous pouvez utiliser la détection de collision pour construire des plates-formes, des murs, des objets de collection et d'autres éléments avec lesquels vos personnages de jeu peuvent interagir.


  • Simulations physiques : Flame prend également en charge les simulations physiques, qui vous permettent de créer des mécanismes de jeu plus dynamiques et plus engageants. Vous pouvez utiliser des simulations physiques pour créer des éléments comme la gravité, les sauts et les rebonds.


  • Audio et effets sonores : vous pouvez utiliser l'audio pour créer une musique de fond, des effets sonores (comme un coup, un saut, etc.) et même un doublage.


  • Gestion de l'état : Flame fournit un certain nombre de fonctionnalités pour gérer l'état de votre jeu. Cela inclut des éléments tels que la tenue des scores, la gestion des niveaux et les données des joueurs.


  • Périphériques d'entrée : Flame prend en charge divers périphériques d'entrée, tels que les écrans tactiles, les claviers et les contrôleurs de jeu. Cela en fait une excellente option pour développer des jeux pour diverses plates-formes.


  • Défilement parallaxe : il prend en charge le défilement parallaxe, ce qui peut ajouter de la profondeur et de l'immersion à votre monde de jeu. Le défilement parallaxe crée l’illusion de profondeur en déplaçant différentes couches de l’arrière-plan à différentes vitesses.


  • Systèmes de particules : Flame prend également en charge les systèmes de particules, qui peuvent être utilisés pour créer une variété d'effets visuels, tels que des explosions, de la fumée et de la pluie.


  • Gameplay multijoueur : cela permet aux joueurs de rivaliser ou de collaborer les uns avec les autres en temps réel.


La plupart des fonctionnalités mentionnées ci-dessus sont essentielles pour de nombreux jeux et ne doivent pas être négligées même au stade du développement du MVP. Ce qui est vraiment important, c'est que Flame augmente considérablement la vitesse de développement des fonctionnalités mentionnées ci-dessus, vous permettant de publier ces fonctionnalités même dans les premières versions du produit.

Essayons

Maintenant, au lieu de parler de Flame, créons un MVP contenant les fonctionnalités de base de notre propre jeu avec ce framework. Avant de commencer, vous devez avoir installé Flutter 3.13 ou supérieur, votre IDE et appareil préférés pour les tests.

Une idée

Ce jeu est inspiré de Chrome Dino. Ah, le fameux Dino Run ! C'est plus qu'un simple jeu de Chrome. C'est un œuf de Pâques bien-aimé caché dans le mode hors ligne du navigateur.


Notre projet aura le gameplay suivant :

  • Vous incarnez Jack, un gars aventureux qui court sans fin à travers une forêt sombre.
  • Les contrôles sont minimes : appuyez sur la barre d'espace ou cliquez sur l'écran pour sauter.
  • Le jeu commence lentement mais augmente progressivement la vitesse, vous gardant sur vos gardes.
  • Votre objectif est simple : éviter les obstacles et courir le plus loin possible, accumulant des points en cours de route.


Et cela s’appellera « Forest Run ! »

Prépare toi

Créez un projet Flutter vide comme vous le faites à chaque fois que vous démarrez une nouvelle application. Pour commencer, nous devons définir les dépendances dans pubspec.yaml pour notre projet. Au moment de la rédaction de cet article, la dernière version de Flame est la 1.14.0. Définissons également tous les chemins d'accès des ressources maintenant, afin qu'il ne soit pas nécessaire de revenir à ce fichier plus tard. Et placez les images dans le répertoire assets/images/. Nous devons le mettre ici car Flame analysera exactement ce chemin :


 environment: sdk: '>=3.2.3 <4.0.0' flutter: '>=3.13.0' dependencies: flutter: sdk: flutter flame: ^1.14.0 flutter: uses-material-design: true assets: - assets/images/ - assets/images/character/ - assets/images/background/ - assets/images/forest/ - assets/images/font/


N'oubliez pas de placer toutes les images sous actifs/images/ car Flame n'analysera pas les autres répertoires.


Vous aurez besoin de beaucoup d’images pour n’importe quel jeu. Mais que se passe-t-il si vous n’êtes pas doué en design ? Heureusement, il existe de nombreux actifs open source que vous pouvez utiliser pour vos projets. Les ressources de ce jeu proviennent de itch.io. Nous utiliserons ces ressources pour notre projet :



Vous pouvez visiter ces liens ou simplement télécharger les ressources préparées (LIEN VERS ASSETS ARCHIVE) pour ce projet et copier tout le contenu dans votre projet.


Flame a une philosophie similaire à Flutter. Dans Flutter, tout est un widget ; dans Flame, tout est un Composant, même le Jeu dans son ensemble. Chaque composant peut remplacer 2 méthodes : onLoad() et update(). onLoad() n'est appelé qu'une seule fois lorsque le composant est monté dans le ComponentTree et update() est déclenché sur chaque image. Très similaire à initState() et build() de StatefulWidget dans Flutter.


Maintenant, écrivons du code. Créez une classe qui étend FlameGame et charge tous nos actifs dans le cache.


 class ForestRunGame extends FlameGame { @override Future<void> onLoad() async { await super.onLoad(); await images.loadAllImages(); } }


Ensuite, utilisez ForestRunGame dans main.dart. Vous pouvez également utiliser les méthodes de Flame.device pour configurer l'orientation du périphérique. Et il y a GameWidget, qui sert de pont entre les widgets et les composants.


 Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await Flame.device.fullScreen(); await Flame.device.setLandscape(); runApp(GameWidget(game: ForestRunGame())); }


À ce stade, nous pouvons déjà démarrer le jeu, mais il n'y aura qu'un écran noir. Nous devons donc ajouter nos composants.

Forêt Noire

Nous diviserons la forêt en deux composantes : l'arrière-plan et le premier plan. Tout d’abord, nous allons gérer l’arrière-plan. Avez-vous déjà parcouru une page qui vous semblait dynamique ? Comme si vous faisiez défiler plusieurs vues à la fois ? C'est un effet de parallaxe, et cela se produit lorsque les différents éléments d'une page se déplacent à des vitesses différentes, créant un effet de profondeur 3D.


Comme vous pouvez le penser, nous utiliserons une parallaxe pour notre arrière-plan. Étendez ParallaxComponent et configurez une pile d'images à l'aide de ParallaxImageData. En outre, il existe baseVelocity pour la vitesse des couches initiales et VelocityMultiplierDelta, qui représente la différence relative de vitesse entre les couches. Et la dernière chose, configurez le champ de priorité (z-index) pour le déplacer derrière les autres composants.


 class ForestBackground extends ParallaxComponent<ForestRunGame> { @override Future<void> onLoad() async { priority = -10; parallax = await game.loadParallax( [ ParallaxImageData('background/plx-1.png'), ParallaxImageData('background/plx-2.png'), ParallaxImageData('background/plx-3.png'), ParallaxImageData('background/plx-4.png'), ParallaxImageData('background/plx-5.png'), ], baseVelocity: Vector2.zero(), velocityMultiplierDelta: Vector2(1.4, 1.0), ); } }


Le fond est terminé ; maintenant, il est temps d'ajouter le premier plan. Étendez le PositionComponent afin que nous puissions aligner le sol avec le bas de l'écran. Nous avons également besoin du mixin HasGameReference pour accéder au cache du jeu.


Pour créer du sol, il vous suffit de mettre l'image du bloc de sol en ligne plusieurs fois. Dans Flame, les composants d'image sont appelés sprites. Un Sprite est une région d'une image qui peut être rendue dans le canevas. Il peut représenter l’image entière ou être l’une des pièces que comprend une feuille de sprite.


N’oubliez pas non plus que l’axe X est orienté vers la droite et que l’axe Y est orienté vers le bas. Le centre des axes est positionné dans le coin supérieur gauche de l'écran.



 class ForestForeground extends PositionComponent with HasGameReference<ForestRunGame> { static final blockSize = Vector2(480, 96); late final Sprite groundBlock; late final Queue<SpriteComponent> ground; @override void onLoad() { super.onLoad(); groundBlock = Sprite(game.images.fromCache('forest/ground.png')); ground = Queue(); } @override void onGameResize(Vector2 size) { super.onGameResize(size); final newBlocks = _generateBlocks(); ground.addAll(newBlocks); addAll(newBlocks); y = size.y - blockSize.y; } List<SpriteComponent> _generateBlocks() { final number = 1 + (game.size.x / blockSize.x).ceil() - ground.length; final lastBlock = ground.lastOrNull; final lastX = lastBlock == null ? 0 : lastBlock.x + lastBlock.width; return List.generate( max(number, 0), (i) => SpriteComponent( sprite: groundBlock, size: blockSize, position: Vector2(lastX + blockSize.x * i, y), priority: -5, ), growable: false, ); } }


Et la dernière chose, ajoutez ces composants à notre ForestRunGame.


 class ForestRunGame extends FlameGame { late final foreground = ForestForeground(); late final background = ForestBackground(); @override Future<void> onLoad() async { await super.onLoad(); await images.loadAllImages(); add(foreground); add(background); } }


Maintenant, essayez de lancer le jeu. À ce stade, nous avons déjà notre forêt.


Un étranger dans la forêt

La forêt a l'air bien, mais ce n'est qu'une image pour le moment. Nous allons donc créer Jack, qui traversera cette forêt sous la direction du joueur. Contrairement aux arbres et au sol, le joueur a besoin d'animations pour se sentir vivant. Nous avons utilisé Sprite pour les blocs au sol, mais nous allons utiliser SpriteAnimation pour Jack. Comment cela marche-t-il? Eh bien, tout est simple, il vous suffit de boucler une séquence de sprites. Par exemple, notre animation de course comporte 8 sprites qui se remplacent avec un petit intervalle de temps.



Jack peut courir, sauter et rester inactif. Pour représenter ses états, nous pouvons ajouter une énumération PlayerState. Créez ensuite un Player qui étend SpriteAnimationGroupComponent et transmettez PlayerState comme argument générique. Ce composant a un champ d'animations où les animations pour chaque PlayerState sont stockées et un champ actuel, qui représente l'état actuel du joueur, qui doit être animé.


 enum PlayerState { jumping, running, idle } class Player extends SpriteAnimationGroupComponent<PlayerState> { @override void onLoad() { super.onLoad(); animations = { PlayerState.running: SpriteAnimation.fromFrameData( game.images.fromCache('character/run.png'), SpriteAnimationData.sequenced( amount: 8, amountPerRow: 5, stepTime: 0.1, textureSize: Vector2(23, 34), ), ), PlayerState.idle: SpriteAnimation.fromFrameData( game.images.fromCache('character/idle.png'), SpriteAnimationData.sequenced( amount: 12, amountPerRow: 5, stepTime: 0.1, textureSize: Vector2(21, 35), ), ), PlayerState.jumping: SpriteAnimation.spriteList( [ Sprite(game.images.fromCache('character/jump.png')), Sprite(game.images.fromCache('character/land.png')), ], stepTime: 0.4, loop: false, ), }; current = PlayerState.idle; } }


Les états du joueur sont prêts. Maintenant, nous devons donner au joueur une taille et une position sur l'écran. Je vais définir sa taille sur 69x102 pixels, mais n'hésitez pas à la modifier à votre guise. Pour se positionner, il faut connaître les coordonnées du terrain. En ajoutant le mixin HasGameReference, nous pouvons accéder au champ de premier plan et obtenir ses coordonnées. Maintenant, remplaçons la méthode onGameResize, qui est appelée à chaque fois que la taille de l'application est modifiée, et définissons ici la position de Jack.


 class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<ForestRunGame> { static const startXPosition = 80.0; Player() : super(size: Vector2(69, 102)); double get groundYPosition => game.foreground.y - height + 20; // onLoad() {...} with animation setup @override void onGameResize(Vector2 size) { super.onGameResize(size); x = startXPosition; y = groundYPosition; } }


Comme cela a été fait précédemment, ajoutez le joueur à notre jeu.


 class ForestRunGame extends FlameGame { // Earlier written code here... late final player = Player(); @override Future<void> onLoad() async { // Earlier written code here... add(player); } }


Si vous démarrez le jeu, vous voyez que Jack est déjà dans la forêt !


Cours Jack, cours !

Notre jeu a trois états : intro, play et game over. Nous allons donc ajouter l'énumération GameState qui représente ces derniers. Pour faire courir Jack, nous avons besoin de variables de vitesse et d’accélération. Nous devons également calculer la distance parcourue (sera utilisée plus tard).


Comme mentionné précédemment, le composant dispose de deux méthodes principales : onLoad() et update(). Nous avons déjà utilisé la méthode onLoad à plusieurs reprises. Parlons maintenant de update(). Cette méthode a un paramètre appelé dt. Il représente le temps écoulé depuis le dernier appel de update().


Pour calculer la vitesse actuelle et la distance parcourue, nous utiliserons la méthode update() et quelques formules cinématiques de base :

  • Distance = vitesse * temps ;
  • Vitesse = accélération * temps ;


 enum GameState { intro, playing, gameOver } class ForestRunGame extends FlameGame { static const acceleration = 10.0; static const maxSpeed = 2000.0; static const startSpeed = 400.0; GameState state = GameState.intro; double currentSpeed = 0; double traveledDistance = 0; // Earlier written code here... @override void update(double dt) { super.update(dt); if (state == GameState.playing) { traveledDistance += currentSpeed * dt; if (currentSpeed < maxSpeed) { currentSpeed += acceleration * dt; } } } }


En fait, nous allons utiliser une astuce pour simplifier le développement : Jack sera stable, mais la forêt se déplacera vers Jack. Nous avons donc besoin de notre forêt pour appliquer la vitesse du jeu.


Pour le fond de parallaxe, il suffit de passer la vitesse du jeu. Et il s’occupera automatiquement du reste.


 class ForestBackground extends ParallaxComponent<ForestRunGame> { // Earlier written code here... @override void update(double dt) { super.update(dt); parallax?.baseVelocity = Vector2(game.currentSpeed / 10, 0); } }


Pour le premier plan, nous devons décaler chaque bloc au sol. Nous devons également vérifier si le premier bloc de la file d’attente a quitté l’écran. Si tel est le cas, supprimez-le et placez-le à la fin de la file d'attente ;


 class ForestForeground extends PositionComponent with HasGameReference<ForestRunGame> { // Earlier written code here... @override void update(double dt) { super.update(dt); final shift = game.currentSpeed * dt; for (final block in ground) { block.x -= shift; } final firstBlock = ground.first; if (firstBlock.x <= -firstBlock.width) { firstBlock.x = ground.last.x + ground.last.width; ground.remove(firstBlock); ground.add(firstBlock); } } }


Tout est prêt, sauf un déclencheur. Nous voulons commencer à courir au clic. Nos cibles sont à la fois les mobiles et les ordinateurs de bureau, nous souhaitons donc gérer les pressions sur l'écran et les événements de clavier.


Heureusement, Flame a un moyen de le faire. Ajoutez simplement un mixin pour votre type d’entrée. Pour le clavier, il s'agit de KeyboardEvents et TapCallbacks pour le tapotement sur l'écran. Ces mixins vous donnent la possibilité de remplacer les méthodes associées et de fournir votre logique.


Le jeu doit démarrer si l'utilisateur appuie sur la barre d'espace ou touche l'écran.


 class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks { // Earlier written code here... @override KeyEventResult onKeyEvent( RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed, ) { if (keysPressed.contains(LogicalKeyboardKey.space)) { start(); } return KeyEventResult.handled; } @override void onTapDown(TapDownEvent event) { start(); } void start() { state = GameState.playing; player.current = PlayerState.running; currentSpeed = startSpeed; traveledDistance = 0; } }


En conséquence, Jack peut maintenant courir après avoir cliqué.


Oh non, un Bush !

Maintenant, nous voulons avoir des obstacles sur la route. Dans notre cas, ils seront représentés comme des buissons venimeux. Bush n'est pas animé, nous pouvons donc utiliser SpriteComponent. De plus, nous avons besoin d'une référence de jeu pour accéder à sa vitesse. Et encore une chose; nous ne voulons pas faire apparaître les buissons un par un, car cette approche peut provoquer une situation dans laquelle Jack ne peut tout simplement pas franchir une ligne de buissons avec un saut. Il s'agit d'un nombre aléatoire provenant d'une plage qui dépend de la vitesse de jeu actuelle.


 class Bush extends SpriteComponent with HasGameReference<ForestRunGame> { late double gap; Bush() : super(size: Vector2(200, 84)); bool get isVisible => x + width > 0; @override Future<void> onLoad() async { x = game.size.x + width; y = -height + 20; gap = _computeRandomGap(); sprite = Sprite(game.images.fromCache('forest/bush.png')); } double _computeRandomGap() { final minGap = width * game.currentSpeed * 100; final maxGap = minGap * 5; return (Random().nextDouble() * (maxGap - minGap + 1)).floor() + minGap; } @override void update(double dt) { super.update(dt); x -= game.currentSpeed * dt; if (!isVisible) { removeFromParent(); } } }


Qui plante les buissons ? La nature, bien sûr. Créons la Nature qui gérera notre génération de brousse.


 class Nature extends Component with HasGameReference<ForestRunGame> { @override void update(double dt) { super.update(dt); if (game.currentSpeed > 0) { final plant = children.query<Bush>().lastOrNull; if (plant == null || (plant.x + plant.width + plant.gap) < game.size.x) { add(Bush()); } } } }


Maintenant, ajoutons Nature à notre ForestForeground.


 class ForestForeground extends PositionComponent with HasGameReference<ForestRunGame> { // Earlier written code here... late final Nature nature; @override void onLoad() { // Earlier written code here... nature = Nature(); add(nature); }


Maintenant, notre forêt a des buissons. Mais attendez, Jack est juste en train de les parcourir. Pourquoi cela arrive-t-il? C'est parce que nous n'avons pas encore implémenté la frappe.


Ici, Hitbox va nous aider. Hitbox est un autre composant du zoo de composants de Flame. Il encapsule la détection de collision et vous donne la possibilité de la gérer avec une logique personnalisée.


Ajoutez-en un pour Jack. N'oubliez pas que la position du composant placera son coin gauche-droit et non son centre. Et avec la taille, vous vous occupez du reste.


 class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<ForestRunGame> { // Earlier written code here... @override void onLoad() { // Earlier written code here... add( RectangleHitbox( position: Vector2(2, 2), size: Vector2(60, 100), ), ); } }


Et un pour la brousse. Ici, nous allons définir le type de collision sur passif pour une certaine optimisation. Par défaut, le type est actif, ce qui signifie que Flame vérifiera si cette hitbox entre en collision avec toutes les autres hitbox. Nous n'avons qu'un joueur et des buissons. Puisque le joueur a déjà un type de collision actif et que les buissons ne peuvent pas entrer en collision les uns avec les autres, nous pouvons définir le type de collision sur passif.


 class Bush extends SpriteComponent with HasGameReference<ForestRunGame> { // Earlier written code here... @override void onLoad() { // Earlier written code here... add( RectangleHitbox( position: Vector2(30, 30), size: Vector2(150, 54), collisionType: CollisionType.passive, ), ); } }


C'est cool, mais je ne vois pas si la position de la hitbox a été correctement ajustée. Comment puis-je le tester ?


Eh bien, vous pouvez définir le champ debugMode de Player et Bush sur true. Cela vous permettra de voir comment sont positionnées vos hitbox. Le violet définit la taille du composant et le jaune indique la hitbox.


Maintenant, nous voulons détecter quand il y a une collision entre le joueur et le buisson. Pour cela, vous devez ajouter le mixin HasCollisionDetection au jeu, puis CollisionCallbacks pour les composants qui doivent gérer les collisions.


 class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks, HasCollisionDetection { // Earlier written code here... }


Pour l'instant, mettez simplement le jeu en pause lorsque la collision est détectée.


 class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<ForestRunGame>, CollisionCallbacks { // Earlier written code here... @override void onCollisionStart( Set<Vector2> intersectionPoints, PositionComponent other, ) { super.onCollisionStart(intersectionPoints, other); game.paused = true; } }


Sauter ou mourir

Si Jack veut éviter ces buissons, il doit sauter. Apprenons-lui. Pour cette fonctionnalité, nous avons besoin de la constante de gravité et de la vitesse verticale initiale du saut de Jack. Ces valeurs ont été choisies à l’œil nu, alors n’hésitez pas à les ajuster.


Alors, comment fonctionne la gravité ? En gros, c'est la même accélération mais orientée vers le sol. Ainsi, nous pouvons utiliser les mêmes formules pour la position verticale et la vitesse. Ainsi, notre saut comportera 3 étapes :

  1. Le saut est déclenché et la vitesse verticale de Jack passe de zéro à la valeur initiale.
  2. Il monte et la gravité modifie progressivement sa vitesse. À un moment donné, Jack cessera de monter et commencera à descendre.
  3. Lorsque Jack touche le sol, nous devons arrêter de lui appliquer la gravité et réinitialiser son état à la course.


 class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<ForestRunGame>, CollisionCallbacks { static const gravity = 1400.0; static const initialJumpVelocity = -700.0; double jumpSpeed = 0; // Earlier written code here... void jump() { if (current != PlayerState.jumping) { current = PlayerState.jumping; jumpSpeed = initialJumpVelocity - (game.currentSpeed / 500); } } void reset() { y = groundYPos; jumpSpeed = 0; current = PlayerState.running; } @override void update(double dt) { super.update(dt); if (current == PlayerState.jumping) { y += jumpSpeed * dt; jumpSpeed += gravity * dt; if (y > groundYPos) { reset(); } } else { y = groundYPos; } } }


Et maintenant déclenchons le saut par clic depuis ForestRunGame


 class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks, HasCollisionDetection { // Earlier written code here... @override KeyEventResult onKeyEvent( RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed, ) { if (keysPressed.contains(LogicalKeyboardKey.space)) { onAction(); } return KeyEventResult.handled; } @override void onTapDown(TapDownEvent event) { onAction(); } void onAction() { switch (state) { case GameState.intro: case GameState.gameOver: start(); break; case GameState.playing: player.jump(); break; } } }


Maintenant, Jack peut gérer les buissons.

Jeu terminé

Une fois le jeu terminé, nous souhaitons afficher du texte à l'écran. Text in Flame fonctionne différemment de Flutter. Vous devez d'abord créer une police. Sous le capot, c'est juste une carte, où char est une clé et sprite est une valeur. Presque toujours, la police du jeu est une image où tous les symboles nécessaires sont rassemblés.


Pour ce jeu, nous n’avons besoin que de chiffres et de lettres majuscules. Alors, créons notre police. Pour ce faire, vous devez transmettre l'image source et les glyphes. Qu'est-ce qu'un glyphe ? Le glyphe est une union d'informations sur le caractère, sa taille et sa position dans l'image source.


 class StoneText extends TextBoxComponent { static const digits = '123456789'; static const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; StoneText({ required Image source, required super.position, super.text = '', }) : super( textRenderer: SpriteFontRenderer.fromFont( SpriteFont( source: source, size: 32, ascent: 32, glyphs: [ _buildGlyph(char: '0', left: 480, top: 0), for (var i = 0; i < digits.length; i++) _buildGlyph(char: digits[i], left: 32.0 * i, top: 32), for (var i = 0; i < letters.length; i++) _buildGlyph( char: letters[i], left: 32.0 * (i % 16), top: 64.0 + 32 * (i ~/ 16), ), ], ), letterSpacing: 2, ), ); static Glyph _buildGlyph({ required String char, required double left, required double top, }) => Glyph(char, left: left, top: top, height: 32, width: 32); }


Maintenant, nous pouvons créer le panneau de jeu et l'utiliser dans le jeu.


 class GameOverPanel extends PositionComponent with HasGameReference<ForestRunGame> { bool visible = false; @override Future<void> onLoad() async { final source = game.images.fromCache('font/keypound.png'); add(StoneText(text: 'GAME', source: source, position: Vector2(-144, -16))); add(StoneText(text: 'OVER', source: source, position: Vector2(16, -16))); } @override void renderTree(Canvas canvas) { if (visible) { super.renderTree(canvas); } } @override void onGameResize(Vector2 size) { super.onGameResize(size); x = size.x / 2; y = size.y / 2; } }


Maintenant, nous pouvons montrer à notre panel, quand Jack frappe le buisson. Modifions également la méthode start(), afin de pouvoir redémarrer le jeu en un clic. Nous devons également éliminer tous les buissons de la forêt.


 class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks, HasCollisionDetection { // Earlier written code here... late final gameOverPanel = GameOverPanel(); @override Future<void> onLoad() async { // Earlier written code here... add(gameOverPanel); } void gameOver() { paused = true; gameOverPanel.visible = true; state = GameState.gameOver; currentSpeed = 0; } void start() { paused = false; state = GameState.playing; currentSpeed = startSpeed; traveledDistance = 0; player.reset(); foreground.nature.removeAll(foreground.nature.children); gameOverPanel.visible = false; } }


Et maintenant, nous devons mettre à jour le rappel de collision dans le lecteur.


 class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<ForestRunGame>, CollisionCallbacks { // Earlier written code here... @override void onCollisionStart( Set<Vector2> intersectionPoints, PositionComponent other, ) { super.onCollisionStart(intersectionPoints, other); game.gameOver(); } }


Désormais, vous pouvez voir Game Over lorsque Jack heurte un buisson. Et redémarrez le jeu en cliquant à nouveau.


Qu'en est-il de mon score ?

Et le calcul final du score tactile.


 class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks, HasCollisionDetection { late final StoneText scoreText; late final StoneText highText; late final StoneText highScoreText; int _score = 0; int _highScore = 0; // Earlier written code here... @override Future<void> onLoad() async { // Earlier written code here... final font = images.fromCache('font/keypound.png'); scoreText = StoneText(source: font, position: Vector2(20, 20)); highText = StoneText(text: 'HI', source: font, position: Vector2(256, 20)); highScoreText = StoneText( text: '00000', source: font, position: Vector2(332, 20), ); add(scoreText); add(highScoreText); add(highText); setScore(0); } void start() { // Earlier written code here... if (_score > _highScore) { _highScore = _score; highScoreText.text = _highScore.toString().padLeft(5, '0'); } _score = 0; } @override void update(double dt) { super.update(dt); if (state == GameState.playing) { traveledDistance += currentSpeed * dt; setScore(traveledDistance ~/ 50); if (currentSpeed < maxSpeed) { currentSpeed += acceleration * dt; } } } void setScore(int score) { _score = score; scoreText.text = _score.toString().padLeft(5, '0'); } }


C'est tout, les amis !


Maintenant, essayez-le et essayez de battre mon meilleur score. Cela fait 2537 points !

Conclusions

C'était beaucoup, mais nous l'avons fait. Nous avons créé un produit minimum viable pour un jeu mobile avec physique, animations, calcul de score et bien plus encore. Il y a toujours place à l'amélioration et, comme tout autre MVP, on peut s'attendre à ce que notre produit soit doté de nouvelles fonctionnalités, mécanismes et modes de jeu à l'avenir.


Il existe également un package flame_audio, que vous pouvez utiliser pour ajouter de la musique de fond, des sons de saut ou de frappe, etc.


Pour l’instant, notre objectif principal était de créer les fonctionnalités de base du produit dans des délais courts et avec une allocation de ressources limitée. La combinaison de Flutter et Flame s'est avérée être la solution idéale pour créer un MVP de jeu pouvant être utilisé pour recueillir les commentaires des utilisateurs et continuer à mettre à niveau l'application à l'avenir.


Vous pouvez vérifier les résultats de nos efforts ici .


Avec leurs fonctionnalités puissantes, leur facilité d’utilisation et leur communauté florissante, Flutter et Flame sont un choix incontournable pour les développeurs de jeux en herbe. Que vous soyez un professionnel chevronné ou un débutant, cette combinaison offre les outils et le potentiel nécessaires pour donner vie à vos idées de jeu. Alors, faites appel à votre créativité, plongez dans le monde de Flutter et Flame et commencez à créer la prochaine sensation de jeu mobile !


Nous espérons que vous avez trouvé cet article agréable et informatif. Si vous souhaitez plus d'informations sur le développement de logiciels ou souhaitez discuter de votre propre projet MVP, n'hésitez pas à explorer Leobit ou à contacter notre équipe technique !