Como posso configurar uma estrutura flexível para lidar com conquistas?

54

Especificamente, qual é a melhor maneira de implementar um sistema de conquistas flexível o suficiente para ir além de conquistas simples baseadas em estatísticas, como "matar x inimigos".

Estou procurando algo mais robusto do que um sistema baseado em estatísticas e algo mais organizado e sustentável do que "codificar todos eles como condições". Alguns exemplos que são impossíveis ou difíceis de usar em um sistema baseado em estatísticas: "Corte uma melancia após um morango", "Desça um cano enquanto for invencível", etc.

lti
fonte

Respostas:

39

Eu acho que um tipo de solução robusta seria seguir o caminho orientado a objetos.

Dependendo do tipo de conquista que você deseja apoiar, você precisa de uma maneira de consultar o estado atual do seu jogo e / ou o histórico de ações / eventos que os objetos do jogo (como o jogador) fizeram.

Digamos que você tenha uma classe base de Conquistas, como:

class AbstractAchievement
{
    GameState& gameState;
    virtual bool IsEarned() = 0;
    virtual string GetName() = 0;
};

AbstractAchievementmantém uma referência ao estado do jogo. É usado para consultar as coisas que estão acontecendo.

Então você faz implementações concretas. Vamos usar seus exemplos:

class MasterSlicerAchievement : public AbstractAchievement
{
    string GetName() { return "Master Slicer"; }
    bool IsEarned()
    {
        Action lastAction = gameState.GetPlayerActionHistory().GetAction(0);
        Action previousAction = gameState.GetPlayerActionHistory().GetAction(1);
        if (lastAction.GetType() == ActionType::Slice &&
            previousAction.GetType() == ActionType::Slice &&
            lastAction.GetObjectType() == ObjectType::Watermelon &&
            previousAction.GetObjectType() == ObjectType::Strawberry)
            return true;
        return false;
    }
};
class InvinciblePipeRiderAchievement : public AbstractAchievement
{
    string GetName() { return "Invincible Pipe Rider"; }
    bool IsEarned()
    {
        if (gameState.GetLocationType(gameState.GetPlayerPosition()) == LocationType::OVER_PIPE &&
            gameState.GetPlayerState() == EntityState::INVINCIBLE)
            return true;
        return false;
    }
};

Cabe a você decidir quando verificar usando o IsEarned()método Você pode verificar a cada atualização do jogo.

Uma maneira mais eficiente seria, por exemplo, ter algum tipo de gerente de eventos. E, em seguida, registre eventos (como PlayerHasSlicedSomethingEventou PlayerGotInvicibleEventou simplesmente PlayerStateChanged) em um método que levaria a conquista no parâmetro Exemplo:

class Game
{
    void Initialize()
    {
        eventManager.RegisterAchievementCheckByActionType(ActionType::Slice, masterSlicerAchievement);
        // Each time an action of type Slice happens,
        // the CheckAchievement() method is invoked with masterSlicerAchievement as parameter.
        eventManager.RegisterAchievementCheckByPlayerState(EntityState::INVINCIBLE, invinciblePiperAchievement);
        // Each time the player gets the INVINCIBLE state,
        // the CheckAchievement() method is invoked with invinciblePipeRiderAchievement as parameter.
    }
    void CheckAchievement(const AbstractAchievement& achievement)
    {
        if (!HasAchievement(player, achievement) && achievement.IsEarned())
        {
            AddAchievement(player, achievement);
        }
    }
};
Splo
fonte
13
estilo nitpick: if(...) return true; else return false;é o mesmo quereturn (...)
BlueRaja - Danny Pflughoeft
2
Esse é um modelo muito bom para implementar um sistema de conquista. Além disso, ele ainda demonstra a ideia de que você precisa ter uma maneira de acompanhar os estados dos jogos. Acho essa provavelmente a ideia mais complicada.
Bryan Harrington
@ Spio você é um mestre homens ...! : D Solução simples e elegante. Parabéns.
Diego Palomar
+1 Acho que um sistema de emissão / coleta de eventos é uma ótima maneira de lidar com esse problema.
ashes999
14

Bem, em resumo, as conquistas são desbloqueadas quando uma determinada condição é atendida. Portanto, você precisa ser capaz de produzir instruções if para verificar a condição desejada.

Por exemplo, se você quiser saber que um nível foi concluído ou que um chefe foi derrotado, seria necessário que a bandeira booleana se tornasse verdadeira quando esses eventos acontecessem.

Então:

if(GlobalFlags.MasterBossDefeated == true && AchievementClass.MasterBossDefeatedAchievement == false)
{
    AchievementClass.MasterBossDefeatedAchievement = true;
    showModalPopUp("You defeated the Master Boss!  30 gamerscore");
}

Você pode tornar isso o mais complexo ou simplista possível, para corresponder à condição desejada.

Algumas informações sobre as conquistas do Xbox 360 podem ser encontradas aqui .

Bryan Denny
fonte
2
+1 Excelente artigo, e essencialmente o que eu ia sugerir. Embora eu faça com que o Modal obtenha seu texto da própria conquista ... apenas para evitar a busca de texto, se você quiser alterar alguma coisa.
Jesse Dorsey
@ Noctrine - não se esqueça de qualquer código postado aqui deve ser tratado como pseudo-código - geralmente é necessário usar código simplificado para entender o ponto.
21410 ChrisF
11
O link de conquistas do Xbox 360 está morto.
hangy
8

E se todas as ações que o jogador executar postar uma mensagem no AchievementManager? Em seguida, o gerente pode verificar internamente se determinadas condições foram atendidas. Os primeiros objetos postam mensagens:

AchievementManager::PostMessage("Jump", "162");

AchievementManager::PostMessage("Slice", "Strawberry");
AchievementManager::PostMessage("Slice", "Watermelon");

AchievementManager::PostMessage("Kill", "Goomba");

E então AchievementManagerverifica se ele precisa fazer alguma coisa:

if (!strcmp(m_Message.name, "Slice") && !strcmp(m_LastMessage.name, "Slice"))
{
    if (!strcmp(m_Message.value, "Watermelon") && !strcmp(m_LastMessage.value, "Strawberry"))
    {
        // achievement unlocked!
    }
}

Você provavelmente desejará fazer isso com enumerações, em vez de seqüências de caracteres. ;)

knight666
fonte
Isso é parecido com o que eu estava pensando, mas ainda depende de um monte de coisas sendo codificadas em uma função. Deixando de lado a manutenção, pelo menos todas as conquistas devem ser verificadas quanto à elegibilidade todas as vezes através da função.
lti
2
Faz? Você pode colocar os condicionais em um script externo, desde que tenha alguma maneira de rastrear o que aconteceu no jogo.
knight666
6
-1 isso cheira a mau design. Se você quiser chamar o AchivementManager diretamente, basta transformar cada uma dessas "mensagens" em uma função separada. Se você vai usar mensagens, crie um gerenciador de mensagens para que outras classes também possam usá-las (tenho certeza que o Goomba estaria interessado em saber que ele foi morto) e remova esse acoplamento AchievementManagerem todos os classe (que é o que o OP estava perguntando como evitar em primeiro lugar). E use um enum ou classes separadas para suas mensagens, não literais de cadeia - usar literais de cadeia para passar o estado é sempre uma má idéia.
BlueRaja - Danny Pflughoeft
4

O último design que usei foi baseado em ter um conjunto de contadores persistentes por usuário e, em seguida, ter as chaves de realizações de um determinado contador atingindo um determinado valor. A maioria era um único par de conquistas / contadores, onde o contador seria apenas 0 ou 1 (e a conquista foi desencadeada em> = 1), mas você pode usar isso para "caras mortos X" ou "baús X encontrados" também. Isso também significa que você pode configurar contadores para algo que não possui conquistas e ainda será rastreado para uso futuro.

coderanger
fonte
3

Quando implementei conquistas no meu último jogo, fiz tudo baseado em estatística. As conquistas são desbloqueadas quando nossas estatísticas atingem um determinado valor. Considere Modern Warfare 2: o jogo está acompanhando toneladas de estatísticas! Quantas fotos você tirou com a SCAR-H? Quantas milhas você correu enquanto usava o benefício Lightweight?

Então, na minha implementação, eu simplesmente criei um mecanismo de estatísticas e, em seguida, criei um gerenciador de conquistas que executa consultas muito simples para verificar o status das conquistas ao longo do jogo.

Embora minha implementação seja bastante simplista, ela faz o trabalho. Eu escrevi sobre isso e compartilhei minhas perguntas aqui .

Reed Olsen
fonte
2

Use Cálculo de Eventos . Em seguida, faça algumas pré-condições e ações que são aplicadas após as pré-condições serem atendidas:

  • pré-condições: você matou 1000 inimigos, você tem 2 pernas
  • ações: me dê pirulito, me dê super-duper-espingarda-13
  • ações pela primeira vez: diga "você é tão incrível!"

Use-o como (não otimizado para velocidade!):

  • Armazene todas as estatísticas.
  • Estatísticas de consulta para pré-condições.
  • Aplique ações.
  • Aplique ações únicas uma vez.

Se você deseja torná-lo rápido:

  • Armazene em cache o que quiser, armazene partes dele em algumas árvores, hashes ...
  • Faça alterações incrementais para que você não aplique todas as ações o tempo todo, mas estas são novas ...)

Nota

É difícil dar os melhores conselhos, pois todas as coisas têm prós e contras.

  • "Qual é a melhor estrutura de dados?" implica "Quais operações você deseja fazer mais com isso? Pesquisando, removendo, adicionando ..."
  • Você basicamente pensa nessas propriedades: facilidade de codificação, velocidade, clareza, tamanho ...
user712092
fonte
0

O que há de errado com as verificações de IF após a ocorrência do evento de conquista?

if (Cinimatics.done)
   Achievement.get(CINIMATICS_SEEN);

if (EnemiesKiled > 200)
   Achievement.get(KILLER);

if (Damage > 2000 && TimeSinceFirstDamage < 2000)
   Achievement.get(MEAT_SHIELD);

InvitationAccepted = Invite.send(BestFriend);
if (InvitationAccepted)
   Achievement.get(A_FRIEND_IN_NEED);
MrValdez
fonte
3
'Estou procurando algo mais organizado e sustentável do que "codificá-los todos como condições". ' Embora este seja definitivamente um bom método do KISS para um jogo pequeno.
O pato comunista