O que é uma maneira de implementar um sistema flexível de buff / debuff?

66

Visão global:

Muitos jogos com estatísticas do tipo RPG permitem "buffs" dos personagens, variando de "Causar 25% de dano extra" a coisas mais complicadas, como "Causar 15 de dano aos atacantes quando atingidos".

As especificidades de cada tipo de buff não são realmente relevantes. Estou procurando uma maneira (presumivelmente orientada a objetos) para lidar com buffs arbitrários.

Detalhes:

No meu caso particular, eu tenho vários personagens em um ambiente de batalha baseado em turnos, então imaginei buffs vinculados a eventos como "OnTurnStart", "OnReceiveDamage" etc. Talvez cada buff seja uma subclasse da classe abstrata Buff principal, em que apenas os eventos relevantes estão sobrecarregados. Cada personagem pode ter um vetor de buffs atualmente aplicado.

Essa solução faz sentido? Eu certamente posso ver dezenas de tipos de eventos sendo necessários, parece que criar uma nova subclasse para cada buff é um exagero e não parece permitir nenhuma "interação" do buff. Ou seja, se eu quisesse implementar um limite para aumentar os danos, de modo que, mesmo se você tivesse 10 buffs diferentes, todos com 25% de dano extra, você faria apenas 100% a mais em vez de 250% a mais.

E há situações mais complicadas que idealmente eu poderia controlar. Tenho certeza de que todos podem apresentar exemplos de como os buffs mais sofisticados podem interagir potencialmente entre si de uma maneira que, como desenvolvedor de jogos, eu talvez não queira.

Como um programador C ++ relativamente inexperiente (geralmente utilizo C em sistemas embarcados), sinto que minha solução é simplista e provavelmente não tira o máximo proveito da linguagem orientada a objetos.

Pensamentos? Alguém aqui já projetou um sistema de buff bastante robusto antes?

Editar: em relação às respostas:

Selecionei uma resposta principalmente com base em bons detalhes e em uma resposta sólida à pergunta que fiz, mas a leitura das respostas me deu mais algumas dicas.

Talvez sem surpresa, os diferentes sistemas ou sistemas aprimorados parecem se aplicar melhor a determinadas situações. O sistema que funciona melhor para o meu jogo dependerá dos tipos, variações e número de buffs que pretendo aplicar.

Para um jogo como Diablo 3 (mencionado abaixo), onde praticamente qualquer equipamento pode mudar a força de um buff, os buffs são apenas estatísticas do personagem, o sistema parece uma boa idéia sempre que possível.

Para a situação baseada em turnos em que estou, a abordagem baseada em eventos pode ser mais adequada.

De qualquer forma, ainda espero que alguém ache uma bala mágica sofisticada "OO", que permita aplicar +2 de distância de movimento por buff por turno , causando 50% do dano recebido ao buff do atacante , e um teletransporte automaticamente para um bloco próximo quando atacado de 3 ou mais blocos de distância em um único sistema sem transformar um bônus de força +5 em sua própria subclasse.

Eu acho que a coisa mais próxima é a resposta que marquei, mas o chão ainda está aberto. Obrigado a todos pela contribuição.

gkimsey
fonte
Não estou postando isso como resposta, pois estou apenas fazendo um brainstorming, mas que tal uma lista de buffs? Cada buff possui uma constante e um modificador de fator. Constante seria +10 de dano, o fator seria 1,10 para um aumento de + 10% no dano. Nos seus cálculos de dano, você itera todos os buffs, para obter um modificador total e impõe as limitações que deseja. Você faria isso para qualquer tipo de atributo modificável. Você precisaria de um método especial para coisas complicadas.
William Mariager
Aliás, eu já havia implementado algo assim para o meu objeto Stats quando estava criando um sistema para armas e acessórios equipáveis. Como você disse, é uma solução decente o suficiente para buffs que modificam apenas os atributos existentes, mas é claro que mesmo assim eu quero que certos buffers expirem após X turnos, outros expirem assim que o efeito ocorrer vezes Y, etc. mencione isso na questão principal, pois já estava ficando muito longo.
precisa saber é o seguinte
11
se você tiver um método "onReceiveDamage" que é chamado por um sistema de mensagens, ou manualmente, ou de alguma outra maneira, deve ser fácil incluir uma referência a quem / o que você está danificando. Então você poderia fazer esta informação disponível para o seu lustre
Certo, eu esperava que cada modelo de evento da classe Buff abstrata incluísse parâmetros relevantes como esse. Certamente funcionaria, mas estou hesitante porque parece que não vai escalar bem. É difícil imaginar que um MMORPG com várias centenas de buffs diferentes tenha uma classe separada definida para cada buff, escolhendo entre uma centena de eventos diferentes. Não que eu esteja criando tantos buffs (provavelmente perto de 30), mas se houver um sistema mais simples, mais elegante ou mais flexível, eu gostaria de usá-lo. Sistema mais flexível = buffs / habilidades mais interessantes.
precisa saber é o seguinte
4
Essa não é uma boa resposta para o problema de interação, mas parece-me que o padrão decorador se aplica bem aqui; basta aplicar mais buffs (decoradores) uns sobre os outros. Talvez com um sistema para lidar com a interação "mesclando" buffs juntos (por exemplo, 10x 25% se mescla em um buff de 100%).
precisa saber é o seguinte

Respostas:

32

