Система достижений (achievements) в Linderdaum Puzzle

от автора

Не так давно на Хабре поднимался вопрос о проектировании системы достижений для игры. В комментах шло бурное и плодотворное обсуждение различных вариантов. Тогда мы уже тестировали нашу игру, готовились к релизу и поучаствовать в дебатах я не смог. Но увидев топик сразу же подумал: «У нас же есть как раз такая работающая система. Почему бы о ней не рассказать?». Подумал и записал в todo-list. Сегодня настало время рассказать, как это работает в нашем игровом проекте Linderdaum Puzzle.

Ачивмент — это такая прямоугольна медалька, которой награждается пользователь за выполнение каких-то действий. В Linderdaum Puzzle таких медалек около сотни. Вот пример, как это выглядит в UI:

image

Несколько мыслей:

  • С каждой ачивкой связана либо последовательность действий (собрать сколько-то картинок, провести сколько-то времени и т.п), либо событие (собрать картинку быстрее чем за 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, наподобии вот такой:

image

Обращу внимание, что игра сохраняется тоже раз в 6 секунд — мы не хотим, чтобы пользователь потерял свой прогресс.

Метод RecheckAchievements() обновляет UI с таблицей всех ачивок, который был на первом скриншоте. Непосредственно управлением UI занимается класс clPuzzl_AchievementsContainer, который будет очень специфичен в зависимости от вашей системы UI. У нас он просто заполняет плашки с кубками (опять см. первой скриншот).

Postmortem

Игра релизнута, система ачивок работает хорошо. У нас есть возможность трэкать статистику авивок через Flurry и наблюдать сколько и каких ачивок получено. Это помогает оттачивать баланс. Для более сложной игры помощь от такого фидбэка будет сложно переоценить.

Из того, что хотелось сделать, но пока не успели:

  • Повысить виральность ачивок, путём вывода сообщения в Твиттер пользователя. Например, как это делает Osmos, FourSquare.
  • Сохранять ачивки не локально, а в облаке в аккаунте пользователя. Здесь стоит попробовать Google App Engine или какие-то подобные сервисы. Туда же можно сохранять и состояние игры. Это особенно важно при сборке паззлов большого размера, когда на одну картинку можно потратить пару часов.

P.S. Игра сделана на движке Linderdaum Engine.

ссылка на оригинал статьи http://habrahabr.ru/post/161949/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *