¿Cómo puedo configurar un marco flexible para manejar los logros?

54

Específicamente, cuál es la mejor manera de implementar un sistema de logros lo suficientemente flexible como para manejar ir más allá de simples logros basados ​​en estadísticas como "matar x enemigos".

Estoy buscando algo más robusto que un sistema basado en estadísticas y algo más organizado y mantenible que "codificarlos a todos como condiciones". Algunos ejemplos que son imposibles o difíciles de manejar en un sistema basado en estadísticas: "Cortar una sandía después de una fresa", "Bajar por una tubería invencible", etc.

lti
fuente

Respuestas:

39

Creo que una solución sólida sería ir orientado a objetos.

Dependiendo del tipo de logro que desee apoyar, necesita una forma de consultar el estado actual de su juego y / o el historial de acciones / eventos que han realizado los objetos del juego (como el jugador).

Digamos que tiene una clase de Logro base como:

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

AbstractAchievementtiene una referencia al estado del juego. Se utiliza para consultar las cosas que están sucediendo.

Luego haces implementaciones concretas. Usemos sus ejemplos:

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

Entonces depende de usted decidir cuándo verificar utilizando el IsEarned()método. Puedes consultar cada actualización del juego.

Una forma más eficiente sería, por ejemplo, tener algún tipo de administrador de eventos. Y luego registre eventos (como PlayerHasSlicedSomethingEvento PlayerGotInvicibleEventsimplemente PlayerStateChanged) en un método que tome el logro en parámetro. Ejemplo:

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
fuente
13
estilo nitpick: if(...) return true; else return false;es lo mismo quereturn (...)
BlueRaja - Danny Pflughoeft
2
Esa es una muy buena plantilla para implementar un sistema de logros. Además, todavía demuestra la idea de que tienes que tener una forma de rastrear los estados de juego. Creo que esa es probablemente la idea más complicada.
Bryan Harrington
@Spio, eres un maestro ... : D Solución simple y elegante. Felicidades
Diego Palomar
+1 Creo que un sistema de emisión / recopilación de eventos es una excelente manera de manejar este problema.
cenizas999
14

Bueno, en resumen, los logros se desbloquean cuando se cumple una determinada condición. Por lo tanto, debe poder generar sentencias if para verificar la condición que desea.

Por ejemplo, si desea saber que un nivel se completó o un jefe fue derrotado, necesitaría que la bandera booleana se volviera verdadera cuando ocurrieran estos eventos.

Entonces:

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

Puede hacer esto tan complejo o simplista como sea necesario para que coincida con la condición que desea.

Puede encontrar información sobre los logros de Xbox 360 aquí .

Bryan Denny
fuente
2
+1 Gran artículo, y esencialmente lo que iba a sugerir. Aunque haría que el modal obtuviera su texto del logro mismo ... solo para evitar la búsqueda de texto si desea cambiar algo.
Jesse Dorsey
@Noctrine: no olvide que cualquier código publicado aquí debe tratarse como un pseudocódigo; a menudo es necesario usar código simplificado para transmitir el punto.
ChrisF
1
El enlace de logros de Xbox 360 está muerto.
Hangy
8

¿Qué pasa si cada acción que el jugador realiza publica un mensaje en el AchievementManager? Luego, el gerente puede verificar internamente si se han cumplido ciertas condiciones. Los primeros objetos publican mensajes:

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

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

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

Y luego AchievementManagercomprueba si necesita hacer algo:

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

Sin embargo, es probable que desee hacer esto con enumeraciones en lugar de cadenas. ;)

caballero666
fuente
Esto está en la línea de lo que estaba pensando, pero aún se basa en un montón de cosas codificadas en una función. Dejando a un lado la mantenibilidad, al menos cada logro debe ser verificado para su elegibilidad cada vez a través de la función.
lti
2
¿Lo hace? Puedes poner los condicionales en un script externo, siempre que tengas alguna forma de rastrear lo que sucedió en el juego.
knight666
66
-1 esto apesta a mal diseño. Si va a llamar directamente al AchivementManager, simplemente haga que cada uno de esos "mensajes" sea una función separada. Si va a usar mensajes, cree un administrador de mensajes para que otras clases también puedan usar los mensajes (estoy seguro de que el Goomba estaría interesado en saber que ha sido asesinado) y eliminar ese acoplamiento AchievementManageren cada clase (que es lo que OP preguntaba cómo evitar en primer lugar). Y use una enumeración o clases separadas para sus mensajes, no literales de cadena; usar literales de cadena para pasar el estado siempre es una mala idea.
BlueRaja - Danny Pflughoeft
4

El último diseño que utilicé se basó en tener un conjunto de contadores persistentes por usuario y luego hacer que los logros eliminen cierto contador que alcanza un cierto valor. La mayoría eran un solo par de logro / contador donde el contador solo sería 0 o 1 (y el logro se activaba en> = 1), pero también puedes usar esto para "tipos X muertos" o "cofres X encontrados". También significa que puede configurar contadores para algo que no tiene logros, y aún se rastreará para su uso futuro.

coderanger
fuente
3

Cuando implementé los logros en mi último juego, lo hice todo basado en estadísticas. Los logros se desbloquean cuando nuestras estadísticas alcanzan cierto valor. Considera Modern Warfare 2: ¡el juego rastrea toneladas de estadísticas! ¿Cuántas tomas has tomado con el SCAR-H? ¿Cuántas millas has corrido mientras usas el beneficio Lightweight?

Entonces, en mi implementación, simplemente creé un motor de estadísticas, luego creé un administrador de logros que ejecuta consultas realmente simples para verificar el estado de los logros durante el juego.

Si bien mi implementación es bastante simplista, hace el trabajo. Escribí sobre eso y compartí mis consultas aquí .

Reed Olsen
fuente
2

Usar cálculo de eventos . Luego, haga algunas condiciones previas y acciones que se aplican después de que se cumplan las condiciones previas:

  • condiciones previas: mataste a 1000 enemigos, tienes 2 piernas
  • acciones: dame chupetín, dame super-duper-shotgun-13
  • acciones por primera vez: di "¡eres increíble!"

Úselo como (¡no optimizado para la velocidad!):

  • Almacenar todas las estadísticas.
  • Consulta de estadísticas para condiciones previas.
  • Aplicar acciones.
  • Aplicar acciones únicas una vez.

Si quieres hacerlo rápido:

  • Guarda en caché lo que quieras, guarda partes de él en algunos árboles, hashes ...
  • Haga cambios incrementales para que no aplique todas las acciones todo el tiempo, pero estas son nuevas ...)

Nota

Es difícil dar el mejor consejo ya que todas las cosas tienen pros y contras.

  • "¿Cuál es la mejor estructura de datos?" implica "¿Qué operaciones quieres hacer con él más? Buscar, eliminar, agregar ..."
  • Básicamente piensa en estas propiedades: facilidad de codificación, velocidad, claridad, tamaño ...
usuario712092
fuente
0

¿Qué tiene de malo una verificación IF después de que ocurra el evento de logro?

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);
Mr Valdez
fuente
3
Estoy buscando algo más organizado y fácil de mantener que "codificarlos todos como condiciones". '. Aunque este es definitivamente un buen método KISS para un juego pequeño.
El pato comunista