Esta é uma questão complicada, porque você está falando sobre algumas coisas diferentes que (atualmente) são agrupadas como 'buffs':

  • modificadores para os atributos de um jogador
  • efeitos especiais que acontecem em determinados eventos
  • combinações dos itens acima.

Eu sempre implemento o primeiro com uma lista de efeitos ativos para um determinado personagem. A remoção da lista, seja com base na duração ou explicitamente, é bastante trivial, portanto não abordarei isso aqui. Cada efeito contém uma lista de modificadores de atributo e pode aplicá-lo ao valor subjacente através da multiplicação simples.

Em seguida, envolvo-o com funções para acessar os atributos modificados. por exemplo.:

def get_current_attribute_value(attribute_id, criteria):
    val = character.raw_attribute_value[attribute_id]
    # Accumulate the modifiers
    for effect in character.all_effects:
        val = effect.apply_attribute_modifier(attribute_id, val, criteria)
    # Make sure it doesn't exceed game design boundaries
    val = apply_capping_to_final_value(val)
    return val

class Effect():
    def apply_attribute_modifier(attribute_id, val, criteria):
        if attribute_id in self.modifier_list:
            modifier = self.modifier_list[attribute_id]
            # Does the modifier apply at this time?
            if modifier.criteria == criteria:
                # Apply multiplicative modifier
                return val * modifier.amount
        else:
            return val

class Modifier():
    amount = 1.0 # default that has no effect
    criteria = None # applies all of the time

Isso permite aplicar efeitos multiplicativos com bastante facilidade. Se você também precisar de efeitos aditivos, decida em que ordem os aplicará (provavelmente o último aditivo) e percorra a lista duas vezes. (Eu provavelmente teria listas de modificadores separadas no Effect, uma para multiplicativa e outra para aditivo).

O valor do critério é permitir que você implemente "+ 20% vs morto-vivo" - defina o valor UNDEAD no efeito e passe apenas o valor UNDEAD para get_current_attribute_value() morto- quando você estiver calculando uma rolagem de dano contra um inimigo morto-vivo.

Aliás, eu não ficaria tentado a escrever um sistema que aplique e aplique valores diretamente ao valor do atributo subjacente - o resultado final é que é muito provável que seus atributos se afastem do valor pretendido devido a erro. (por exemplo, se você multiplicar algo por 2, mas depois limitar, quando você o dividir por 2 novamente, será mais baixo do que começou.)

Quanto aos efeitos baseados em eventos, como "Cause 15 de dano aos atacantes quando atingidos", você pode adicionar métodos à classe Effect para isso. Mas se você deseja um comportamento distinto e arbitrário (por exemplo, alguns efeitos para o evento acima podem refletir danos, alguns podem curá-lo, pode teleportá-lo aleatoriamente, seja qual for), você precisará de funções ou classes personalizadas para lidar com isso. Você pode atribuir funções aos manipuladores de eventos sobre o efeito; basta chamar os manipuladores de eventos para quaisquer efeitos ativos.

# This is a method on a Character, called during combat
def on_receive_damage(damage_info):
    for effect in character.all_effects:
        effect.on_receive_damage(character, damage_info)

class Effect():
    self.on_receive_damage_handler = DoNothing # a default function that does nothing
    def on_receive_damage(character, damage_info):
        self.on_receive_damage_handler(character, damage_info)

def reflect_damage(character, damage_info):
    damage_info.attacker.receive_damage(15)

reflect_damage_effect = new Effect()
reflect_damage_effect.on_receive_damage_handler = reflect_damage
my_character.all_effects.add(reflect_damage_effect)

Obviamente, sua classe Effect terá um manipulador de eventos para cada tipo de evento, e você pode atribuir funções de manipulador a quantas forem necessárias em cada caso. Você não precisa subclassificar Effect, pois cada um é definido pela composição dos modificadores de atributo e manipuladores de eventos que ele contém. (Provavelmente também conterá um nome, uma duração etc.)

Kylotan
fonte
2
+1 para obter excelentes detalhes. Esta é a resposta mais próxima de responder oficialmente à minha pergunta como eu já vi. A configuração básica aqui parece permitir muita flexibilidade e uma pequena abstração do que poderia ser uma lógica de jogo confusa. Como você disse, os efeitos mais descolados ainda precisariam de suas próprias classes, mas isso lida com a maior parte das necessidades típicas de um sistema "buff", eu acho.
precisa saber é o seguinte
+1 para apontar as diferenças conceituais ocultas aqui. Nem todos eles funcionarão com a mesma lógica de atualização baseada em eventos. Veja a resposta de @ Ross para uma aplicação totalmente diferente. Ambos terão que existir um ao lado do outro.
ctietze
22

Em um jogo em que trabalhei com um amigo de uma turma, criamos um sistema de buff / debuff para quando o usuário fica preso em grama alta e ladrilhos de aceleração e o que não é, e algumas coisas menores, como sangramentos e venenos.

A ideia era simples e, enquanto a aplicávamos no Python, era bastante eficaz.

