📜 ⬆️ ⬇️

Achievement system (achievements) in Linderdaum Puzzle

Not so long ago, Habré raised the issue of designing a system of achievements for the game. In the comments there was a stormy and fruitful discussion of various options. Then we have already tested our game, prepared for release and I could not participate in the debate. But when I saw the topic I immediately thought: “We also have just such a working system. Why not talk about it? ". I thought and wrote to the todo-list. Today it’s time to tell how it works in our game project Linderdaum Puzzle .



Achievement is such a rectangular medal that awards the user for performing some actions. In the Linderdaum Puzzle there are about a hundred of such medals. Here is an example of how it looks in UI:
')
image

A few thoughts:



Begin to code. For starters, we start a hefty enum, in which we list everything that we have,

enum LAchievement { LA_SUPPORTER = 0, LA_REVIEWER, LA_MONTHLING, LA_CASUAL, LA_ENTHUSIAST, LA_FANATIC, LA_PUZZLENEWBIE3X3, // ... // -  ,    }; 


In new versions of the game, you can freely add new achievements to the end of the list. But to change the order in the already unreleased version is impossible. I think it is clear why.

Declare two types:

 typedef bool (*HasAchievementProc)(void); // ,    typedef LString (*GetNoteProc)(void); //  -, , " 99 " 


To determine the secret achievement or not, we define this type:

 enum AchievementVisibility { L_VIS, L_HID, }; 


It is clear that it was possible to get along with just a bool (and it was right at the beginning), but during the development process they refused bool-a, because when filling in the constants in the table, the different bulls started to ruffle in the eyes.

The description of one Achievement ended up looking like this:

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


Then begins the creative work of inventing themselves achivok and monkey-work on filling a huge table of elements of sAchievement. This is the heart of our entire system of achievement. Here are a few lines from it:

 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 }, // ... // -  ,    } 


The Check_ * functions perform conditional checks for receiving actions of the “sequence of actions” type. Typical content of such a function:

 bool Check_Monthling() { LDate FirstRun = LDate( FirstRunDate.GetString() ); LDate Today; int Days = Today-FirstRun; return Days >= 30; } 


It is worth noting that for single event type events, such functions are not needed and the table for them is NULL. Placing such achivok in the queue for rewarding is carried out directly in the game code:

 if ( Time < 5.0 ) g_Achievements->Award( LA_BLINKOFANEYE ); 


You also probably noticed that there is FProgressNote and FNoteProc . Why it was impossible to manage only one FNoteProc and return a phrase from it right away? It's simple. In order to make localization of the phrase to the current language. The template is localized, and then a string number is inserted into it, which is returned from FNoteProc .

Now everything is ready to breathe life into static data. For this you need to program a bit more. We need an Achievement Manager and a UI Achievement Manager. Let's see what they do.

 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 () is called every 6 seconds and distributes the elephant medals. This is achieved by such a challenge:

 Env->SendAsyncCapsule( BindCapsule( &clAchievementsManager::ProcessAchievements, this ), 6.0 ); 


Inside there is something like this code (a bit jumped):

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


Nothing complicated. Just checking the conditions and distributing events like “event” from the queue, in which the Award () method sets. The clAchievementAnnouncer class draws a beautiful tablet on top of the entire UI, like this:

image

Please note that the game is also saved once every 6 seconds - we do not want the user to lose his progress.

The RecheckAchievements () method updates the UI with a table of all the tables that was in the first screenshot. The UP class is managed directly by the clPuzzl_AchievementsContainer class, which will be very specific depending on your UI system. Here he simply fills the dice with cups (again, see the first screenshot).

Postmortem


The game is released, the system achivok works well. We have the ability to track statistics of avivok through Flurry and see how much and what kind of achievements are received. It helps to hone the balance. For a more difficult game, the help from such a feedback will be difficult to overestimate.

From what I wanted to do, but have not had time yet:



PS The game is made on the engine Linderdaum Engine .

Source: https://habr.com/ru/post/161949/


All Articles