はじめに
ゲームを遊んでいて、「あ、今の行動で何か達成した感あるな」と思った瞬間ってありませんか?
その“達成感”を分かりやすく形にしてくれるのが、実績(Achievement)システムです。
実績は、プレイヤーの行動を肯定し、やり込みや継続プレイを自然に促してくれる、とても強力な仕組み。
でもいざ実装しようとすると、
- 条件の管理がごちゃごちゃする
- 保存とロードが不安
- UI更新や通知演出が後回しになりがち
……こんな壁にぶつかる人も多いんですよね。
この記事では、Unityで実績(Achievement)システムを一から組み立てる方法を、
「条件管理 → 保存 → UI表示 → 解除通知」という流れで、順番に解説していきます。
まずはPlayerPrefsを使った最小構成で完成させ、
そこから「保存を強化したい」「クラウド対応したい」となったときの拡張ポイントまでカバーします。
実績システムは、最初から完璧を目指すより、
壊れにくく、後から育てられる設計がいちばん大事。
「小さな成功体験を、ちゃんとゲームに刻みたい」
そんな人に向けて、できるだけ分かりやすく、実践寄りで進めていきますね 😊
1. 実績(Achievement)システムの設計方針
まず最初に大事なのは、「どう作るか」より「どう壊れにくくするか」を考えることです。
実績システムは、ゲームが成長するほど条件や種類が増えやすく、後から修正しづらい要素でもあります。
そこで今回は、次の3点を軸に設計していきます。
- 実績の定義とロジックをできるだけ分離する
- 条件判定はシンプルに、拡張しやすくする
- 保存・UI・演出を後から差し替えられる構造にする
実績システムを構成する基本要素
実績は一見シンプルですが、実際には複数の役割が組み合わさっています。
- 実績データ:タイトル、説明、解除条件、必要数などの定義情報
- 進捗データ:撃破数やクリア回数など、現在のカウント状態
- 判定ロジック:条件を満たしたかどうかをチェックする処理
- 保存処理:ゲーム終了後も状態を保持する仕組み
- UI表示:一覧表示、ロック/解除状態の見せ方
- 通知演出:解除時のポップアップや効果音
これらを1つのスクリプトに詰め込まないのが、長く使える設計のコツです。
よくある失敗パターン
実績システムでよく見かけるのが、こんな状態です。
- 敵を倒した処理の中に実績判定がベタ書きされている
- 実績IDが文字列で散らばっていて管理できない
- UI更新とデータ更新が密結合している
- 保存方式を後から変えられない
最初は動いていても、実績が増えた瞬間に一気に辛くなります…。
この記事で目指すゴール
本記事では、次のような状態をゴールにします。
- 実績をデータとして定義できる
- 条件カウントと判定を整理して管理できる
- PlayerPrefsで確実に保存・復元できる
- 一覧UIと解除通知が正しく連動する
- 後から保存方式やUIを差し替えられる

次の章からは、まず実績データそのものをどう作るかを見ていきましょう。
ここが整理できると、後の実装が一気に楽になりますよ 🙂
2. 実績データの作り方(定義:ScriptableObject推奨)
実績システムで最初に整えておきたいのが、「実績そのものの定義」です。
ここがコードにベタ書きだと、実績を1個増やすたびに修正箇所が増えて、だんだん地獄を見ます…😇
そこでおすすめなのが、ScriptableObjectで実績をデータ化するやり方。
「実績一覧を編集したい」「説明文や必要数を調整したい」といった運用が、Inspectorだけでできるようになります。
AchievementDataに持たせたい項目(例)
まずは、実績1件分が持つべき情報を整理します。最小構成なら次のような項目があると便利です。
- ID(ユニークな識別子:例
kill_10) - タイトル(一覧表示用)
- 説明(何を達成すればいいか)
- アイコン(未解除/解除で差し替えるなら両方)
- 条件タイプ(撃破数、クリア回数などをenumで)
- 必要数(例:10回、100個など)
「解除済みかどうか」「現在の進捗」は、実績の“定義”ではなく“状態(進捗)”なので、
後の章で別管理(保存対象)にするのが分かりやすいです。
条件タイプをenumで作る(まずは少数でOK)
条件を文字列で管理するとミスが増えやすいので、enumにしておくのが安全です。
public enum AchievementConditionType
{
EnemyKillCount,
ClearCount,
GameOverCount,
// 必要になったら増やす
}後から増えても大丈夫なように、最初は「自分のゲームで確実に使う条件だけ」入れればOKです。
ScriptableObjectで実績を定義する
プロジェクトウィンドウを右クリック「Create」→「C# Script」を選んで、
新しいスクリプトを作成し、「AchievementData」と名前を付けます。
using UnityEngine;
[CreateAssetMenu(menuName = "Achievements/Achievement Data")]
public class AchievementData : ScriptableObject
{
[Header("Basic")]
public string id;
public string title;
[TextArea] public string description;
[Header("Visual")]
public Sprite lockedIcon;
public Sprite unlockedIcon;
[Header("Condition")]
public AchievementConditionType conditionType;
public int targetValue = 1;
}これで、Inspectorから「実績の一覧」を作れるようになりました。
ScriptableObjectにするメリット
- 実績を追加してもコードを書かなくて済む(運用が楽)
- タイトルや説明、必要数を調整しやすい(バランス調整向き)
- UIの一覧表示が作りやすい(データを並べるだけ)
次にやること:進捗(カウント)と判定の仕組みを作る
ここまでで「実績の設計図(定義)」ができました。
次の章では、撃破数などのカウントをどう集めて、どう判定して、どう解除するかを作っていきます。

