Unityを触り始めた頃って、つい Update() の中に「入力処理」「移動」「当たり判定」「UI更新」「データ保存」…と、全部まとめて書いてしまいがちですよね。
動いているうちはいいのですが、あとから「ゲームオーバー時に分析用データを送りたい」「ステージクリアの統計も取りたい」といった要件が増えてくると、巨大なスクリプトに手を入れるたびにバグが出たり、どこを触ればいいのか分からなくなります。

そこでこの記事では、「分析送信」という一つの責務だけを担当するコンポーネント AnalyticsSender を作って、
「どのステージで死んだか」「何秒でクリアしたか」といった統計データを、きれいに分離して送信できる形にしてみましょう。

【Unity】死亡ステージをスマートに記録!「AnalyticsSender」コンポーネント

ここでは、Unity標準の UnityWebRequest を使って、ゲーム内イベント(死亡・クリアなど)に応じて分析用データを送信するコンポーネントを実装します。
ポイントは以下の3つです。

  • 「分析送信」を 1コンポーネントに責務分離 して、ゲームロジックから切り離す
  • シーン名やステージID、死亡理由などを 柔軟に送れるAPI にする
  • 失敗してもゲーム進行を止めない 非同期送信(コルーチン) にする

フルコード: AnalyticsSender.cs


using System;
using System.Collections;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;

namespace Sample.Analytics
{
    /// <summary>
    /// ゲーム内の統計情報(どのステージで死んだか、クリアしたかなど)を
    /// サーバーに送信するためのコンポーネント。
    /// 
    /// ・ゲームロジックからは「イベントを通知する」ことだけに集中させる
    /// ・実際の送信処理はこのコンポーネントが担当する
    /// という責務分離を行うことで、コードの見通しを良くします。
    /// </summary>
    public class AnalyticsSender : MonoBehaviour
    {
        [Header("送信先設定")]
        [SerializeField]
        [Tooltip("分析サーバーのエンドポイントURL(例: https://example.com/analytics)")]
        private string endpointUrl = "https://example.com/analytics";

        [SerializeField]
        [Tooltip("送信時に付与するゲームIDやアプリID(任意)")]
        private string gameId = "com.example.mygame";

        [Header("送信オプション")]
        [SerializeField]
        [Tooltip("エディタ実行時にも送信を行うかどうか")]
        private bool sendInEditor = false;

        [SerializeField]
        [Tooltip("送信ログをConsoleに表示するかどうか")]
        private bool enableDebugLog = true;

        [SerializeField]
        [Tooltip("タイムアウト秒数(0以下でUnityのデフォルト)")]
        private int timeoutSeconds = 10;

        /// <summary>
        /// 「プレイヤーがステージで死亡した」ことを記録するためのAPI。
        /// 
        /// 例:
        /// analyticsSender.SendDeathEvent("Stage_1", "Spike", 35.2f);
        /// </summary>
        /// <param name="stageId">ステージIDやシーン名など</param>
        /// <param name="reason">死亡理由(例: "Enemy", "Fall", "TimeOver" など)</param>
        /// <param name="elapsedTime">ステージ開始から死亡までの経過時間(秒)</param>
        public void SendDeathEvent(string stageId, string reason, float elapsedTime)
        {
            var payload = new AnalyticsPayload
            {
                gameId = gameId,
                eventType = "death",
                stageId = stageId,
                reason = reason,
                elapsedTime = elapsedTime,
                timestampUtc = DateTime.UtcNow.ToString("o"), // ISO8601
                sessionId = GetOrCreateSessionId()
            };

            Send(payload);
        }

        /// <summary>
        /// 「プレイヤーがステージをクリアした」ことを記録するためのAPI。
        /// </summary>
        /// <param name="stageId">ステージIDやシーン名など</param>
        /// <param name="elapsedTime">ステージ開始からクリアまでの経過時間(秒)</param>
        /// <param name="score">スコア(不要なら0でOK)</param>
        public void SendClearEvent(string stageId, float elapsedTime, int score)
        {
            var payload = new AnalyticsPayload
            {
                gameId = gameId,
                eventType = "clear",
                stageId = stageId,
                reason = string.Empty,
                elapsedTime = elapsedTime,
                score = score,
                timestampUtc = DateTime.UtcNow.ToString("o"),
                sessionId = GetOrCreateSessionId()
            };

            Send(payload);
        }

