Programmation de la séquence de combat dans un jeu de rôle


13

J'essaie d'écrire un court "jeu" où un joueur fait le tour et combat des monstres mais je n'ai aucune idée de comment gérer le combat.

Par exemple, disons que j'ai un "guerrier" et un "troll". Comment les deux se combattent-ils? Je sais que je peux faire quelque chose comme

Conan = Warrior.new();
CaveTroll = Troll.new();
Conan.attack(CaveTroll);
CaveTroll.attack(Conan);

Mais quelle partie du jeu contrôle le monstre? Dois-je simplement coller la séquence ci-dessus en boucle jusqu'à ce que l'un d'eux meure? Ou le "moteur" du jeu doit-il avoir une partie qui traite spécifiquement du combat? Ou est-ce un aspect de l'intelligence artificielle du Troll qui doit prendre soin de ses actions?

De plus, qui / quoi détermine les actions que le monstre entreprend? Peut-être qu'un troll peut frapper, donner des coups de pied, mordre, lancer des sorts, boire des potions, utiliser un objet magique. Le moteur de jeu détermine-t-il quelle action le Troll prend ou est-ce quelque chose que la classe Troll gère?

Désolé, je ne peux pas être plus précis, mais j'ai besoin de conseils sur la direction à suivre.


cool! ne savait pas que ce site existait. est-il possible de déplacer ma question là-bas? ou devrais-je simplement le couper / coller là-bas?

Pas de soucis, un mod devrait le déplacer très prochainement! Ou vous pouvez supprimer la question ici et recréer sur Game Dev
LiamB

@Fendo Je m'excuse de demander, mais de quel site parlez-vous? Développement de jeu?
user712092

Réponses:


12

J'imagine une séquence de bataille comme un mini-jeu dans votre jeu. Les ticks de mise à jour (ou turn ticks) sont dirigés vers un composant gérant ces événements. Cette approche encapsule la logique de séquence de bataille dans une classe distincte, laissant votre boucle de jeu principale libre pour la transition entre les états de jeu.

void gameLoop() {
    while(gameRunning) {
        if (state == EXPLORATION) {
            // Perform actions for when player is simply walking around
            // ...
        }
        else if (state == IN_BATTLE) {
            // Perform actions for when player is in battle
            currentBattle.HandleTurn()
        }
        else if (state == IN_DIALOGUE) {
            // Perform actions for when player is talking with npcs
            // ...
        }
    }

}

La classe de séquence de bataille ressemblerait à ceci:

class BattleSequence {
    public:
        BattleSequence(Entity player, Entity enemy);
        void HandleTurn();
        bool battleFinished();

    private:
        Entity currentlyAttacking;
        Entity currentlyReceiving;
        bool finished;
}

Votre Troll et votre Guerrier héritent tous deux d'une superclasse commune appelée Entité. Dans HandleTurn, l'entité attaquante est autorisée à se déplacer. Cela équivaut à une routine de réflexion sur l'IA.

void HandleTurn() {
    // Perform turn actions
    currentlyAttacking.fight(currentlyReceiving);

    // Switch sides
    Entity temp = currentlyAttacking;
    currentlyAttacking = currentlyReceiving;
    currentlyReceiving = temp;

    // Battle end condition
    if (currentlyReceiving.isDead() || currentlyAttacking.hasFled()) {
        finished = true;
    }
}

La méthode de combat décide de ce que l'entité va faire. Notez que cela n'a pas besoin d'impliquer l'entité adverse, comme boire une potion ou s'enfuir.

Mise à jour: pour prendre en charge plusieurs monstres et un groupe de joueurs, vous introduisez une classe de groupe:

class Group {
    public:
        void fight(Group opponents) {
            // Loop through all group members so everyone gets
            // a shot at the opponents
            for (int i = 0; i < memberCount; i++) {
                Entity attacker = members[i];
                attacker.fight(opponents);
            }
        }

        Entity get(int targetID) {
            // TODO: Bounds checking
            return members[targetID];
        }

        bool isDead() {
            bool dead = true;
            for (int i = 0; i < memberCount; i++) {
                dead = dead && members[i].isDead();
            }
            return dead;
        }

        bool hasFled() {
            bool fled = true;
            for (int i = 0; i < memberCount; i++) {
                fled = fled && members[i].hasFled();
            }
            return fled;
        }

    private:
        Entity[] members;
        int memberCount;
}

La classe de groupe remplacera toutes les occurrences d'entité dans la classe BattleSequence. La sélection et l'attaque seront gérées par la classe Entity elle-même, afin que l'IA puisse prendre en compte l'ensemble du groupe lors de la sélection de la meilleure ligne de conduite.