実績システムはここが心臓部。いよいよ動かしていきましょう 🙂
3. 条件管理(カウント・判定・解除フロー)
実績データ(AchievementData)を作れたら、次は「進捗(カウント)」と「判定」です。
ここでのコツは、条件判定をゲーム中のあちこちに散らさず、1か所に集約すること。
今回はシンプルに、実績システム側で「カウント更新 → 判定 → 解除」を完結させます。
(将来的にEventで疎結合にしたい場合も、この形がベースになります)
実績の「状態」を表すクラスを用意する
AchievementDataは“定義”なので、解除済みかどうか・現在の進捗は別で持ちます。
まずは実績の状態を表すために、AchievementStateのようなクラスを用意します。
[System.Serializable]
public class AchievementState
{
public string id;
public int currentValue;
public bool unlocked;
public AchievementState(string id)
{
this.id = id;
currentValue = 0;
unlocked = false;
}
}「定義(ScriptableObject)」と「状態(進捗/解除)」を分けると、保存もしやすくなります。
AchievementSystem(管理役)を作る
次に、実績データの一覧を受け取り、状態を管理するクラスを作ります。
プロジェクトウィンドウを右クリック「Create」→「C# Script」で、AchievementSystemを作成してください。
using System;
using System.Collections.Generic;
using UnityEngine;
public class AchievementSystem : MonoBehaviour
{
public static AchievementSystem Instance { get; private set; }
[Header("Definitions")]
[SerializeField] private List<AchievementData> achievementDefinitions;
private Dictionary<string, AchievementState> states = new();
private Dictionary<AchievementConditionType, int> stats = new();
public event Action<AchievementData> OnUnlocked;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
InitializeStates();
InitializeStats();
}
private void InitializeStates()
{
states.Clear();
foreach (var def in achievementDefinitions)
{
if (def == null || string.IsNullOrEmpty(def.id)) continue;
states[def.id] = new AchievementState(def.id);
}
}
private void InitializeStats()
{
stats.Clear();
foreach (AchievementConditionType type in Enum.GetValues(typeof(AchievementConditionType)))
{
stats[type] = 0;
}
}
public IReadOnlyList<AchievementData> GetDefinitions() => achievementDefinitions;
public AchievementState GetState(string id)
{
states.TryGetValue(id, out var state);
return state;
}
}ここでは、次の2つを用意しています。
- states:各実績の進捗と解除状態(ID→State)
- stats:撃破数など、ゲーム中の統計(ConditionType→数値)
「何を」「どこで」カウントするか
実績の条件は、ゲーム中のイベント(敵撃破、クリア、死亡など)で増えます。
重要なのは、増やす側は“実績のことを考えない”こと。
たとえば敵を倒したとき、ゲーム側はこう呼ぶだけにします。
AchievementSystem.Instance.AddProgress(AchievementConditionType.EnemyKillCount, 1);
「この撃破でどの実績が解除されるか?」はAchievementSystemが勝手にやる。
これが散らからない設計の基本です。
カウント更新 → 判定 → 解除 の流れを実装する
AchievementSystemに、進捗を加算して判定するメソッドを追加します。
public void AddProgress(AchievementConditionType type, int amount)
{
if (!stats.ContainsKey(type)) stats[type] = 0;
stats[type] += amount;
// この条件タイプに紐づく実績だけチェックする
foreach (var def in achievementDefinitions)
{
if (def == null) continue;
if (def.conditionType != type) continue;
if (!states.TryGetValue(def.id, out var state)) continue;
if (state.unlocked) continue;
// 今回は「統計 = 進捗」として扱う(シンプル)
state.currentValue = stats[type];
if (state.currentValue >= def.targetValue)
{
Unlock(def);
}
}
}
private void Unlock(AchievementData def)
{
if (!states.TryGetValue(def.id, out var state)) return;
if (state.unlocked) return;
state.unlocked = true;
// 通知/UI側に知らせる
OnUnlocked?.Invoke(def);
// ※保存は次の章で実装(ここで呼ぶ想定)
}これで、条件が加算されるたびに「該当する実績だけ」を判定できるようになります。
解除時にやること(最低限これだけ)
実績が解除された瞬間にやりたいことは、だいたいこの3つです。
- 解除フラグをON(二重解除を防ぐ)
- UIに通知(一覧更新、ポップアップ表示)
- セーブ予約(ゲーム再起動でも解除状態が残るように)
ここまでの段階では、まだセーブは未実装なので「解除フラグON+通知」まで完成しています。
次にやること:保存とロード(PlayerPrefs)
実績システムが一度動き始めたら、次に必ず必要になるのが永続化(セーブ/ロード)です。