Basicamente, aqui está como foi:

  • O usuário tinha uma lista de buffs e debuffs atualmente aplicados (observe que um buff e debuff são relativamente iguais, é apenas o efeito que tem um resultado diferente)
  • Os buffs têm uma variedade de atributos, como duração, nome e texto para exibir informações e tempo de vida. Os importantes são tempo de vida, duração e uma referência ao ator ao qual esse buff é aplicado.
  • Para o Buff, quando ele é anexado ao player via player.apply (buff / debuff), ele chamaria o método start (), isso aplicaria as alterações críticas ao jogador, como aumentar a velocidade ou diminuir a velocidade.
  • Iríamos então iterar através de cada buff em um loop de atualização e os buffs seriam atualizados, isso aumentaria seu tempo de vida. As subclasses implementariam coisas como envenenar o jogador, fornecer HP ao longo do tempo etc.
  • Quando o buff era feito por, significando timeAlive> = duration, a lógica de atualização removia o buff e chamava um método finish (), que variava de remover as limitações de velocidade de um jogador até causar um pequeno raio (pense em um efeito de bomba) depois de um ponto)

Agora, como realmente aplicar buffs do mundo é uma história diferente. Aqui está minha comida para pensar.

Ross
fonte
11
Isso soa como uma explicação melhor do que eu estava tentando descrever acima. É relativamente simples, certamente fácil de entender. Você essencialmente mencionou três "eventos" lá (OnApply, OnTimeTick, OnExpired) para associá-lo ainda mais ao meu pensamento. No momento, ele não suporta coisas como devolver dano ao ser atingido e assim por diante, mas é melhor para muitos buffs. Prefiro não limitar o que meus buffs podem fazer (que = limitar o número de eventos que tenho que chamar pela lógica principal do jogo), mas a escalabilidade do buff pode ser mais importante. Obrigado pela sua contribuição!
precisa saber é o seguinte
Sim, não implementamos nada disso. Parece muito legal e um ótimo conceito (como um fã de Thorns).
Ross
@gkimsey Para coisas como Thorns e outros buffs passivos, eu implementaria a lógica na sua classe Mob como uma estatística passiva semelhante a dano ou saúde e aumentaria essa estatística ao aplicar o buff. Isso simplifica muito o caso quando você tem vários buffs de espinhos, além de manter a interface limpa (10 buffs mostrariam 1 dano de retorno em vez de 10) e permite que o sistema de buffs permaneça simples.
3Doubloons
Essa é uma abordagem quase intuitivamente simples, mas comecei a pensar em mim mesma ao jogar Diablo 3. Notei que roubo de vida, vida ao acerto, dano a atacantes corpo a corpo, etc., eram todas as suas próprias estatísticas na janela do personagem. É verdade que o D3 não possui o sistema ou as interações de buffers mais complicados do mundo, mas é dificilmente trivial. Isso faz muitosentido. Ainda assim, existem potencialmente 15 buffs diferentes com 12 efeitos diferentes que se encaixariam nisso. Parece preenchimento estranho o caráter Status folha ....
gkimsey
11

Não tenho certeza se você ainda está lendo isso, mas luto com esse tipo de problema há muito tempo.

Eu projetei vários tipos diferentes de sistemas afetados. Vou examiná-los brevemente agora. Tudo isso é baseado na minha experiência. Não pretendo saber todas as respostas.


Modificadores estáticos

Esse tipo de sistema depende principalmente de números inteiros simples para determinar quaisquer modificações. Por exemplo, +100 a Max HP, +10 a atacar e assim por diante. Este sistema também pode lidar com porcentagens também. Você só precisa garantir que o empilhamento não fique fora de controle.

Eu nunca realmente armazenei em cache os valores gerados para esse tipo de sistema. Por exemplo, se eu quisesse exibir a saúde máxima de alguma coisa, geraria o valor no local. Isso impedia que as coisas fossem propensas a erros e apenas mais fáceis de entender para todos os envolvidos.

(Eu trabalho em Java, o que segue é baseado em Java, mas deve funcionar com algumas modificações para outras linguagens.) Esse sistema pode ser feito facilmente usando enumerações para os tipos de modificação e depois números inteiros. O resultado final pode ser colocado em algum tipo de coleção que possui pares de chave e valor ordenados. Serão pesquisas e cálculos rápidos, portanto o desempenho é muito bom.

No geral, ele funciona muito bem com apenas modificadores estáticos. No entanto, o código deve existir nos locais apropriados para que os modificadores sejam usados: getAttack, getMaxHP, getMeleeDamage e assim por diante.

Onde esse método falha (para mim) é uma interação muito complexa entre os buffs. Não existe uma maneira muito fácil de interagir, exceto por guiá-lo um pouco. Ele tem algumas possibilidades de interação simples. Para fazer isso, você deve fazer uma modificação na maneira como armazena os modificadores estáticos. Em vez de usar uma enumeração como chave, você usa uma String. Essa String seria o nome do Enum + variável extra. 9 vezes em 10, a variável extra não é usada; portanto, você ainda mantém o nome da enumeração como chave.

Vamos dar um exemplo rápido: se você quiser modificar o dano contra criaturas mortas-vivas, poderá ter um par ordenado como este: (DAMAGE_Undead, 10) O DAMAGE é o Enum e o Undead é a variável extra. Portanto, durante o seu combate, você pode fazer algo como:

dam += attacker.getMod(Mod.DAMAGE + npc.getRaceFamily()); //in this case the race family would be undead

De qualquer forma, funciona bastante bem e é rápido. Mas falha em interações complexas e em ter código "especial" em todos os lugares. Por exemplo, considere a situação de "25% de chance de se teletransportar na morte". Este é um "bastante" complexo. O sistema acima pode lidar com isso, mas não com facilidade, pois você precisa do seguinte:

  1. Determine se o jogador tem este mod.
  2. Em algum lugar, tenha algum código para executar o teletransporte, se for bem-sucedido. A localização desse código é uma discussão em si!
  3. Obtenha os dados corretos no mapa Mod. O que o valor significa? É a sala onde eles se teletransportam também? E se um jogador tiver dois mods de teleporte? Os montantes não serão somados ?????? FALHA!