class Entity {
    public:
        void fight(Group opponents) {
            // Algorithm for selecting an entity from the group
            // ...
            int targetID = 0; // Or just pick the first one

            Entity target = opponents.get(targetID);

            // Fighting algorithm
            target.applyDamage(10);
        }
}

Je suppose que cela ne fonctionnera que pour un joueur contre un monstre. Ou serait-il facile de mettre à jour cela pour fonctionner pour un joueur contre plusieurs monstres?
Harv

Il est assez facile d'ajouter un support pour les groupes tant du côté monstre que du côté joueur (dans votre cas, le groupe de joueurs ne contiendra qu'un seul membre: le personnage du joueur). J'ai mis à jour la réponse pour ce scénario.
fantôme

1

J'aurais un objet Combat dédié qui gère le combat. Il encapsulerait l'état de combat complet, y compris des éléments tels que la liste des personnages joueurs, la liste des ennemis, le tour en cours, le champ de bataille, etc. Le combat peut alors avoir une méthode de mise à jour qui gère la logique de combat. Ce n'est pas une bonne idée de simplement mettre le code de combat dans une boucle simple, car cela se terminerait très rapidement. Normalement, vous auriez un timing et différentes étapes de bataille.

Pour les actions entreprises, vous pouvez certainement le rendre aléatoire, mais cela n'aurait aucun sens pour un monstre avec des HP complets de lancer un sort de guérison. Il est utile d'avoir une logique de base pour déterminer quelle action entreprendre. Par exemple, certaines actions pourraient avoir plus de priorité que d'autres (par exemple, les trolls donnent un coup de pied 30% du temps), ainsi que d'autres conditions pour rendre les batailles plus intéressantes (par exemple, lorsque les HP trolls sont inférieurs à 10% des HP complets, il y a 20% chance de lancer un sort de soin, sinon la chance est de 1%). Cela pourrait être aussi complexe que vous le souhaitez.

Je pense que la classe de monstre devrait gérer la sélection de l'action à effectuer, l'objet de bataille demande au monstre une action et le monstre fait un choix puis procède à l'appliquer. Une idée est d'avoir un objet de stratégie que vous connectez aux monstres et qui sélectionne dans la liste des actions de monstres possibles en fonction des priorités, des catégories et des conditions assignées à chaque action de combat. Ensuite, vous pouvez avoir une classe OffensiveStrategy par exemple qui priorise les attaques sur les compétences défensives, et une autre CautiousStrategy qui est plus susceptible de guérir. Un boss peut être en mesure de changer dynamiquement sa stratégie en fonction de son état actuel.

Une dernière chose. Vous voudrez peut-être que les personnages joueurs et les monstres héritent de la même classe, être des instances de la même classe (acteur ou combattant par exemple), ou partager un objet commun qui encapsule la fonctionnalité commune. Cela réduit la duplication de code et cela vous permettrait également d'avoir des PNJ contrôlés par l'IA de votre côté qui peuvent mettre en œuvre les mêmes stratégies que vous avez déjà codées pour les monstres.


1

Oui, vous devez avoir une partie spéciale dans votre moteur qui gère le combat.

Je ne sais pas exactement comment vous faites votre combat, mais je suppose que les joueurs errent dans le monde du jeu, rencontrent des monstres et la bataille se déroule en temps réel. Si c'est le cas, le troll a besoin de connaître les environs dans une certaine zone, peut-être de définir dans quelle mesure le troll peut voir quelque chose en face (le troll gère cela).

À propos de l'IA, je pense que le moteur doit le gérer lui-même, alors disons que vous avez plus d'un type d'ennemi qui peut faire la même chose (mordre), vous pouvez simplement assigner l'IA à un autre monstre et c'est parti!


0

Votre joueur et votre troll ne sont que des ensembles de données, ce que nous appelons le modèle de données qui décrit votre monde. La vie, l'inventaire, les capacités d'attaque, leur connaissance du monde même - tout se compose du modèle de données.

