【Cocos Creator 3.8】AnalyticsSender の実装:アタッチするだけで「どのステージで死んだか」などの統計データを送信する汎用スクリプト
このガイドでは、Cocos Creator 3.8.7 と TypeScript で、ゲーム内の「死亡イベント」や任意のタイミングで統計データを送信できる汎用コンポーネント AnalyticsSender を実装します。
特定の GameManager などに依存せず、このコンポーネント単体をノードにアタッチして、インスペクタから送信先URLやステージIDなどを設定するだけで使えるようにします。
代表的な例として、「どのステージで死んだか」を送信するケースをベースにしつつ、同じ仕組みで「クリア時」「ボタン押下時」などにも使い回せる設計にします。
コンポーネントの設計方針
要件整理
- HTTP(S) 経由で分析サーバー(またはモックURL)に JSON データを送信する。
- 「どのステージで死んだか」を送信できるように、ステージID や 死亡原因 などをプロパティで設定可能にする。
- 「いつ送るか」を柔軟にするため、以下のトリガーをサポートする:
- ノードが有効化されたとき(
onEnable)に自動送信 - 任意のタイミングで
sendNow()を呼び出して送信(他スクリプトからの呼び出しも可能) - 一定時間経過後に自動送信
- ノードが有効化されたとき(
- 外部のカスタムスクリプトには一切依存しない。必要な情報は全てインスペクタから設定。
- 送信に失敗した場合はエラーログを出し、成功/失敗をコンソールで確認できるようにする。
- エディタ上での動作確認がしやすいように、送信内容をコンソールにダンプするオプションを用意する。
インスペクタで設定可能なプロパティ
AnalyticsSender では、以下のようなプロパティを用意します。
- endpointUrl: string
- 分析サーバーのエンドポイントURL。
- 例:
https://example.com/analytics - 空の場合は送信を行わず、警告ログを出す。
- autoSendOnEnable: boolean
- ノードが有効化されたタイミング(
onEnable)で自動送信するかどうか。 - デバッグ用途や「シーン開始時に1回だけ送信したい」ケースに便利。
- ノードが有効化されたタイミング(
- autoSendDelay: number
autoSendOnEnableが true のときに有効。- ノード有効化から何秒後に送信するか。
- 0 の場合は即時送信。
- eventType: string
- 送信するイベントの種類を表す文字列。
- 例:
player_death(プレイヤー死亡)stage_clear(ステージクリア)button_click(ボタンクリック)
- サーバー側でイベント種別を判別するためのキーとして利用。
- stageId: string
- どのステージかを表すID。
- 例:
stage_1,tutorial,boss_3など。 - 「どのステージで死んだか」を記録したい場合はここにステージ名を設定。
- cause: string
- 死亡原因やイベントの詳細理由。
- 例:
hit_enemy,fall_pit,timeoutなど。 - 任意の文字列。空でも可。
- playerId: string
- プレイヤーを識別するID。
- ログインIDや端末IDがある場合はここに設定(なければ空でも可)。
- 外部スクリプトに依存しないため、ここも手動設定 or コードから設定する前提。
- extraJson: string
- 追加で送りたい任意のJSON文字列。
- 例:
{"hp": 10, "score": 500, "weapon": "sword"} - JSONパースに失敗した場合は警告を出し、このフィールドは無視される。
- logPayloadInConsole: boolean
- 送信前に、実際に送るJSONペイロードをコンソールに表示するかどうか。
- 開発・デバッグ時に true、本番では false 推奨。
- timeoutMs: number
- HTTPリクエストのタイムアウト時間(ミリ秒)。
- 0 以下の場合はタイムアウトを設定しない(ブラウザ/ランタイム依存)。
このコンポーネントは、標準コンポーネント(Spriteなど)を必須としないため、特別な getComponent チェックは不要です。
ただし、fetch が利用できない特殊環境の場合はエラーになる可能性があるため、その点はログで通知します。
TypeScriptコードの実装
import { _decorator, Component, JsonAsset } from 'cc';
const { ccclass, property } = _decorator;
/**
* AnalyticsSender
*
* 任意のタイミングで分析サーバーにJSONデータを送信する汎用コンポーネント。
* - 他のカスタムスクリプトに依存しない。
* - インスペクタから送信先URLやイベント種別などを設定可能。
* - onEnable時の自動送信、遅延送信、手動送信(sendNow)に対応。
*/
@ccclass('AnalyticsSender')
export class AnalyticsSender extends Component {
@property({
tooltip: '分析サーバーのエンドポイントURL。\n例: https://example.com/analytics\n空の場合は送信を行わず警告を表示します。'
})
public endpointUrl: string = '';
@property({
tooltip: 'ノードが有効化されたときに自動で送信を行うかどうか。'
})
public autoSendOnEnable: boolean = false;
@property({
tooltip: 'autoSendOnEnable が true のときのみ有効。\nノード有効化から何秒後に送信するかを指定します。\n0 の場合は即時送信します。'
})
public autoSendDelay: number = 0;
@property({
tooltip: 'イベントの種類を表す文字列。\n例: player_death, stage_clear, button_click など。'
})
public eventType: string = 'player_death';
@property({
tooltip: 'ステージIDやレベル名など。\n例: stage_1, tutorial, boss_3 など。'
})
public stageId: string = '';
@property({
tooltip: '死亡原因やイベントの詳細理由。\n例: hit_enemy, fall_pit, timeout など。\n任意の文字列で、空でも構いません。'
})
public cause: string = '';
@property({
tooltip: 'プレイヤーを識別するID。\nログインIDや端末IDなどを手動で設定してください。\n空でも構いません。'
})
public playerId: string = '';
@property({
tooltip: '追加で送りたい任意のJSON文字列。\n例: {"hp": 10, "score": 500, "weapon": "sword"}\n無効なJSONの場合は無視されます。'
})
public extraJson: string = '';
@property({
tooltip: '送信前に、実際に送るJSONペイロードをコンソールに表示するかどうか。\n開発時は true、本番では false を推奨します。'
})
public logPayloadInConsole: boolean = true;
@property({
tooltip: 'HTTPリクエストのタイムアウト時間(ミリ秒)。\n0 以下の場合はタイムアウトなしとして扱います。'
})
public timeoutMs: number = 5000;
@property({
type: JsonAsset,
tooltip: '任意のJSONアセットを指定すると、その内容を extraJson よりも後にマージして送信します。\nキーが重複する場合は、このJSONアセット側が優先されます。\n必須ではありません。'
})
public extraJsonAsset: JsonAsset | null = null;
// 内部フラグ: すでに自動送信を行ったかどうか
private _autoSent: boolean = false;
onLoad () {
// 特に必須コンポーネントはないため、ここではログのみ。
if (typeof fetch === 'undefined') {
console.error('[AnalyticsSender] この環境では fetch が利用できないため、HTTP送信が行えません。');
}
}
onEnable () {
// autoSendOnEnable が true の場合にのみ自動送信を行う。
if (this.autoSendOnEnable && !this._autoSent) {
this._autoSent = true;
if (this.autoSendDelay > 0) {
// 遅延送信
this.scheduleOnce(() => {
this.sendNow().catch((err) => {
console.error('[AnalyticsSender] 自動送信中にエラーが発生しました:', err);
});
}, this.autoSendDelay);
} else {
// 即時送信
this.sendNow().catch((err) => {
console.error('[AnalyticsSender] 自動送信中にエラーが発生しました:', err);
});
}
}
}
/**
* 任意のタイミングで呼び出して送信するための公開メソッド。
* 他のスクリプトから this.node.getComponent(AnalyticsSender).sendNow() のように利用できます。
*/
public async sendNow(): Promise<void> {
if (!this.endpointUrl) {
console.warn('[AnalyticsSender] endpointUrl が設定されていないため、送信を中止します。');
return;
}
if (typeof fetch === 'undefined') {
console.error('[AnalyticsSender] この環境では fetch が利用できないため、HTTP送信が行えません。');
return;
}
const payload = this._buildPayload();
if (this.logPayloadInConsole) {
console.log('[AnalyticsSender] 送信ペイロード:', JSON.stringify(payload, null, 2));
}
try {
const controller = (this.timeoutMs > 0 && typeof AbortController !== 'undefined')
? new AbortController()
: null;
let timeoutId: any = null;
if (controller && this.timeoutMs > 0) {
timeoutId = setTimeout(() => {
controller.abort();
}, this.timeoutMs);
}
const response = await fetch(this.endpointUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
signal: controller ? controller.signal : undefined,
});
if (timeoutId) {
clearTimeout(timeoutId);
}
if (!response.ok) {
console.error('[AnalyticsSender] 送信に失敗しました。HTTPステータス:', response.status, response.statusText);
} else {
console.log('[AnalyticsSender] 送信に成功しました。HTTPステータス:', response.status);
}
} catch (error: any) {
if (error && error.name === 'AbortError') {
console.error('[AnalyticsSender] リクエストがタイムアウトしました。timeoutMs =', this.timeoutMs);
} else {
console.error('[AnalyticsSender] 送信中にエラーが発生しました:', error);
}
}
}
/**
* 実際に送信するJSONペイロードを組み立てる内部メソッド。
*/
private _buildPayload(): any {
const basePayload: any = {
eventType: this.eventType,
stageId: this.stageId,
cause: this.cause,
playerId: this.playerId,
timestamp: Date.now(), // クライアント時刻
nodeName: this.node.name, // どのノードから送信されたか
};
// extraJson (文字列) をパースしてマージ
if (this.extraJson && this.extraJson.trim().length > 0) {
try {
const parsed = JSON.parse(this.extraJson);
if (parsed && typeof parsed === 'object') {
Object.assign(basePayload, parsed);
} else {
console.warn('[AnalyticsSender] extraJson はオブジェクトではありません。無視します。');
}
} catch (e) {
console.warn('[AnalyticsSender] extraJson のJSONパースに失敗しました。無視します。値:', this.extraJson);
}
}
// extraJsonAsset (JsonAsset) をマージ(文字列よりも後にマージするので、こちらが優先)
if (this.extraJsonAsset && this.extraJsonAsset.json) {
if (typeof this.extraJsonAsset.json === 'object') {
Object.assign(basePayload, this.extraJsonAsset.json);
} else {
console.warn('[AnalyticsSender] extraJsonAsset の内容がオブジェクトではありません。無視します。');
}
}
return basePayload;
}
}
コードのポイント解説
- onLoad
fetchが存在しない環境を検出し、エラーログを出します。- 必須コンポーネントはないため、
getComponentチェックは不要です。
- onEnable
autoSendOnEnableが true の場合のみ、自動送信を行います。autoSendDelayが 0 より大きければscheduleOnceで遅延送信、0 なら即時送信します。- 2重送信防止のため、内部フラグ
_autoSentで一度だけ自動送信するようにしています。
- sendNow()
- 公開メソッドとして定義し、他スクリプトからも呼び出せるようにしています。
endpointUrlが空なら警告を出して送信を中止します。_buildPayload()で組み立てた JSON をfetchで POST 送信します。timeoutMs> 0 かつAbortControllerが利用可能な場合、タイムアウト処理を行います。- 成功/失敗/タイムアウトそれぞれで分かりやすいログを出します。
- _buildPayload()
- 基本フィールド(eventType, stageId, cause, playerId, timestamp, nodeName)を設定。
extraJson(文字列)を JSON.parse し、オブジェクトであればObject.assignでマージ。extraJsonAsset(JsonAsset)が指定されていれば、そのjsonを更にマージ(こちらが優先)。- パースエラーや不正な型の場合は警告ログを出し、そのデータは無視します。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリックします。
Create → TypeScriptを選択します。- ファイル名を
AnalyticsSender.tsに変更します。 - 作成された
AnalyticsSender.tsをダブルクリックし、上記のコード全文を貼り付けて保存します。
2. テスト用シーン/ノードの準備
- Hierarchy パネルで右クリックし、
Create → 2D Object → Spriteなど、任意のノードを作成します。- 名前を分かりやすく
AnalyticsTestNodeなどに変更しておくと便利です。
- 名前を分かりやすく
3. コンポーネントをノードにアタッチ
- Hierarchy で先ほど作成した
AnalyticsTestNodeを選択します。 - Inspector パネル下部の
Add Componentボタンをクリックします。 Customカテゴリ内からAnalyticsSenderを選択して追加します。
4. プロパティの設定例(「どのステージで死んだか」を送信)
Inspector で AnalyticsSender の各プロパティを以下のように設定してみます。
- endpointUrl:
- テスト用として、リクエスト内容を確認しやすいサービスなどを使うとよいです。
- 例:
https://httpbin.org/post(レスポンスに送信したJSONが含まれるため確認しやすい)
- autoSendOnEnable:
true - autoSendDelay:
0(シーン再生直後に送信されます) - eventType:
player_death - stageId:
stage_1 - cause:
hit_enemy - playerId:
test_player_001 - extraJson:
- 例:
{"hp": 0, "score": 1200, "retryCount": 3}
- 例:
- logPayloadInConsole:
true - timeoutMs:
5000 - extraJsonAsset: 空のままでOK(必要になったら後で設定)
5. シーンを再生して動作確認
- エディタ右上の
Playボタンを押してシーンを再生します。 - ゲームビューが立ち上がったら、Console(ログ出力)を確認します。
[AnalyticsSender] 送信ペイロード: ...として、送信されるJSONが表示されているはずです。- さらに、
[AnalyticsSender] 送信に成功しました。HTTPステータス: 200のようなログが出ていれば送信成功です。 - エラーの場合は、ステータスコードやエラーメッセージが表示されます。
https://httpbin.org/postを使っている場合:- ブラウザのデベロッパーツールなどでレスポンスを確認すると、
jsonフィールドに送信した内容がそのまま返ってきます。
- ブラウザのデベロッパーツールなどでレスポンスを確認すると、
6. 任意タイミングでの送信(sendNow の利用例)
例えば「プレイヤーが実際に死んだタイミングで送信したい」場合、死亡処理を行うスクリプトから sendNow() を呼び出します。
// 例: PlayerController.ts 内など
import { _decorator, Component, Node } from 'cc';
import { AnalyticsSender } from './AnalyticsSender';
const { ccclass, property } = _decorator;
@ccclass('PlayerController')
export class PlayerController extends Component {
// 何らかの死亡処理
public onPlayerDead(cause: string) {
// 同じノード、または任意のノードに付いている AnalyticsSender を取得
const analytics = this.getComponent(AnalyticsSender);
if (analytics) {
analytics.cause = cause; // 死亡原因を更新
analytics.stageId = 'stage_2'; // 必要ならステージIDも更新
analytics.sendNow(); // このタイミングで送信
} else {
console.warn('[PlayerController] AnalyticsSender コンポーネントが見つかりませんでした。');
}
// 残りの死亡処理...
}
}
このように、AnalyticsSender 自体は他スクリプトに依存していませんが、必要であれば他スクリプトからプロパティを更新して再利用できます。
7. extraJsonAsset の利用(共通情報の追加)
全ての分析イベントに共通して送りたい情報(例: アプリバージョン、プラットフォーム名など)がある場合は、JSONアセットを利用すると便利です。
- Assets パネルで右クリック →
Create → Jsonを選択し、CommonAnalyticsData.jsonを作成します。 - 作成した JSON ファイルを開き、例えば以下のような内容にします:
{ "appVersion": "1.0.0", "platform": "web", "buildNumber": 100 } AnalyticsTestNodeの Inspector で、AnalyticsSenderの extraJsonAsset にこの JSON アセットをドラッグ&ドロップして設定します。- 再度シーンを再生すると、送信ペイロードに
appVersion,platform,buildNumberが追加されていることが確認できます。
まとめ
AnalyticsSender コンポーネントは、
- 送信先URL、イベント種別、ステージID、死亡原因などを インスペクタから設定でき、
- ノードに アタッチするだけで「シーン開始時に1回送信」「一定時間後に送信」などが簡単に実現でき、
sendNow()メソッドを通じて、任意タイミングでの送信にも対応する
という汎用的な分析送信スクリプトです。
このコンポーネントは、外部の GameManager やシングルトンに依存せず、単体で完結しているため、
- 死亡イベント以外にも「ステージクリア」「アイテム獲得」「ボタン押下」など、様々なイベントに対して簡単に使い回せる
- プロジェクト間でのコピー&ペーストやパッケージ化が容易
- 分析の設計変更(送信内容の追加・削除)が この1ファイル+インスペクタ設定 だけで完結する
といったメリットがあります。
まずはテスト用URLに対して送信内容を確認しつつ、自分のゲームに必要なフィールド(例: スコア、プレイ時間、選択キャラなど)を extraJson や JSONアセットで拡張していくと、実運用に耐える分析基盤を手軽に構築できます。