        /// <summary>
        /// 任意のイベントタイプで送信したい場合の汎用API。
        /// 必要になったら拡張して使いましょう。
        /// </summary>
        public void SendCustomEvent(string eventType, string stageId, string detail)
        {
            var payload = new AnalyticsPayload
            {
                gameId = gameId,
                eventType = eventType,
                stageId = stageId,
                reason = detail,
                elapsedTime = 0f,
                timestampUtc = DateTime.UtcNow.ToString("o"),
                sessionId = GetOrCreateSessionId()
            };

            Send(payload);
        }

        /// <summary>
        /// 実際の送信処理を行う共通メソッド。
        /// 外部からは直接呼ばず、SendDeathEvent などのAPIを経由して使う想定です。
        /// </summary>
        /// <param name="payload">送信したいデータ</param>
        private void Send(AnalyticsPayload payload)
        {
#if UNITY_EDITOR
            if (!sendInEditor)
            {
                if (enableDebugLog)
                {
                    Debug.Log($"[AnalyticsSender] Editor上では送信をスキップしました: {payload.eventType}");
                }
                return;
            }
#endif
            if (string.IsNullOrEmpty(endpointUrl))
            {
                if (enableDebugLog)
                {
                    Debug.LogWarning("[AnalyticsSender] endpointUrl が設定されていません。送信を中止します。");
                }
                return;
            }

            // JSON文字列に変換
            string json = JsonUtility.ToJson(payload);

            if (enableDebugLog)
            {
                Debug.Log($"[AnalyticsSender] 送信開始: {json}");
            }

            // コルーチンで非同期送信
            StartCoroutine(SendCoroutine(json));
        }

        /// <summary>
        /// UnityWebRequest を使って非同期にPOST送信するコルーチン。
        /// </summary>
        private IEnumerator SendCoroutine(string json)
        {
            byte[] bodyRaw = Encoding.UTF8.GetBytes(json);

            using (UnityWebRequest request = new UnityWebRequest(endpointUrl, UnityWebRequest.kHttpVerbPOST))
            {
                request.uploadHandler = new UploadHandlerRaw(bodyRaw);
                request.downloadHandler = new DownloadHandlerBuffer();
                request.SetRequestHeader("Content-Type", "application/json; charset=utf-8");

                if (timeoutSeconds > 0)
                {
                    request.timeout = timeoutSeconds;
                }

                yield return request.SendWebRequest();

#if UNITY_2020_2_OR_NEWER
                if (request.result != UnityWebRequest.Result.Success)
#else
                if (request.isNetworkError || request.isHttpError)
#endif
                {
                    if (enableDebugLog)
                    {
                        Debug.LogWarning($"[AnalyticsSender] 送信失敗: {request.error}");
                    }
                }
                else
                {
                    if (enableDebugLog)
                    {
                        Debug.Log($"[AnalyticsSender] 送信成功: {request.responseCode}");
                    }
                }
            }
        }

        // ------------------------------
        // セッションID管理(簡易実装)
        // ------------------------------

        private const string PlayerPrefsSessionKey = "AnalyticsSender_SessionId";
        private string cachedSessionId;

        /// <summary>
        /// 1プレイ中で共通して使えるセッションIDを取得します。
        /// なければ新規作成して PlayerPrefs に保存します。
        /// </summary>
        private string GetOrCreateSessionId()
        {
            if (!string.IsNullOrEmpty(cachedSessionId))
            {
                return cachedSessionId;
            }

            // すでに保存されていればそれを使う
            if (PlayerPrefs.HasKey(PlayerPrefsSessionKey))
            {
                cachedSessionId = PlayerPrefs.GetString(PlayerPrefsSessionKey);
                return cachedSessionId;
            }

            // なければ新規発行(簡易的にGUIDを使用)
            cachedSessionId = Guid.NewGuid().ToString("N");
            PlayerPrefs.SetString(PlayerPrefsSessionKey, cachedSessionId);
            PlayerPrefs.Save();
            return cachedSessionId;
        }