Então isso me leva ao meu próximo:


O melhor sistema de buff complexo

Uma vez tentei escrever um MMORPG 2D sozinho. Este foi um erro terrível, mas eu aprendi muito!

Reescrevi o sistema afetado 3 vezes. O primeiro usou uma variação menos poderosa do que foi dito acima. O segundo foi o que eu vou falar.

Esse sistema tinha uma série de classes para cada modificação, assim como: ChangeHP, ChangeMaxHP, ChangeHPByPercent, ChangeMaxByPercent. Eu tinha um milhão desses caras - até coisas como TeleportOnDeath.

Minhas aulas tinham coisas que fariam o seguinte:

  • applyAffect
  • removeAffect
  • checkForInteraction <--- importante

Aplicar e remover se explicam (embora em coisas como porcentagens, o efeito acompanhe o quanto aumentou o HP para garantir quando o efeito se dissipou, apenas removeria a quantidade adicionada. demorei muito tempo para me certificar de que estava certo. Ainda não tive um bom pressentimento.).

O método checkForInteraction era um trecho de código horrivelmente complexo. Em cada uma das classes afeta (ie: ChangeHP), ele teria código para determinar se isso deve ser modificado pelo efeito de entrada. Por exemplo, se você tivesse algo como ...

  • Buff 1: Causa 10 de dano de Fogo ao ataque
  • Buff 2: Aumenta em 25% todo o dano de fogo.
  • Buff 3: aumenta em 15 todo o dano de fogo.

O método checkForInteraction lidaria com todos esses efeitos. Para fazer isso, cada efeito em TODOS os jogadores por perto tinha que ser verificado! Isso ocorre porque o tipo de afeto que eu lidei com vários jogadores ao longo de uma área. Isso significa que o código NUNCA TINHA quaisquer declarações especiais como acima - "se acabamos de morrer, devemos verificar se há teletransporte na morte". Este sistema trataria automaticamente corretamente no momento certo.

Tentar escrever esse sistema levou dois meses e explodiu de cabeça várias vezes. No entanto, era REALMENTE poderoso e poderia fazer uma quantidade insana de coisas - especialmente quando você leva em consideração os dois fatos a seguir para habilidades no meu jogo: 1. Eles tinham intervalos de alvo (ou seja: único, auto, apenas grupo, PB AE auto). , PB AE alvo, AE alvo e assim por diante). 2. As habilidades podem ter mais de um efeito sobre elas.

Como mencionei acima, este foi o segundo do terceiro sistema de afetação para este jogo. Por que me afastei disso?

Este sistema teve o pior desempenho que eu já vi! Era muito lento, pois tinha que fazer muita verificação para cada coisa que acontecia. Tentei melhorá-lo, mas considerou um fracasso.

Então chegamos à minha terceira versão (e outro tipo de sistema de buffs):


Classe afetada complexa com manipuladores

Portanto, é praticamente uma combinação dos dois primeiros: podemos ter variáveis ​​estáticas em uma classe Affect que contém muita funcionalidade e dados extras. Em seguida, basta chamar manipuladores (para mim, praticamente alguns métodos de utilidade estática em vez de subclasses para ações específicas. Mas tenho certeza de que você poderia usar subclasses para ações, se quisesse também) quando quisermos fazer alguma coisa.

A classe Affect teria todos os itens bons e interessantes, como tipos de destino, duração, número de usos, chance de executar e assim por diante.

Ainda teríamos que adicionar códigos especiais para lidar com as situações, por exemplo, teleportar na morte. Ainda teríamos que verificar isso manualmente no código de combate e, se existisse, obteríamos uma lista de efeitos. Esta lista de afetos contém todos os afetos atualmente aplicados no jogador que lidou com o teletransporte na morte. Depois, olhamos cada uma delas e verificamos se ela foi executada e foi bem-sucedida. Se fosse bem-sucedido, chamaríamos o manipulador para cuidar disso.

A interação pode ser feita, se você quiser também. Teria apenas que escrever o código para procurar buffs específicos nos players / etc. Por ter um bom desempenho (veja abaixo), deve ser bastante eficiente fazer isso. Só precisaria de manipuladores mais complexos e assim por diante.

