Programowanie sekwencji walki w grze RPG

13

Próbuję napisać krótką „grę”, w której gracz chodzi i walczy z potworami, ale nie mam pojęcia, jak sobie z tym poradzić.

Powiedzmy na przykład, że mam „Wojownika” i „Trolla”. Jak walczą ze sobą? Wiem, że mogę coś zrobić

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

Ale która część gry kontroluje potwora? Czy po prostu trzymam powyższą sekwencję w pętli, dopóki jedna z nich nie umrze? Czy też gra „silnik” musi mieć część, która dotyczy konkretnie walki? Czy jest to aspekt sztucznej inteligencji Trolla, który musi dbać o swoje działania?

Ponadto, kto / co określa działania podejmowane przez potwora? Może Troll może uderzać, kopać, gryźć, rzucać zaklęcia, pić mikstury, używać magicznego przedmiotu. Czy silnik gry określa, jakie działania podejmuje Troll, czy też jest czymś, czym zarządza klasa Troll?

Przepraszam, nie mogę być bardziej szczegółowy, ale potrzebuję wskazówek, w jakim kierunku pójść w tym kierunku.

Harv
źródło
chłodny! nie wiedziałem, że ta strona istnieje. czy istnieje sposób, aby przenieść tam moje pytanie? czy powinienem go tam po prostu wyciąć / wkleić?
Nie martw się, mod powinien go wkrótce przenieść! Możesz też usunąć pytanie tutaj i odtworzyć ponownie na Game Dev
LiamB
@Fendo Przepraszam, że pytam, ale o jaką stronę chodzi? Game Dev?
user712092,

Odpowiedzi:

12

Wyobrażam sobie sekwencję bitew jako minigrę w twojej grze. Tiki aktualizacji (lub tiki zwrotne) są kierowane do komponentu obsługującego te zdarzenia. Takie podejście obejmuje logikę sekwencji bitew w osobnej klasie, pozostawiając główną pętlę gry swobodną w przechodzeniu między stanami gry.

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

}

Klasa sekwencji bitew wyglądałaby następująco:

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

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

Twój Troll i Wojownik dziedziczą po wspólnej nadklasie zwanej Istotą. W ramach HandleTurn atakująca istota może się poruszać. Jest to odpowiednik rutyny sztucznej inteligencji 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;
    }
}

Metoda walki decyduje o tym, co jednostka zrobi. Pamiętaj, że nie musi to angażować przeciwnej istoty, takiej jak wypicie mikstury lub ucieczka.

Aktualizacja: Aby wesprzeć wiele potworów i drużynę gracza, wprowadzasz klasę grupową:

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

Klasa Group zastąpi wszystkie wystąpienia Entity w klasie BattleSequence. Wybieranie i atakowanie będzie obsługiwane przez samą klasę Istoty, więc AI może wziąć pod uwagę całą grupę przy wyborze najlepszego sposobu działania.

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);
        }
}
duch
źródło
Zakładam, że to zadziała tylko dla jednego gracza przeciwko jednemu potworowi. Czy łatwo byłoby to zaktualizować, aby działało dla jednego gracza w porównaniu do wielu potworów?
Harv
Łatwo jest dodać wsparcie dla grup po stronie potwora jako po stronie gracza (w twojej sytuacji grupa graczy będzie zawierać tylko jednego członka: postać gracza). Zaktualizowałem odpowiedź dla tego scenariusza.
duch
1

Miałbym dedykowany obiekt walki, który zarządza walką. Zawierałby pełny stan walki, w tym takie rzeczy jak lista postaci graczy, lista wrogów, aktualna tura, teren bitwy i tak dalej. Walka może wtedy mieć metodę aktualizacji, która zarządza logiką bitwy. Umieszczenie kodu walki w prostej pętli nie jest dobrym pomysłem, ponieważ skończyłby się naprawdę szybko. Zwykle masz trochę czasu i różne etapy bitwy.

Jeśli chodzi o podejmowane działania, z pewnością możesz sprawić, że będzie losowy, ale nie ma sensu, aby potwór z pełnym HP rzucił zaklęcie leczące. Opłaca się mieć podstawową logikę określania, które działanie należy podjąć. Na przykład niektóre akcje mogą mieć wyższy priorytet niż inne (np. Kopnięcia trolla w 30% przypadków), a także inne warunki, dzięki którym bitwy będą ciekawsze (np. Gdy troll HP ma mniej niż 10% pełnego HP, jest 20% szansa na rzucenie zaklęcia leczącego, w przeciwnym razie szansa wynosi 1%). To może być tak złożone, jak chcesz.

Myślę, że klasa potworów powinna poradzić sobie z wyborem akcji do wykonania, obiekt bitwy prosi potwora o akcję, a potwór dokonuje wyboru, a następnie przystępuje do jego zastosowania. Jednym z pomysłów jest posiadanie obiektu strategii, który podłączasz do potworów i który wybiera z listy możliwych akcji potworów na podstawie priorytetów, kategorii i warunków przypisanych do każdej akcji bitwy. Następnie możesz mieć na przykład klasę OffensiveStrategy, która ma pierwszeństwo przed atakami umiejętności defensywnych, oraz inną CautiousStrategy, która ma większe szanse na wyleczenie. Boss może być w stanie dynamicznie zmieniać strategię w oparciu o jej obecny stan.