次の章では、まずは導入しやすいPlayerPrefsで、解除状態と進捗を保存・復元する仕組みを作ります。
4. 保存とロード(まずはPlayerPrefsで完成させる)
実績システムで「それっぽく動いた!」の次に必ず来るのが、保存(セーブ)です。
実績って、ゲームを終了しても残っていてほしいですよね。むしろ残らないと悲しい…😢
まずは実装コストが低いPlayerPrefsで完成させます。
ここで“ちゃんと保存できる形”を作っておくと、後からEasy Saveやクラウドに置き換える時もスムーズです。
保存方針(キー設計)
PlayerPrefsはキーが文字列なので、実績ID(例:kill_10)を使って保存します。
ただしキー衝突を避けるために、接頭辞を付けるのがおすすめです。
- 解除状態:
ach_unlock_{id}(0 or 1) - 進捗値:
ach_value_{id}(int)
※PlayerPrefsはboolを直接保存しづらいので、0/1で保存するのが定番です。
AchievementSystemにSave/Loadを追加する
さっそくAchievementSystemに、保存とロードの処理を追加します。
まずはキー生成用のヘルパーを用意します。
private string UnlockKey(string id) => $"ach_unlock_{id}";
private string ValueKey(string id) => $"ach_value_{id}";
ロード(起動時に状態を復元)
最初にロードを作ります。Awakeの初期化が終わった後に呼ぶのがポイントです。
AchievementSystemのAwakeの最後あたりで、Load()を呼びましょう。
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
InitializeStates();
InitializeStats();
Load(); // ★ここで復元
}Load本体はこちらです。
public void Load()
{
foreach (var def in achievementDefinitions)
{
if (def == null || string.IsNullOrEmpty(def.id)) continue;
if (!states.TryGetValue(def.id, out var state)) continue;
int unlockedInt = PlayerPrefs.GetInt(UnlockKey(def.id), 0);
int valueInt = PlayerPrefs.GetInt(ValueKey(def.id), 0);
state.unlocked = unlockedInt == 1;
state.currentValue = valueInt;
// 進捗の基準として stats も合わせて復元しておく(シンプル構成)
if (!stats.ContainsKey(def.conditionType)) stats[def.conditionType] = 0;
stats[def.conditionType] = Mathf.Max(stats[def.conditionType], state.currentValue);
}
}ここでのポイントは、states(実績ごとの進捗)と、stats(統計)を矛盾しないように合わせることです。
今回の実装では「統計値=進捗の元」なので、ロード時に統計も戻しておくと判定が安定します。
セーブ(解除時・進捗更新時に保存)
次にセーブです。シンプルに、解除されたら必ず保存するようにします。
(必要なら、進捗更新ごとに保存する方式にもできますが、まずは解除時だけでもOK)
先ほど作ったUnlock()の最後でSaveOne(def.id)を呼びます。
private void Unlock(AchievementData def)
{
if (!states.TryGetValue(def.id, out var state)) return;
if (state.unlocked) return;
state.unlocked = true;
OnUnlocked?.Invoke(def);
SaveOne(def.id); // ★解除したら保存
}保存処理(1件分)はこんな感じ。
public void SaveOne(string id)
{
if (!states.TryGetValue(id, out var state)) return;
PlayerPrefs.SetInt(UnlockKey(id), state.unlocked ? 1 : 0);
PlayerPrefs.SetInt(ValueKey(id), state.currentValue);
PlayerPrefs.Save();
}これで「解除した実績は、次回起動しても解除状態が残る」ようになりました 🎉
進捗も保存したい場合(AddProgressで保存)
「解除だけじゃなく、途中の進捗も残したい」なら、AddProgress内で対象実績の進捗を保存すればOKです。
ただし頻繁に保存すると負荷が増えることがあるので、まずは解除時だけで運用して、必要なら追加するのがおすすめです。
// AddProgressの中で、state.currentValueを書き換えた後に
SaveOne(def.id);
テスト時の初期化(DeleteAllの扱い注意)
実績のテストをするときは、保存データを消して何度も試したくなります。
その時に使えるのがPlayerPrefs.DeleteAll()です。
public void ResetAllForDebug()
{
PlayerPrefs.DeleteAll();
PlayerPrefs.Save();
InitializeStates();
InitializeStats();
}ただしこれは開発中専用です。
リリース版に残してしまうと、プレイヤーの実績が消える事故につながるので、最後は必ず削除(またはビルド条件で無効化)してくださいね。
次にやること:UI表示(一覧・セル・フィルタ)
ここまでで、実績の「定義・判定・保存」が揃いました。