Portanto, ele tem muito desempenho do primeiro sistema e ainda muita complexidade como o segundo (mas não tanto). Pelo menos em Java, você pode fazer algumas coisas complicadas para obter o desempenho da quase primeira nos MOST casos (por exemplo: ter um mapa de enumeração ( http://docs.oracle.com/javase/6/docs/api/java) /util/EnumMap.html ) com Enums como as chaves e ArrayList de afeta como os valores.Isto permite que você veja se você tem afetos rapidamente [já que a lista seria 0 ou o mapa não teria a enumeração] e não possui para iterar continuamente as listas de afetos do jogador sem motivo. Não me importo de repetir os afetos se precisarmos deles neste momento. Otimizarei mais tarde se isso se tornar um problema.

Atualmente, estou reabrindo (reescrevendo o jogo em Java, em vez da base de código FastROM em que ele estava originalmente) meu MUD que terminou em 2005 e recentemente me deparei com como quero implementar meu sistema de buff? Vou usar esse sistema porque funcionou muito bem no meu jogo com falha anterior.

Bem, espero que alguém, em algum lugar, ache algumas dessas idéias úteis.

dayrinni
fonte
6

Uma classe diferente (ou função endereçável) para cada buff não é um exagero se o comportamento desses buffs for diferente um do outro. Uma coisa seria ter buffs de + 10% ou + 20% (que, é claro, seria melhor representado como dois objetos da mesma classe), outra seria a implementação de efeitos totalmente diferentes que exigiriam código personalizado de qualquer maneira. No entanto, acredito que é melhor ter maneiras padrão de personalizar a lógica do jogo, em vez de deixar cada buff fazer o que bem entender (e possivelmente interferir entre si de formas imprevistas, perturbando o equilíbrio do jogo).

Sugiro dividir cada "ciclo de ataque" em etapas, onde cada etapa tem um valor base, uma lista ordenada de modificações que podem ser aplicadas a esse valor (talvez limitado) e um limite final. Cada modificação possui uma transformação de identidade como padrão e pode ser influenciada por zero ou mais buffs / debuffs. As especificidades de cada modificação dependeriam da etapa aplicada. Você decide como o ciclo é implementado (incluindo a opção de uma arquitetura orientada a eventos, como você está discutindo).

Um exemplo de ciclo de ataque pode ser:

  • calcular ataque do jogador (base + mods);
  • calcular defesa do oponente (base + mods);
  • faça a diferença (e aplique mods) e determine o dano básico;
  • calcule qualquer efeito de desvio / armadura (mods no dano base) e aplique dano;
  • calcule qualquer efeito de recuo (mods no dano base) e aplique ao atacante.

O importante a ser observado é que, quanto mais cedo no ciclo um buff é aplicado, mais efeito ele terá no resultado . Portanto, se você quiser um combate mais "tático" (onde a habilidade do jogador é mais importante que o nível do personagem), crie muitos buffs / debuffs nas estatísticas básicas. Se você quiser um combate mais "equilibrado" (onde o nível é mais importante - importante nos MMOGs para limitar a taxa de progresso), use apenas buffs / debuffs mais tarde no ciclo.

A distinção entre "Modificações" e "Buffs" que mencionei anteriormente tem um propósito: as decisões sobre regras e equilíbrio podem ser implementadas no primeiro, de modo que quaisquer alterações nelas não precisam refletir mudanças em todas as classes do último. OTOH, os números e tipos de buffs são limitados apenas pela sua imaginação, pois cada um deles pode expressar o comportamento desejado sem ter que levar em consideração qualquer interação possível entre eles e os outros (ou mesmo a existência de outros).

Então, respondendo à pergunta: não crie uma classe para cada Buff, mas uma para cada (tipo de) Modificação, e amarre a Modificação ao ciclo de ataque, não ao personagem. Os buffs podem ser simplesmente uma lista de tuplas (modificação, chave, valor) e você pode aplicar um buff a um personagem simplesmente adicionando / removendo-o do conjunto de buffs do caractere. Isso também reduz a janela de erro, já que as estatísticas do personagem não precisam ser alteradas quando os buffs são aplicados (portanto, há menos risco de restaurar uma estatística para o valor errado depois que um buff expirar).

mgibsonbr
fonte
Essa é uma abordagem interessante, pois fica em algum lugar entre as duas implementações que eu havia considerado - ou seja, restringindo os buffs a modificadores razoáveis ​​de dano de resultado e resultado, ou criando um sistema muito robusto, mas alto, capaz de lidar com qualquer coisa. É como uma expansão do primeiro para permitir os "espinhos", mantendo uma interface simples. Embora eu não ache que seja a bala mágica para o que eu preciso, certamente parece que facilita muito o balanceamento do que outras abordagens, portanto pode ser o caminho a seguir. Obrigado pela sua contribuição!
precisa saber é o seguinte
3

Não sei se você ainda está lendo, mas eis como estou fazendo agora (o código é baseado no UE4 e C ++). Depois de refletir sobre o problema por mais de duas semanas (!!), finalmente encontrei o seguinte:

http://gamedevelopment.tutsplus.com/tutorials/using-the-composite-design-pattern-for-an-rpg-attributes-system--gamedev-243

E eu pensei que, bem, encapsular um único atributo dentro de classe / estrutura não é uma má idéia, afinal. No entanto, lembre-se de que estou aproveitando muito o sistema de reflexão de código do UE4, portanto, sem alguns retrabalhos, isso pode não ser adequado em todos os lugares.

De qualquer forma, comecei a partir do atributo wrapping em uma única estrutura:

USTRUCT(BlueprintType)
struct GAMEATTRIBUTES_API FGAAttributeBase
{
    GENERATED_USTRUCT_BODY()
public:
    UPROPERTY()
        FName AttributeName;
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Value")
        float BaseValue;
    /*
        This is maxmum value of this attribute.
    */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Value")
        float ClampValue;
protected:
    float BonusValue;
    //float OldCurrentValue;
    float CurrentValue;
    float ChangedValue;

    //map of modifiers.
    //It could be TArray, but map seems easier to use in this case
    //we need to keep track of added/removed effects, and see 
    //if this effect affected this attribute.
    TMap<FGAEffectHandle, FGAModifier> Modifiers;

public:

    inline float GetFinalValue(){ return BaseValue + BonusValue; };
    inline float GetCurrentValue(){ return CurrentValue; };
    void UpdateAttribute();

    void Add(float ValueIn);
    void Subtract(float ValueIn);

    //inline float GetCurrentValue()
    //{
    //  return FMath::Clamp<float>(BaseValue + BonusValue + AccumulatedBonus, 0, GetFinalValue());;
    //}

    void AddBonus(const FGAModifier& ModifiersIn, const FGAEffectHandle& Handle);
    void RemoveBonus(const FGAEffectHandle& Handle);

    void InitializeAttribute();

    void CalculateBonus();

    inline bool operator== (const FGAAttributeBase& OtherAttribute) const
    {
        return (OtherAttribute.AttributeName == AttributeName);
    }

    inline bool operator!= (const FGAAttributeBase& OtherAttribute) const
    {
        return (OtherAttribute.AttributeName != AttributeName);
    }

    inline bool IsValid() const
    {
        return !AttributeName.IsNone();
    }
    friend uint32 GetTypeHash(const FGAAttributeBase& AttributeIn)
    {
        return AttributeIn.AttributeName.GetComparisonIndex();
    }
};

Ainda não está terminado, mas a ideia base é que essa estrutura monitore seu estado interno. Os atributos podem ser modificados apenas por Efeitos. Tentar modificá-los diretamente não é seguro e não é exposto aos designers. Estou assumindo que tudo, que pode interagir com atributos, é Effect. Incluindo bônus simples de itens. Quando um novo item é equipado, um novo efeito (junto com a alça) é criado e adicionado ao mapa dedicado, que lida com bônus de duração infinita (aqueles que devem ser removidos manualmente pelo jogador). Quando um novo efeito está sendo aplicado, um novo identificador para ele é criado (identificador é apenas int, envolvido com struct) e, em seguida, esse identificador é passado ao redor como um meio de interagir com esse efeito, além de acompanhar se o efeito é Ainda ativo. Quando o efeito é removido, seu identificador é transmitido para todos os objetos interessados,

A parte realmente importante disso é o TMap (TMap é um mapa de hash). FGAModifier é uma estrutura muito simples:

struct FGAModifier
{
    EGAAttributeOp AttributeMod;
    float Value;
};

Ele contém o tipo de modificação:

UENUM()
enum class EGAAttributeOp : uint8
{
    Add,
    Subtract,
    Multiply,
    Divide,
    Set,
    Precentage,

    Invalid
};

E Value, que é o valor final calculado, vamos aplicar ao atributo.

Adicionamos um novo efeito usando a função simples e, em seguida, chamamos:

void FGAAttributeBase::CalculateBonus()
{
    float AdditiveBonus = 0;
    auto ModIt = Modifiers.CreateConstIterator();
    for (ModIt; ModIt; ++ModIt)
    {
        switch (ModIt->Value.AttributeMod)
        {
        case EGAAttributeOp::Add:
            AdditiveBonus += ModIt->Value.Value;
                break;
            default:
                break;
        }
    }
    float OldBonus = BonusValue;
    //calculate final bonus from modifiers values.
    //we don't handle stacking here. It's checked and handled before effect is added.
    BonusValue = AdditiveBonus; 
    //this is absolute maximum (not clamped right now).
    float addValue = BonusValue - OldBonus;
    //reset to max = 200
    CurrentValue = CurrentValue + addValue;
}

Essa função deve recalcular toda a pilha de bônus, cada vez que o efeito é adicionado ou removido. A função ainda não está concluída (como você pode ver), mas você pode ter uma ideia geral.

Minha maior reclamação no momento é lidar com o atributo Damaging / Healing (sem envolver o recálculo da pilha inteira), acho que resolvi isso um pouco, mas ainda exige mais testes para ser 100%.

Em qualquer caso, os Atributos são definidos assim (+ macros irreais, omitidas aqui):

FGAAttributeBase Health;
FGAAttributeBase Energy;

etc.

Também não tenho 100% de certeza sobre como lidar com o CurrentValue do atributo, mas deve funcionar. Do jeito que estão agora.

De qualquer forma, espero que isso salve algumas pessoas no cache principal, sem ter certeza se essa é a melhor ou até boa solução, mas eu gosto mais do que rastrear efeitos independentemente de atributos. Tornar cada atributo de rastreamento em seu próprio estado é muito mais fácil nesse caso e deve ser menos propenso a erros. Existe essencialmente apenas um ponto de falha, que é a classe bastante curta e simples.

Łukasz Baran
fonte
Obrigado pelo link e explicação do seu trabalho! Eu acho que você está caminhando em direção essencialmente ao que eu estava pedindo. Algumas coisas que vêm à mente são a ordem das operações (por exemplo, 3 efeitos "adicionam" e 2 efeitos "multiplicam" no mesmo atributo, o que deve acontecer primeiro?), E isso é apenas suporte a atributos. Há também a noção de gatilhos (como os efeitos "perder 1 PA ao atingir") para resolver, mas isso provavelmente seria uma investigação separada.
22415 Ghimsey #
A ordem de operação, no caso de apenas calcular o bônus de atributo, é fácil de fazer. Você pode ver aqui o que eu tenho lá e trocar. Para iterar sobre todos os bônus atuais (que podem ser adicionados, subtraídos, multiplicados, divididos etc.), e depois apenas acumulá-los. Você faz algo como BonusValue = (BonusValue * MultiplyBonus + AddBonus-SubtractBonus) / DivideBonus, ou, no entanto, deseja procurar essa equação. Devido ao único ponto de entrada, é fácil experimentar com ele. Quanto aos gatilhos, eu não escrevi sobre isso, porque esse é o outro problema que eu penso, e eu já tentei 3-4 (limite)
Łukasz Baran
soluções, nenhuma delas funcionou da maneira que eu queria (meu objetivo principal é que elas sejam amigáveis ​​ao designer). Minha idéia geral é usar tags e verificar os efeitos recebidos contra tags. Se a tag corresponder, o efeito poderá desencadear outro efeito. (tag é um nome legível humano simples, como Damage.Fire, Attack.Physical etc). No núcleo, é um conceito muito fácil, a questão é organizar dados, para ser facilmente acessível (rápido para pesquisa) e facilitar a adição de novos efeitos. Você pode verificar o código aqui github.com/iniside/ActionRPGGame (GameAttributes é o módulo que você vai se interessar)
Łukasz Baran
2

Eu trabalhei em um pequeno MMO e todos os itens, poderes, buffs etc. tinham 'efeitos'. Um efeito era uma classe que tinha variáveis ​​para 'AddDefense', 'InstantDamage', 'HealHP' etc. etc. Os poderes, itens, etc. lidariam com a duração desse efeito.

Quando você lança um poder ou coloca um item, ele aplica o efeito ao personagem pela duração especificada. Então o ataque principal, etc cálculos levariam em conta os efeitos aplicados.

Por exemplo, você tem um bônus que adiciona defesa. Haveria no mínimo um EffectID e Duration para esse buff. Ao convertê-lo, ele aplicaria o EffectID ao caractere pela duração especificada.

Outro exemplo para um item teria os mesmos campos. Mas a duração seria infinita ou até que o efeito seja removido removendo o item do personagem.

Este método permite que você itere sobre uma lista de efeitos atualmente aplicados.

Espero ter explicado esse método com bastante clareza.

Grau
fonte
Pelo que entendi com minha experiência mínima, essa é a maneira tradicional de implementar mods estatísticos em jogos de RPG. Funciona bem e é fácil de entender e implementar. A desvantagem é que não me deixa espaço para fazer coisas como o lustre "espinhos", ou algo mais avançado ou situacional. Historicamente, também tem sido a causa de algumas explorações em RPGs, embora sejam bastante raras, e desde que estou fazendo um jogo para um jogador, se alguém encontrar uma exploração, não estou realmente preocupado. Obrigado pela contribuição.
precisa saber é o seguinte
2
  1. Se você é um usuário de unidade, aqui está algo para começar: http://www.stevegargolinski.com/armory-a-free-and-unfinished-stat-inventory-and-buffdebuff-framework-for-unity/

Estou usando ScriptableOjects como buffs / spells / talentos

public class Spell : ScriptableObject 
{
    public SpellType SpellType = SpellType.Ability;
    public SpellTargetType SpellTargetType = SpellTargetType.SingleTarget;
    public SpellCategory SpellCategory = SpellCategory.Ability;
    public MagicSchools MagicSchool = MagicSchools.Physical;
    public CharacterClass CharacterClass = CharacterClass.None;
    public string Description = "no description available";
    public SpellDragType DragType = SpellDragType.Active; 
    public bool Active = false;
    public int TargetCount = 1;
    public float CastTime = 0;
    public uint EffectRange = 3;
    public int RequiredLevel = 1;
    public virtual void OnGUI()
    {
    }
}

using UnityEngine; using System.Collections.Generic;

enumeração pública BuffType {Buff, Debuff} [System.Serializable] classe pública BuffStat {public Stat Stat = Stat.Strength; flutuação pública ModValueInPercent = 0.1f; }

public class Buff : Spell
{
    public BuffType BuffType = BuffType.Buff;
    public BuffStat[] ModStats;
    public bool PersistsThroughDeath = false;
    public int AmountPerTick = 3;
    public bool UseTickTimer = false;
    public float TickTime = 1.5f;
    [HideInInspector]
    public float Ticktimer = 0;
    public float Duration = 360; // in seconds
    public float ModifierPerStack = 1.1f;
    [HideInInspector]
    public float Timer = 0;
    public int Stack = 1;
    public int MaxStack = 1;
}

BuffModul:

using System;
using RPGCore;
using UnityEngine;

public class Buff_Modul : MonoBehaviour
{
    private Unit _unit;

    // Use this for initialization
    private void Awake()
    {
        _unit = GetComponent<Unit>();
    }

    #region BUFF MODUL

    public virtual void RUN_BUFF_MODUL()
    {
        try
        {
            foreach (var buff in _unit.Attr.Buffs)
            {
                CeckBuff(buff);
            }
        }
        catch(Exception e) {throw new Exception(e.ToString());}
    }

    #endregion BUFF MODUL

    public void ClearBuffs()
    {
        _unit.Attr.Buffs.Clear();
    }

    public void AddBuff(string buffName)
    {
        var buff = Instantiate(Resources.Load("Scriptable/Buff/" + buffName, typeof(Buff))) as Buff;
        if (buff == null) return;
        buff.name = buffName;
        buff.Timer = buff.Duration;
        _unit.Attr.Buffs.Add(buff);
        foreach (var buffStat in buff.ModStats)
        {
            switch (buff.BuffType)
            {
                case BuffType.Buff:
                    _unit.Attr.AddBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] + _unit.Attr.StatsItem[buffStat.Stat]) * buffStat.ModValueInPercent));
                    break;
                case BuffType.Debuff:
                    _unit.Attr.RemoveBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] /*+ unit.character.StatsItem[_stat.stat]*/) * buffStat.ModValueInPercent));
                    break;
            }
            Core.StatController(_unit.Attr, buffStat.Stat);
        }
    }

    public void RemoveBuff(Buff buff)
    {
        foreach (var buffStat in buff.ModStats)
        {
            switch (buff.BuffType)
            {
                case BuffType.Buff:
                    _unit.Attr.RemoveBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] + _unit.Attr.StatsItem[buffStat.Stat]) * buffStat.ModValueInPercent));
                    break;
                case BuffType.Debuff:
                    _unit.Attr.AddBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat]  /*+ unit.character.StatsItem[_stat.stat]*/) * buffStat.ModValueInPercent));
                    break;
            }
            Core.StatController(_unit.Attr, buffStat.Stat);
        }
        _unit.Attr.Buffs.Remove(buff);
    }

    void CeckBuff(Buff buff)
    {
        buff.Timer -= Time.deltaTime;
        if (!_unit.IsAlive && !buff.PersistsThroughDeath)
        {
            if (buff.ModStats != null)
                foreach (var stat in buff.ModStats)
                {
                    _unit.Attr.StatsBuff[stat.Stat] = 0;
                }

            RemoveBuff(buff);
        }
        if (_unit.IsAlive && buff.Timer <= 0)
        {
            RemoveBuff(buff);
        }
    }
}
user22475
fonte
0

