Linderdaumパズルの達成システム

少し前に、ゲームの達成システムの設計に関する質問がHabréで提起されました。 コメントには、さまざまなオプションの嵐のような実り多い議論がありました。 それから私たちはすでにゲームをテストし、リリースの準備ができていたので、議論に参加できませんでした。 しかし、彼はそのトピックを見たとき、すぐに考えました。 彼女について話してみませんか?」 考えて、todoリストに書いた。 今日は、 Linderdaum Puzzleゲームプロジェクトでそれがどのように機能するかを伝える時です。







達成はこのような長方形のメダルであり、ユーザーは何らかのアクションを実行することで表彰されます。 Linderdaumパズルには、このようなメダルが約100個あります。 UIでの表示例を次に示します。



画像



いくつかの考え:







コーディングを開始します。 開始するには、多額の列挙を開始します。この列挙には、所有しているすべてのものがリストされています。



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







ゲームの新しいバージョンでは、リストの最後に新しい実績を自由に追加できます。 ただし、すでにリリースされているバージョンでは順序を変更できません。 理由は明らかだと思います。



2つのタイプを宣言します。



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







秘密の成果かどうかを判断するために、このタイプを定義します。



 enum AchievementVisibility { L_VIS, L_HID, };
      
      







ブールだけでうまくいくことが可能であったことは明らかです(そして、それは非常に最初でした)が、テーブルの定数を埋めると、さまざまなブールからの成果が目に見え始めたため、ブールは開発プロセス中に放棄されました。



ある成果の説明は、最終的に次のようになり始めました。



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







次に、成果自体を発明する創造的な作業と、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 );
      
      







また、 FProgressNoteFNoteProcがあることにお気づきでしょう。 FNoteProcを1つだけ実行して、すぐにフレーズを返すことができないのはなぜですか? すべてがシンプルです。 フレーズを現在の言語にローカライズするため。 テンプレートはローカライズされ、string-numberがテンプレートに返されます 。これはFNoteProcから返されます



これで、すべてが静的データに命を吹き込む準備ができました。 これを行うには、もう少しプログラムする必要があります。 Achievement ManagerとAchievementsのUI Managerが必要です。 彼らが何をするか見てみましょう。



 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システムに応じて非常に具体的になります。 私たちと一緒に、彼はサイコロをカップで満たすだけです(もう一度、最初のスクリーンショットを見てください)。



死後




ゲームがリリースされ、成果のシステムがうまく機能します。 Flurryを通じてavivokの統計を追跡し、受信した成果の数と成果を観察する機会があります。 これはバランスを整えるのに役立ちます。 より複雑なゲームの場合、そのようなフィードバックの助けを過大評価することは困難です。



私がしたかったが、時間がなかったから:







PSゲームはLinderdaum Engineで行われます。



All Articles