Programando a sequência de combate em um RPG

13

Estou tentando escrever um pequeno "jogo" em que um jogador circula e luta contra monstros, mas não tenho idéia de como lidar com o combate.

Por exemplo, digamos que eu tenho um "guerreiro" e um "troll". Como os dois lutam entre si? Eu sei que posso fazer algo como

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

Mas que parte do jogo controla o monstro? Eu apenas coloco a sequência acima em um loop até que um deles morra? Ou o "mecanismo" do jogo precisa ter uma parte que lide especificamente com o combate? Ou esse é um aspecto da inteligência artificial do Troll que precisa cuidar de suas ações?

Além disso, quem / o que determina as ações que o monstro executa? Talvez um Troll possa bater, chutar, morder, lançar feitiços, beber poções, usar um item mágico. O mecanismo de jogo determina que ação o Troll executa ou isso é algo que a classe Troll gerencia?

Desculpe, não posso ser mais específico, mas preciso de algumas orientações sobre a direção a seguir.

Harv
fonte
legal! não sabia que o site existia. existe uma maneira de mover minha pergunta para lá? ou devo apenas cortar / colar lá?
Não se preocupe, um mod deve movê-lo em breve! Ou você pode excluir a pergunta aqui e recriar no Game Dev
LiamB 3/11/11
@Fendo Peço desculpas por perguntar, mas que site você quer dizer? Jogo Dev?
precisa saber é o seguinte

Respostas:

12

Eu imagino uma sequência de batalha como um minigame dentro do seu jogo. Os ticks de atualização (ou turn ticks) são direcionados para um componente que manipula esses eventos. Essa abordagem encapsula a lógica da sequência de batalha em uma classe separada, deixando o loop principal do jogo livre para a transição entre os estados do jogo.

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

}

A classe de sequência de batalha ficaria assim:

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

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

Seu Troll e Warrior herdam de uma superclasse comum chamada Entidade. No HandleTurn, a entidade atacante pode se mover. Isso é equivalente a uma rotina de pensamento da 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;
    }
}

O método de luta decide o que a entidade fará. Observe que isso não precisa envolver a entidade adversária, como beber uma poção ou fugir.

Atualização: Para oferecer suporte a vários monstros e um grupo de jogadores, você introduz uma classe de grupo:

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

A classe Group substituirá todas as ocorrências de Entidade na classe BattleSequence. A seleção e o ataque serão tratados pela própria classe Entity, para que a IA possa levar todo o grupo em consideração ao selecionar o melhor curso de ação.

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
fonte
Estou assumindo que isso funcionará apenas para um jogador contra um monstro. Ou seria fácil atualizar isso para funcionar para um jogador contra vários monstros?
Harv
É muito fácil adicionar suporte para grupos tanto do lado do monstro quanto do lado do jogador (na sua situação, o grupo de jogadores conterá apenas um membro: o personagem do jogador). Atualizei a resposta para este cenário.
Ghost
1

Eu teria um objeto de combate dedicado que gerencia o combate. Encapsularia o estado de combate completo, incluindo itens como a lista de personagens dos jogadores, lista de inimigos, turno atual, terreno de batalha e assim por diante. O combate pode então ter um método de atualização que gerencia a lógica da batalha. Não é uma boa ideia apenas colocar o código de combate em um loop simples, porque terminaria muito rápido. Normalmente você tem tempo e estágios de batalha diferentes.

Para as ações tomadas, você certamente pode torná-lo aleatório, mas faria pouco sentido para um monstro com HP completo lançar um feitiço de cura. Vale a pena ter alguma lógica básica para determinar qual ação executar. Por exemplo, algumas ações podem ter mais prioridade do que outras (por exemplo, trolls chutam 30% do tempo), além de outras condições para tornar as batalhas mais interessantes (por exemplo, quando o troll HP é inferior a 10% do HP total, há 20% chance de lançar feitiço de cura, caso contrário, a chance é de 1%). Isso pode ser tão complexo quanto você quiser.

Eu acho que a classe de monstros deveria lidar com a seleção de qual ação fazer, o objeto de batalha pede ao monstro uma ação e o monstro faz uma escolha e depois passa a aplicá-la. Uma idéia é ter um objeto de estratégia que você conecte aos monstros e que selecione na lista de possíveis ações de monstros com base nas prioridades, categorias e condições atribuídas a cada ação de batalha. Então você pode ter uma classe OffensiveStrategy, por exemplo, que prioriza ataques em detrimento de habilidades defensivas, e outra CautiousStrategy com maior probabilidade de recuperação. Um chefe pode mudar dinamicamente a estratégia com base em sua condição atual.

