Programmazione della sequenza di combattimento in un gioco di ruolo


13

Sto cercando di scrivere un breve "gioco" in cui un giocatore va in giro e combatte i mostri, ma non ho idea di come gestire il combattimento.

Ad esempio, supponiamo che io abbia un "Guerriero" e un "Troll". In che modo i due si combattono? So di poter fare qualcosa del genere

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

Ma quale parte del gioco controlla il mostro? Attacco semplicemente la sequenza sopra in un ciclo fino a quando uno di loro muore? O il "motore" del gioco deve avere una parte che si occupa specificamente di combattimento? O è questo un aspetto dell'intelligenza artificiale del Troll che deve occuparsi delle sue azioni?

Inoltre, chi / cosa determina le azioni che il mostro compie? Forse un Troll può colpire, calciare, mordere, lanciare incantesimi, bere pozioni, usare un oggetto magico. Il motore di gioco determina l'azione intrapresa dal Troll o è qualcosa che la classe Troll gestisce?

Mi dispiace non posso essere più specifico, ma ho bisogno di una guida su quale direzione andare con questo.


freddo! non sapevo che quel sito esistesse. c'è un modo in cui posso spostare la mia domanda laggiù? o dovrei semplicemente tagliarlo / incollarlo lì?

Nessun problema, una mod dovrebbe spostarla abbastanza presto! Oppure puoi eliminare la domanda qui e ricrearla su Game Dev
LiamB,

@Fendo, mi scuso per averlo chiesto, ma quale sito intendi? Game Dev?
user712092,

Risposte:


12

Immagino una sequenza di battaglie come un minigioco all'interno del tuo gioco. I tick di aggiornamento (o i tick di turno) sono diretti a un componente che gestisce questi eventi. Questo approccio incapsula la logica della sequenza di battaglia in una classe separata, lasciando il tuo ciclo di gioco principale libero alla transizione tra gli stati di gioco.

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 della sequenza di battaglia sarebbe simile a questa:

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

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

Troll e Warrior ereditano entrambi da una superclasse comune chiamata Entity. All'interno di HandleTurn, l'entità attaccante può muoversi. Ciò equivale a una routine di pensiero AI.

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

Il metodo di combattimento decide cosa farà l'entità. Nota che questo non ha bisogno di coinvolgere l'entità avversaria, come bere una pozione o scappare.

Aggiornamento: per supportare più mostri e una squadra di giocatori, si introduce una classe di gruppo:

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 Gruppo sostituirà tutte le occorrenze di Entità nella classe BattleSequence. La selezione e l'attacco saranno gestiti dalla classe Entity stessa, quindi l'IA può prendere in considerazione l'intero gruppo quando si seleziona il miglior modo di agire.

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

Suppongo che funzionerà solo per un giocatore contro un mostro. O sarebbe facile aggiornarlo per far funzionare un giocatore contro più mostri?
Harv,

È abbastanza facile aggiungere supporto per i gruppi sia sul lato del mostro che sul lato del giocatore (nella tua situazione, il gruppo di giocatori conterrà un solo membro: il personaggio del giocatore). Ho aggiornato la risposta per questo scenario.
ghost

1

Avrei un oggetto di combattimento dedicato che gestisce il combattimento. Incapsulerebbe l'intero stato di combattimento, tra cui l'elenco dei personaggi del giocatore, l'elenco dei nemici, il turno in corso, il terreno di battaglia e così via. Il combattimento può quindi avere un metodo di aggiornamento che gestisce la logica di battaglia. Non è una buona idea mettere il codice di combattimento in un semplice ciclo, perché finirà molto velocemente. Normalmente avresti un po 'di tempo e diverse fasi di battaglia.

Per le azioni intraprese, puoi certamente renderlo casuale, ma avrebbe poco senso per un mostro con PS completi lanciare un incantesimo di cura. Vale la pena avere una logica di base per determinare quale azione intraprendere. Ad esempio, alcune azioni potrebbero avere più priorità di altre (ad esempio, i troll danno il calcio il 30% delle volte), così come altre condizioni per rendere le battaglie più interessanti (ad esempio, quando i troll HP sono inferiori al 10% dei PS completi, c'è un 20% possibilità di lanciare un incantesimo di cura, altrimenti la probabilità è dell'1%). Questo potrebbe essere complesso come vuoi.

Penso che la classe di mostri dovrebbe gestire selezionando quale azione fare, l'oggetto di battaglia chiede un'azione al mostro e il mostro fa una scelta e quindi procede ad applicarla. Un'idea è quella di avere un oggetto strategico da collegare ai mostri e che seleziona dall'elenco delle possibili azioni dei mostri in base a priorità, categorie e condizioni assegnate a ciascuna azione di battaglia. Quindi puoi avere una classe OffensiveStrategy per esempio che dà la priorità agli attacchi rispetto alle abilità difensive e un'altra CautiousStrategy che ha maggiori probabilità di guarire. Un boss potrebbe essere in grado di cambiare dinamicamente la strategia in base alle sue condizioni attuali.

