David Demelier

J'écris du code, de la musique et je contribue à mes projets opensource préférés.

L’aventure de Molko : Les combats

Afin de donner des nouvelles de l’aventure de Molko, je vais écrire des articles pour documenter certaines implémentations du code et les raisons de ces choix.

Aujourd’hui je vais démarrer avec le point presque le plus essentiel d’un RPG, les combats.

Fonctionnement

Avant d’entrer dans la partie technique, je tiens à rappeler le principe de fonctionnement des combats dans l’aventure de Molko.

L’implémentation des combats est probablement la partie qui m’a pris le plus de temps et n’est toujours pas finie, le détail du reste à faire sera documenté plus bas. À l’heure où j’écris ces lignes il y a déjà 2140 lignes pour leur implémentation.

États de combats

Au départ, je pensais naïvement que les combats pouvaient s’implémenter en quelques fonctions. La réalité est tout autre et j’ai donc dégainé une implémentation orienté états. C’est grâce à différents états qu’il est plus facile de découper le code de la gestion du combat. Cela facilite aussi la personnalisation des combats lorsqu’on souhaite faire quelque chose différent (cinématique, dialogues, etc). De plus, vu la quantité de transitions différentes le découpage du code permet une meilleure maintenance. Par exemple, si je souhaite modifier la sélection des cibles, je sais exactement quel fichier modifier.

Pour visualiser les différents états, voici un diagramme de transition possible :

battle-states.png

Implémentation

Dans un jeu vidéo, la plupart du temps tout est orienté input, update, draw. Cela signifie que chaque structure à l’écran doit se mettre à jour, se dessiner et optionellement recevoir les entrées utilisateurs.

En C, on peut architecturer l’état d’un combat avec quelques pointeurs de fonctions et une structure de données annexe.

struct battle_state {
    void *data;
    void (*handle)(struct battle_state *, struct battle *, const union event *);
    bool (*update)(struct battle_state *, struct battle *, unsigned int);
    void (*draw)(const struct battle_state *, const struct battle *);
    void (*finish)(struct battle_state *, struct battle *);
};

Pour le moment, le combat en lui même s’occupe de dessiner certaines structures lui même :

À l’heure actuelle, cela ne pose pas problème mais si la flexibilité n’est pas suffisante, on pourra déplacer le code afin que les états puissent faire autre chose s’ils ont besoin.

Exemple avec “Opening”

Prenons l’exemple le plus simple, l’animation d’ouverture. Par défaut il s’agit de deux bandes noires qui s’ouvrent au fur et à mesure.

Note: Pour faciliter la lecture de l’article, je ne mentionne pas les entêtes ni les commentaires afin d’avoir un code plus simple.

Pour implémenter cet état nous avons besoin de stocker le temps écoulé entre chaque itération.

struct opening {
    struct battle_state self;
    unsigned int elapsed;
};

On stocke l’état dans les données car le combat ne fait qu’une référence sur ce dernier, il est donc nécessaire qu’il soit stocké quelque part.

Pour mettre à jour l’état, on incrémente simplement le temps écoulé.

static bool
update(struct battle_state *st, struct battle *bt, unsigned int ticks)
{
    struct opening *opening = st->data;

    opening->elapsed += ticks;

    if (opening->elapsed >= DELAY)
        battle_state_check(bt);

    return false;
}

Une fois que le temps est écoulé, on change d’état à “Check” qui s’occupe de vérifier l’état du combat (victoire, défaite, etc…) et de donner la main au joueur suivant. On renvoie faux car le combat n’est pas considéré terminé et donc le combat est toujours considéré comme valide.

Pour finir, on affiche les bordures en fonction du temps passé.

static void
draw(const struct battle_state *st, const struct battle *bt)
{
    const struct opening *opening = st->data;
    const unsigned int w = window.w;
    const unsigned int h = window.h / 2;
    const unsigned int ch = opening->elapsed * h / DELAY;

    painter_set_color(0x000000ff);
    painter_draw_rectangle(0, 0, w, h - ch);
    painter_draw_rectangle(0, h + ch, w, h - ch);
}

Il faut néanmoins une petite fonction pour supprimer la structure de données opening qu’on alloue dynamiquement.

static void
finish(struct battle_state *st, struct battle *bt)
{
    free(st->data);
}

Pour activer cette état, on rajoute une fonction qui créé les données et change l’état du combat.

void
battle_state_opening(struct battle *bt)
{
    struct opening *opening;

    if (!(opening = alloc_new0(sizeof (*opening))))
        panic();

    opening->self.data = opening;
    opening->self.update = update;
    opening->self.draw = draw;
    opening->self.finish = finish;

    battle_switch(bt, &opening->self);
}

État des entités

Maintenant que les états de combat sont implémentés, on peut commencer à s’intéresser aux entités visibles. Comme les états du combat, ces derniers aussi sont implémentés via le même patron de conception. Cela simplifie la gestion des entités à travers les différents états du combat. Par exemple, si je veux faire déplacer mon joueur avec une animation, je pourrais le faire quelque soit l’état actuel du combat.

À l’heure actuelle, contrairement aux états des combats ceux des entités ne s’auto changent pas vers d’autres états. C’est à l’état du combat de s’en occuper. Cela pourrait changer dans le futur mais ne pose pas de problème pour le moment.

Voici les états implémentés pour le moment :

battle-entity-states.png

Quand un membre de l’équipe attaque, il se déplace vers l’entité ennemie (état “Moving”), passe à l’attaque (animation “Attacking”) puis revient à sa position initiale (état “Moving” à nouveau). Tout cela se passe sous la responsabilité de l’état “Attacking” du combat.

Résultat

Et voici le prototype. Notez que le visuel, le style et d’autres caractéristiques sont complètement temporaires car les graphismes officiels ne sont pas encore créés.

Ce qui reste à faire

Il reste encore quelques points à terminer pour que ce soit terminé :