Programando la secuencia de combate en un juego de rol

13

Estoy tratando de escribir un breve "juego" en el que un jugador da vueltas y lucha contra monstruos, pero no tengo idea de cómo manejar el combate.

Por ejemplo, digamos que tengo un "Guerrero" y un "Troll". ¿Cómo se pelean los dos? Sé que puedo hacer algo como

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

Pero, ¿qué parte del juego controla al monstruo? ¿Acabo de pegar la secuencia anterior en un bucle hasta que uno de ellos muere? ¿O el "motor" del juego debe tener una parte que se ocupe específicamente del combate? ¿O es este un aspecto de la inteligencia artificial del Troll que necesita ocuparse de sus acciones?

Además, ¿quién / qué determina las acciones que toma el monstruo? Tal vez un Troll puede golpear, patear, morder, lanzar hechizos, beber pociones, usar un objeto mágico. ¿El motor del juego determina qué acción toma el Troll o es algo que la clase Troll maneja?

Lo siento, no puedo ser más específico, pero necesito alguna orientación sobre qué dirección tomar con esto.

Harv
fuente
¡frio! No sabía que ese sitio existía. ¿Hay alguna manera de que pueda mover mi pregunta allí? o debería cortarlo / pegarlo allí?
No se preocupe, ¡un mod debería moverlo muy pronto! O bien, puede eliminar la pregunta aquí y volver a crear en Game Dev
LiamB
@Fendo Pido disculpas por preguntar, pero ¿a qué sitio te refieres? ¿Desarrollo de juegos?
user712092

Respuestas:

12

Me imagino una secuencia de batalla como minijuego dentro de tu juego. Los ticks de actualización (o ticks de giro) se dirigen a un componente que maneja estos eventos. Este enfoque encapsula la lógica de la secuencia de batalla en una clase separada, dejando libre el bucle principal del juego para la transición entre los estados del juego.

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 clase de secuencia de batalla se vería así:

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

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

Tu Troll y Guerrero ambos heredan de una superclase común llamada Entidad. Dentro de HandleTurn, la entidad atacante puede moverse. Esto es equivalente a una rutina de inteligencia artificial.

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

El método de lucha decide lo que la entidad va a hacer. Tenga en cuenta que esto no necesita involucrar a la entidad opuesta, como beber una poción o huir.

Actualización: para admitir varios monstruos y un grupo de jugadores, presentas una clase grupal:

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 clase de grupo reemplazará todas las ocurrencias de la entidad en la clase BattleSequence. La selección y el ataque serán manejados por la propia clase Entidad, por lo que la IA puede tener en cuenta a todo el grupo al seleccionar el mejor curso de acción.

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);
        }
}
fantasma
fuente
Supongo que esto solo funcionará para un jugador contra un monstruo. ¿O sería fácil actualizar esto para que funcione para un jugador frente a múltiples monstruos?
Harv
Es bastante fácil agregar soporte para grupos tanto del lado del monstruo como del lado del jugador (en su situación, el grupo de jugadores solo contendrá un miembro: el personaje del jugador). He actualizado la respuesta para este escenario.
fantasma
1

Tendría un objeto de combate dedicado que gestiona el combate. Encapsularía el estado de combate completo, incluyendo cosas como la lista de personajes de jugadores, la lista de enemigos, el turno actual, el terreno de batalla, etc. El combate puede tener un método de actualización que gestiona la lógica de la batalla. No es una buena idea poner el código de combate en un bucle simple, porque terminaría muy rápido. Normalmente tendrías algo de tiempo y diferentes etapas de batalla.

Para las acciones tomadas, ciertamente puedes hacerlo aleatorio, pero no tendría mucho sentido que un monstruo con HP completo lanzara un hechizo de curación. Vale la pena tener una lógica básica para determinar qué acción tomar. Por ejemplo, algunas acciones podrían tener más prioridad que otras (p. Ej., El troll patea el 30% del tiempo), así como otras condiciones para hacer que las batallas sean más interesantes (p. Ej., Cuando el troll HP es inferior al 10% del HP completo, hay un 20% posibilidad de lanzar un hechizo de curación, de lo contrario, la probabilidad es del 1%). Esto podría ser tan complejo como quieras.

