1. Introduction
Découvrez comment créer un jeu de plates-formes avec Flutter et Flame. Inspiré par Doodle Jump, le jeu Doodle Dash vous permet de jouer avec Dash (la mascotte Flutter) ou avec son meilleur ami Sparky (la mascotte Firebase) pour sauter le plus haut possible sur des plates-formes.
Points abordés
- Créer un jeu multiplate-forme dans Flutter.
- Créer des composants de jeu réutilisables que vous pouvez afficher et mettre à jour via la boucle de jeu Flame.
- Contrôler et animer le mouvement de vos personnages (appelés lutins) via la physique du jeu.
- Ajouter et gérer la détection de collision.
- Définir les commandes de jeu par saisie au clavier ou saisie tactile.
Conditions préalables
Cet atelier de programmation suppose que vous avez une certaine expérience de Flutter. Dans le cas contraire, vous devriez d'abord vous familiariser avec ses principes de base en suivant l'atelier de programmation Créer sa première application Flutter.
Objectifs de l'atelier
Dans cet atelier de programmation, vous allez apprendre à créer le jeu Doodle Dash. Dans ce jeu de plates-formes, incarnez Dash (la mascotte Flutter) ou son meilleur ami Sparky (la mascotte Firebase) (même si la suite de cet atelier de programmation fait référence à Dash, les étapes s'appliquent également à Sparky). Le jeu propose les fonctionnalités suivantes :
- Un lutin qui peut se déplacer à l'horizontale et à la verticale
- Des plates-formes générées de façon aléatoire
- Un effet de gravité qui attire le lutin vers le bas
- Des menus de jeu
- Des commandes intégrées (pause et reprise, par exemple)
- L'enregistrement de votre score
Gameplay
Pour jouer à Doodle Dash, déplacez Dash vers la gauche ou la droite, sautez sur les plates-formes et utilisez les bonus pour accroître ses pouvoirs tout au long du jeu. Pour commencer le jeu, vous devez choisir le niveau de difficulté (de 1 à 5), puis cliquer sur Start (Démarrer).
Niveaux
Le jeu comporte cinq niveaux. À chaque nouveau niveau (après le niveau 1), vous pouvez débloquer de nouvelles fonctionnalités.
- Niveau 1 (par défaut) : génère les plates-formes
NormalPlatform
etSpringBoard
. À sa création, une plate-forme a 20 % de chances d'être mobile. - Niveau 2 (score ≥ à 20) : ajoute une
BrokenPlatform
sur laquelle vous ne pouvez sauter qu'une seule fois. - Niveau 3 (score ≥ à 40) : débloque le bonus
NooglerHat
. Cette plate-forme spéciale dure cinq secondes et augmente la puissance de saut de Dash de 2,5 fois la vélocité normale. Pendant ces cinq secondes, Dash porte un chapeau de Noogleur. - Niveau 4 (score ≥ à 80) : débloque le bonus
Rocket
. Avec cette nouvelle plate-forme représentée par une fusée, Dash est invincible. Elle augmente aussi la puissance de saut de Dash de 3,5 fois la vélocité normale. - Niveau 5 (score ≥ à 100) : débloque la plate-forme
Enemy
. Si Dash touche un ennemi, la partie est automatiquement terminée.
Types de plates-formes par niveau
Niveau 1 (par défaut)
|
|
Niveau 2 (score ≥ à 20) | Niveau 3 (score ≥ à 40) | Niveau 4 (score ≥ à 80) | Niveau 5 (score ≥ à 100) |
|
|
|
|
Fin de la partie
La partie se termine de deux façons :
- Dash tombe au-dessous de la partie inférieure de l'écran.
- Dash touche un ennemi (les ennemis sont générés au niveau 5).
Bonus
Les bonus améliorent les capacités du personnage. Par exemple, ils augmentent sa vélocité de saut et/ou le rendent invincible aux ennemis. Doodle Dash propose deux types de bonus. Vous ne pouvez activer qu'un seul bonus à la fois.
- Le chapeau de Noogleur augmente la puissance de saut de Dash de 2,5 fois la hauteur normale. Pendant ce bonus, Dash porte un chapeau de Noogleur.
- Avec le bonus fusée, Dash est invincible aux plates-formes ennemies (toucher un ennemi n'a aucun effet). Le bonus augmente aussi sa puissance de saut de 3,5 fois la hauteur normale. Dash vole dans une fusée jusqu'à ce que la gravité ait raison de sa vélocité et le fasse atterrir sur une plate-forme.
2. Obtenir le code de démarrage de l'atelier de programmation
Téléchargez la version initiale du projet sur GitHub :
- Dans la ligne de commande, clonez le dépôt GitHub dans un répertoire
flutter-codelabs
:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
Le code de cet atelier de programmation se trouve dans le répertoire flutter-codelabs/flame-building-doodle-dash
. Le répertoire comporte le code du projet finalisé pour chaque étape de l'atelier de programmation.
Importer l'application de départ
- Importez le répertoire
flutter-codelabs/flame-building-doodle-dash/step_02
dans l'IDE de votre choix.
Installer les packages
- Tous les packages nécessaires (comme Flame) ont déjà été ajoutés au fichier
pubspec.yaml
du projet. Si votre IDE n'installe pas automatiquement les dépendances, ouvrez un terminal de ligne de commande et exécutez la commande suivante depuis la racine du projet Flutter pour extraire les dépendances du projet :
flutter pub get
Configurer votre environnement de développement Flutter
Pour cet atelier de programmation, vous avez besoin des éléments suivants :
3. À la découverte du code
Parcourez ensuite le code.
Examinez le fichier lib/game/doodle_dash.dart
comportant le jeu Doodle Dash qui étend FlameGame
. Vous utilisez l'instance FlameGame
(le premier composant de base de Flame, semblable à un Scaffold
Flutter) pour enregistrer vos composants. Elle permet de rendre et de mettre à jour l'ensemble des composants enregistrés durant le gameplay. Vous pouvez la considérer comme l'épine dorsale de votre jeu.
Que sont les composants ? De la même manière qu'une application Flutter est constituée de Widgets
, un FlameGame
est composé de Components
, à savoir tous les éléments de base qui forment le jeu. (Comme pour les widgets Flutter, ils peuvent également avoir des composants enfants.) Le lutin du personnage, l'arrière-plan du jeu et l'objet responsable de générer les nouveaux éléments de jeu (comme les ennemis) sont tous des composants. En réalité, le FlameGame
est en soi un Component
. Flame l'appelle le "Flame Component System".
Les composants sont hérités d'une classe Component
abstraite. Implémentez les méthodes abstraites de Component
pour créer la mécanique de la classe FlameGame
. Par exemple, les méthodes suivantes sont régulièrement implémentées dans Doodle Dash :
onLoad
: réalise l'initialisation asynchrone d'un composant (comme la méthodeinitState
de Flutter).update
: met un composant à jour à chaque tick de la boucle de jeu (comme la méthodebuild
de Flutter).
De plus, la méthode add
enregistre les composants avec le moteur Flame.
Ainsi, le fichier lib/game/world.dart
comporte la classe World
qui étend ParallaxComponent
pour rendre l'arrière-plan du jeu. Cette classe reprend une liste de composants Image et les affiche sous la forme de couches. Chaque couche se déplace à une vélocité différente pour un rendu plus réaliste. La classe DoodleDash
comporte une instance ParallaxComponent
et l'ajoute au jeu dans la méthode onLoad
de Doodle Dash :
lib/game/world.dart
class World extends ParallaxComponent<DoodleDash> {
@override
Future<void> onLoad() async {
parallax = await gameRef.loadParallax(
[
ParallaxImageData('game/background/06_Background_Solid.png'),
ParallaxImageData('game/background/05_Background_Small_Stars.png'),
ParallaxImageData('game/background/04_Background_Big_Stars.png'),
ParallaxImageData('game/background/02_Background_Orbs.png'),
ParallaxImageData('game/background/03_Background_Block_Shapes.png'),
ParallaxImageData('game/background/01_Background_Squiggles.png'),
],
fill: LayerFill.width,
repeat: ImageRepeat.repeat,
baseVelocity: Vector2(0, -5),
velocityMultiplierDelta: Vector2(0, 1.2),
);
}
}
lib/game/doodle_dash.dart
class DoodleDash extends FlameGame
with HasKeyboardHandlerComponents, HasCollisionDetection {
...
final World _world = World();
...
@override
Future<void> onLoad() async {
await add(_world);
...
}
}
Gestion de l'état
Le répertoire lib/game/managers
comporte trois fichiers permettant de gérer l'état de Doodle Dash, à savoir game_manager.dart
, object_manager.dart
et level_manager.dart
.
La classe GameManager
(dans game_manager.dart
) permet de suivre l'état de jeu global et les scores.
La classe ObjectManager
(dans object_manager.dart
) permet de gérer où et quand les plates-formes sont générées et supprimées. Vous allez compléter cette classe ultérieurement.
Enfin, la classe LevelManager
(dans level_manager.dart
) permet de gérer le niveau de difficulté du jeu, ainsi que les configurations qui s'appliquent lorsque les joueurs passent au niveau supérieur. Le jeu comporte cinq niveaux de difficulté. Le joueur passe au niveau supérieur lorsqu'il atteint un certain score. Plus le niveau augmente, plus le jeu est difficile et plus Dash doit sauter haut. La gravité est constante tout au long du jeu. De ce fait, la vitesse de saut augmente progressivement afin de tenir compte des distances accrues.
Le score du joueur augmente dès qu'il franchit une plate-forme. Lorsque le joueur atteint un certain nombre de points, le jeu passe au niveau supérieur et débloque de nouvelles plates-formes spéciales qui accentuent la difficulté et le caractère ludique du jeu.
4. Ajouter un player au jeu
Cette étape permet d'ajouter un personnage au jeu (Dash, dans ce cas). Le player commande le personnage et l'ensemble de la logique est contenue dans la classe Player
(dans le fichier player.dart
). La classe Player
étend la classe SpriteGroupComponent
de Flame qui contient les méthodes abstraites à remplacer pour implémenter la logique personnalisée. Cela consiste à charger les éléments et les lutins, à positionner le player (à l'horizontale et à la verticale), à configurer la détection de collision et à accepter l'entrée utilisateur.
Chargement des éléments
Dash apparaît avec différents lutins qui représentent les différentes versions du personnage et des bonus. Par exemple, les icônes ci-dessous représentent Dash et Sparky, de face et de côté (gauche et droit).
Le SpriteGroupComponent
de Flame permet de gérer plusieurs états de lutin avec la propriété sprites
comme vous pouvez le constater dans la méthode _loadCharacterSprites
.
Dans la classe Player
, ajoutez les lignes suivantes à la méthode onLoad
pour charger les éléments de lutin et configurer les états de lutin du Player
de face :
lib/game/sprites/player.dart
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadCharacterSprites(); // Add this line
current = PlayerState.center; // Add this line
}
Examinez le code permettant de charger les lutins et des éléments dans _loadCharacterSprites
. Ce code peut être directement implémenté dans la méthode onLoad
. Cependant, le placer dans une autre méthode permet d'organiser le code source et d'améliorer sa lisibilité. Cette méthode permet d'effectuer un mappage avec la propriété sprites
qui associe chaque état de personnage avec un élément de lutin chargé, comme cela est représenté ci-dessous :
lib/game/sprites/player.dart
Future<void> _loadCharacterSprites() async {
final left = await gameRef.loadSprite('game/${character.name}_left.png');
final right = await gameRef.loadSprite('game/${character.name}_right.png');
final center =
await gameRef.loadSprite('game/${character.name}_center.png');
final rocket = await gameRef.loadSprite('game/rocket_4.png');
final nooglerCenter =
await gameRef.loadSprite('game/${character.name}_hat_center.png');
final nooglerLeft =
await gameRef.loadSprite('game/${character.name}_hat_left.png');
final nooglerRight =
await gameRef.loadSprite('game/${character.name}_hat_right.png');
sprites = <PlayerState, Sprite>{
PlayerState.left: left,
PlayerState.right: right,
PlayerState.center: center,
PlayerState.rocket: rocket,
PlayerState.nooglerCenter: nooglerCenter,
PlayerState.nooglerLeft: nooglerLeft,
PlayerState.nooglerRight: nooglerRight,
};
}
Mettre à jour le composant "player"
Flame appelle la méthode update
d'un composant à chaque tick (ou cadre) de la boucle d'événements pour rappeler tous les composants qui ont été modifiés (comme la méthode build
de Flutter). Ensuite, ajoutez la logique à la méthode update
de la classe Player
pour positionner le personnage à l'écran.
Ajoutez le code suivant à la méthode update
de Player
pour calculer la vélocité et la position actuelles du personnage :
lib/game/sprites/player.dart
void update(double dt) {
// Add lines from here...
if (gameRef.gameManager.isIntro || gameRef.gameManager.isGameOver) return;
_velocity.x = _hAxisInput * jumpSpeed; // ... to here.
final double dashHorizontalCenter = size.x / 2;
if (position.x < dashHorizontalCenter) { // Add lines from here...
position.x = gameRef.size.x - (dashHorizontalCenter);
}
if (position.x > gameRef.size.x - (dashHorizontalCenter)) {
position.x = dashHorizontalCenter;
} // ... to here.
// Core gameplay: Add gravity
position += _velocity * dt; // Add this line
super.update(dt);
}
Avant de déplacer le player, la méthode update
vérifie que le jeu ne se trouve pas dans un état non jouable (le player ne peut pas bouger), comme l'état initial (premier chargement du jeu) ou l'état de partie terminée.
Si le jeu se trouve dans un état jouable, la position de Dash est calculée à l'aide de l'équation new_position = current_position + (velocity * time-elapsed-since-last-game-loop-tick)
ou comme représenté dans le code :
position += _velocity * dt
Lorsque vous créez Doodle Dash, il est également essentiel de s'assurer que les bords latéraux n'ont pas de limites. Dash peut sauter du côté gauche de l'écran pour revenir côté droit, et inversement.
Pour implémenter cette fonctionnalité, vérifiez si la position de Dash a dépassé la bordure gauche ou droite de l'écran, et repositionnez-le du côté opposé,le cas échéant.
Événements clés
À l'origine, Doodle Dash s'exécute sur le Web et les ordinateurs. Il doit donc accepter la saisie au clavier qui permet au joueur de contrôler les mouvements du personnage. La méthode onKeyEvent
permet aux composants Player
de reconnaître les appuis sur les flèches afin de déterminer si Dash doit regarder et se déplacer vers la gauche ou la droite.
Dash regarde vers la gauche lorsqu'il se déplace dans cette direction | Dash regarde vers la droite lorsqu'il se déplace dans cette direction |
Implémentez ensuite la fonction de déplacement à l'horizontale de Dash (comme défini dans la variable _hAxisInput
). Vous devez aussi faire en sorte que Dash regarde dans la direction dans laquelle il se déplace.
Modifier les méthodes moveLeft
et moveRight
de la classe Player
pour définir la direction actuelle de Dash :
lib/game/sprites/player.dart
void moveLeft() {
_hAxisInput = 0;
current = PlayerState.left; // Add this line
_hAxisInput += movingLeftInput; // Add this line
}
void moveRight() {
_hAxisInput = 0;
current = PlayerState.right; // Add this line
_hAxisInput += movingRightInput; // Add this line
}
Modifiez la méthode onKeyEvent
de la classe Player
pour appeler respectivement la méthode moveLeft
ou moveRight
lorsque l'utilisateur appuie sur la flèche vers la gauche ou la droite :
lib/game/sprites/player.dart
@override
bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
_hAxisInput = 0;
// Add lines from here...
if (keysPressed.contains(LogicalKeyboardKey.arrowLeft)) {
moveLeft();
}
if (keysPressed.contains(LogicalKeyboardKey.arrowRight)) {
moveRight();
} // ... to here.
// During development, it's useful to "cheat"
if (keysPressed.contains(LogicalKeyboardKey.arrowUp)) {
// jump();
}
return true;
}
La classe Player
est maintenant fonctionnelle, et le jeu Doodle Dash peut l'utiliser.
dans le fichier DoodleDash, importez sprites.dart
pour rendre la classe Player
disponible :
lib/game/doodle_dash.dart
import 'sprites/sprites.dart'; // Add this line
Créez une instance Player
dans la classe DoodleDash
:
lib/game/doodle_dash.dart
class DoodleDash extends FlameGame
with HasKeyboardHandlerComponents, HasCollisionDetection {
DoodleDash({super.children});
final World _world = World();
LevelManager levelManager = LevelManager();
GameManager gameManager = GameManager();
int screenBufferSpace = 300;
ObjectManager objectManager = ObjectManager();
late Player player; // Add this line
...
}
Initialisez et configurez la vitesse de saut du Player
en fonction du niveau de difficulté sélectionné par le joueur, puis ajoutez le composant Player
au FlameGame
. Complétez la méthode setCharacter
avec le code suivant :
lib/game/doodle_dash.dart
void setCharacter() {
player = Player( // Add lines from here...
character: gameManager.character,
jumpSpeed: levelManager.startingJumpSpeed,
);
add(player); // ... to here.
}
Appelez la méthode setCharacter
au début d'initializeGameStart
.
lib/game/doodle_dash.dart
void initializeGameStart() {
setCharacter(); // Add this line
...
}
Dans initializeGameStart
, appelez également resetPosition
sur le player afin de le placer sur la position de départ à chaque démarrage du jeu.
lib/game/doodle_dash.dart
void initializeGameStart() {
...
levelManager.reset();
player.resetPosition(); // Add this line
objectManager = ObjectManager(
minVerticalDistanceToNextPlatform: levelManager.minDistance,
maxVerticalDistanceToNextPlatform: levelManager.maxDistance);
...
}
Exécutez l'application. Lancez le jeu : Dash apparaît à l'écran.
Des problèmes ?
Si votre application ne s'exécute pas correctement, vérifiez que vous n'avez pas fait de faute de frappe. Si nécessaire, utilisez le code disponible via les liens ci-dessous pour résoudre le problème.
5. Ajouter des plates-formes
Cette étape vise à ajouter des plates-formes (permettant à Dash de se poser et de sauter), ainsi que la logique de détection de collision pour déterminer à quel moment Dash doit sauter.
Tout d'abord, examiner la classe abstraite Platform
:
lib/game/sprites/platform.dart
abstract class Platform<T> extends SpriteGroupComponent<T>
with HasGameRef<DoodleDash>, CollisionCallbacks {
final hitbox = RectangleHitbox();
bool isMoving = false;
Platform({
super.position,
}) : super(
size: Vector2.all(100),
priority: 2,
);
@override
Future<void>? onLoad() async {
await super.onLoad();
await add(hitbox);
}
}
Qu'est-ce qu'un masque de collision ?
Chaque composant "platform" introduit dans Doodle Dash étend la classe abstraite Platform<T>
, qui est un SpriteComponent
avec un masque de collision. Le masque de collision permet à un composant "sprite" de détecter toute collision avec d'autres objets dotés d'un masque de collision. Flame accepte plusieurs formes de masque de collision (rectangles, cercles, et polygones, par exemple). Par exemple, Doodle Dash utilise un masque de collision rectangulaire pour les plates-formes et un masque de collision circulaire pour Dash. Flame gère la formule mathématique qui détermine la collision.
La classe Platform
ajoute un masque de collision et des rappels de collision à tous les sous-types.
Ajouter une plate-forme standard
La classe Platform
ajoute les plates-formes dans le jeu. Une plate-forme standard est représentée par quatre images choisies de manière aléatoire : un écran, un téléphone, un terminal ou un ordinateur portable. Le choix de l'image n'affecte pas le comportement de la plate-forme.
|
Ajoutez une plate-forme statique standard en insérant une énumération NormalPlatformState
et une classe NormalPlatform
:
lib/game/sprites/platform.dart
enum NormalPlatformState { only } // Add lines from here...
class NormalPlatform extends Platform<NormalPlatformState> {
NormalPlatform({super.position});
final Map<String, Vector2> spriteOptions = {
'platform_monitor': Vector2(115, 84),
'platform_phone_center': Vector2(100, 55),
'platform_terminal': Vector2(110, 83),
'platform_laptop': Vector2(100, 63),
};
@override
Future<void>? onLoad() async {
var randSpriteIndex = Random().nextInt(spriteOptions.length);
String randSprite = spriteOptions.keys.elementAt(randSpriteIndex);
sprites = {
NormalPlatformState.only: await gameRef.loadSprite('game/$randSprite.png')
};
current = NormalPlatformState.only;
size = spriteOptions[randSprite]!;
await super.onLoad();
}
} // ... to here.
Ensuite, générez les plates-formes avec lesquelles le personnage va interagir.
La classe ObjectManager
étend la classe Component
de Flame et génère les objets Platform
tout au long du jeu. Implémentez la fonction pour générer les plates-formes dans les méthodes update
et onMount
d'ObjectManager
.
Générez les plates-formes dans la classe ObjectManager
en créant une méthode appelée _semiRandomPlatform
. Pour le moment, renvoyez simplement un NormalPlatform
(vous mettrez à jour cette méthode pour renvoyer différents types de plates-formes par la suite) :
lib/game/managers/object_manager.dart
Platform _semiRandomPlatform(Vector2 position) { // Add lines from here...
return NormalPlatform(position: position);
} // ... to here.
Remplacez la méthode update
d'ObjectManager
et utilisez la méthode _semiRandomPlatform
pour générer une plate-forme et l'ajouter en jeu :
lib/game/managers/object_manager.dart
@override // Add lines from here...
void update(double dt) {
final topOfLowestPlatform =
_platforms.first.position.y + _tallestPlatformHeight;
final screenBottom = gameRef.player.position.y +
(gameRef.size.x / 2) +
gameRef.screenBufferSpace;
if (topOfLowestPlatform > screenBottom) {
var newPlatY = _generateNextY();
var newPlatX = _generateNextX(100);
final nextPlat = _semiRandomPlatform(Vector2(newPlatX, newPlatY));
add(nextPlat);
_platforms.add(nextPlat);
gameRef.gameManager.increaseScore();
_cleanupPlatforms();
// Losing the game: Add call to _maybeAddEnemy()
// Powerups: Add call to _maybeAddPowerup();
}
super.update(dt);
} // ... to here.
Répétez l'opération avec la méthode onMount
d'ObjectManager
, de sorte que la méthode _semiRandomPlatform
génère une plate-forme de départ et l'ajoute au jeu lorsqu'il s'exécute pour la première fois.
Ajoutez la méthode onMount
avec le code suivant :
lib/game/managers/object_manager.dart
@override // Add lines from here...
void onMount() {
super.onMount();
var currentX = (gameRef.size.x.floor() / 2).toDouble() - 50;
var currentY =
gameRef.size.y - (_rand.nextInt(gameRef.size.y.floor()) / 3) - 50;
for (var i = 0; i < 9; i++) {
if (i != 0) {
currentX = _generateNextX(100);
currentY = _generateNextY();
}
_platforms.add(
_semiRandomPlatform(
Vector2(
currentX,
currentY,
),
),
);
add(_platforms[i]);
}
} // ... to here.
Ainsi, dans le code suivant, la méthode configure
permet au jeu Doodle Dash de reconfigurer des distances minimales et maximales entre les plates-formes, et d'activer les plates-formes spéciales lorsque le niveau de difficulté augmente :
lib/game/managers/object_manager.dart
void configure(int nextLevel, Difficulty config) {
minVerticalDistanceToNextPlatform = gameRef.levelManager.minDistance;
maxVerticalDistanceToNextPlatform = gameRef.levelManager.maxDistance;
for (int i = 1; i <= nextLevel; i++) {
enableLevelSpecialty(i);
}
}
L'instance DoodleDash
(dans la méthode initializeGameStart
) crée un objet ObjectManager
qui est initialisé, configuré selon le niveau de difficulté, puis ajouté au jeu Flame :
lib/game/doodle_dash.dart
void initializeGameStart() {
gameManager.reset();
if (children.contains(objectManager)) objectManager.removeFromParent();
levelManager.reset();
player.resetPosition();
objectManager = ObjectManager(
minVerticalDistanceToNextPlatform: levelManager.minDistance,
maxVerticalDistanceToNextPlatform: levelManager.maxDistance);
add(objectManager);
objectManager.configure(levelManager.level, levelManager.difficulty);
}
L'ObjectManager
réapparaît dans la méthode checkLevelUp
. Lorsque le joueur passe au niveau supérieur, l'ObjectManager
reconfigure les paramètres pour générer les plates-formes en fonction du niveau de difficulté.
lib/game/doodle_dash.dart
void checkLevelUp() {
if (levelManager.shouldLevelUp(gameManager.score.value)) {
levelManager.increaseLevel();
objectManager.configure(levelManager.level, levelManager.difficulty);
}
}
Effectuez un hot reload (ou redémarrez si le test est réalisé sur le Web) pour appliquer les modifications. (Enregistrez les fichiers, utilisez le bouton de votre IDE ou saisissez r
dans la ligne de commande pour effectuer un hot reload.) Lancez le jeu. Dash et certaines plates-formes apparaissent à l'écran :
Des problèmes ?
Si votre application ne s'exécute pas correctement, vérifiez que vous n'avez pas fait de faute de frappe. Si nécessaire, utilisez le code disponible via les liens ci-dessous pour résoudre le problème.
6. Gameplay de base
Vous venez d'implémenter les widgets individuels Player
et Platform
. Vous pouvez maintenant commencer à rassembler tous les éléments. Cette étape vise à implémenter la fonctionnalité de base, la détection de collision et les mouvements de caméra.
Gravité
Pour que le jeu soit plus réaliste, Dash doit être soumis à la force de gravité qui l'attire vers le bas à chaque saut. Dans notre version de Doodle Dash, la gravité est une valeur positive toujours constante qui attire systématiquement Dash vers le bas. Cependant, vous serez libre de modifier la gravité pour créer d'autres effets par la suite.
Dans la classe Player
, ajoutez une propriété _gravity
avec une valeur de 9 :
lib/game/sprites/player.dart
class Player extends SpriteGroupComponent<PlayerState>
with HasGameRef<DoodleDash>, KeyboardHandler, CollisionCallbacks {
...
Character character;
double jumpSpeed;
final double _gravity = 9; // Add this line
@override
Future<void> onLoad() async {
...
}
...
}
Modifiez la méthode update
de Player
pour ajouter la variable _gravity
et modifier la vélocité verticale de Dash :
lib/game/sprites/player.dart
void update(double dt) {
if (gameRef.gameManager.isIntro || gameRef.gameManager.isGameOver) return;
_velocity.x = _hAxisInput * jumpSpeed;
final double dashHorizontalCenter = size.x / 2;
if (position.x < dashHorizontalCenter) {
position.x = gameRef.size.x - (dashHorizontalCenter);
}
if (position.x > gameRef.size.x - (dashHorizontalCenter)) {
position.x = dashHorizontalCenter;
}
_velocity.y += _gravity; // Add this line
position += _velocity * dt;
super.update(dt);
}
Détection de collision
Flame propose une détection de collision prête à l'emploi. Pour l'activer dans votre jeu Flame, ajoutez le mixin HasCollisionDetection
. Si vous examinez la classe DoodleDash
, vous constatez que le mixin s'y trouve déjà :
lib/game/doodle_dash.dart
class DoodleDash extends FlameGame
with HasKeyboardHandlerComponents, HasCollisionDetection {
...
}
Ensuite, ajoutez la détection de collision à chaque composant de jeu en utilisant le mixin CollisionCallbacks
. Ce mixin délivre un accès au composant au rappel onCollision
. Une collision entre deux objets dotés d'un masque de collision déclenche un rappel onCollision
et transmet une référence aux objets concernés, afin que vous puissiez implémenter une logique concernant le comportement qu'ils doivent adopter.
Rappelez-vous qu'à l'étape précédente, la classe abstraite Platform
comportait déjà le mixin CollisionCallbacks
et un masque de collision. La classe Player
comporte également le mixin CollisionCallbacks
. Il vous suffit donc d'ajouter un CircleHitbox
à la classe Player
. Le masque de collision de Dash est un cercle, car sa forme est plus circulaire que rectangulaire.
Dans la classe Player
, importez sprites.dart
pour qu'il puisse accéder aux différentes classes Platform
:
lib/game/sprites/player.dart
import 'sprites.dart';
Ajoutez un CircleHitbox
à la méthode onLoad
de la classe Player
:
lib/game/sprites/player.dart
@override
Future<void> onLoad() async {
await super.onLoad();
await add(CircleHitbox()); // Add this line
await _loadCharacterSprites();
current = PlayerState.center;
}
Dash a besoin d'une méthode "jump" pour pouvoir sauter lorsqu'il touche une plate-forme.
Ajoutez une méthode jump
qui accepte un specialJumpSpeed
facultatif :
lib/game/sprites/player.dart
void jump({double? specialJumpSpeed}) {
_velocity.y = specialJumpSpeed != null ? -specialJumpSpeed : -jumpSpeed;
}
Remplacez la méthode onCollision
de Player
en ajoutant le code suivant :
lib/game/sprites/player.dart
@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollision(intersectionPoints, other);
bool isCollidingVertically =
(intersectionPoints.first.y - intersectionPoints.last.y).abs() < 5;
if (isMovingDown && isCollidingVertically) {
current = PlayerState.center;
if (other is NormalPlatform) {
jump();
return;
}
}
}
Ce rappel appelle la méthode jump
de Dash dès que le personnage tombe vers le bas et touche la partie supérieure d'une NormalPlatform
. L'instruction isMovingDown && isCollidingVertically
garantit que Dash se déplace vers le haut d'une plate-forme à l'autre sans déclencher de saut.
Mouvements de caméra
La caméra doit suivre Dash lorsqu'il se déplace vers le haut et rester statique lorsque le personnage chute vers le bas.
Dans Flame, si le "monde" est plus grand que l'écran, utilisez le worldBounds
de la caméra pour définir des limites qui indiquent à Flame quelle partie du monde afficher. Pour donner l'impression que la caméra se déplace vers le haut tout en restant fixe à l'horizontale, adaptez les limites haut/bas à chaque actualisation, en fonction de la position du player, et conservez des limites gauche/droite identiques.
Dans la classe DoodleDash
, ajoutez le code suivant à la méthode update
pour que la caméra puisse suivre Dash pendant le gameplay :
lib/game/doodle_dash.dart
@override
void update(double dt) {
super.update(dt);
if (gameManager.isIntro) {
overlays.add('mainMenuOverlay');
return;
}
if (gameManager.isPlaying) {
checkLevelUp();
// Add lines from here...
final Rect worldBounds = Rect.fromLTRB(
0,
camera.position.y - screenBufferSpace,
camera.gameSize.x,
camera.position.y + _world.size.y,
);
camera.worldBounds = worldBounds;
if (player.isMovingDown) {
camera.worldBounds = worldBounds;
}
var isInTopHalfOfScreen = player.position.y <= (_world.size.y / 2);
if (!player.isMovingDown && isInTopHalfOfScreen) {
camera.followComponent(player);
} // ... to here.
}
}
Vous devez ensuite réinitialiser la position du Player
et les limites de la caméra à chaque redémarrage du jeu.
Ajoutez le code suivant dans la méthode initializeGameStart
:
lib/game/doodle_dash.dart
void initializeGameStart() {
...
levelManager.reset();
// Add the lines from here...
player.reset();
camera.worldBounds = Rect.fromLTRB(
0,
-_world.size.y,
camera.gameSize.x,
_world.size.y +
screenBufferSpace,
);
camera.followComponent(player);
// ... to here.
player.resetPosition();
...
}
Augmenter la vitesse de saut à chaque niveau
Pour le dernier élément du gameplay de base, la vitesse de saut de Dash doit augmenter avec le niveau de difficulté, de même que les distances qui séparent les plates-formes générées.
Ajoutez un appel à la méthode setJumpSpeed
et indiquez la vitesse de saut associé au niveau actuel :
lib/game/doodle_dash.dart
void checkLevelUp() {
if (levelManager.shouldLevelUp(gameManager.score.value)) {
levelManager.increaseLevel();
objectManager.configure(levelManager.level, levelManager.difficulty);
player.setJumpSpeed(levelManager.jumpSpeed); // Add this line
}
}
Effectuez un hot reload (ou redémarrez si vous êtes sur le Web) pour appliquer les modifications. (Enregistrez les fichiers, utilisez le bouton de votre IDE ou saisissez r
dans la ligne de commande pour effectuer un hot reload.) :
Des problèmes ?
Si votre application ne s'exécute pas correctement, vérifiez que vous n'avez pas fait de faute de frappe. Si nécessaire, utilisez le code disponible via les liens ci-dessous pour résoudre le problème.
7. Focus sur les plates-formes
ObjectManager
génère désormais des plates-formes sur lesquelles Dash peut sauter. Vous pouvez maintenant proposer différentes plates-formes spéciales parfaitement ludiques.
Ajoutez les classes BrokenPlatform
et SpringBoard
. Comme leur nom l'indique, la BrokenPlatform
se brise après un saut et la SpringBoard
est un trampoline qui permet à Dash de sauter plus haut plus rapidement.
|
|
Comme la classe Player
, la représentation de l'état actuel de ces deux classes "platform" dépend des enums
.
lib/game/sprites/platform.dart
enum BrokenPlatformState { cracked, broken }
La modification de l'état current
d'une plate-forme modifie également le lutin qui apparaît dans le jeu. Définissez le mappage entre l'énumération State
et les composants Image de la propriété sprites
pour déterminer le lien entre chaque lutin et chaque état.
Ajoutez une énumération BrokenPlatformState
et la classe BrokenPlatform
:
lib/game/sprites/platform.dart
enum BrokenPlatformState { cracked, broken } // Add lines from here...
class BrokenPlatform extends Platform<BrokenPlatformState> {
BrokenPlatform({super.position});
@override
Future<void>? onLoad() async {
await super.onLoad();
sprites = <BrokenPlatformState, Sprite>{
BrokenPlatformState.cracked:
await gameRef.loadSprite('game/platform_cracked_monitor.png'),
BrokenPlatformState.broken:
await gameRef.loadSprite('game/platform_monitor_broken.png'),
};
current = BrokenPlatformState.cracked;
size = Vector2(115, 84);
}
void breakPlatform() {
current = BrokenPlatformState.broken;
}
} // ... to here.
Ajoutez une énumération SpringState
et la classe SpringBoard
:
lib/game/sprites/platform.dart
enum SpringState { down, up } // Add lines from here...
class SpringBoard extends Platform<SpringState> {
SpringBoard({
super.position,
});
@override
Future<void>? onLoad() async {
await super.onLoad();
sprites = <SpringState, Sprite>{
SpringState.down:
await gameRef.loadSprite('game/platform_trampoline_down.png'),
SpringState.up:
await gameRef.loadSprite('game/platform_trampoline_up.png'),
};
current = SpringState.up;
size = Vector2(100, 45);
}
@override
void onCollisionStart(
Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollisionStart(intersectionPoints, other);
bool isCollidingVertically =
(intersectionPoints.first.y - intersectionPoints.last.y).abs() < 5;
if (isCollidingVertically) {
current = SpringState.down;
}
}
@override
void onCollisionEnd(PositionComponent other) {
super.onCollisionEnd(other);
current = SpringState.up;
}
} // ... to here.
Activez ensuite ces plates-formes spéciales dans ObjectManager
. Ces plates-formes spéciales ne doivent pas nécessairement apparaître en permanence dans le jeu. Appliquer un facteur de probabilité à leur génération : 15 % pour SpringBoard
et 10 % pour BrokenPlatform
.
Dans la méthode _semiRandomPlatform
d'ObjectManager
et avant que l'instruction renvoie une NormalPlatform
, ajouter le code suivant pour conditionner l'affichage d'une plate-forme spéciale :
lib/game/managers/object_manager.dart
Platform _semiRandomPlatform(Vector2 position) {
if (specialPlatforms['spring'] == true && // Add lines from here...
probGen.generateWithProbability(15)) {
return SpringBoard(position: position);
}
if (specialPlatforms['broken'] == true &&
probGen.generateWithProbability(10)) {
return BrokenPlatform(position: position);
} // ... to here.
return NormalPlatform(position: position);
}
L'un des plaisirs du jeu consiste à débloquer de nouveaux défis et de nouvelles fonctionnalités lorsque vous passez au niveau supérieur.
Vous pouvez intégrer le trampoline dès le niveau 1 et proposer de débloquer la BrokenPlatform
lorsque Dash atteint le niveau 2, pour augmenter légèrement la difficulté.
Dans la classe ObjectManager
, modifiez la méthode enableLevelSpecialty
(actuellement un bouchon) en ajoutant une instruction switch
qui active les plates-formes SpringBoard
au niveau 1 et BrokenPlatform
au niveau 2 :
lib/game/managers/object_manager.dart
void enableLevelSpecialty(int level) {
switch (level) { // Add lines from here...
case 1:
enableSpecialty('spring');
break;
case 2:
enableSpecialty('broken');
break;
} // ... to here.
}
Permettez ensuite aux plates-formes de se déplacer à l'horizontale. Dans la classe abstraite Platform
, ajoutez la méthode _move
suivante :
lib/game/sprites/platform.dart
void _move(double dt) {
if (!isMoving) return;
final double gameWidth = gameRef.size.x;
if (position.x <= 0) {
direction = 1;
} else if (position.x >= gameWidth - size.x) {
direction = -1;
}
_velocity.x = direction * speed;
position += _velocity * dt;
}
Lorsque la plate-forme est en mouvement, elle change de sens lorsqu'elle atteint la bordure de l'écran de jeu. Comme pour Dash, la position de la plate-forme est déterminée en multipliant _direction
par la speed
de la plate-forme afin d'obtenir la vélocité. Multipliez ensuite la vélocité par le time-elapsed
et ajoutez la distance obtenue à la position
actuelle de la plate-forme.
Remplacez la méthode update
de la classe Platform
pour appeler la méthode _move
:
lib/game/sprites/platform.dart
@override
void update(double dt) {
_move(dt);
super.update(dt);
}
Pour déclencher la Platform
en mouvement, configurez de façon aléatoire le paramètre booléen isMoving
sur true
à 20 % du temps dans la méthode onLoad
.
lib/game/sprites/platform.dart
@override
Future<void>? onLoad() async {
await super.onLoad();
await add(hitbox);
final int rand = Random().nextInt(100); // Add this line
if (rand > 80) isMoving = true; // Add this line
}
Enfin, dans le Player
, modifier la méthode onCollision
de la classe Player
pour identifier les collisions avec une Springboard
ou une BrokenPlatform
. Notez qu'une SpringBoard
appelle un jump
avec un multiplicateur de vitesse de 2 et qu'une BrokenPlatform
n'appelle un jump
que si l'état est .cracked
(et non .broken
) (le personnage a déjà sauté dessus) :
lib/game/sprites/player.dart
@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollision(intersectionPoints, other);
bool isCollidingVertically =
(intersectionPoints.first.y - intersectionPoints.last.y).abs() < 5;
if (isMovingDown && isCollidingVertically) {
current = PlayerState.center;
if (other is NormalPlatform) {
jump();
return;
} else if (other is SpringBoard) { // Add lines from here...
jump(specialJumpSpeed: jumpSpeed * 2);
return;
} else if (other is BrokenPlatform &&
other.current == BrokenPlatformState.cracked) {
jump();
other.breakPlatform();
return;
} // ... to here.
}
}
Redémarrez l'application et lancez une partie pour afficher les plates-formes en mouvement (SpringBoard
et BrokenPlatform
).
Des problèmes ?
Si votre application ne s'exécute pas correctement, vérifiez que vous n'avez pas fait de faute de frappe. Si nécessaire, utilisez le code disponible via les liens ci-dessous pour résoudre le problème.
8. Fin de la partie
Cette étape permet d'ajouter les conditions de fin de partie du jeu Doodle Dash. Le joueur peut perdre la partie de deux façons :
- Dash rate une plate-forme et tombe au-dessous de la partie inférieure de l'écran.
- Dash touche une plate-forme
Enemy
.
Avant d'implémenter ces conditions de fin de partie, vous devez ajouter une logique qui définit l'état du jeu Doodle Dash comme gameOver
.
Dans la classe DoodleDash
, ajoutez une méthode onLose
appelée dès qu'une partie doit prendre fin. Elle définit l'état du jeu, supprime le player à l'écran et active le menu ou la superposition **Game Over** (Partie terminée).
lib/game/sprites/doodle_dash.dart
void onLose() { // Add lines from here...
gameManager.state = GameState.gameOver;
player.removeFromParent();
overlays.add('gameOverOverlay');
} // ... to here.
Menu Game Over (Partie terminée) :
En haut de la méthode update
de DoodleDash
, ajoutez le code suivant pour arrêter d'actualiser la partie lorsque l'état du jeu est GameOver
:
lib/game/sprites/doodle_dash.dart
@override
void update(double dt) {
super.update(dt);
if (gameManager.isGameOver) { // Add lines from here...
return;
} // ... to here.
...
}
Dans la méthode update
, appelez également onLose
lorsque le player est tombé au-dessous la partie inférieure de l'écran.
lib/game/sprites/doodle_dash.dart
@override
void update(double dt) {
...
if (gameManager.isPlaying) {
checkLevelUp();
final Rect worldBounds = Rect.fromLTRB(
0,
camera.position.y - screenBufferSpace,
camera.gameSize.x,
camera.position.y + _world.size.y,
);
camera.worldBounds = worldBounds;
if (player.isMovingDown) {
camera.worldBounds = worldBounds;
}
var isInTopHalfOfScreen = player.position.y <= (_world.size.y / 2);
if (!player.isMovingDown && isInTopHalfOfScreen) {
camera.followComponent(player);
}
// Add lines from here...
if (player.position.y >
camera.position.y +
_world.size.y +
player.size.y +
screenBufferSpace) {
onLose();
} // ... to here.
}
}
Les ennemis prennent de nombreuses formes et de nombreuses tailles. Dans Doodle Dash, ils sont signalés par une icône en forme de poubelle ou de dossier endommagé. Les joueurs doivent éviter de toucher ces ennemis sous peine de perdre aussitôt la partie.
|
Créez un type de plate-forme ennemie en ajoutant une énumération EnemyPlatformState
et la classe EnemyPlatform
:
lib/game/sprites/platform.dart
enum EnemyPlatformState { only } // Add lines from here...
class EnemyPlatform extends Platform<EnemyPlatformState> {
EnemyPlatform({super.position});
@override
Future<void>? onLoad() async {
var randBool = Random().nextBool();
var enemySprite = randBool ? 'enemy_trash_can' : 'enemy_error';
sprites = <EnemyPlatformState, Sprite>{
EnemyPlatformState.only:
await gameRef.loadSprite('game/$enemySprite.png'),
};
current = EnemyPlatformState.only;
return super.onLoad();
}
} // ... to here.
La classe EnemyPlatform
étend le supertype Platform
. L'ObjectManager
génère et gère les plates-formes ennemies comme toute autre plate-forme.
Dans ObjectManager
, ajoutez le code suivant pour générer et gérer les plates-formes ennemies :
lib/game/managers/object_manager.dart
final List<EnemyPlatform> _enemies = []; // Add lines from here...
void _maybeAddEnemy() {
if (specialPlatforms['enemy'] != true) {
return;
}
if (probGen.generateWithProbability(20)) {
var enemy = EnemyPlatform(
position: Vector2(_generateNextX(100), _generateNextY()),
);
add(enemy);
_enemies.add(enemy);
_cleanupEnemies();
}
}
void _cleanupEnemies() {
final screenBottom = gameRef.player.position.y +
(gameRef.size.x / 2) +
gameRef.screenBufferSpace;
while (_enemies.isNotEmpty && _enemies.first.position.y > screenBottom) {
remove(_enemies.first);
_enemies.removeAt(0);
}
} // ... to here.
L'ObjectManager
gère une liste d'objets ennemis, _enemies
. _maybeAddEnemy
génère des ennemis avec une probabilité de 20 % et ajoute l'objet à la liste des ennemis. La méthode _cleanupEnemies()
supprime les objets EnemyPlatform
obsolètes qui ne sont plus visibles.
Sous ObjectManager
, générez des plates-formes ennemies en appelant _maybeAddEnemy()
dans la méthode update
:
lib/game/managers/object_manager.dart
@override
void update(double dt) {
final topOfLowestPlatform =
_platforms.first.position.y + _tallestPlatformHeight;
final screenBottom = gameRef.player.position.y +
(gameRef.size.x / 2) +
gameRef.screenBufferSpace;
if (topOfLowestPlatform > screenBottom) {
var newPlatY = _generateNextY();
var newPlatX = _generateNextX(100);
final nextPlat = _semiRandomPlatform(Vector2(newPlatX, newPlatY));
add(nextPlat);
_platforms.add(nextPlat);
gameRef.gameManager.increaseScore();
_cleanupPlatforms();
_maybeAddEnemy(); // Add this line
}
super.update(dt);
}
Ajoutez la méthode onCollision
de Player
pour vérifier s'il existe une collision avec une EnemyPlatform
. Le cas échéant, appelez la méthode onLose()
.
lib/game/sprites/player.dart
@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollision(intersectionPoints, other);
if (other is EnemyPlatform) { // Add lines from here...
gameRef.onLose();
return;
} // ... to here.
bool isCollidingVertically =
(intersectionPoints.first.y - intersectionPoints.last.y).abs() < 5;
if (isMovingDown && isCollidingVertically) {
current = PlayerState.center;
if (other is NormalPlatform) {
jump();
return;
} else if (other is SpringBoard) {
jump(specialJumpSpeed: jumpSpeed * 2);
return;
} else if (other is BrokenPlatform &&
other.current == BrokenPlatformState.cracked) {
jump();
other.breakPlatform();
return;
}
}
}
Enfin, de modifier la méthode enableLevelSpecialty
d'ObjectManager
pour ajouter le niveau 5 à l'instruction switch
:
lib/game/managers/object_manager.dart
void enableLevelSpecialty(int level) {
switch (level) {
case 1:
enableSpecialty('spring');
break;
case 2:
enableSpecialty('broken');
break;
case 5: // Add lines from here...
enableSpecialty('enemy');
break; // ... to here.
}
}
Le jeu est maintenant plus complexe. Effectuez un hot reload pour appliquer les modifications. (Enregistrez les fichiers, utilisez le bouton de votre IDE ou saisissez r
dans la ligne de commande pour effectuer un hot reload.) :
Attention aux ennemis en forme de dossiers endommagés. Ils sont sournois et se confondent avec l'arrière-plan !
Des problèmes ?
Si votre application ne s'exécute pas correctement, vérifiez que vous n'avez pas fait de faute de frappe. Si nécessaire, utilisez le code disponible via les liens ci-dessous pour résoudre le problème.
9. Bonus
Cette étape permet d'ajouter des fonctionnalités de jeu optimisées afin d'améliorer Dash tout au long de la partie. Doodle Dash comporte deux options de bonus : le chapeau de Noogleur et la fusée. Vous pouvez considérer ces bonus comme un autre type de plates-formes spéciales. Lorsque Dash saute et touche un bonus, il prend la forme du chapeau de Noogleur ou de la fusée, et sa vitesse augmente.
|
|
Les chapeaux de Noogleur sont générés au niveau 3, lorsque le joueur atteint un score ≥ à 40. Lorsque Dash touche ce bonus, il porte un chapeau de Noogleur et bénéficie d'une accélération de 2,5 fois la vitesse normale. L'effet dure cinq secondes.
Les fusées sont générées au niveau 4, lorsque le joueur atteint un score ≥ à 80. Lorsque Dash touche ce bonus, son lutin est remplacé par une fusée. Il bénéficie d'une accélération de 3,5 fois la vitesse normale jusqu'à ce qu'il se pose sur une plate-forme. Avec le bonus fusée, Dash est également invincible aux ennemis.
Les lutins en forme de chapeau de Noogleur et de fusée étendent la classe abstraite PowerUp
. Comme pour la classe abstraite Platform
, la classe abstraite PowerUp
représentée ci-dessous comprend la taille et un masque de collision.
lib/game/sprites/powerup.dart
abstract class PowerUp extends SpriteComponent
with HasGameRef<DoodleDash>, CollisionCallbacks {
final hitbox = RectangleHitbox();
double get jumpSpeedMultiplier;
PowerUp({
super.position,
}) : super(
size: Vector2.all(50),
priority: 2,
);
@override
Future<void>? onLoad() async {
await super.onLoad();
await add(hitbox);
}
}
Créez une classe Rocket
qui étend la classe abstraite PowerUp
. Lorsque Dash touche une fusée, il bénéficie d'une accélération de 3,5 fois sa vitesse normale.
lib/game/sprites/powerup.dart
class Rocket extends PowerUp { // Add lines from here...
@override
double get jumpSpeedMultiplier => 3.5;
Rocket({
super.position,
});
@override
Future<void>? onLoad() async {
await super.onLoad();
sprite = await gameRef.loadSprite('game/rocket_1.png');
size = Vector2(50, 70);
}
} // ... to here.
Créez une classe NooglerHat
qui étend la classe abstraite PowerUp
. Lorsque Dash touche un NooglerHat
, il bénéficie d'une accélération de 2,5 fois sa vitesse normale. Cette accélération dure cinq secondes.
lib/game/sprites/powerup.dart
class NooglerHat extends PowerUp { // Add lines from here...
@override
double get jumpSpeedMultiplier => 2.5;
NooglerHat({
super.position,
});
final int activeLengthInMS = 5000;
@override
Future<void>? onLoad() async {
await super.onLoad();
sprite = await gameRef.loadSprite('game/noogler_hat.png');
size = Vector2(75, 50);
}
} // ... to here.
Vous venez d'implémenter les bonus NooglerHat
et Rocket
. mettez maintenant à jour l'ObjectManager
pour les générer dans le jeu.
Modifiez la classe ObjectManger
pour ajouter une liste qui conserve les bonus générés ainsi que les deux nouvelles méthodes (_maybePowerup
et _cleanupPowerups
) pour générer et supprimer les nouvelles plates-formes "powerup".
lib/game/managers/object_manager.dart
final List<PowerUp> _powerups = []; // Add lines from here...
void _maybeAddPowerup() {
if (specialPlatforms['noogler'] == true &&
probGen.generateWithProbability(20)) {
var nooglerHat = NooglerHat(
position: Vector2(_generateNextX(75), _generateNextY()),
);
add(nooglerHat);
_powerups.add(nooglerHat);
} else if (specialPlatforms['rocket'] == true &&
probGen.generateWithProbability(15)) {
var rocket = Rocket(
position: Vector2(_generateNextX(50), _generateNextY()),
);
add(rocket);
_powerups.add(rocket);
}
_cleanupPowerups();
}
void _cleanupPowerups() {
final screenBottom = gameRef.player.position.y +
(gameRef.size.x / 2) +
gameRef.screenBufferSpace;
while (_powerups.isNotEmpty && _powerups.first.position.y > screenBottom) {
if (_powerups.first.parent != null) {
remove(_powerups.first);
}
_powerups.removeAt(0);
}
} // ... to here.
La méthode _maybeAddPowerup
génère un chapeau de Noogleur à 20 % du temps ou une fusée à 15 % du temps. La méthode _cleanupPowerups
est appelée pour supprimer les bonus qui se trouvent au-dessous de la limite inférieure de l'écran.
Modifiez la méthode update
d'ObjectManager
pour appeler _maybePowerup
à chaque tick de la boucle de jeu.
lib/game/managers/object_manager.dart
@override
void update(double dt) {
final topOfLowestPlatform =
_platforms.first.position.y + _tallestPlatformHeight;
final screenBottom = gameRef.player.position.y +
(gameRef.size.x / 2) +
gameRef.screenBufferSpace;
if (topOfLowestPlatform > screenBottom) {
var newPlatY = _generateNextY();
var newPlatX = _generateNextX(100);
final nextPlat = _semiRandomPlatform(Vector2(newPlatX, newPlatY));
add(nextPlat);
_platforms.add(nextPlat);
gameRef.gameManager.increaseScore();
_cleanupPlatforms();
_maybeAddEnemy();
_maybeAddPowerup(); // Add this line
}
super.update(dt);
}
Modifiez la méthode enableLevelSpecialty
pour ajouter deux nouveaux cas à l'instruction "switch". Le premier permet d'activer les NooglerHat
au niveau 3 et le second d'activer les Rocket
au niveau 4 :
lib/game/managers/object_manager.dart
void enableLevelSpecialty(int level) {
switch (level) {
case 1:
enableSpecialty('spring');
break;
case 2:
enableSpecialty('broken');
break;
case 3: // Add lines from here...
enableSpecialty('noogler');
break;
case 4:
enableSpecialty('rocket');
break; // ... to here.
case 5:
enableSpecialty('enemy');
break;
}
}
Ajouter les getters booléens suivants dans la classe Player
. Plusieurs états permettent de représenter les cas où Dash active un bonus. Ces getters simplifient l'identification du bonus actif.
lib/game/sprites/player.dart
bool get hasPowerup => // Add lines from here...
current == PlayerState.rocket ||
current == PlayerState.nooglerLeft ||
current == PlayerState.nooglerRight ||
current == PlayerState.nooglerCenter;
bool get isInvincible => current == PlayerState.rocket;
bool get isWearingHat =>
current == PlayerState.nooglerLeft ||
current == PlayerState.nooglerRight ||
current == PlayerState.nooglerCenter; // ... to here.
Modifiez la méthode onCollision
de Player
afin de réagir à toute collision avec un NooglerHat
ou une Rocket
. Ce code garantit également que Dash ne peut activer un nouveau bonus que s'il en est dépourvu.
lib/game/sprites/player.dart
@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollision(intersectionPoints, other);
if (other is EnemyPlatform && !isInvincible) {
gameRef.onLose();
return;
}
bool isCollidingVertically =
(intersectionPoints.first.y - intersectionPoints.last.y).abs() < 5;
if (isMovingDown && isCollidingVertically) {
current = PlayerState.center;
if (other is NormalPlatform) {
jump();
return;
} else if (other is SpringBoard) {
jump(specialJumpSpeed: jumpSpeed * 2);
return;
} else if (other is BrokenPlatform &&
other.current == BrokenPlatformState.cracked) {
jump();
other.breakPlatform();
return;
}
}
if (!hasPowerup && other is Rocket) { // Add lines from here...
current = PlayerState.rocket;
other.removeFromParent();
jump(specialJumpSpeed: jumpSpeed * other.jumpSpeedMultiplier);
return;
} else if (!hasPowerup && other is NooglerHat) {
if (current == PlayerState.center) current = PlayerState.nooglerCenter;
if (current == PlayerState.left) current = PlayerState.nooglerLeft;
if (current == PlayerState.right) current = PlayerState.nooglerRight;
other.removeFromParent();
_removePowerupAfterTime(other.activeLengthInMS);
jump(specialJumpSpeed: jumpSpeed * other.jumpSpeedMultiplier);
return;
} // ... to here.
}
Lorsque Dash touche une fusée, le PlayerState
bascule sur Rocket
et permet de sauter avec un jumpSpeedMultiplier
de 3,5.
Lorsque Dash touche un chapeau de Noogleur et en fonction de la direction du PlayerState
actuel (.center
, .left
ou .right
), le PlayerState
bascule sur le PlayerState
du Noogleur correspondant. Le personnage porte le chapeau de Noogleur et bénéficie d'un jumpSpeedMultiplier
de 2,5. La méthode _removePowerupAfterTime
supprime le bonus après cinq secondes et rebascule l'état du bonus du PlayerState
sur center
.
L'appel de other.removeFromParent
supprime les plates-formes des lutins en forme de chapeau de Noogleur et de fusée à l'écran, afin de montrer que Dash a obtenu un bonus.
Modifiez les méthodes moveLeft
et moveRight
du player pour prendre en compte le lutin NooglerHat
. Il n'est pas nécessaire de tenir compte du bonus Rocket
, puisque son lutin se trouve toujours dans la même direction.
lib/game/sprites/player.dart
void moveLeft() {
_hAxisInput = 0;
if (isWearingHat) { // Add lines from here...
current = PlayerState.nooglerLeft;
} else if (!hasPowerup) { // ... to here.
current = PlayerState.left;
} // Add this line
_hAxisInput += movingLeftInput;
}
void moveRight() {
_hAxisInput = 0;
if (isWearingHat) { // Add lines from here...
current = PlayerState.nooglerRight;
} else if (!hasPowerup) { //... to here.
current = PlayerState.right;
} // Add this line
_hAxisInput += movingRightInput;
}
Dash est invincible aux ennemis lorsqu'il utilise le bonus Rocket
, ce qui empêche de perdre la partie durant cette période.
Modifiez le rappel onCollision
pour vérifier l'état isInvincible
de Dash avant de déclencher une fin de partie lorsque le personnage touche une EnemyPlatform
:
lib/game/sprites/player.dart
if (other is EnemyPlatform && !isInvincible) { // Modify this line
gameRef.onLose();
return;
}
Redémarrez l'application et lancez une partie pour afficher les bonus.
Des problèmes ?
Si votre application ne s'exécute pas correctement, vérifiez que vous n'avez pas fait de faute de frappe. Si nécessaire, utilisez le code disponible via les liens ci-dessous pour résoudre le problème.
10. Superpositions
Vous pouvez encapsuler un jeu Flame dans un widget pour simplifier son intégration avec d'autres widgets dans une application Flutter. Vous pouvez également afficher les widgets Flutter en superposition, en haut de votre jeu Flame. Cela est pratique pour les composants hors gameplay qui ne dépendent pas de la boucle de jeu, comme les menus, l'écran de pause, les boutons et les curseurs.
L'affichage du score dans le jeu ainsi que tous les menus de Doodle Dash sont des widgets Flutter standard (et non des composants Flame). Le code de tous les widgets se trouve dans lib/game/widgets
. Par exemple, le menu Game Over (Partie terminée) est une simple colonne qui contient d'autres widgets (tels que Text
et ElevatedButton
), comme représenté dans le code suivant :
lib/game/widgets/game_over_overlay.dart
class GameOverOverlay extends StatelessWidget {
const GameOverOverlay(this.game, {super.key});
final Game game;
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.background,
child: Center(
child: Padding(
padding: const EdgeInsets.all(48.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Game Over',
style: Theme.of(context).textTheme.displayMedium!.copyWith(),
),
const WhiteSpace(height: 50),
ScoreDisplay(
game: game,
isLight: true,
),
const WhiteSpace(
height: 50,
),
ElevatedButton(
onPressed: () {
(game as DoodleDash).resetGame();
},
style: ButtonStyle(
minimumSize: MaterialStateProperty.all(
const Size(200, 75),
),
textStyle: MaterialStateProperty.all(
Theme.of(context).textTheme.titleLarge),
),
child: const Text('Play Again'),
),
],
),
),
),
);
}
}
Pour utiliser un widget en superposition dans un jeu Flame, définissez une propriété overlayBuilderMap
sur GameWidget
avec une key
représentant la superposition (comme une String
) et la value
d'une fonction widget qui affiche un widget, comme représenté dans le code suivant :
lib/main.dart
GameWidget(
game: game,
overlayBuilderMap: <String, Widget Function(BuildContext, Game)>{
'gameOverlay': (context, game) => GameOverlay(game),
'mainMenuOverlay': (context, game) => MainMenuOverlay(game),
'gameOverOverlay': (context, game) => GameOverOverlay(game),
},
)
Une fois ajoutée, la superposition peut être utilisée partout dans le jeu. Affichez une superposition avec overlays.add
, puis masquez-la avec overlays.remove
, comme représenté dans le code suivant :
lib/game/doodle_dash.dart
void resetGame() {
startGame();
overlays.remove('gameOverOverlay');
}
void onLose() {
gameManager.state = GameState.gameOver;
player.removeFromParent();
overlays.add('gameOverOverlay');
}
11. Compatibilité avec les mobiles
Développé sur Flutter et Flame, Doodle Dash s'exécute déjà sur les plates-formes compatibles avec Flutter. Mais jusqu'ici, Doodle Dash accepte seulement la saisie au clavier. Pour les appareils qui n'ont pas de clavier (comme les téléphones mobiles), vous pouvez facilement ajouter des commandes tactiles à l'écran, en superposition.
Ajoutez une variable d'état booléenne au GameOverlay
qui détermine les cas où le jeu est exécuté sur une plate-forme mobile :
lib/game/widgets/game_overlay.dart
class GameOverlayState extends State<GameOverlay> {
bool isPaused = false;
// Add this line
final bool isMobile = !kIsWeb && (Platform.isAndroid || Platform.isIOS);
@override
Widget build(BuildContext context) {
...
}
}
Des boutons directionnels (vers la gauche et la droite) apparaissent désormais en superposition lorsque le jeu est exécuté sur un mobile. Dans la lignée de la logique des événements clés de l'étape 4, appuyer sur le bouton de gauche permet de déplacer Dash vers la gauche. Appuyer sur le bouton de droite permet de le déplacer vers la droite.
dans la méthode build
de GameOverlay
, ajoutez une section isMobile
qui suivre le même comportement qu'à l'étape 4 : appuyer sur le bouton de gauche invoque moveLeft
et appuyer sur le bouton de droite invoque moveRight
. Relâcher le bouton appelle resetDirection
. Dash ne se déplace plus à l'horizontale.
lib/game/widgets/game_overlay.dart
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: Stack(
children: [
Positioned(... child: ScoreDisplay(...)),
Positioned(... child: ElevatedButton(...)),
if (isMobile) // Add lines from here...
Positioned(
bottom: MediaQuery.of(context).size.height / 4,
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(left: 24),
child: GestureDetector(
onTapDown: (details) {
(widget.game as DoodleDash).player.moveLeft();
},
onTapUp: (details) {
(widget.game as DoodleDash).player.resetDirection();
},
child: Material(
color: Colors.transparent,
elevation: 3.0,
shadowColor: Theme.of(context).colorScheme.background,
child: const Icon(Icons.arrow_left, size: 64),
),
),
),
Padding(
padding: const EdgeInsets.only(right: 24),
child: GestureDetector(
onTapDown: (details) {
(widget.game as DoodleDash).player.moveRight();
},
onTapUp: (details) {
(widget.game as DoodleDash).player.resetDirection();
},
child: Material(
color: Colors.transparent,
elevation: 3.0,
shadowColor: Theme.of(context).colorScheme.background,
child: const Icon(Icons.arrow_right, size: 64),
),
),
),
],
),
),
), // ... to here.
if (isPaused)
...
],
),
);
}
Et voilà ! Désormais, l'application Doodle Dash détecte automatiquement le type de plate-forme sur lequel il s'exécute et adapte ses saisies en conséquence.
Exécutez l'application sur iOS ou Android pour afficher les boutons directionnels.
Des problèmes ?
Si votre application ne s'exécute pas correctement, vérifiez que vous n'avez pas fait de faute de frappe. Si nécessaire, utilisez le code disponible via les liens ci-dessous pour résoudre le problème.
12. Étapes suivantes
Félicitations !
Vous avez terminé cet atelier de programmation et appris à utiliser le moteur de jeu Flame pour développer un jeu dans Flutter.
Points abordés
- Utiliser le package Flame pour créer un jeu de plates-formes, y compris :
- Ajouter un personnage
- Ajouter différents types de plates-formes
- Implémenter la détection de collision
- Ajouter un composant "gravity"
- Définir les mouvements de caméra
- Créer des ennemis
- Créer des bonus
- Identifier la plate-forme sur laquelle le jeu s'exécute
- Utiliser cette information pour basculer entre les commandes de jeu par saisie au clavier et par saisie tactile.
Ressources
Nous espérons que cet atelier vous aidera à créer des jeux dans Flutter !
Les ressources suivantes peuvent également vous être utiles et même constituer une source d'inspiration :
- Documentation Flame et package Flame sur pub.dev
- Vidéo YouTube Basics of the Flame game engine (Les bases du moteur de jeu Flame) de Lukas Klingsbo
- Simple Platformer, The Flame + Flutter game series (Simple Platformer, série sur les jeux Flame + Flutter) DevKage
- Dino Run, The Flutter Game Development series (Dino Run, série sur le développement de jeux Flutter) de DevKage
- Spacescape, The Flutter Game Development series (Spacescape, série sur le développement de jeux Flutter) de DevKage
- Jeux Flutter
- Page Casual Games toolkit de Flutter (kit de jeux grand public) et modèles de mise en route correspondants (même si le kit de jeux grand public n'utilise pas le moteur Flame, il est conçu pour accepter les annonces pour mobiles et les achats d'applications dans le jeu.)
- Vidéo Build your own game in Flutter (Créer votre propre jeu dans Flutter) sur le kit de jeux grand public
- La page Flutter Puzzle Hack (un concours organisé en janvier 2022) et la vidéo sur les lauréats