🚀

UnityでSteamの累計実績を作る: Statsを使って「敵を累計100体倒す」を実装する

に公開

第1回と第2回では、ACH_CLEAR のように「ある瞬間に解除される実績」を扱いました。

ただ、実際のゲームでは「敵を累計100体倒したら解除」「アイテムを累計1000個集めたら解除」のように、複数プレイにまたがって累計値で解除する実績もよく出てきます。

このタイプの実績は、第1回で使った SetAchievement だけでは作れません。「いま累計何体倒しているか」をどこかに保存しておく必要があるからです。

そこで使うのが、Steamの Stats という仕組みです。

第3回では、Steam Statsで累計値を保存し、Steam側で条件を満たしたときに実績が自動で解除されるようにします。Unity側からは SetAchievement を呼ばず、Statsを更新するだけで実績が解除されるのが特徴です。

この記事で作るもの

この記事では、次の累計実績を作ります。

項目
Steam Statの名前 stat_total_enemies_defeated
Steam実績のAPI Name ACH_DEFEAT_100
想定する実績名 撃破王
解除条件 敵を累計100体倒す
Unityコードで使う処理 SteamUserStats.GetStat / SteamUserStats.SetStat
Steamへ保存する処理 SteamUserStats.StoreStats

第1回・第2回と違い、第3回では SteamUserStats.SetAchievement をUnity側から呼びません。Statsを増やすだけで、Steam側が「100体に達したから ACH_DEFEAT_100 を解除する」と判断してくれます。

累計実績は直接解除型と何が違うのか

第1回・第2回で扱った直接解除型(ACH_CLEAR)と、第3回で扱う累計型(ACH_DEFEAT_100)は、考え方が大きく違います。

項目 直接解除型(第1回) 累計型(第3回)
解除条件 ゲームクリアの瞬間など 累計値が条件に達したとき
Unity側の処理 SetAchievement を呼ぶ SetStat で累計値を更新するだけ
実績解除の判定 Unity側で行う Steam側が自動で行う
保存が必要なデータ 解除済みフラグだけ 累計値そのもの

直接解除型は、Unity側で「クリアした」と判断したら SetAchievement を呼ぶ、というシンプルな流れでした。

累計型は、Unity側からは「累計値を更新する」しか呼びません。Steam側に「この累計値が100に達したら、この実績を解除する」というルールを登録しておき、Unityから累計値が更新されると、Steamが条件を満たしたかどうかを自分でチェックして解除してくれます。

そのため、累計型の実績を作るには、Steamworksパートナーサイト側で次の2つを準備する必要があります。

  1. 累計値を保存するためのStatを作る
  2. 累計値が条件に達したら解除される実績を作り、その実績にStatを紐付ける

順番に見ていきます。

Steam側にStatを作る

まずSteamworksパートナーサイトで、累計値を保存するためのStatを作ります。

Steamworksパートナーサイトで対象アプリを開き、Steamworks設定を編集 から データ&実績データ を開きます。英語表示の場合は、Edit Steamworks SettingsStats & AchievementsStats です。

そこで 新しい統計 ボタンを押してStatを追加し、少なくとも次の項目を設定します。英語表示では New Stat と表示されます。

設定項目 入力例 説明
API名 stat_total_enemies_defeated Unityコードから呼ぶ名前。英語表示では API Name
表示名 累計敵撃破数 Steamworks上の表示名。英語表示では Display Name
タイプ INT 整数の累計値。英語表示では Type
デフォルト値 0 初期値。英語表示では Default Value
集計タイプ Sum 累積されるStat。英語表示では Aggregation Type
増加のみ Yes 値が減らないようにする。英語表示では Increment Only
最大変化量 1000 1回の更新で増えてよい最大値(任意)。英語表示では Max Change

増加のみIncrement Only)を Yes にしておくと、誤って小さい値で SetStat してもStatの値が減らなくなります。累計実績では、減らさないようにしておくほうが安全です。

最大変化量Max Change)は、1回の SetStat で増やせる最大値です。例えば 1000 にしておくと、バグで一気に1000000などを送ってしまっても、Steam側で弾かれます。任意項目ですが、設定しておくと安心です。

設定したら、Steamworks側の変更を公開しておきます。英語表示では Publish と表示されます。

ここで入力した API Name とUnityコード内の文字列は、第1回の実績と同じく完全一致している必要があります。大文字小文字も含めて一致していないと、別のStatとして扱われます。

Statに紐付けた実績を作る

次に、第1回と同じ手順で実績を1つ追加し、そこにいま作ったStatを紐付けます。