Uma última coisa. Você pode querer que os dois personagens e monstros herdem da mesma classe, sejam instâncias da mesma classe (ator ou combatente por exemplo) ou compartilhem um objeto comum que encapsule a funcionalidade comum. Isso reduz a duplicação de código e também permite que você tenha NPCs controlados por IA do seu lado, que podem implementar as mesmas estratégias que você já codificou para monstros.

Firas Assaad
fonte
1

Sim, você precisa ter uma parte especial em seu mecanismo que lide com o combate.

Não sei exatamente como você está fazendo o seu combate, mas presumo que os jogadores perambulam pelo mundo do jogo, se encontrem com monstros e a batalha será em tempo real. Nesse caso, o troll precisa conhecer o ambiente em uma determinada área, talvez defina até que ponto o troll pode ver algo à sua frente (o troll lida com isso).

Sobre a IA, acho que o mecanismo precisa lidar com ela mesma, de modo que digamos que você tem mais de um tipo de inimigo que pode fazer a mesma coisa (mordida), basta atribuir a AI a outro monstro e pronto!

Daggio
fonte
0

Seu jogador e seu troll nada mais são que conjuntos de dados, o que chamamos de Modelo de dados que descreve seu mundo. Vida, inventário, recursos de ataque, até mesmo o conhecimento do mundo - tudo consiste no modelo de dados.

Mantenha um único objeto Modelo principal que mantenha todos os dados que descrevem seu mundo. Ele conterá informações gerais do mundo, como dificuldade, parâmetros físicos etc. Também conterá uma lista / matriz de dados de entidades específicas , como descrevi acima. Este modelo principal pode consistir em muitos subobjetos para descrever seu mundo. Em nenhum lugar do seu modelo você deve ter funções que controlem a lógica do jogo ou exibam a lógica; getters são a única exceção e seriam usados ​​apenas para permitir que você obtenha dados do modelo mais rapidamente (se membros públicos ainda não fizerem o truque).

Em seguida, crie funções em uma ou mais classes "controladoras"; você pode escrevê-los todos como funções auxiliares na classe principal, embora isso possa ficar um pouco grande depois de um tempo. Eles serão chamados a cada atualização para atuar sobre os dados das entidades para diferentes finalidades (movimento, ataque etc.). Manter essas funções fora de uma classe de entidade é mais eficiente em termos de recursos e, depois de saber o que descreve sua entidade, você saberá automaticamente quais funções precisam agir sobre ela.

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.

}

Uma observação final é que também é útil manter sua lógica de exibição separada da lógica do jogo. A lógica de exibição seria: "Onde eu desenho isso na tela e em que cor?" vs. lógica do jogo, como descrevi no pseudocódigo acima.

(Nota do desenvolvedor: ao usar classes, isso segue livremente uma abordagem de programação funcional que considera todos os métodos como idealmente sem estado, permitindo um modelo de dados limpo e uma abordagem de processamento que minimiza os bugs causados ​​pelo estado retido. FP é o MVC definitivo, pois atinge MVCs objetivo de separar as preocupações explicitamente. Veja esta pergunta .)

Engenheiro
fonte
1
"Mantenha um único objeto Modelo principal que mantenha todos os dados que descrevem seu mundo. Ele manterá informações gerais do mundo, como dificuldade, parâmetros físicos etc." Dificuldade e parâmetros físicos? Fale sobre a fusão de preocupações! -1.
2
@ Joe - Você quer que eu descreva a ele toda a hierarquia de configuração? Estamos mantendo as coisas simples aqui, não estamos? Eu apreciaria se você pensasse antes de votar.
Engenheiro de
3
Bem, o resto do post é uma tentativa bizarra de cobrir o MVC sem cobrir o V ou qualquer coisa normalmente reconhecível como C, e não acho que o MVC seja um bom conselho para a programação de jogos. Eu apreciaria se você pensasse antes de responder, mas nem sempre conseguimos o que queremos.
1
@ Joe: Eu concordo que o MVC é uma escolha difícil para um jogo, mas tenho certeza de que o papel de V aqui é óbvio.
Zach Conn
4
@Zach: Quando declarações como "FP é o MVC definitivo" são feitas, nada é óbvio, exceto talvez o pôster não consiga entender tanto o MVC quanto a programação funcional.