Unity TopDownEngineのAchievementsのドキュメントとコードを読んだ

# 筆者のステータス
ゲームを作りたくなってTopDownEngineを衝動買いしたけどUnityまったく未経験につき一瞬で挫折。デモ(MinimalSandbox3D)の起動で止まっているので、とりあえずTopDownEngineのドキュメントとコードを読もうかなと思いました。という状態です。（したがって想定読者層は私と同じようなUnity未経験者です。）
![Achievements](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1826988/3093e442-b536-0115-600f-54a1a7008f92.png)
Unityのバージョンは2020.3.30f1
TopDownEngineのバージョンは2.3.1

# Achievementsとは
よくゲームで、本筋とは別に「敵を○○体撃破した！」みたいな実績が解除されていくアレです。たとえば、MinimalSandbox3Dでは10回ジャンプしたら画面の右下に"Jump Around"と書かれた四角いやつが出てきて、実績が解除されます。
もし手元の環境で10回ジャンプしても何も出てこない場合は、既に実績が解除されてしまっている可能性があるので、メニューバーからTools > More Mountains > Reset all achievementsを選択してリセットしてみてください。

# AchievementList.asset
まずは↓を読みました。

https://topdown-engine-docs.moremountains.com/achievements.html

ProjectビューからCommon/Resources/Achievementsの中にAchievementListっていうassetがあって、それを選ぶとInspectorビューにAchievementsが表示されます。
![Achievement List](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1826988/759c8c5f-789a-2b7f-f401-8d75ec6d9b43.png)
リストはResourcesフォルダ内にないとUnityがリストをロードできないから注意なんだそうです。
<div align="center">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1826988/c183a02c-b4a3-ba44-e0c8-d1574ea6869f.png" width=60%>
</div>
リストにあるAchievementsのうち、"SteppingStone"というのを開いてみました。ここで表示される実績のタイトルや説明を変更できそうです。Achievement TypeにはSimpleとProgressの2種類があって、Progressを使用する場合はProgressTargetを必ず指定する必要があるそうです。実績を解除できるのは1回までで、リセットするまで保持されます。
実績解除はコードのどこでも実装できるけど、AchievementRuleクラスに書くのがお勧めとのことです。

# AchievementsRules.cs
HierarchyビューのMinimalSandbox3D/Managers/AchievementRulesから、あるいは直接Common/Scripts/Achievements/AchievementsRules.csからコードを見ることができます。

![AchievementRules](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1826988/0b9fc989-945d-f1bf-ee88-d781ad3c21ac.png)

具体的なことはドキュメントには記載がなかったですが、リファレンスの方に載ってました。

https://topdown-engine-docs.moremountains.com/API/class_more_mountains_1_1_top_down_engine_1_1_achievement_rules.html

AchievementRules.csに書かれているAchievementRulesクラスの定義を見ると、MMAchievementRulesクラス(これがbase classらしい)と複数のイベントリスナーを継承していました。リファレンスに書いてあるとおりです(doxygenだから当たり前か)。

```cs:AchievementRules.csより
/// <summary>
/// This class describes how the Top Down Engine demo achievements are triggered.
/// It extends the base class MMAchievementRules
/// It listens for different event types
/// </summary>
public class AchievementRules : MMAchievementRules, 
								MMEventListener<MMGameEvent>, 
								MMEventListener<MMCharacterEvent>, 
								MMEventListener<TopDownEngineEvent>,
								MMEventListener<MMStateChangeEvent<CharacterStates.MovementStates>>,
								MMEventListener<MMStateChangeEvent<CharacterStates.CharacterConditions>>,
                                MMEventListener<PickableItemEvent>,
                                MMEventListener<CheckPointEvent>,
                                MMEventListener<MMInventoryEvent>
{
...<後略>
```

引き続きAchievementRules.csの中を読んでいくと、理解できる所を見つけられました。

```cs:AchievementRules.csより
public virtual void OnMMEvent(MMCharacterEvent characterEvent)
{
	if (characterEvent.TargetCharacter.CharacterType == Character.CharacterTypes.Player)
	{
		switch (characterEvent.EventType)
		{
			case MMCharacterEventTypes.Jump:
				MMAchievementManager.AddProgress ("JumpAround", 1);
				break;
		}	
	}
}
```
上のコードは、characterEventをトリガにして、EventTypeがJumpなら"JumpAround"のProgressを1進める。という処理のようです。ためしに`MMAchievementManager.AddProgress ("JumpAround", 2);`に書き換えると、キャラクターが1回ジャンプするごとに"JumpAround"のProgressが2加算され、その結果5回ジャンプしただけで実績が解除されました。
ちなみにJumpというイベントトリガは、Common/Scripts/Characters/Core/CharacterEvents.csに定義らしきものがありましたが、今は深追いしないことにします。