次の章では、これを一覧UIとして見える形にしていきます。
“解除済みはキラッと、未解除はグレー”みたいな、あの気持ちいい画面を作りましょう 🙂
5. UI表示(一覧・セル・フィルタ)
実績は「解除された」だけだと地味なので、一覧として見える化すると一気に気持ちよくなります。
ここでは、よくある「実績一覧画面」を想定して、 AchievementsUI(一覧管理)とAchievementCell(1件表示)に分けて作っていきます。
UIのざっくり構成
まずはUnityのUIをこんな感じで用意します(名前は例です)。
- Canvas
- AchievementsPanel(実績画面の親)
- Scroll View(一覧)
- (任意)FilterButtons(未解除のみ/解除済みなど)
Scroll Viewの中身(Content)に、AchievementCellを並べる形式が定番です。
AchievementCell(1件分の表示)を作る
まずはPrefab化するセルを用意します。セルの中には最低限これがあるとOKです。
- Image:アイコン
- Text:タイトル
- Text:説明
- Text:進捗(例:7 / 10)
- (任意)Image:ロック用のオーバーレイ(暗くする用)
プロジェクトウィンドウを右クリック「Create」→「C# Script」で、 AchievementCellを作成します。
using UnityEngine;
using UnityEngine.UI;
public class AchievementCell : MonoBehaviour
{
[Header("UI")]
[SerializeField] private Image iconImage;
[SerializeField] private Text titleText;
[SerializeField] private Text descriptionText;
[SerializeField] private Text progressText;
[SerializeField] private GameObject lockedOverlay; // 任意
private string achievementId;
public void Setup(AchievementData def, AchievementState state)
{
achievementId = def.id;
titleText.text = def.title;
descriptionText.text = def.description;
bool unlocked = state != null && state.unlocked;
// アイコン切り替え(未解除/解除)
iconImage.sprite = unlocked ? def.unlockedIcon : def.lockedIcon;
// 進捗表示
int current = state != null ? state.currentValue : 0;
progressText.text = unlocked
? "Unlocked!"
: $"{Mathf.Clamp(current, 0, def.targetValue)} / {def.targetValue}";
// 未解除を暗くするなど(任意)
if (lockedOverlay != null)
lockedOverlay.SetActive(!unlocked);
}
}これで、セル1個に「定義+状態」を渡せば表示できるようになりました。
AchievementsUI(一覧管理)を作る
次に、実績定義のリストからセルを生成して並べる管理クラスです。
プロジェクトウィンドウを右クリック「Create」→「C# Script」で、 AchievementsUIを作成します。
using System.Collections.Generic;
using UnityEngine;
public class AchievementsUI : MonoBehaviour
{
[Header("UI References")]
[SerializeField] private Transform contentParent;
[SerializeField] private AchievementCell cellPrefab;
private readonly List<AchievementCell> spawnedCells = new();
private void OnEnable()
{
if (AchievementSystem.Instance != null)
{
AchievementSystem.Instance.OnUnlocked += HandleUnlocked;
}
RebuildList();
}
private void OnDisable()
{
if (AchievementSystem.Instance != null)
{
AchievementSystem.Instance.OnUnlocked -= HandleUnlocked;
}
}
public void RebuildList()
{
// 既存セルを削除
foreach (var cell in spawnedCells)
{
if (cell != null) Destroy(cell.gameObject);
}
spawnedCells.Clear();
var defs = AchievementSystem.Instance.GetDefinitions();
foreach (var def in defs)
{
var state = AchievementSystem.Instance.GetState(def.id);
var cell = Instantiate(cellPrefab, contentParent);
cell.Setup(def, state);
spawnedCells.Add(cell);
}
}
private void HandleUnlocked(AchievementData def)
{
// 解除されたら一覧を更新
// (軽量化したいなら「該当セルだけ更新」に変更OK)
RebuildList();
}
}これで、実績が解除されたら自動的に一覧が更新されます。
「Unlocked!」表示が増えていくの、ちょっと嬉しいやつです🙂
フィルタ(未解除のみ / 解除済みのみ)を付けたい場合
一覧が増えてくると、フィルタがあると便利です。
まずは簡易フィルタとして、表示対象を絞るだけでOKです。
例として「未解除のみ」を表示するなら、RebuildListで生成前にチェックします。
public enum AchievementFilter
{
All,
LockedOnly,
UnlockedOnly
}
[SerializeField] private AchievementFilter filter = AchievementFilter.All;
public void SetFilter(int filterIndex)
{
filter = (AchievementFilter)filterIndex;
RebuildList();
}そしてRebuildList内で分岐を追加します。
// RebuildListのforeach内で
var state = AchievementSystem.Instance.GetState(def.id);
bool unlocked = state != null && state.unlocked;
if (filter == AchievementFilter.LockedOnly && unlocked) continue;
if (filter == AchievementFilter.UnlockedOnly && !unlocked) continue;ボタン側は、ButtonのOnClickにSetFilterを登録して、
引数に 0=All / 1=LockedOnly / 2=UnlockedOnly を渡せばOKです。
次にやること:解除通知の演出(ポップアップ)
一覧UIができたら、次は「解除した瞬間に出る通知」です。

