Programmieren der Kampfsequenz in einem Rollenspiel

13

Ich versuche ein kurzes "Spiel" zu schreiben, in dem ein Spieler herumläuft und Monster bekämpft, aber ich habe keine Ahnung, wie ich mit dem Kampf umgehen soll.

Angenommen, ich habe einen "Krieger" und einen "Troll". Wie kämpfen die beiden gegeneinander? Ich weiß, dass ich so etwas tun kann

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

Aber welcher Teil des Spiels kontrolliert das Monster? Halte ich die obige Sequenz einfach in eine Schleife, bis einer von ihnen stirbt? Oder muss die "Engine" des Spiels einen Teil haben, der sich speziell mit Kämpfen befasst? Oder ist dies ein Aspekt der künstlichen Intelligenz des Trolls, der sich um seine Handlungen kümmern muss?

Und wer / was bestimmt die Aktionen, die das Monster ausführt? Vielleicht kann ein Troll zuschlagen, treten, beißen, Zauber wirken, Tränke trinken, einen magischen Gegenstand benutzen. Bestimmt die Spiel-Engine, welche Aktion der Troll ausführt, oder ist das etwas, was die Troll-Klasse verwaltet?

Entschuldigung, ich kann nicht genauer sein, aber ich brauche eine Anleitung, in welche Richtung ich gehen soll.

Harv
quelle
cool! Ich wusste nicht, dass diese Seite existiert. Gibt es eine Möglichkeit, meine Frage dort hin zu verschieben? oder soll ich es einfach dort ausschneiden / einfügen?
Keine Sorge, ein Mod sollte es ziemlich bald bewegen! Oder Sie können die Frage hier löschen und bei Game Dev
LiamB
@Fendo Ich entschuldige mich für die Frage, aber welche Seite meinst du? Spieleentwickler?
User712092

Antworten:

12

Ich stelle mir eine Kampfsequenz als Minispiel in Ihrem Spiel vor. Aktualisierungs-Ticks (oder Turn-Ticks) werden an eine Komponente gerichtet, die diese Ereignisse behandelt. Dieser Ansatz kapselt die Schlachtsequenzlogik in einer separaten Klasse und lässt Ihrer Hauptspielschleife den Übergang zwischen den Spielstatus frei.

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
            // ...
        }
    }

}

Die Battle Sequence-Klasse würde folgendermaßen aussehen:

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

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

Ihr Troll und Ihr Krieger erben beide von einer gemeinsamen Superklasse namens Entity. Innerhalb von HandleTurn darf sich das angreifende Objekt bewegen. Dies entspricht einer AI-Denkroutine.

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;
    }
}

Die Kampfmethode entscheidet, was die Entität tun wird. Beachten Sie, dass dies nicht die gegnerische Entität betreffen muss, wie das Trinken eines Tranks oder das Weglaufen.

Update: Um mehrere Monster und eine Spieler-Gruppe zu unterstützen, führen Sie eine Gruppenklasse ein:

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;
}

Die Gruppenklasse ersetzt alle Vorkommen von Entitäten in der Klasse BattleSequence. Das Auswählen und Angreifen wird von der Entity-Klasse selbst durchgeführt, sodass die KI die gesamte Gruppe bei der Auswahl der besten Vorgehensweise berücksichtigen kann.

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);
        }
}
Geist
quelle
Ich gehe davon aus, dass dies nur für einen Spieler gegen ein Monster funktioniert. Oder wäre es einfach, dies zu aktualisieren, um für einen Spieler gegen mehrere Monster zu funktionieren?
Harv
Es ist ganz einfach, Gruppen sowohl auf der Monsterseite als auch auf der Spielerseite zu unterstützen (in Ihrer Situation enthält die Spielergruppe nur ein Mitglied: den Spielercharakter). Ich habe die Antwort für dieses Szenario aktualisiert.
Geist
1

Ich hätte ein dediziertes Kampfobjekt, das den Kampf verwaltet. Es würde den vollständigen Kampfzustand einschließen, einschließlich der Liste der Spielercharaktere, der Liste der Feinde, der aktuellen Runde, des Kampfgeländes und so weiter. Combat kann dann eine Aktualisierungsmethode haben, die die Kampflogik verwaltet. Es ist keine gute Idee, den Kampfcode einfach in eine einfache Schleife zu setzen, da er sehr schnell enden würde. Normalerweise hat man ein wenig Zeit und verschiedene Kampfphasen.

Für die durchgeführten Aktionen kann man es sicherlich nur zufällig machen, aber für ein Monster mit vollen HP wäre es wenig sinnvoll, einen Heilzauber zu wirken. Es lohnt sich, eine grundlegende Logik zu haben, um zu bestimmen, welche Aktion durchgeführt werden soll. Einige Aktionen haben möglicherweise eine höhere Priorität als andere (z. B. Troll-Kicks 30% der Zeit) und andere Bedingungen, um Schlachten interessanter zu gestalten (z. B. wenn die Troll-HP weniger als 10% der vollen HP beträgt, gibt es 20%. Chance, einen Heilzauber zu wirken, sonst beträgt die Chance 1%). Dies kann so komplex sein, wie Sie möchten.

Ich denke, die Monsterklasse sollte die Auswahl der auszuführenden Aktion übernehmen. Das Kampfobjekt fordert das Monster auf, eine Aktion auszuführen, und das Monster trifft eine Auswahl und wendet diese dann an. Eine Idee ist, ein Strategieobjekt zu haben, das Sie in Monster einbinden und das aus der Liste der möglichen Monsteraktionen basierend auf den Prioritäten, Kategorien und Bedingungen, die jeder Kampfaktion zugewiesen sind, auswählt. Dann können Sie zum Beispiel eine OffensiveStrategy-Klasse haben, die Angriffe gegenüber defensiven Fähigkeiten priorisiert, und eine andere CautiousStrategy, die mit größerer Wahrscheinlichkeit heilt. Ein Boss ist möglicherweise in der Lage, die Strategie basierend auf seinem aktuellen Zustand dynamisch zu ändern.