Steamworksパートナーサイトで対象アプリを開き、Steamworks設定を編集 から データ&実績実績 を開きます。英語表示の場合は、Edit Steamworks SettingsStats & AchievementsAchievements です。

新しい実績を追加し、少なくとも次の項目を設定します。

設定項目 入力例 説明
API名 ACH_DEFEAT_100 Unityコードから参照する名前。英語表示では API Name
表示名 撃破王 Steam上に表示される名前。英語表示では Display Name
説明 敵を累計100体倒す 実績の説明。英語表示では Description
進捗の統計 stat_total_enemies_defeated さきほど作ったStatのAPI名。英語表示では Progress Stat
解除基準値 100 この値に達したら解除。英語表示では Min Progress の上限値や Max Progress などで指定
達成済みアイコン 任意の画像 解除済みアイコン。英語表示では Achieved Icon
未達成アイコン 任意の画像 未解除アイコン。英語表示では Unachieved Icon

ここでのポイントは 進捗の統計Progress Stat)です。第1回の ACH_CLEAR では なし でしたが、今回はさきほど作った stat_total_enemies_defeated を選びます。

進捗の統計 を設定すると、その実績は「指定したStatが基準値に達したときに自動で解除される」動作になります。Unity側から SetAchievement を呼ぶ必要はありません。

設定したら、Steam側の変更を公開しておきます。

Unityに累計値更新のコードを書く

Statと実績の準備ができたら、Unity側から累計値を更新するコードを書きます。

第1回と同じ Assets/Scripts/Steam フォルダにスクリプトを追加します。

  1. Assets/Scripts/Steam フォルダを右クリックする
  2. 作成 > Scripting > Empty C# Script を選ぶ
  3. ファイル名を SteamStats にする

作成した SteamStats.cs を開き、次のコードに置き換えます。

using UnityEngine;
using Steamworks;

public static class SteamStats
{
    public static void AddTotalEnemiesDefeated(int count)
    {
        if (!SteamManager.Initialized)
        {
            Debug.LogWarning("Steamを使う準備ができていないため、Statsを更新できません");
            return;
        }

        bool getOk = SteamUserStats.GetStat("stat_total_enemies_defeated", out int current);
        if (!getOk)
        {
            Debug.LogWarning("Steam Statの現在値を取得できませんでした: stat_total_enemies_defeated");
            return;
        }

        int next = current + count;

        bool setOk = SteamUserStats.SetStat("stat_total_enemies_defeated", next);
        if (!setOk)
        {
            Debug.LogWarning($"Steam Statを更新できませんでした: stat_total_enemies_defeated = {next}");
            return;
        }

        bool storeOk = SteamUserStats.StoreStats();
        if (!storeOk)
        {
            Debug.LogWarning($"Steam Statを保存できませんでした: stat_total_enemies_defeated = {next}");
            return;
        }

        Debug.Log($"Steam Statを更新しました: stat_total_enemies_defeated = {next}");
    }
}

このコードでやっていることは4つです。

  1. SteamManager.Initialized でSteamを使える状態か確認する
  2. SteamUserStats.GetStat で現在の累計値を読み出す
  3. SteamUserStats.SetStat で「現在値 + 加算分」をセットする
  4. SteamUserStats.StoreStats() でSteam側へ保存する

第1回の SteamAchievement.Unlock と違うのは、SetAchievement を呼ばないことです。実績の解除判定はSteam側で行われるので、Unityからは累計値を更新するだけで十分です。

コードを保存したらUnityエディターに戻り、読み込みが終わるまで待ちます。コンソール ウィンドウに赤いエラーが出ていないことを確認してください。

Unity UIのボタンから加算を試す

第1回・第2回と同じく、まずはUnity UIのボタンから加算処理を呼べるようにします。

Assets/Scripts/Steam フォルダを右クリックして 作成 > Scripting > Empty C# Script を選び、ファイル名を SteamStatsTestButton にします。

中身を次のコードに置き換えます。

using UnityEngine;

public class SteamStatsTestButton : MonoBehaviour
{
    public void AddOneDefeat()
    {
        SteamStats.AddTotalEnemiesDefeated(1);
    }

    public void AddTenDefeats()
    {
        SteamStats.AddTotalEnemiesDefeated(10);
    }
}

ボタン1回ごとに1体と10体を加算できるようにしておきます。100体まで貯めて実績解除を確認するときに、10体ボタンを10回押すなどで素早くテストできます。

保存してUnityエディターに戻り、コンソール に赤いエラーがないことを確認します。

テスト用ゲームオブジェクトを作る