“ポン!”って出るだけで達成感が倍増するので、ここはぜひ作りましょう。次の章でやります!
6. 解除通知の演出(ポップアップ)
実績システムの“気持ちよさ”を決めるのが、解除時の通知です。
一覧だけでも機能的にはOKなんだけど、解除した瞬間に「実績解除!」って出るとテンションが上がります🙂
ここでは、画面外からスッ…と出てきて、数秒後にスッ…と消えるポップアップを作ります。
実績が連続で解除された場合も想定して、キュー(順番待ち)で表示できる形にしておくのが安心です。
UIを用意する(NotificationPanel)
Canvasの中に、通知用のパネルを作ります。構成例はこんな感じ。
- NotificationPanel(Image)
- ├ TitleText(Text:実績名)
├ Icon(Image:実績アイコン)
└ MessageText(Text:固定文でもOK)
NotificationPanelはRectTransformで、初期位置を画面外に置いておきます(左でも上でもOK)。
出現位置と退場位置を座標で管理して、Coroutineで動かします。
AchievementsNotification(通知管理)を作る
プロジェクトウィンドウを右クリック「Create」→「C# Script」で、AchievementsNotificationを作成します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class AchievementsNotification : MonoBehaviour
{
[Header("UI")]
[SerializeField] private RectTransform panel;
[SerializeField] private Text titleText;
[SerializeField] private Text messageText;
[SerializeField] private Image iconImage;
[Header("Motion")]
[SerializeField] private Vector2 hiddenPos = new Vector2(-800f, 0f);
[SerializeField] private Vector2 shownPos = new Vector2(40f, 0f);
[SerializeField] private float slideDuration = 0.25f;
[SerializeField] private float staySeconds = 2.0f;
private readonly Queue<AchievementData> queue = new();
private Coroutine playRoutine;
private void Awake()
{
if (panel != null)
{
panel.anchoredPosition = hiddenPos;
}
}
private void OnEnable()
{
if (AchievementSystem.Instance != null)
{
AchievementSystem.Instance.OnUnlocked += Enqueue;
}
}
private void OnDisable()
{
if (AchievementSystem.Instance != null)
{
AchievementSystem.Instance.OnUnlocked -= Enqueue;
}
}
private void Enqueue(AchievementData def)
{
if (def == null) return;
queue.Enqueue(def);
if (playRoutine == null)
{
playRoutine = StartCoroutine(PlayQueue());
}
}
private IEnumerator PlayQueue()
{
while (queue.Count > 0)
{
var def = queue.Dequeue();
SetupUI(def);
// 出現
yield return Slide(panel, hiddenPos, shownPos, slideDuration);
// 滞在
yield return new WaitForSeconds(staySeconds);
// 退場
yield return Slide(panel, shownPos, hiddenPos, slideDuration);
// 連続解除が多いとき、少し間を空けると見やすい(任意)
yield return new WaitForSeconds(0.05f);
}
playRoutine = null;
}
private void SetupUI(AchievementData def)
{
if (titleText != null) titleText.text = def.title;
if (messageText != null) messageText.text = "Achievement Unlocked!";
if (iconImage != null) iconImage.sprite = def.unlockedIcon != null ? def.unlockedIcon : def.lockedIcon;
}
private IEnumerator Slide(RectTransform rt, Vector2 from, Vector2 to, float duration)
{
if (rt == null) yield break;
float t = 0f;
rt.anchoredPosition = from;
while (t < duration)
{
t += Time.unscaledDeltaTime; // UIは一時停止中も動かしたいことが多いので unscaled
float p = Mathf.Clamp01(t / duration);
// ちょいイージング(直線より気持ちいい)
float eased = 1f - Mathf.Pow(1f - p, 3f);
rt.anchoredPosition = Vector2.LerpUnclamped(from, to, eased);
yield return null;
}
rt.anchoredPosition = to;
}
}これで、実績解除イベント(OnUnlocked)が来たら通知キューに積まれて、順番に表示されます。
よくあるUIバグ対策(連続解除)
実績が連続解除されると「通知が上書きされる」「途中で消える」などが起きがちです。
今回の仕組みはキュー+コルーチンなので、基本的に崩れにくいです。
それでも気になる場合は、
- staySecondsを少し短くする
- 同一フレームで複数解除する設計なら、解除をまとめて1件にする
- 通知に効果音を付ける(AudioSourceでOK)
次にやること:保存を強化したい場合(Easy Save)
ここまでで、実績の「条件管理・保存・UI一覧・通知」まで一通り完成しました 🎉