Conservez un seul objet Model principal contenant toutes les données décrivant votre monde. Il contiendra des informations générales sur le monde telles que la difficulté, les paramètres physiques, etc. Il contiendra également une liste / tableau de données d' entités spécifiques comme je l'ai décrit ci-dessus. Ce modèle principal peut se composer de nombreux sous-objets afin de décrire votre monde. Nulle part dans votre modèle vous ne devriez avoir de fonctions qui contrôlent la logique du jeu ou la logique d'affichage; les getters sont la seule exception, et ne seraient utilisés que pour vous permettre d'obtenir plus facilement les données du modèle (si les membres du public ne font pas déjà l'affaire).

Ensuite, créez des fonctions dans une ou plusieurs classes "contrôleur"; vous pouvez tous les écrire en tant que fonctions d'assistance dans votre classe principale, bien que cela puisse devenir un peu volumineux après un certain temps. Ceux-ci seront appelés à chaque mise à jour pour agir sur les données des entités à différentes fins (mouvement, attaque, etc.). Garder ces fonctions en dehors d'une classe d'entité est plus efficace en termes de ressources, et une fois que vous saurez ce qui décrit votre entité, vous saurez automatiquement quelles fonctions doivent agir sur elle.

class Main
{

//...members variables...
var model:GameModel = new GameModel();

//...member functions...
function realTimeUpdate() //called x times per second, on a timer.
{
    for each (var entity in model.entities)
    {
        //command processing
        if (entity == player)
            decideActionsFromPlayerInput(entity);
        else //everyone else is your enemy!
            decideActionsThroughDeviousAI(entity);

        act(entity);
    }
}
//OR
function turnBasedUpdate()
{
    if (model.whoseTurn == "player")
    {
        decideActionsFromInput(model.player); //may be some movement or none at all
        act(player);
    }
    else
    {
        var enemy;
        for each (var entity in model.entities)
        {
            if (entity != model.player)
            {
                enemy = entity;
                decideActions(enemy);
                act(enemy);
            }
        }
    }
}

//AND THEN... (common to both turn-based and real-time)
function decideActionsThroughDeviousAI(enemy)
{
    if (distanceBetween(enemy, player) <= enemy.maximumAttackDistance)
        storeAttackCommand(enemy, "kidney punch", model.player);
    else
        storeMoveCommand(player, getVectorFromTo(enemy, model.player));

}

function decideActionsFromPlayerInput(player)
{
    //store commands to your player data based on keyboard input
    if (KeyManager.isKeyDown("A"))
        storeMoveCommand(player, getForwardVector(player));
    if (KeyManager.isKeyDown("space"))
        storeAttackCommand(player, "groin slam", currentlyHighlightedEnemy);
}
function storeAttackCommand(entity, attackType, target)
{
    entity.target = target;

    entity.currentAttack = attackType;
    //OR
    entity.attackQueue.add(attackType);
}
function storeMoveCommand(entity, motionVector)
{
    entity.motionVector = motionVector;
}
function act(entity)
{
    entity.position += entity.motionVector;
    attack(entity.target, entity.currentAttack);
}
}

class GameModel
{
    var entities:Array = []; //or List<Entity> or whatever!
    var player:Entity; //will often also appear in the entity list, above
    var difficultyLevel:int;
    var globalMaxAttackDamage:int;
    var whoseTurn:Boolean; //if turnbased
    //etc.

}

Une note finale est qu'il est également utile de séparer votre logique d'affichage de votre logique de jeu. La logique d'affichage serait: "Où puis-je dessiner cela à l'écran et dans quelle couleur?" contre la logique du jeu étant ce que j'ai décrit dans le pseudcode ci-dessus.

(Note du développeur: lors de l'utilisation des classes, cela suit une approche de programmation fonctionnelle qui considère toutes les méthodes comme étant idéalement sans état, permettant un modèle de données et une approche de traitement propres qui minimisent les bogues causés par l'état conservé. FP est le MVC ultime, car il atteint les MVC objectif de séparation explicite des préoccupations. Voir cette question .)


1
"Gardez un seul objet modèle principal qui contient toutes les données décrivant votre monde. Il contiendra des informations générales sur le monde telles que la difficulté, les paramètres physiques, etc." Paramètres de difficulté et de physique? Parlez de confusion des préoccupations! -1.

2
@Joe - Voulez-vous que je lui présente la hiérarchie de configuration entière? Nous restons simples ici, n'est-ce pas? Je vous serais reconnaissant de bien vouloir réfléchir avant de voter.
Ingénieur

3
Eh bien, le reste de l'article est une tentative bizarre de couvrir MVC sans couvrir V ou quoi que ce soit normalement reconnaissable comme C, et je ne pense pas que MVC soit un bon conseil pour la programmation de jeux en premier lieu. J'apprécierais que vous réfléchissiez avant de répondre, mais nous ne pouvons pas toujours obtenir ce que nous voulons.

1
@Joe: Je suis d'accord que MVC est un choix difficile pour un jeu, mais je suis sûr que le rôle de V ici est évident.
Zach Conn

4
@Zach: Lorsque des affirmations comme "FP est le MVC ultime" sont faites, rien n'est évident, sauf peut-être que l'affiche ne comprend pas à la fois le MVC et la programmation fonctionnelle.
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.