        /// <summary>
        /// デバッグ用:セッションIDをリセットしたい場合に呼び出します。
        /// </summary>
        public void ResetSessionId()
        {
            cachedSessionId = null;
            PlayerPrefs.DeleteKey(PlayerPrefsSessionKey);
            if (enableDebugLog)
            {
                Debug.Log("[AnalyticsSender] セッションIDをリセットしました。");
            }
        }

        // ------------------------------
        // 送信データの定義
        // ------------------------------

        /// <summary>
        /// サーバーに送るJSONデータの型。
        /// JsonUtility でシリアライズできるように public フィールドにしています。
        /// </summary>
        [Serializable]
        private class AnalyticsPayload
        {
            public string gameId;
            public string eventType;     // "death", "clear", "custom" など
            public string stageId;       // ステージID or シーン名
            public string reason;        // 死亡理由や詳細テキスト
            public float elapsedTime;    // 経過時間(秒)
            public int score;            // スコア(不要なら0)
            public string timestampUtc;  // 送信時刻(UTC)
            public string sessionId;     // プレイセッションID
        }
    }
}

使い方の手順

ここからは、実際に「どのステージで死んだか」を送信する具体的な例を交えながら、使い方をステップ形式で見ていきましょう。

  1. ① AnalyticsSender コンポーネントを用意する
    • プロジェクト内で AnalyticsSender.cs を作成し、上記コードを貼り付けて保存します。
    • シーン内に空の GameObject を作成し、名前を Analytics などにします。
    • その GameObject に AnalyticsSender コンポーネントをアタッチします。
    • Endpoint Url に、実際に受け取りたいサーバーのURLを設定します(テスト時はダミーURLでもOK)。
  2. ② ゲームロジック側から「死亡イベント」を通知する
    例として、プレイヤーがダメージを受けて死亡したときに送信するスクリプトを用意します。
    プレイヤーの GameObject に以下のような簡単な死亡処理スクリプトをアタッチしてみましょう。
    
    using UnityEngine;
    using Sample.Analytics;
    
    public class PlayerDeathExample : MonoBehaviour
    {
        [SerializeField]
        private string stageId = "Stage_1";
    
        [SerializeField]
        private float startTime;
    
        private AnalyticsSender analyticsSender;
    
        private void Start()
        {
            // ステージ開始時間を記録
            startTime = Time.time;
    
            // シーン内のAnalyticsSenderを探す(本番ではDIやシングルトンで管理してもOK)
            analyticsSender = FindObjectOfType<AnalyticsSender>();
            if (analyticsSender == null)
            {
                Debug.LogWarning("[PlayerDeathExample] AnalyticsSender がシーン内に見つかりません。分析送信は行われません。");
            }
        }
    
        // 何らかのトリガーで死亡したと仮定
        public void Die(string reason)
        {
            float elapsed = Time.time - startTime;
    
            if (analyticsSender != null)
            {
                analyticsSender.SendDeathEvent(stageId, reason, elapsed);
            }
    
            // ここにゲームオーバー処理などを記述
            Debug.Log($"Player died at {stageId}, reason: {reason}, elapsed: {elapsed}");
        }
    
        // デモ用:スペースキーで死亡させてみる
        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                Die("Demo_SpaceKey");
            }
        }
    }
    

    これで、スペースキーを押して死亡させると、AnalyticsSender 経由で「Stage_1で死亡した」「理由はDemo_SpaceKey」「経過時間は〇秒」といったデータが送信されます。

  3. ③ ステージクリア時にも統計を送る
    同じく、ステージクリア時に呼ばれる処理から SendClearEvent を呼び出せばOKです。
    例えば、動く床やゴールオブジェクトに以下のようなスクリプトを付けておきます。
    
    using UnityEngine;
    using Sample.Analytics;
    
    public class StageGoalExample : MonoBehaviour
    {
        [SerializeField]
        private string stageId = "Stage_1";
    
        [SerializeField]
        private int scoreOnClear = 100;
    
        private float startTime;
        private AnalyticsSender analyticsSender;
    
        private void Start()
        {
            startTime = Time.time;
            analyticsSender = FindObjectOfType<AnalyticsSender>();
        }
    
        private void OnTriggerEnter(Collider other)
        {
            if (!other.CompareTag("Player")) return;
    
            float elapsed = Time.time - startTime;
    
            if (analyticsSender != null)
            {
                analyticsSender.SendClearEvent(stageId, elapsed, scoreOnClear);
            }
    
            Debug.Log($"Stage cleared: {stageId}, time: {elapsed}, score: {scoreOnClear}");
            // ここで次のステージをロードするなどの処理を行う
        }
    }
    

    プレイヤーがゴールに触れたタイミングで、クリア情報が送信されるようになります。

  4. ④ 敵やトラップなど、複数の死亡要因から使い回す
    敵やトラップなど、死亡理由が複数ある場合でも、ゲームロジック側では「どこで死んだか/何で死んだか」を決めるだけにしておき、送信処理はすべて AnalyticsSender に任せられます。
    
    using UnityEngine;
    using Sample.Analytics;
    
    public class SpikeTrap : MonoBehaviour
    {
        [SerializeField]
        private string stageId = "Stage_2";
    
        private AnalyticsSender analyticsSender;
    
        private void Start()
        {
            analyticsSender = FindObjectOfType<AnalyticsSender>();
        }
    
        private void OnTriggerEnter(Collider other)
        {
            if (!other.CompareTag("Player")) return;
    
            if (analyticsSender != null)
            {
                // トゲトラップで死亡した、という情報を送る
                analyticsSender.SendDeathEvent(stageId, "SpikeTrap", Time.timeSinceLevelLoad);
            }
    
            // プレイヤーに死亡処理を呼ぶなど
            Debug.Log("Player killed by SpikeTrap.");
        }
    }
    

    こうしておけば、ステージが増えても、「どこで死んだか」を簡単に集計できるようになります。