次の章では、PlayerPrefsから一歩進んで、保存をもっと安全&拡張しやすくする選択肢としてEasy Saveの考え方を紹介します。
7. 発展:保存を「壊れにくく」したいなら(Easy Save)
ここまでで、PlayerPrefsを使った実績システムは完成です。おめでとう〜!🎉
ただ、実際の開発ではこんな瞬間が来がちです。
- 実績の数が増えて、保存キーが大量になる
- 「進捗」「解除日時」「統計」「複数条件」など、保存したい情報が増える
- アップデートで実績IDを変えたくなった(でも既存セーブが怖い)
- 端末移行やバックアップを意識し始めた
こうなると、PlayerPrefsは「手軽」な反面、だんだん管理がしんどくなります。
そこで選択肢に入ってくるのが、保存専用ツールです。
Easy Saveで何が楽になる?
Easy Saveの強みは、ざっくり言うと「保存・復元の面倒をまるっと減らせる」こと。
実績システムで嬉しいポイントは特にこのへんです。
- 複数データをまとめて保存しやすい(IDごとにキー地獄になりにくい)
- リストや辞書などの構造を保存しやすい(進捗の集合を丸ごと扱える)
- 将来的な拡張(実績データの項目増加)に対応しやすい
「今はPlayerPrefsで十分だけど、拡張で破綻しそう…」という場合、移行先として有力です。
置き換えの考え方:PlayerPrefs層を“保存サービス”にする
後から保存方式を差し替えたいなら、AchievementSystemの中に保存処理をベタ書きせず、
「保存役(サービス)」を挟むのがコツです。
たとえば、保存に必要なのは最終的にこの2つです。
- 実績ごとの状態(AchievementState:解除済み/進捗)
- (必要なら)統計(stats:撃破数など)
これを「Save/Loadクラス」に分けておけば、今はPlayerPrefs、将来はEasy Saveへ差し替えができます。
実績状態をまとめて保存する例(イメージ)
ここでは“考え方”だけ軽く紹介します。
実績状態(states)をListにして丸ごと保存できるようにすると、管理がかなり楽になります。
// 例:DictionaryをListにしてまとめる
[System.Serializable]
public class AchievementsSaveData
{
public List<AchievementState> states = new();
}
PlayerPrefsだと「1件ずつキー保存」になりがちですが、保存ツールだとこういう構造を扱いやすいです。
どのタイミングでEasy Saveを検討する?(目安)
- 実績数が増えてきて、キー管理がつらい
- 解除状態だけでなく、複雑な進捗や追加情報も保存したい
- アップデートでデータ構造が変わる可能性が高い
- 今後、セーブデータ全体をしっかり管理したい
逆に、ミニゲーム規模や短編作品ならPlayerPrefsで十分なことも多いです。
「規模に合わせて選ぶ」でOKです🙂
Easy Save(商品リンク)
実績だけでなく、セーブデータ全体を堅牢にしたいなら、専用ツールを使うのも手です。
Easy Save
✅ Asset Storeでチェックする