```cs:AchievementRules.csより
public virtual void OnMMEvent(CheckPointEvent checkPointEvent)
{
    if (checkPointEvent.Order > 0)
    {
        MMAchievementManager.UnlockAchievement("SteppingStone");
    }
}
```
上のコードでは、checkPointEventをトリガにして、Orderが0より大きければ実績が解除されるみたいです。ためしにチェックポイントを追加してみました。(コアラ2DのDemoを見てマネしました)。
1. Levelの下にCreate EmptyでCheckPointAという空のオブジェクトを作成(一応Create Empty ParentでCheckPointsも作成)
2. CheckPointAにAdd ComponentでBox Colliderを追加
3. Is Triggerにチェック
4. Box Colliderのサイズを3×3×3に変更
5. Scene上で見やすいようにアイコンを選択(多分必須ではない操作)
6. さらにAdd ComponentでCheckpointを追加
7. Check Point Orderを1に変更

![Adding check point](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1826988/137796d5-31e5-e59e-1b6a-54645c8065be.png)
これで、キャラクターがCheckPointAに行くと"SteppingStone"の実績が解除されました。これが正しい手順なのかはまだ不明ですが、今は深追いしないことにします。

# AchievementDisplay
ドキュメントには細かい説明がなく、コード見たり色々触って徐々にわかりはじめました。AchievementDisplayは3つのファイル(もっとあるかも)で構成されています。

## AchievementDisplay.prefab
実績解除時に画面に表示される四角いやつはThirdParty/MoreMountains/MMTools/Tools/MMAchievements/Prefabsの中にAchievementDisplayというprefabで位置や寸法やレイアウトが定義されています。
![AchievementDisplay](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1826988/2a7cbdaf-61e3-73cb-f628-883cf358becb.png)
## MMAchievementDisplayItem.cs
さっきのAchievementDisplay.prefabのInspectorビューに、MMAchievementDisplayItemというComponentがあったかと思います。そこには
1. Background Locked
1. Background Unlocked
1. Icon
1. Title
1. Description
1. Progress Bar Display

という項目があって、prefabの各要素と対応して紐づけられています。
後述のMMAchievementDisplayer.csの中で、解除された実績に応じた画像や文字列をメンバに格納することで、解除した実績を動的に表示することができています。

## MMAchievementDisplayer.cs
コードはThirdParty/MoreMountains/MMTools/ToolsForMMFeedbacks/Tools/Achievementsの中にあります。見た感じ、`OnMMEvent()`でMMAchievementUnlockedEventを引っかけて、`DisplayAchievement()`の中で`instance`から`achievementDisplay`を生成して、しかるべき画像やメッセージをAchievementItemの各メンバに格納する仕組みになっているようです。
```cs:MMAchievementDisplayer.csより
// we instantiate our achievement display prefab, and add it to the group that will automatically handle its position
GameObject instance = (GameObject)Instantiate(AchievementDisplayPrefab.gameObject);
instance.transform.SetParent(this.transform,false);

// we get the achievement displayer
MMAchievementDisplayItem achievementDisplay = instance.GetComponent<MMAchievementDisplayItem> ();
if (achievementDisplay == null)
{
	yield break;
}

// we fill our achievement
achievementDisplay.Title.text = achievement.Title;
achievementDisplay.Description.text = achievement.Description;
achievementDisplay.Icon.sprite = achievement.UnlockedImage;
...<後略>
```

そして同じく`instance`から`achievementCanvasGroup`を生成して、画像のフェードイン・フェードアウトを実行しているようです。
```cs:MMAchievementDisplayer.csより
// we fade it in and out
CanvasGroup achievementCanvasGroup = instance.GetComponent<CanvasGroup> ();
if (achievementCanvasGroup != null)
{
	achievementCanvasGroup.alpha = 0;
	StartCoroutine(MMFade.FadeCanvasGroup(achievementCanvasGroup, AchievementFadeDuration, 1));
	yield return _achievementFadeOutWFS;
	StartCoroutine(MMFade.FadeCanvasGroup(achievementCanvasGroup, AchievementFadeDuration, 0));
}
```

# 次の記事
以上で大まかな構造や仕組みが理解できたところで、次の記事では実際にAchievementの機能を使ってみようと思います。

https://qiita.com/items/343376cef878f48bd27d