第1回で作った SteamTestScene を開いて作業します。

  1. ヒエラルキー ウィンドウを右クリックする
  2. 空のオブジェクトを作成 を選ぶ
  3. ゲームオブジェクト名を SteamStatsTester にする
  4. インスペクターコンポーネントを追加 を押す
  5. SteamStatsTestButton を検索して追加する

Add 1 Defeat ボタンを作る

1体加算用のボタンを作ります。

  1. ヒエラルキー ウィンドウを右クリックする
  2. UI > ボタン - TextMeshPro を選ぶ
  3. 子オブジェクトのテキストを Add 1 Defeat に変更する
  4. インスペクターButton コンポーネントを探す
  5. クリック時()+ ボタンを押す
  6. SteamStatsTester ゲームオブジェクトを空欄にドラッグする
  7. 関数選択ドロップダウンから SteamStatsTestButton > AddOneDefeat() を選ぶ

Add 10 Defeats ボタンを作る

10体加算用のボタンも同じ流れで作ります。

  1. ヒエラルキー ウィンドウを右クリックする
  2. UI > ボタン - TextMeshPro を選ぶ
  3. 子オブジェクトのテキストを Add 10 Defeats に変更する
  4. インスペクターButtonクリック時()+ ボタンを押す
  5. SteamStatsTester ゲームオブジェクトを空欄にドラッグする
  6. 関数選択ドロップダウンから SteamStatsTestButton > AddTenDefeats() を選ぶ
  7. シーンを保存する

これで、再生中に Add 1 Defeat または Add 10 Defeats ボタンを押すと、Steam Statsの累計値が増えていきます。

Play Modeで動作を確認する

確認手順は次の通りです。

  1. Steamクライアントを起動してログインしておく
  2. Unityエディター上部の再生ボタンを押す
  3. Add 10 Defeats ボタンを1回押す
  4. コンソール ウィンドウに Steam Statを更新しました: stat_total_enemies_defeated = 10 が出ることを確認する
  5. もう一度押して = 20 になることを確認する
  6. 合計が100を超えるまで押し続ける
  7. 100を超えた時点でSteam側から 撃破王 実績の解除通知が出ることを確認する

Add 10 Defeats を10回押すと累計100に達するので、Steam側で自動的に ACH_DEFEAT_100 が解除されます。Unity側から SetAchievement を呼んでいないのに実績が解除されるのは、実績に 進捗の統計 を紐付けたからです。

AppID 480や、まだStatsと実績をPublishしていないAppIDでは、累計値の保存も実績解除も動きません。本番AppIDで両方をPublishしてから確認します。

すでに ACH_DEFEAT_100 を解除済みのSteamアカウントでテストする場合、第2回で作った Reset All ボタンで累計値と実績の解除状態をまとめてリセットすると、もう一度テストできます。

いつ StoreStats() を呼ぶか

累計実績で気をつけたいのが、StoreStats() を呼ぶタイミングです。

このコードでは、AddTotalEnemiesDefeated の中で毎回 StoreStats() を呼んでいます。テスト用のボタンから1回押すたびに呼ぶ分には問題ありません。

ただし、実際のゲーム本編では、敵を1体倒すたびに AddTotalEnemiesDefeated(1) を呼んで、その中で毎回 StoreStats() を呼ぶのは避けたほうがよいです。

理由は次の通りです。

  • StoreStats() はSteamサーバへの通信を発生させます
  • 敵を倒すたびに呼ぶと、短時間に何度も通信が走ります
  • Steam側でも、頻繁な保存はレートリミットの対象になることがあります
  • ゲームのフレームレートにも影響することがあります

実用的なやり方は、次の通りです。

  • 敵を倒したときは、Unity側のローカル変数(defeatsThisRun など)でカウントするだけ
  • 1プレイが終わってリザルト画面に入るタイミングで、まとめて AddTotalEnemiesDefeated(defeatsThisRun) を呼ぶ
  • これにより、StoreStats() の呼び出しは1プレイで1回だけになる

第3回のコード自体は1回ごとに StoreStats() を呼んでいますが、これは「テスト用に簡単に確認できる」ためです。ゲーム本編に組み込むときは、呼ぶタイミングを「プレイ終了時にまとめて1回」にします。

ゲーム本編に組み込む

テスト用ボタンで動作確認できたら、実際のゲーム本編から呼びます。

リザルト画面やゲームオーバー画面を管理しているスクリプトに、次の1行を追加します。

public void OnResult(int defeatsThisRun)
{
    SteamStats.AddTotalEnemiesDefeated(defeatsThisRun);

    // ここから先はリザルト画面の表示処理など、既存のコード
}