次の章では、さらに発展としてクラウド同期(UGS)やプラットフォーム実績の方向性も触れていきます。
「端末を変えても実績を引き継ぎたい!」ってなったら、ここが出番です。
8. 発展:クラウド同期(UGS / Google Play Games ざっくり導入方針)
ローカル実績(PlayerPrefsなど)が完成すると、次に気になってくるのがクラウド同期です。
端末を変えたり再インストールしたりしても、実績が残っていてほしい…!というやつですね。
ここでは実装の細部まで踏み込むというより、「どの選択肢があるか」「どう棲み分けるか」を整理します。
方向性が決まるだけで、後の設計がかなり楽になりますよ🙂
選択肢1:Unity Gaming Services(UGS)でクラウド同期する
Unity公式のサービスを使って、実績をクラウドに寄せる方法です。
ざっくり言うと、次の流れになります。
- Authenticationでユーザーを識別(匿名ログインなど)
- Achievements(Building Blocks)で実績定義を用意
- Cloud Saveで進捗を保存して端末間同期
- 必要に応じてCloud Codeで判定や報酬付与をサーバー側に寄せる
“クラウドで同期したい”が主目的なら、UGSはかなり現実的な選択肢です。
特に「iOS/Android/PCなど複数プラットフォームで同じ実績を持たせたい」場合に相性が良いです。
UGSの設計でよくあるパターン
- 判定はローカル(今の記事のAchievementSystem)で行い、結果だけクラウド保存
- チート対策が必要なら、判定や報酬をCloud Code側へ寄せる
最初は「ローカル判定+クラウド保存」から始めると、作業量が増えすぎません。
選択肢2:Google Play Games Services(Android向け)
Androidで「プラットフォーム標準の実績」を使うなら、Google Play Games Servicesが候補です。
これはUGSとは目的が少し違っていて、 “Googleの実績UIに載せる”のがメインになります。
やれることのイメージはこんな感じ。
- 実績解除(進捗100%で解除)
- 増分実績(例:敵を100体倒す→1体ごとに加算)
- 標準UI表示(実績一覧のGoogle標準画面を出せる)
Androidだけ特別に対応する場合は強いですが、マルチプラットフォーム前提だと管理が複雑になりがちなので、 “UGS中心+必要ならPlay Games”という棲み分けが多いです。
ローカル実績とプラットフォーム実績、どう棲み分ける?
ここは悩みやすいところなので、よくある方針を整理します。
- ローカル(自前)実績:ゲーム内のやり込み・収集・隠し実績など、柔軟に増やしたいもの
- プラットフォーム実績:代表的な実績だけ(チュートリアル完了、エンディング到達など)
全部をプラットフォームに寄せると、実績の追加・調整が面倒になることが多いので、
「ゲーム内実績は自前」「目立つ実績だけ外部にも連携」がバランス取りやすいです。