Un'ultima cosa. Potresti voler avere sia personaggi del giocatore che mostri ereditati dalla stessa classe, essere istanze della stessa classe (attore o combattente per esempio) o condividere un oggetto comune che incapsula la funzionalità comune. Ciò riduce la duplicazione del codice e ti consentirebbe anche di avere NPC controllati dall'IA dalla tua parte che possono implementare le stesse strategie che hai già codificato per i mostri.


1

Sì, devi avere una parte speciale nel tuo motore che gestisca il combattimento.

Non so come stai esattamente combattendo, ma suppongo che i giocatori vagano per il mondo di gioco, incontrano i mostri e la battaglia si svolge in tempo reale. In tal caso, il troll deve conoscere l'ambiente circostante all'interno di una determinata area, forse definire fino a che punto il troll può vedere qualcosa di fronte (il troll gestisce questo).

Per quanto riguarda l'IA, penso che il motore debba gestirlo da solo, quindi diciamo che hai più di un tipo di nemico che può fare la stessa cosa (morso), puoi semplicemente assegnare l'IA a un altro mostro e il gioco è fatto!


0

Il tuo giocatore e il tuo troll non sono altro che insiemi di dati, quello che chiamiamo il Modello di dati che descrive il tuo mondo. Vita, inventario, capacità di attacco, persino la loro conoscenza del mondo: tutto consiste nel modello di dati.

Mantieni un singolo oggetto Modello principale che contiene tutti i dati che descrivono il tuo mondo. Conterrà informazioni generali sul mondo come difficoltà, parametri fisici, ecc. Conterrà anche un elenco / matrice di dati di entità specifiche come ho descritto sopra. Questo modello principale può consistere in molti oggetti secondari per descrivere il tuo mondo. In nessun punto del tuo modello dovresti avere delle funzioni che controllano la logica di gioco o la logica di visualizzazione; i getter sono l'unica eccezione e verrebbero utilizzati solo per consentire all'utente di ottenere dati dal modello più prontamente (se i membri pubblici non fanno già il trucco).

Quindi, creare funzioni in una o più classi "controller"; puoi scriverli tutti come funzioni di aiuto nella tua classe principale, anche se dopo un po 'potrebbe diventare un po' grande. Questi saranno chiamati ogni aggiornamento per agire sui dati delle entità per scopi diversi (movimento, attacco ecc.). Mantenere queste funzioni al di fuori di una classe di entità è più efficiente in termini di risorse e una volta che sai cosa descrive la tua entità, saprai automaticamente quali funzioni devono agire su di essa.

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.

}

Un'ultima nota è che è anche utile mantenere la logica di visualizzazione separata dalla logica di gioco. La logica di visualizzazione sarebbe "Dove posso disegnarlo sullo schermo e con quale colore?" vs. la logica di gioco è quella che ho delineato nello pseudcode sopra.

(Nota dello sviluppatore: durante l'utilizzo delle classi, segue vagamente un approccio di programmazione funzionale che considera tutti i metodi idealmente apolidi, consentendo un modello di dati pulito e un approccio di elaborazione che minimizza i bug causati dallo stato mantenuto. FP è l'MVC finale, poiché raggiunge gli MVC obiettivo della separazione esplicita delle preoccupazioni. Vedi questa domanda .)


1
"Mantieni un singolo oggetto Modello principale che contiene tutti i dati che descrivono il tuo mondo. Conterrà informazioni generali sul mondo come difficoltà, parametri fisici ecc." Difficoltà e parametri fisici? Parla della conflazione delle preoccupazioni! -1.

2
@Joe - Vuoi che gli delinei l'intera gerarchia di configurazione? Lo stiamo mantenendo semplice qui, no? Gradirei se pensassi prima di effettuare il downgrade.
Ingegnere

3
Bene, il resto del post è un bizzarro tentativo di coprire MVC senza coprire V o qualsiasi cosa normalmente riconoscibile come C, e non credo che MVC sia un buon consiglio per la programmazione di giochi in primo luogo. Ti sarei grato se ci pensassi prima di rispondere, ma non possiamo sempre ottenere ciò che vogliamo.

1
@Joe: sono d'accordo sul fatto che MVC sia una scelta approssimativa per un gioco, ma sono abbastanza sicuro che il ruolo di V qui sia ovvio.
Zach Conn

4
@Zach: quando vengono fatte affermazioni come "FP è l'ultimo MVC", nulla è ovvio, tranne forse che il poster non riesce a comprendere sia MVC che la programmazione funzionale.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.