defeatsThisRun は、1プレイで倒した敵の数です。リザルト画面に渡すために、1プレイの間カウントしている値をそのまま渡せば十分です。

例えば、敵を倒すたびにスコア管理スクリプトの変数を1ずつ増やしていて、リザルト画面でその合計を表示しているなら、その表示と同じ変数を AddTotalEnemiesDefeated に渡します。

Unity Editorで実際に1プレイしてリザルト画面まで進めると、コンソール ウィンドウに Steam Statを更新しました のログが出ます。

これを何度か繰り返して累計100を超えると、Steam側で ACH_DEFEAT_100 が解除されます。

よくある失敗

Steam Statを更新しても実績が解除されない

最初に確認するのは、実績に 進捗の統計 が設定されているかです。

Steamworksパートナーサイトで ACH_DEFEAT_100 を開き、進捗の統計stat_total_enemies_defeated が選ばれているか確認してください。

ここが空欄になっていると、Statsを更新しても実績解除の判定が行われません。

設定したら、Steam側の変更をPublishする必要もあります。Publishしていない場合、エディター側からStatsを送っても、Steam側ではまだ実績解除条件のルールが反映されていません。

Steam Statの現在値を取得できませんでした と出る

Stat名が一致していない可能性があります。

Steamworksパートナーサイトの API名 と、Unityコード内の文字列が完全一致しているか確認してください。例えば、Steam側が stat_total_enemies_defeated なのにコード側が stat_total_enemies_defeat のように1文字違うと、別のStatとして扱われます。

また、AppID 480では stat_total_enemies_defeated のような自作Stat名は存在しないため、取得に失敗します。本番AppIDに切り替えて、Statを登録してPublishした状態で確認します。

累計値が増えていかない

増加のみIncrement Only)を Yes にしているStatに、現在値より小さい値で SetStat すると、Steam側で更新が無視されます。

このコードでは「現在値 + 加算分」を SetStat しているので、GetStat で取得した値より小さい値を渡すことはありません。それでも増えない場合は、GetStat で取得した current の値が想定通りか、Debug.Log で確認してみてください。

1回押すたびに毎回 StoreStats() を呼んでしまっている

第3回のサンプルコードは、ボタン1回ごとに StoreStats() を呼ぶ実装です。テスト中はこれで構いません。

ただし、ゲーム本編に組み込むときには、「敵1体倒すたび」ではなく「1プレイ終了時にまとめて」呼ぶようにします。短時間に大量の敵を倒すゲームほど、頻繁な StoreStats() の影響は大きくなります。

累計値をリセットしたい

第2回で作った Reset All ボタンを押すと、累計値も実績の解除状態もまとめて消えます。

累計100に達した状態をテストし直したいときは、Reset All で累計値を0に戻し、Add 10 Defeats を10回押して再度確認します。

Steamの解除通知が出ない

すでに同じSteamアカウントで ACH_DEFEAT_100 を解除済みの場合、もう一度100に達してもSteamクライアントの解除通知は出ないことがあります。

Steamは「すでに解除済みの実績を再度解除した」ときの通知を抑制します。コードが正しく動いているかは、コンソール ウィンドウのログとSteam側の実績一覧で確認してください。

第3回では扱わないこと

この記事では、Statsを使って累計実績を作るところまでを扱いました。

次のような話は第4回で扱います。

  • 複数の実績とStatsをまとめて管理する SteamAchievementManager の設計
  • UserStatsReceived_t などのCallbackで「Stats受信完了」を待つ作り
  • Steam未起動時にゲームが落ちないようにする工夫
  • 同一フレーム内で複数のStatsを更新するときに、StoreStats() をまとめて1回だけ呼ぶ作り
  • 実績IDやStat IDを散らばらせない設計

第3回時点では、累計値を更新するたびに StoreStats() を呼ぶシンプルな実装で十分です。実績が増えてきたタイミングで、第4回の管理方法に進んでください。

次回予告

ここまで第1回〜第3回で、Steam実績の基本的な3つの形を一通り扱いました。

  • 直接解除型: SetAchievement で瞬間的に解除する
  • リセット: ClearAchievement / ResetAllStats で開発中にやり直せるようにする
  • 累計型: Statsを更新して、Steam側に実績解除を任せる

実際の製品に組み込むと、もう少し気をつけたい点が出てきます。例えば、Steamが起動していないときの挙動、Stats受信が終わる前に GetStat を呼んでしまうと正しく動かないこと、複数の実績更新を同じフレームで行ったときに StoreStats() を1回にまとめたいこと、などです。

次回は、これらを整理した SteamAchievementManager を作り、複数の実績とStatsを安全に扱える形に発展させます。

Discussion