Creo que la clase de monstruos debería manejar la selección de qué acción hacer, el objeto de batalla le pide al monstruo una acción y el monstruo toma una decisión y luego procede a aplicarla. Una idea es tener un objeto de estrategia que se conecte a los monstruos y que seleccione de la lista de posibles acciones de monstruos en función de las prioridades, categorías y condiciones asignadas a cada acción de batalla. Entonces puede tener una clase de estrategia ofensiva, por ejemplo, que prioriza los ataques sobre las habilidades defensivas, y otra estrategia cautelosa que es más probable que sane. Un jefe puede cambiar dinámicamente la estrategia en función de su condición actual.

Una última cosa. Es posible que desee que tanto los personajes jugadores como los monstruos hereden de la misma clase, sean instancias de la misma clase (actor o combatiente, por ejemplo) o comparta un objeto común que encapsule la funcionalidad común. Esto reduce la duplicación de código y también te permitiría tener NPC controlados por la IA de tu lado que pueden implementar las mismas estrategias que ya codificaste para los monstruos.

Firas Assaad
fuente
1

Sí, debes tener una parte especial en tu motor que maneje el combate.

No sé exactamente cómo estás haciendo tu combate, pero asumiré que los jugadores deambulan por el mundo del juego, se encuentran con monstruos y la batalla se desarrolla en tiempo real. Si es así, el troll necesita conocer los alrededores dentro de un área determinada, tal vez definir qué tan lejos puede ver algo delante de él (el troll maneja esto).

Sobre la IA, creo que el motor necesita manejarlo por sí mismo, así que digamos que tienes más de un tipo de enemigo que puede hacer lo mismo (mordisco), ¡puedes asignar la IA a otro monstruo y listo!

Daggio
fuente
0

Su jugador y su troll no son más que conjuntos de datos, lo que llamamos el Modelo de datos que describe su mundo. Vida, inventario, capacidades de ataque, incluso su conocimiento del mundo, todo consiste en el modelo de datos.

Mantenga un único objeto Modelo principal que contenga todos los datos que describan su mundo. Contendrá información general del mundo, como dificultad, parámetros físicos, etc. También contendrá una lista / matriz de datos de entidades específicas como he descrito anteriormente. Este modelo principal puede consistir en muchos subobjetos para describir su mundo. En ninguna parte de su modelo debería tener alguna función que controle la lógica del juego o la lógica de visualización; los captadores son la única excepción, y se usarían solo para permitirle obtener datos del modelo con mayor facilidad (si los miembros públicos aún no hacen el truco).

A continuación, cree funciones en una o más clases de "controlador"; puede escribirlos todos como funciones auxiliares en su clase principal, aunque esto puede aumentar un poco después de un tiempo. Se llamará a cada actualización para actuar sobre los datos de las entidades para diferentes propósitos (movimiento, ataque, etc.). Mantener estas funciones fuera de una clase de entidad es más eficiente en cuanto a recursos, y una vez que sepa qué describe su entidad, sabrá automáticamente qué funciones deben actuar sobre ella.

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.

}

Una nota final es que también es útil mantener su lógica de visualización separada de la lógica de su juego. La lógica de visualización sería: "¿Dónde dibujo esto en la pantalla y de qué color?" vs. la lógica del juego es lo que describí en el pseudcode anterior.

(Nota de Dev: Al usar clases, esto sigue un enfoque de programación funcional que considera todos los métodos como idealmente sin estado, lo que permite un modelo de datos limpio y un enfoque de procesamiento que minimiza los errores causados ​​por el estado retenido. FP es el MVC definitivo, ya que logra MVC objetivo de separación de preocupaciones explícitamente. Ver esta pregunta .)

Ingeniero
fuente
1
"Mantenga un único objeto Modelo principal que contenga todos los datos que describan su mundo. Contendrá información general del mundo como dificultad, parámetros de física, etc." Parámetros de dificultad y física? ¡Habla sobre la combinación de preocupaciones! -1.
2
@ Joe - ¿Quieres que le describa toda la jerarquía de configuración? Aquí lo estamos manteniendo simple, ¿no es así? Te agradecería que lo pienses antes de votar.
Ingeniero
3
Bueno, el resto de la publicación es un extraño intento de cubrir MVC sin cubrir V o cualquier cosa normalmente reconocible como C, y no creo que MVC sea un buen consejo para la programación de juegos en primer lugar. Le agradecería que lo piense antes de responder, pero no siempre podemos obtener lo que queremos.
1
@ Joe: Estoy de acuerdo en que MVC es una elección aproximada para un juego, pero estoy bastante seguro de que el papel de V aquí es obvio.
Zach Conn
44
@Zach: Cuando se hacen afirmaciones como "FP es el MVC definitivo", nada es obvio, excepto tal vez que el póster no comprende la programación funcional y MVC.