Як я можу створити гнучку основу для обробки досягнень?


54

Зокрема, що є найкращим способом впровадження системи досягнень, достатньо гнучкою для того, щоб виходити за рамки простих статистичних досягнень, таких як "вбити ворогів x ".

Я шукаю щось більш надійне, ніж система, що базується на статистиці, і щось більш організоване та підтримуване, ніж "жорсткий код їх усіх як умов". Деякі приклади, які неможливі або непрості в системі, що базується на статистиці: "Наріжте кавун після полуниці", "Спустіться в трубу, непереможний" тощо.

Відповіді:


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

13
style nitpick: if(...) return true; else return false;такий же, якreturn (...)
BlueRaja - Danny Pflughoeft

2
Це дуже хороший шаблон для впровадження системи досягнення. Більше того, це все ще демонструє ідею, що ви повинні мати спосіб відстежувати ігрові стани. Я вважаю це, мабуть, найскладнішою ідеєю.
Брайан Харрінгтон

@Spio ви майстри-чоловіки ...! : D Просте і елегантне рішення. Конграт.
Дієго Паломар

+1 Я думаю, що система передачі / збирання подій - це відмінний спосіб вирішити цю проблему.
ashes999

14

Ну, коротше кажучи, досягнення розблоковуються, коли виконується певна умова. Таким чином, ви повинні мати можливість виробляти, якщо заяви перевірити стан, який ви хочете.

Наприклад, якщо ви хочете знати, що рівень був завершений або бос був розбитий, вам потрібно буде мати булевий прапор, коли ці події відбудуться.

Тоді:

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 мертве.
ханний


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

Ви, мабуть, захочете це зробити за допомогою перерахунків замість рядків. ;)


Це узгоджується з тим, про що я думав, але все ще покладається на купу речей, які жорстко кодуються в одній функції. Убік ремонтопридатності, щонайменше, кожне досягнення має перевірятися на придатність щоразу через функцію.
lti

2
Робить це? Ви можете помістити умовні умови у зовнішній скрипт, якщо у вас є якийсь спосіб відстеження того, що сталося в грі.
knight666

6
-1 це нагадує поганий дизайн. Якщо ви збираєтеся зателефонувати безпосередньо до AchivementManager, просто зробіть кожне з цих "повідомлень" окремою функцією. Якщо ви збираєтеся використовувати повідомлення, створіть менеджер повідомлень, щоб інші класи також могли використовувати його (я впевнений, що Goomba був би зацікавлений у тому, щоб він знав, що його вбили), а також видалити цю зв'язок на AchievementManagerкожен клас (який ОП запитував, як уникнути в першу чергу). І використовуйте enum або окремі класи для своїх повідомлень, а не рядкових літералів - використання рядкових літералів для передачі стану завжди погана ідея.
BlueRaja - Danny Pflughoeft

4

Останній дизайн, який я використав, ґрунтувався на тому, щоб мати набір стійких лічильників на кожного користувача, а потім мати досягнення, відмінні від певного лічильника, що вражає певне значення. Більшість - це одна пара досягнень / лічильників, коли лічильник колись колись дорівнюватиме 0 або 1 (а досягнення запускається на> = 1), але ви можете використовувати це і для "вбитих хлопців X" або "знайдених X скринь". Це також означає, що ви можете налаштувати лічильники на те, що не має досягнень, і це все ще буде відстежено для подальшого використання.


3

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

Тому в моїй реалізації я просто створив механізм статистики, потім створив менеджер досягнень, який виконує дійсно прості запити, щоб перевірити стан досягнень у всьому ігровому процесі.

Хоча моя реалізація є досить спрощеною, вона робить роботу. Я написав про це і поділився своїми запитами тут .


2

Використовуйте обчислення подій . Потім зробіть деякі передумови та дії, які застосовуються після виконання попередніх умов:

  • передумови: ти вбив 1000 ворогів, у тебе 2 ноги
  • дії: дай мені льодяник, дай мені супер-пупер-рушницю-13
  • перші дії: скажіть "ви такі приголомшливі!"

Використовуйте його так (неоптимізовано для швидкості!):

  • Зберігати всю статистику.
  • Статистика запитів щодо передумов.
  • Застосовуйте дії.
  • Застосовуйте разові дії один раз.

Якщо ви хочете зробити це швидко:

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

Примітка

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

  • "Яка найкраща структура даних?" мається на увазі "Які операції ви хочете зробити з цим найбільше? Пошук, видалення, додавання ..."
  • Ви в основному думаєте про ці властивості: простота кодування, швидкість, чіткість, розмір ...

0

Що не так у проведенні перевірки ІФ після події досягнення?

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

3
"Я шукаю щось більш організоване та підтримуване, ніж" жорсткий код їх усіх як умов ". '. Хоча це, безумовно, хороший метод KISS для невеликої гри.
Качка комуніста
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.