メリットと応用

AnalyticsSender を使う最大のメリットは、「分析送信」という関心事をゲームロジックから切り離せる ことです。

  • プレイヤーや敵のスクリプトは「死んだ」「クリアした」という状態変化だけを管理すればよい
  • 送信先URLの変更や、送信形式の変更は AnalyticsSender だけを触ればよい
  • プレハブ化しておけば、どのシーンにも同じ分析機能を簡単に持ち込める

レベルデザインの観点でも、

  • 「Stage_3 の中ボスでの死亡率が高い」
  • 「落下トラップよりも敵弾で死んでいるプレイヤーが多い」

といった情報が取れるようになるので、どこを調整すべきか が見えやすくなります。
また、プレハブとして Analytics オブジェクト(中に AnalyticsSender を含む)を作っておけば、新しいステージシーンにドラッグ&ドロップするだけで分析基盤を共有できます。

さらに、応用として 「イベントキュー」 のような仕組みを追加すれば、通信失敗時にローカルにキューしておき、次回起動時に再送する、といった高度なことも可能です。

簡単な改造案として、「エディタ専用のテスト送信ボタン」を追加してみましょう。
Unityの ContextMenu 属性を使うと、インスペクタの右クリックメニューから呼び出せるデバッグ関数を生やせます。


        /// <summary>
        /// インスペクタのコンテキストメニューからテスト送信を行うためのサンプル。
        /// 右クリック > Test Send Death Event で実行できます。
        /// </summary>
        [ContextMenu("Test Send Death Event")]
        private void TestSendDeathEventFromContextMenu()
        {
            SendDeathEvent("TestStage", "EditorTest", 12.34f);
        }

このように、小さなコンポーネント単位で責務を分けておくと、
「分析機能だけ差し替える」「本番環境だけ実際のURLに切り替える」といった運用がとても楽になります。
巨大なGodクラスに詰め込まず、「AnalyticsSender = 分析送信担当」 という役割をはっきりさせていきましょう。