Esta foi uma pergunta real para mim. Eu tenho uma ideia sobre isso.

  1. Como dito anteriormente, precisamos implementar uma Bufflista e um atualizador lógico para os buffs.
  2. Precisamos, então, alterar todas as configurações específicas de cada quadro em cada subclasse do Buff classe.
  3. Em seguida, obtemos as configurações atuais do player no campo de configurações alteráveis.

class Player {
  settings: AllPlayerStats;

  private buffs: Array<Buff> = [];
  private baseSettings: AllPlayerStats;

  constructor(settings: AllPlayerStats) {
    this.baseSettings = settings;
    this.resetSettings();
  }

  addBuff(buff: Buff): void {
    this.buffs.push(buff);
    buff.start(this);
  }

  findBuff(predcate(buff: Buff) => boolean): Buff {...}

  removeBuff(buff: Buff): void {...}

  update(dt: number): void {
    this.resetSettings();
    this.buffs.forEach((item) => item.update(dt));
  }

  private resetSettings(): void {
    //some way to copy base to settings
    this.settings = this.baseSettings.copy();
  }
}

class Buff {
    private owner: Player;        

    start(owner: Player) { this.owner = owner; }

    update(dt: number): void {
      //here we change anything we want in subclasses like
      this.owner.settings.hp += 15;
      //if we need base value, just make owner.baseSettings public but don't change it! only read

      //also here logic for removal buff by time or something
    }
}