Ostatnia rzecz. Możesz chcieć mieć zarówno postacie graczy, jak i potwory dziedziczące po tej samej klasie, być instancjami tej samej klasy (na przykład aktor lub walczący), lub udostępnić wspólny obiekt, który zawiera wspólną funkcjonalność. Zmniejsza to duplikację kodu, a także pozwoli ci mieć po swojej stronie kontrolowanych przez AI NPC, którzy mogą wdrożyć te same strategie, które już zakodowałeś dla potworów.

Firas Assaad
źródło
1

Tak, musisz mieć specjalną część w silniku, która obsługuje walkę.

Nie wiem, jak dokładnie walczysz, ale założę, że gracze wędrują po świecie gry, spotykają się z potworami, a bitwa trwa w czasie rzeczywistym. Jeśli tak, to troll musi znać otoczenie w określonym obszarze, może określić, jak daleko troll widzi coś przed sobą (troll sobie z tym radzi).

Jeśli chodzi o AI, myślę, że silnik musi sobie z tym poradzić, więc powiedzmy, że masz więcej niż jednego wroga, który może zrobić to samo (ugryzienie), możesz po prostu przypisać AI innemu potworowi i gotowe!

Daggio
źródło
0

Twój gracz i twój troll są niczym innym jak zestawami danych, które nazywamy modelem danych opisującym twój świat. Życie, ekwipunek, możliwości ataku, a nawet ich znajomość świata - wszystko opiera się na modelu danych.

Zachowaj jeden główny obiekt modelu, który przechowuje wszystkie dane opisujące Twój świat. Będzie zawierał ogólne informacje o świecie, takie jak trudność, parametry fizyki itp. Będzie także zawierał listę / tablicę danych określonych bytów , jak to opisano powyżej. Ten główny model może składać się z wielu podobiektów w celu opisania twojego świata. Nigdzie w twoim modelu nie powinieneś mieć żadnych funkcji sterujących logiką gry lub logiką wyświetlania; gettery są jedynym wyjątkiem i byłyby używane tylko w celu umożliwienia łatwiejszego pobierania danych z modelu (jeśli członkowie publiczni jeszcze tego nie zrobią).

Następnie utwórz funkcje w jednej lub więcej klasach „kontrolerów”; możesz napisać je wszystkie jako funkcje pomocnicze w swojej klasie głównej, chociaż po pewnym czasie może to nieco wzrosnąć. Będą one nazywane każdą aktualizacją, aby działać na danych podmiotów w różnych celach (ruch, atak itp.). Trzymanie tych funkcji poza klasą encji jest bardziej zasobooszczędne, a gdy wiesz, co opisuje twoją encję, automatycznie wiesz, jakie funkcje muszą na nią działać.

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.

}

Ostatnia uwaga jest taka, że ​​przydatne jest także oddzielenie logiki wyświetlania od logiki gry. Logika wyświetlania brzmiałaby: „Gdzie narysować to na ekranie i w jakim kolorze?” vs. logika gry jest tym, co zarysowałem w powyższym pseudokodzie.

(Nota dewelopera: Podczas korzystania z klas, to luźno podąża za funkcjonalnym podejściem programistycznym, które uznaje wszystkie metody za idealnie bezstanowe, pozwalając na czysty model danych i podejście do przetwarzania, które minimalizuje błędy spowodowane stanem zatrzymanym. FP jest ostatecznym MVC, ponieważ osiąga MVC cel wyraźnego rozdzielenia obaw. Zobacz to pytanie ).

Inżynier
źródło
1
„Zachowaj jeden główny obiekt modelu, który przechowuje wszystkie dane opisujące Twój świat. Będzie zawierał ogólne informacje o świecie, takie jak trudność, parametry fizyki itp.” Trudności i parametry fizyki? Mów o pomieszaniu obaw! -1.
2
@Joe - Czy chcesz, abym przedstawił mu całą hierarchię konfiguracji? Uprościmy to tutaj, prawda? Byłbym wdzięczny, gdybyś pomyślał przed oddaniem głosu.
Inżynier
3
Cóż, reszta tego wpisu jest dziwną próbą pokrycia MVC bez pokrycia V lub czegokolwiek normalnie rozpoznawalnego jako C, i nie sądzę, że MVC jest dobrą radą dla programowania gier. Byłbym wdzięczny, gdybyś pomyślał przed odpowiedzią, ale nie zawsze możemy dostać to, czego chcemy.
1
@Joe: Zgadzam się, że MVC jest trudnym wyborem do gry, ale jestem prawie pewien, że rola V jest tutaj oczywista.
Zach Conn
4
@Zach: Gdy pojawiają się twierdzenia, że ​​„FP jest ostatecznym MVC”, nic nie jest oczywiste, z wyjątkiem tego, że plakat nie rozumie zarówno MVC, jak i programowania funkcjonalnego.