Projet de POO

Réalisation d’un jeu vidéo 2D : UBGarden.

UBGargen

Principes du jeu

Une princesse est perdue et désespérée. Votre mission, si vous l’acceptez, est d’aller la secourir. Pour cela, vous devrez traverser plusieurs jardins, plus fantastiques les uns que les autres.

Chaque jardin est représenté par une carte (rectangulaire) composée de cellules. Des portes vous permettront de passer de jardin en jardin. Certaines portes seront fermées à clé et nécessiteront d’avoir une clé pour les ouvrir. Attention, des abeilles vivent dans ces jardins et vous êtes allergique aux piqures. Le joueur et les abeilles peuvent se déplacer de cellule en cellule, dans un espace géométrique discret. Les arbres sont des éléments de décor infranchissables. Seules les abeilles peuvent traverser les bosquets de fleurs, le joueur ne peut pas s’y aventurer. Chaque déplacement du joueur consomme de l’énergie, en fonction du type de sol sur lequel il marche. Ce coût de déplacement est multiplié par diseaseLevel, initialisé à 1. Les abeilles peuvent voler autant qu’elles veulent sans jamais se fatiguer. Le joueur récupère vite et gagne un point d’énergie au bout de energyRecoverDuration secondes sans bouger, dans la limite de son niveau d’énergie initial playerEnergy. Le niveau d’énergie actuel du joueur est affiché dans la barre d’état.

En cas de piqure d’une abeille, vous perdez un point de vie, mais l’excès d’adrénaline vous rendra invincible pendant playerInvincibilityDuration secondes. Les abeilles ne survivent pas aux piqures qu’elles vous infligent. Heureusement, vous avez plusieurs points de vies et pouvez ramasser des cœurs pour augmenter vos points de vies. Le nombre de vies restantes du joueur est affiché dans la barre d’état. Lorsque le joueur n’a plus de points de vie, la partie est perdue. Les abeilles n’aiment pas les courants d’air et ne peuvent pas passer sur les portes.

Le joueur ramasse automatiquement les bonus qui se trouvent sur des cellules d’herbe quand il marche dessus. Les abeilles n’interagissent pas avec les bonus. Si le joueur ramasse un trognon de pomme pourrie, il devient malade pendant diseaseDuration secondes et son degré de fatigue diseaseLevel augmente de 1. Le degré de fatigue actuel du joueur (diseaseLevel) est affiché dans la barre d’état. Si le joueur ramasse des pommes, il gagne un surplus d’énergie de energyBoost et guérit instantanément (la valeur de diseaseLevel vaut 1).

Image Description Effet
Herbe Le joueur consomme 1 * diseaseLevel points d’énergie par déplacement
Terre Le joueur consomme 2 * diseaseLevel points d’énergie par déplacement
Carottes Le joueur consomme 3 * diseaseLevel points d’énergie par déplacement
Pomme empoisonnée Augmente diseaseLevel de 1 pendant une durée de diseaseDuration secondes
Pomme Réinitialise diseaseLevel à 1 et augmente la quantité d’énergie du joueur de energyBoost
Bosquet de fleurs Infranchissable pour le joueur, franchissable pour les abeilles
Cœur Ajoute un point de vie au joueur
Porte fermée  
Porte ouverte  
Clé Permet d’ouvrir une porte fermée lorsque le joueur marche dessus. Le joueur ne peut marcher sur une porte fermée que s’il possède une clé.

Prise en main

Nous vous fournissons une première ébauche du jeu. Vous pouvez consulter le diagrame de classes du projet pour avoir un apperçu des dépendances entre les différentes classes. Le lancement du jeu fait apparaître une carte minimaliste (MapLevelDefaultStart), chargée statiquement en mémoire, dans laquelle le joueur peut se déplacer dans toutes les directions, quelle que soit la nature des cellules. Les paramètres du jeu sont définis dans un fichier de propriétés. Les fichiers properties en Java permettent de facilement stocker des couples de clés/valeurs. Les valeurs par défauts sont les suivantes.

Paramètres   Valeur par défaut
playerLives   5
playerInvincibilityDuration   4s
beeMoveFrequency   1 / s
playerEnergy   100
energyBoost   50
energyRecoverDuration   5s
diseaseDuration   5s

Travail à fournir

Archive du jeu de base : UBGarden-student.zip

Premiers pas

Dans cette première version, les abeilles sont immobiles. Nous utiliserons pour le moment la carte par défaut.

Donnons vie aux abeilles

Gestion des mondes

Dans la version de base, le jeu ne dispose que d’un seul niveau codé en dur dans le code. Nous allons maintenant charger une configuration complète de jeu depuis un fichier. Vous trouverez un répertoire world à la racine du projet avec deux fichiers d’extension .properties représentant un monde avec 2 niveaux (avec ou sans compression des niveaux). Nous utilisons le codage LRE vu en TD avec ou sans compression en fonction du paramètre compression dans le fichier de configuration.

Invincibilité du joueur

Le joueur peut perdre une vie s’il se fait piquer par une abeille. Le joueur bénéficie alors d’une temporisation de playerInvincibilityDurationl secondes pendant laquelle il est invulnérable. Si le joueur n’a plus de vie, la partie se termine. Faire en sorte que l’image du joueur change pendant sa période d’invincibilité (ajoutez un effet à l’image).

Fin de partie

La partie est finie lorsque le joueur n’a plus de vie ou s’il arrive à rejoindre la princesse. La touche [ESCAPE] permet de quitter la partie à tout moment.

Double dispatch

Lorsque le joueur marche sur un bonus, il faut pourvoir le ramasser. Imaginons que les classes Apple et PoisonedApple soient des sous-classes de la classe Bonus. On aimerait pouvoir écrire dans le code de la classe Player quelque chose comme ça :

class Player {
    void take(Apple apple) { ... }
    void take(PoisonedApple poisonedApple) { ... }

    void doMove(Direction direction) {
        Position nextPos = direction.nextPosition(getPosition());
        Decor next = game.world().getGrid().get(nextPos);
        setPosition(nextPos);
        if (next != null)
            take(next);
    }
}

Le problème est qu’en Java, on ne peut pas faire un dispatch de méthode dynamique en fonction du type d’un paramètre. Il est possible de faire un dispatch statique (surcharge) ou dynamique (redéfinition et polymorphisme). Pour résoudre le problème, nous allons faire ce qu’on appelle de l’inversion de dépendance.

class Player {
    // [...]
    void doMove(Direction direction) {
        // ... 
        if (next != null)
            next.takenBy(this);
    }
}

Plutôt que le joueur ramasse un bonus, on va demander au bonus de se faire ramasser par le joueur. Avec le dispatch dynamique, c’est la bonne méthode takenBy qui sera invoquée. Il faut ensuite invoqué dans chacune des classes Apple et PoisonedApple la bonne méthode de ramassage du joueur. Il s’agit dans ce cas de dispatch statique.

public interface Takeable {
    default void takenBy(Player player) {}
}

public abstract class Bonus implements Takeable {}

public class Apple extends Bonus { 
    public void takenBy(Player player) {
        player.take(this);
    }
}

public class PoisonedApple extends Bonus { 
    public void takenBy(Player player) {
        player.take(this);
    }
 }