これで、ローカルで完結する実績システムから、クラウド・プラットフォーム連携の方向性まで一通り見えました。
まとめ
ここまで、Unityで実績(Achievement)システムを作る流れを、 「条件管理 → 保存 → UI表示 → 通知演出」まで一通りつなげて解説してきました。
この記事の要点(サクッと振り返り)
- 実績は「定義(AchievementData)」と「状態(進捗・解除)」を分けると整理しやすい
- 条件カウントはゲーム中に散らさず、AchievementSystemに集約すると破綻しにくい
- まずはPlayerPrefsで最小構成を完成させると、全体の動作確認が早い
- 一覧UIはAchievementsUI(一覧)とAchievementCell(1件)に分けると拡張しやすい
- 解除通知はキュー+Coroutineで、連続解除でも安定して表示できる
- 規模が大きくなるなら、保存はEasy Saveなどで“壊れにくく”する選択もあり
- 端末間同期が欲しいなら、UGS(クラウド)やGoogle Play Gamesなどの方向性を検討
実績って「たくさん用意すること」よりも、運用しやすく、後から増やせることの方が大事だったりします。
最初は少数の実績でもOK。まずはシステムを完成させて、ゲームの手触りに合わせて増やしていくのがいちばん強いです🙂
そして、実績はプレイヤーの行動に「ちゃんと反応してくれる」仕組みなので、
作り込むほどゲームが生き生きして見えるようになります。ぜひ、自分のゲームに合う形に育てていってくださいね!
あわせて読みたい
実績システムを作っていると、
「保存まわりをもう少し理解したい」「イベント設計を整理したい」「UIを強化したい」
というポイントが必ず出てきます。
そんなときに役立つ、関連性の高い記事をピックアップしました👇
- Unityのシリアライズ完全ガイド!データの保存・読み込みを自由自在に
実績データや進捗管理を拡張したくなったときに必須の知識。 - 【実践】UnityでJSONデータ管理!セーブ&ロードを最適化する方法
PlayerPrefsから一歩進んだ保存方式を検討したい人向け。 - Unity初心者必見!シングルトンの正しい実装方法とよくあるミス
AchievementSystemをSingletonで管理する際の注意点を整理。 - 【Unity C#】カスタムイベントの作り方!EventとActionを使いこなそう
実績解除通知やUI更新を疎結合にしたい場合におすすめ。 - Unityの使い方⑪ UI(ユーザーインターフェース)を作ってみよう
実績一覧UIや通知パネルを作る前に押さえておきたい基礎。
実績システムは、保存・イベント・UIの知識が一気につながるテーマです。
気になるところから、少しずつ理解を深めていくのがおすすめですよ 🙂
参考書(1冊で体系的に学びたい人へ)
実績システムだけでなく、Unityゲーム開発全体の設計・実装をまとめて整理したいならこちら。
Unityゲーム プログラミング・バイブル 2nd Generation
✅ Amazonでチェックする| ✅ 楽天でチェックする
参考文献
- Unity Gaming Services Achievements(公式)
- Unity Cloud Code(公式)
- Unity PlayerPrefs(Unity公式スクリプトリファレンス)
- Google Play Games plugin for Unity(GitHub)
よくある質問(FAQ)
- Q実績の条件が増えてきたら、管理が大変になりませんか?
- A
なります。なので、最初から「増える前提」で設計するのが大切です。
この記事で紹介したように、
- 実績の定義は ScriptableObject にまとめる
- 条件タイプは enum で管理する
- カウントと判定は AchievementSystem に集約する
この形にしておくと、実績が増えても「データを追加するだけ」で済むようになります。
実績数が10個でも100個でも、コードの複雑さがほとんど変わらないのが理想です。
- QPlayerPrefsで実績を保存するのって、安全なんですか?
- A
小〜中規模のゲームなら、実用上は問題ありません。
ただし、PlayerPrefsは
- データ構造が複雑になると管理しづらい
- キーの増加や仕様変更に弱い
- チート対策や暗号化には向かない
という弱点もあります。
なのでおすすめは、
- 最初は PlayerPrefsで完成させる
- 規模が大きくなったら Easy Saveやクラウド保存に移行
という段階的な考え方です。
「いきなり完璧」を目指さない方が、結果的に楽ですよ 🙂
- Q実績解除が一気に起きると、UIがバグりませんか?
- A
何も考えずに作ると、かなり高確率でバグります…😅
よくあるのが、
- 通知が上書きされる
- 途中で消える
- 音だけ何回も鳴る
この記事では、キュー(Queue)+Coroutineで順番に表示する方式を紹介しました。
この形にしておくと、連続解除が起きても安定して表示できます。実績解除の演出は「気持ちよさ」に直結する部分なので、
多少コードが増えても、ここは丁寧に作るのがおすすめです。







※当サイトはアフィリエイト広告を利用しています。リンクを経由して商品を購入された場合、当サイトに報酬が発生することがあります。
※本記事に記載しているAmazon商品情報(価格、在庫状況、割引、配送条件など)は、執筆時点のAmazon.co.jp上の情報に基づいています。
最新の価格・在庫・配送条件などの詳細は、Amazonの商品ページをご確認ください。