Eine letzte Sache. Möglicherweise möchten Sie entweder, dass sowohl Spielercharaktere als auch Monster von derselben Klasse geerbt werden, Instanzen derselben Klasse (z. B. Schauspieler oder Kombattant) sind oder ein gemeinsames Objekt gemeinsam nutzen, in dem die gemeinsamen Funktionen zusammengefasst sind. Dies reduziert die Codeduplizierung und ermöglicht es Ihnen, KI-gesteuerte NPCs an Ihrer Seite zu haben, die die gleichen Strategien implementieren können, die Sie bereits für Monster codiert haben.

Firas Assaad
quelle
1

Ja, Sie müssen ein spezielles Teil in Ihrem Motor haben, das für den Kampf zuständig ist.

Ich weiß nicht genau, wie du deinen Kampf ausführst, aber ich gehe davon aus, dass die Spieler durch die Spielwelt streifen, sich mit Monstern treffen und der Kampf in Echtzeit stattfindet. Wenn ja, muss der Troll die Umgebung in einem bestimmten Bereich kennen, vielleicht definieren, wie weit der Troll etwas davor sehen kann (Troll handhabt dies).

Was die KI anbelangt, muss die Engine selbst damit fertig werden. Nehmen wir also an, Sie haben mehr als eine Art von Feind, der dasselbe tun kann (Biss). Sie können die KI einfach einem anderen Monster zuweisen und los geht's!

Daggio
quelle
0

Dein Spieler und dein Troll sind nichts als Datensätze, was wir das Datenmodell nennen, das deine Welt beschreibt. Leben, Inventar, Angriffsmöglichkeiten, sogar ihr Wissen über die Welt - alles besteht im Datenmodell.

Behalten Sie ein einzelnes Hauptmodellobjekt bei, das alle Daten enthält, die Ihre Welt beschreiben. Es wird allgemeine Weltinformationen wie Schwierigkeitsgrad, physikalische Parameter usw. enthalten. Es wird auch eine Liste / ein Array von Daten bestimmter Entitäten enthalten , wie ich oben beschrieben habe. Dieses Hauptmodell kann aus vielen Unterobjekten bestehen, um Ihre Welt zu beschreiben. Nirgendwo in Ihrem Modell sollten Sie Funktionen haben, die die Spiel- oder Anzeigelogik steuern. Getter sind die einzige Ausnahme und werden nur verwendet, um Ihnen den einfacheren Abruf von Daten aus dem Modell zu ermöglichen (wenn öffentliche Mitglieder dies nicht bereits tun).

Als nächstes erstellen Sie Funktionen in einer oder mehreren "Controller" -Klassen. Sie können sie alle als Hilfsfunktionen in Ihre Hauptklasse schreiben, obwohl dies nach einer Weile ein bisschen groß werden kann. Diese werden als jedes Update bezeichnet, um auf die Daten der Entitäten für verschiedene Zwecke (Bewegung, Angriff usw.) zu reagieren. Das Aufbewahren dieser Funktionen außerhalb einer Entitätsklasse ist ressourceneffizienter. Wenn Sie wissen, was Ihre Entität beschreibt, wissen Sie automatisch, welche Funktionen darauf reagieren müssen.

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.

}

Ein letzter Hinweis ist, dass es auch nützlich ist, die Anzeigelogik von der Spielelogik zu trennen. Die Anzeigelogik würde lauten: "Wo und in welcher Farbe zeichne ich das auf dem Bildschirm?" Spiellogik ist das, was ich oben im Pseudocode umrissen habe.

(Anmerkung von Dev: Während der Verwendung von Klassen folgt dies locker einem funktionalen Programmieransatz, der alle Methoden als ideal zustandslos ansieht, was ein sauberes Datenmodell und einen Verarbeitungsansatz ermöglicht, der Fehler minimiert, die durch den beibehaltenen Zustand verursacht werden. FP ist die ultimative MVC, da sie MVCs erreicht Ziel der Trennung von Anliegen explizit. Siehe diese Frage .)

Ingenieur
quelle
1
"Behalten Sie ein einzelnes Hauptmodellobjekt bei, das alle Daten enthält, die Ihre Welt beschreiben. Es enthält allgemeine Weltinformationen wie Schwierigkeitsgrad, physikalische Parameter usw." Schwierigkeitsgrad und physikalische Parameter? Sprechen Sie über die Verschmelzung von Bedenken! -1.
2
@ Joe - Soll ich ihm die gesamte Konfigurationshierarchie erläutern? Wir halten es hier einfach, nicht wahr? Ich würde mich freuen, wenn Sie darüber nachdenken würden, bevor Sie abstimmen.
Ingenieur
3
Nun, der Rest des Beitrags ist ein bizarrer Versuch, MVC abzudecken, ohne V oder irgendetwas, das normalerweise als C erkennbar ist, abzudecken. Ich würde mich freuen, wenn Sie überlegen, bevor Sie antworten, aber wir können nicht immer das bekommen, was wir wollen.
1
@ Joe: Ich stimme zu, dass MVC eine schwierige Wahl für ein Spiel ist, aber ich bin mir ziemlich sicher, dass die Rolle von V hier offensichtlich ist.
Zach Conn
4
@Zach: Wenn Behauptungen wie "FP ist die ultimative MVC" aufgestellt werden, ist nichts offensichtlich, außer vielleicht, dass das Poster sowohl MVC als auch funktionale Programmierung nicht versteht.