Как я могу настроить гибкую структуру для обработки достижений?

54

В частности, каков наилучший способ реализовать систему достижений, достаточно гибкую, чтобы справиться с выходом за рамки простых достижений, основанных на статистике, таких как «убить х врагов».

Я ищу что-то более надежное, чем система, основанная на статистике, и что-то более организованное и обслуживаемое, чем «жестко их кодировать как условия». Некоторые примеры, которые невозможны или громоздки в системе, основанной на статистике: «Нарежьте арбуз за клубникой», «Идите по трубе, пока неуязвимы» и т. Д.

LTI
источник

Ответы:

39

Я думаю, что одним из надежных решений было бы пойти по объектно-ориентированному пути.

В зависимости от того, какое достижение вы хотите поддержать, вам нужен способ запросить текущее состояние вашей игры и / или историю действий / событий, совершенных игровыми объектами (например, игроком).

Допустим, у вас есть базовый класс достижения, такой как:

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

AbstractAchievementсодержит ссылку на состояние игры. Он используется для запроса того, что происходит.

Затем вы делаете конкретные реализации. Давайте использовать ваши примеры:

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

Тогда вам решать, когда проверять, используя IsEarned()метод. Вы можете проверить при каждом обновлении игры.

Например, более эффективным способом было бы иметь какой-то менеджер событий. А затем зарегистрируйте события (например, PlayerHasSlicedSomethingEventили PlayerGotInvicibleEventили просто PlayerStateChanged) для метода, который будет принимать достижение в параметре. Пример:

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
источник
13
Стиль Нитпик: if(...) return true; else return false;такой же, какreturn (...)
BlueRaja - Дэнни Пфлюгофт
2
Это очень хороший шаблон для внедрения системы достижений. Более того, это все еще демонстрирует идею, что у вас должен быть способ отследить игровые состояния. Я считаю, что это, наверное, самая сложная идея.
Брайан Харрингтон
@ Спио, ты мастер ...! : D Простое и элегантное решение. Congrat.
Диего Паломар
+1 Я думаю, что система отправки / сбора событий - отличный способ справиться с этой проблемой.
ashes999
14

Короче говоря, достижения разблокируются, когда выполняется определенное условие. Таким образом, вы должны иметь возможность создавать операторы if для проверки желаемого условия.

Например, если вы хотите знать, что уровень был пройден или босс был побежден, вам нужно, чтобы булев флаг стал истинным, когда происходят эти события.

Затем:

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

Вы можете сделать это настолько сложным или упрощенным, насколько это необходимо, чтобы соответствовать желаемому условию.

Некоторую информацию о достижениях Xbox 360 можно найти здесь .

Брайан Денни
источник
2
+1 Отличная статья, и по сути то, что я собирался предложить. Хотя я хотел бы, чтобы Модал получал свой текст от самого достижения ... просто, чтобы избежать поиска текста, если вы хотите что-то изменить.
Джесси Дорси
@Noctrine - не забывайте, что любой код, размещенный здесь, должен рассматриваться как псевдокод - часто необходимо использовать упрощенный код, чтобы понять суть.
ChrisF
1
Ссылка достижений Xbox 360 мертва.
hangy
8

Что если вы при каждом действии игрока отправляете сообщение на AchievementManager? Затем менеджер может проверить внутренне, были ли выполнены определенные условия. Первые объекты публикуют сообщения:

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

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

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

А затем AchievementManagerпроверяет, нужно ли что-либо делать:

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

Возможно, вы захотите сделать это с помощью перечислений, а не строк. ;)

knight666
источник
Это похоже на то, о чем я думал, но все еще опирается на кучу вещей, жестко закодированных в одной функции. За исключением возможности обслуживания, по крайней мере, каждое достижение должно проверяться на соответствие требованиям каждый раз с помощью функции.
LTI
2
Является ли? Вы можете поместить условные выражения во внешний скрипт, если у вас есть какой-то способ отслеживания того, что произошло в игре.
knight666
6
-1 это пахнет плохим дизайном. Если вы собираетесь вызывать AchivementManager напрямую, просто сделайте каждое из этих «сообщений» отдельной функцией. Если вы собираетесь использовать сообщения, создайте менеджер сообщений, чтобы другие классы также могли использовать сообщения (я уверен, что Гумбе было бы интересно узнать, что он был убит), и удалить эту связь с AchievementManagerкаждым класс (это то, что ОП спрашивал, как избежать в первую очередь). И используйте перечисления или отдельные классы для своих сообщений, а не строковые литералы - использование строковых литералов для передачи состояния всегда плохая идея.
BlueRaja - Дэнни Пфлугхофт
4

Последний дизайн, который я использовал, был основан на наборе постоянных счетчиков для каждого пользователя, а затем на достижении определенного счетчика при достижении определенного значения. Большинство из них представляли собой одну пару достижений / счетчиков, в которой счетчик всегда был бы равен 0 или 1 (и достижение срабатывало при> = 1), но вы можете использовать его и для «убитых Х чуваков» или «найденных Х сундуков». Это также означает, что вы можете настроить счетчики для чего-то, что не имеет достижений, и все равно будет отслеживаться для будущего использования.

coderanger
источник
3

Когда я реализовал достижения в моей последней игре, я сделал все это на основе статистики. Достижения разблокируются, когда наша статистика достигает определенного значения. Рассмотрим Modern Warfare 2: игра отслеживает тонны статистики! Сколько снимков вы сделали со SCAR-H? Сколько миль вы пробежали, используя легкий вес?

Поэтому в своей реализации я просто создал статистический движок, а затем создал менеджер достижений, который выполняет действительно простые запросы для проверки состояния достижений в течение всего игрового процесса.

Хотя моя реализация довольно проста, она выполняет свою работу. Я написал об этом и поделился своими вопросами здесь .

Рид Олсен
источник
2

Используйте исчисление событий . Затем выполните некоторые предварительные условия и действия, которые применяются после выполнения предварительных условий:

  • предпосылки: ты убил 1000 врагов, у тебя 2 ноги
  • действия: дай мне lollypop, дай мне super-duper-shotgun-13
  • первый раз: скажи "ты такой классный!"

Используйте это как (не оптимизировано для скорости!):

  • Храните всю статистику.
  • Статистика запросов для предварительных условий.
  • Применить действия.
  • Применить одноразовые действия один раз.

Если вы хотите сделать это быстро:

  • Кэшируйте все, что захотите, храните части этого в некоторых деревьях, хешах ...
  • Вносите изменения постепенно, чтобы Вы не применяли все действия все время, кроме новых ...)

Запись

Трудно дать лучший совет, так как все вещи имеют свои плюсы и минусы.

  • "Какая структура данных лучше?" подразумевает "Какие операции вы хотите сделать с ним больше всего? Поиск, удаление, добавление ..."
  • Вы в основном думаете об этих свойствах: простота кодирования, скорость, четкость, размер ...
user712092
источник
0

Что не так с проверками IF после события достижения?

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
источник
3
«Я ищу что-то более организованное и обслуживаемое, чем« жестко закодировать их все как условия ». ». Хотя это, безусловно, хороший метод KISS для маленькой игры.
Коммунистическая утка