Dessa forma, pode ser fácil adicionar novas estatísticas de jogadores, sem alterar a lógica das Buffsubclasses.

DantaliaN
fonte
0

Eu sei que isso é bastante antigo, mas foi vinculado em um post mais recente e tenho algumas ideias sobre o assunto que gostaria de compartilhar. Infelizmente, não tenho minhas anotações comigo no momento, então tentarei dar uma visão geral do que estou falando e editarei nos detalhes e em alguns exemplos de código quando tiver na frente. mim.

Primeiro, acho que, de uma perspectiva de design, a maioria das pessoas está muito interessada em saber que tipos de buffs podem ser criados e como eles são aplicados, além de esquecer os princípios básicos da programação orientada a objetos.

O que eu quero dizer? Realmente não importa se algo é um buff ou um debuff, ambos são modificadores que apenas afetam algo de maneira positiva ou negativa. O código não se importa com qual é qual. Na verdade, não importa se algo está adicionando estatísticas ou multiplicando-as, esses são apenas operadores diferentes e, novamente, o código não se importa com qual é.

Então, onde eu vou com isso? Que projetar uma boa classe de buff / debuff (leia-se: simples, elegante) não é tão difícil, o difícil é projetar os sistemas que calculam e mantêm o estado do jogo.

Se eu estivesse projetando um sistema de buff / debuff, aqui estão algumas coisas que eu consideraria:

  • Uma classe buff / debuff para representar o efeito em si.
  • Uma classe do tipo buff / debuff para conter as informações sobre o que o buff afeta e como.
  • Personagens, itens e possivelmente locais precisariam ter uma propriedade de lista ou coleção para conter buffs e debuffs.

Algumas especificidades para quais tipos de buff / debuff devem conter:

  • Quem / o que pode ser aplicado, IE: jogador, monstro, localização, item, etc.
  • Que tipo de efeito é (positivo, negativo), se é multiplicativo ou aditivo, e que tipo de status afeta, IE: ataque, defesa, movimento, etc.
  • Quando deve ser verificado (combate, hora do dia, etc).
  • Se pode ser removido e, em caso afirmativo, como pode ser removido.

Isso é apenas um começo, mas a partir daí você está apenas definindo o que deseja e agindo usando o seu estado normal de jogo. Por exemplo, digamos que você queira criar um item amaldiçoado que reduz a velocidade de movimento ...

Desde que eu coloque os tipos adequados, é simples criar um registro de buff que diz:

  • Tipo: Maldição
  • ObjectType: Item
  • StatCategory: Utilitário
  • StatAffected: MovementSpeed
  • Duração: Infinito
  • Gatilho: OnEquip

E assim por diante, e quando eu crio um buff, atribuo a ele o BuffType of Curse e tudo depende do mecanismo ...

Aithos
fonte