Ачивмент — это такая прямоугольна медалька, которой награждается пользователь за выполнение каких-то действий. В Linderdaum Puzzle таких медалек около сотни. Вот пример, как это выглядит в UI:
Несколько мыслей:
- С каждой ачивкой связана либо последовательность действий (собрать сколько-то картинок, провести сколько-то времени и т.п), либо событие (собрать картинку быстрее чем за 5 секунд, сходит на фейсбук и т.п.).
- У некоторых ачивок есть поясняющий текст, показывающий текущий прогресс на пути к этой ачивке.
- Бывают секретные ачивки, которые не видны, пока их не получишь.
- Бывают ачивки, недоступные в бесплатной версии игры. 🙂
- Ачивки нужно беречь, чтобы пользователь не потерял свои достижения.
Начинаем кодить. Для начала заводим здоровенный enum, в котором перечислим всё, что у нас есть,
enum LAchievement { LA_SUPPORTER = 0, LA_REVIEWER, LA_MONTHLING, LA_CASUAL, LA_ENTHUSIAST, LA_FANATIC, LA_PUZZLENEWBIE3X3, // ... // много-много таких строк, они все здесь };
В новых версиях игры можно будет свободно добавлять новые ачивки в конец списка. А вот менять порядок в уже зарелизенной версии нельзя. Думаю понятно почему.
Объявим два типа:
typedef bool (*HasAchievementProc)(void); // проверяет, получен ли ачивмент typedef LString (*GetNoteProc)(void); // даёт инфо-текст, например, "собрано 99 картинок"
Для определения, секретный ачивмент или нет, определим вот такой тип:
enum AchievementVisibility { L_VIS, L_HID, };
Понятно, что можно было обойтись просто bool-ом (и так и было в самом начале), но в процессе разработки от bool-a отказались, потому что при заполнении констант в таблице ачивок от различных булов стало рябить в глазах.
Описание одного ачивмента в конечном итоге стало выглядить вот как:
struct sAchievement { int FID; // закастованный LAchievement bool FPaidVersion; // доступен только в платной версии? const char* FName; // текстовое название, которое видит пользователь const char* FDescription; // описание, которое тоже видит пользователь HasAchievementProc FProc; AchievementVisibility FHidden; const char* FProgressNote; // шаблон строка с подсказкой, например "%s solved" GetNoteProc FNoteProc; bool FShowNoteAfterAwarding; // показывать ли подсказку и после получений этой ачивки // Дальше идут поля, в которых кэшируется всякая полезность. // Чтобы не тратить время на постоянные поиски. // generated at runtime iGUIView* FViewPlate; iGUIView* FViewNote; clCVar* FAwarded; };
Дальше начинается креативная работа придумывания самих ачивок и monkey-работа по заполнению огромной таблицы из элементов sAchievement. Это сердце всей нашей системы достижений. Вот несколько строк из неё:
static sAchievement Achievements[] = { { LA_SUPPORTER, false, "Supporter", "Purchased Linderdaum Puzzle HD", &Check_Supporter, L_VIS, NULL }, { LA_REVIEWER, false, "Reviewer", "Added a review on Google Play", &Check_Reviewer, L_VIS, NULL }, { LA_MONTHLING, false, "Month's campaign", "Used the game for one month", &Check_Monthling, L_VIS, "%s days", &Get_DaysSinceFirstUse, true }, { LA_CASUAL, false, "Casual", "Spent half an hour in game", &Check_Casual, L_VIS, "%s minutes", &Get_MinutesInGame, false }, { LA_ENTHUSIAST, false, "Enthusiast", "Spent 2 hours in game", &Check_Enthusiast, L_VIS, "%s minutes", &Get_MinutesInGame, false }, { LA_FANATIC, true, "Fanatic", "Spent 10 hours in game", &Check_Fanatic, L_VIS, "%s hours", &Get_HoursInGame, false }, // ... // много-много таких строк, они все здесь }
Функции Check_* выполняют проверку условий для получения ачивок типа «последовательность действий». Типичное содержаение такой функции:
bool Check_Monthling() { LDate FirstRun = LDate( FirstRunDate.GetString() ); LDate Today; int Days = Today-FirstRun; return Days >= 30; }
Стоит обратить внимание, что для ачивок типа «одиночное событие» такие функции не нужны и в таблице для них стоит NULL. Постановка таких ачивок в очередь на награждение осуществляется прямо в игровом коде:
if ( Time < 5.0 ) g_Achievements->Award( LA_BLINKOFANEYE );
Ещё вы наверняка заметили, что есть FProgressNote и FNoteProc. Почему нельзя было обойтись только одной FNoteProc и возвращать из неё сразу фразу? Всё просто. Для того, чтобы сделать локализацию фразы на текущий язык. Шаблон локализуется, а потом в него подставляется строка-число, которая возвращается из FNoteProc.
Теперь всё готово, чтобы вдохнуть жизнь в статичные данные. Для этого нужно ещё чуток попрограммировать. Нам нужен менеджер ачивментов и менеджер UI для ачивментов. Давайте разберёмся, что они делают.
class clAchievementsManager: public iObject { public: // тут немного поскипано // // clAchievementsManager // /// trigger the award for a one-time achievement virtual void Award( LAchievement Achievement ); virtual void AwardName( const LString& AchievementName ); virtual bool IsAwarded( LAchievement Achievement ) const; /// called automatically every 6 seconds or so to check new achievements virtual void ProcessAchievements(); virtual void RecheckAchievements(); // тут ещё немного поскипано - код для сохранения ачивок public: std::deque<LAchievement> FPendingAwards; iGUIView* FAchievementsText; mlNode* FNode_Awarded; };
ProcessAchievements() вызывается раз в 6 секунд и раздаёт слонов медальки. Достигается это вот таким вызовом:
Env->SendAsyncCapsule( BindCapsule( &clAchievementsManager::ProcessAchievements, this ), 6.0 );
Внутри примерно вот такой код (немного поскипано):
void clAchievementsManager::ProcessAchievements() { // save gamestate // ... RecheckAchievements(); // check achievements once in a while Env->SendAsyncCapsule( BindCapsule( &clAchievementsManager::ProcessAchievements, this ), 6.0 ); // nothing new to award if ( FPendingAwards.empty() ) return; LAchievement A = FPendingAwards.front(); FPendingAwards.pop_front(); // this achievement had been awarded long time ago if ( Achievements[ A ].FAwarded->GetBool() ) return; Achievements[ A ].FAwarded->SetBool( true ); // don't lose achievements in case of crash g_Game->SaveAchievements( g_SaveAchievementsFileName ); // show nice message here Env->Renderer->GetCanvas()->AnnounceObject( Construct<clAchievementAnnouncer>( Env, A, FNode_Awarded ), 0.0, 5.0 ); clPuzzl_AchievementsContainer* C = Env->GUI->FindView<clPuzzl_AchievementsContainer>("AchievementsContainer"); // update UI if ( C ) C->RecreateSubViews(); }
Ничего сложного. Просто проверка условий и раздача ачивок типа «событие» из очереди, в которую из ставит метод Award(). Класс clAchievementAnnouncer рисует красивую табличку поверх всего UI, наподобии вот такой:
Обращу внимание, что игра сохраняется тоже раз в 6 секунд — мы не хотим, чтобы пользователь потерял свой прогресс.
Метод RecheckAchievements() обновляет UI с таблицей всех ачивок, который был на первом скриншоте. Непосредственно управлением UI занимается класс clPuzzl_AchievementsContainer, который будет очень специфичен в зависимости от вашей системы UI. У нас он просто заполняет плашки с кубками (опять см. первой скриншот).
Postmortem
Игра релизнута, система ачивок работает хорошо. У нас есть возможность трэкать статистику авивок через Flurry и наблюдать сколько и каких ачивок получено. Это помогает оттачивать баланс. Для более сложной игры помощь от такого фидбэка будет сложно переоценить.
Из того, что хотелось сделать, но пока не успели:
- Повысить виральность ачивок, путём вывода сообщения в Твиттер пользователя. Например, как это делает Osmos, FourSquare.
- Сохранять ачивки не локально, а в облаке в аккаунте пользователя. Здесь стоит попробовать Google App Engine или какие-то подобные сервисы. Туда же можно сохранять и состояние игры. Это особенно важно при сборке паззлов большого размера, когда на одну картинку можно потратить пару часов.
P.S. Игра сделана на движке Linderdaum Engine.
ссылка на оригинал статьи http://habrahabr.ru/post/161949/
Добавить комментарий