Godotでゲームを作っていると、「どのステージでプレイヤーがよく死ぬのか」とか、「どの敵が一番危険なのか」といった統計情報を取りたくなりますよね。
でも、素直に実装しようとすると…
- 各ステージごとに
HTTPRequestノードを置いて、専用スクリプトを書く Player.gdの中で直接HTTPアクセスを書き始めて、どんどん肥大化する- 「ステージID」「デス原因」などのパラメータを、毎回ベタ書きする
こんな感じで、「継承された巨大なPlayerクラス」や「ステージごとにバラバラな実装」になりがちです。
しかも、あとから「送信先URLを変えたい」「送信フォーマットを変えたい」と思ったときに、あちこち修正する羽目になります。
そこで今回は、どのノードにもポン付けできる「分析送信コンポーネント」として、AnalyticsSender を用意してみましょう。
プレイヤーでも敵でも、チェックポイントでも、「死んだ」「クリアした」「到達した」といったイベントをコンポーネントに投げるだけで、裏側でHTTP送信してくれる仕組みです。
【Godot 4】イベントを投げるだけで分析ログ送信!「AnalyticsSender」コンポーネント
以下が、AnalyticsSender コンポーネントのフルコードです。
シーンのどこかにこのノードを追加して、他ノードから send_event() を呼ぶだけで使えるようにしています。
extends Node
class_name AnalyticsSender
## 分析用イベントをHTTP経由で送信するコンポーネント
##
## 使い方イメージ:
## - シーンのどこかに AnalyticsSender を1つ置く
## - Player や Enemy から:
## analytics_sender.send_event("player_death", {"stage": "Stage1", "cause": "spike"})
## のように呼び出すだけ
@export_category("Network Settings")
## 分析サーバーのエンドポイントURL
@export var endpoint_url: String = "https://example.com/analytics"
## HTTPメソッド。基本は POST を想定
@export_enum("POST", "GET") var http_method: String = "POST"
## タイムアウト秒数(0以下で無制限)
@export var timeout_seconds: float = 10.0
@export_category("Game Context")
## ゲーム全体で共通のタイトル名やゲームID
@export var game_id: String = "my_cool_game"
## バージョン情報。ABテストやバージョン比較に便利
@export var game_version: String = "1.0.0"
## 現在のステージID。シーンごとに上書きしてもOK
@export var stage_id: String = ""
@export_category("Privacy & Debug")
## 実機ビルドでのみ送信し、エディタ実行中は送信しない
@export var disable_in_editor: bool = true
## ログをコンソールに表示するか(開発中のデバッグ用)
@export var verbose_log: bool = true
## 実際に送信せず、コンソールに内容だけ出す「ドライラン」モード
@export var dry_run: bool = false
## 内部で使う HTTPRequest ノード
var _http_request: HTTPRequest
func _ready() -> void:
# HTTPRequest ノードを自動で生成して子として追加
_http_request = HTTPRequest.new()
add_child(_http_request)
_http_request.request_completed.connect(_on_request_completed)
_http_request.timeout = int(timeout_seconds)
if verbose_log:
print("[AnalyticsSender] Ready. endpoint_url=%s, method=%s" % [endpoint_url, http_method])
# ステージIDが未設定なら、現在のシーン名を使う
if stage_id.is_empty():
var tree := get_tree()
if tree and tree.current_scene:
stage_id = tree.current_scene.name
## 分析イベントを送信するメインAPI
##
## event_name: "player_death", "stage_clear" などのイベント名
## payload: イベント固有の追加データ(辞書)。任意。
func send_event(event_name: String, payload: Dictionary = {}) -> void:
# エディタ実行中は送信をスキップ(設定による)
if disable_in_editor and Engine.is_editor_hint():
if verbose_log:
print("[AnalyticsSender] Skip send_event in editor: %s" % event_name)
return
# エンドポイント未設定なら何もしない
if endpoint_url.is_empty():
push_warning("[AnalyticsSender] endpoint_url is empty. Event '%s' not sent." % event_name)
return
# ベースの共通データ
var body: Dictionary = {
"game_id": game_id,
"game_version": game_version,
"stage_id": stage_id,
"event_name": event_name,
"timestamp": Time.get_unix_time_from_system(),
}
# 追加のペイロードをマージ
for key in payload.keys():
body[key] = payload[key]
if verbose_log:
print("[AnalyticsSender] send_event: %s" % JSON.stringify(body))
# ドライランモードなら送信しない
if dry_run:
return
# JSONにエンコード
var json_body := JSON.stringify(body)
var headers := [
"Content-Type: application/json",
]
var err: Error
if http_method == "GET":
# GETの場合、クエリパラメータとして送る簡易実装
var url_with_query := endpoint_url + "?data=" + URI.encode_component(json_body)
err = _http_request.request(url_with_query)
else:
# POSTを想定
err = _http_request.request(endpoint_url, headers, HTTPClient.METHOD_POST, json_body)
if err != OK:
push_warning("[AnalyticsSender] HTTP request error: %s" % error_string(err))
## HTTPRequest のコールバック
func _on_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
if not verbose_log:
return
var body_text := ""
if body.size() > 0:
body_text = body.get_string_from_utf8()
print("[AnalyticsSender] Request completed. result=%d, code=%d, body=%s" % [
result,
response_code,
body_text,
])
## 分かりやすいヘルパー関数例: 「プレイヤー死亡」を送信
##
## cause: "spike", "enemy", "fall" など
## extra: 追加情報(例: {"hp": 0, "x": 120.0, "y": 200.0})
func send_player_death(cause: String, extra: Dictionary = {}) -> void:
var payload: Dictionary = {
"cause": cause,
}
for key in extra.keys():
payload[key] = extra[key]
send_event("player_death", payload)
## ステージクリアの送信用ヘルパー
func send_stage_clear(time_sec: float, extra: Dictionary = {}) -> void:
var payload: Dictionary = {
"clear_time": time_sec,
}
for key in extra.keys():
payload[key] = extra[key]
send_event("stage_clear", payload)
使い方の手順
ここでは、2Dアクションゲームを例に、プレイヤーが死んだときに「どのステージで」「何が原因で」死んだかを送信する流れを見ていきましょう。
手順①: シーンに AnalyticsSender を追加する
まずは、ゲーム全体のルート(例: Main シーン)か、各ステージシーンに AnalyticsSender を1つ追加します。
Main (Node) ├── Player (CharacterBody2D) ├── LevelTiles (TileMap) ├── UI (CanvasLayer) └── AnalyticsSender (Node)
AnalyticsSender ノードを選択して、インスペクタで以下を設定します。
endpoint_url: 実際の分析サーバーのURL(例:https://your-server.com/api/analytics)game_id: ゲーム固有のID(例:"my_platformer")game_version: バージョン(例:"1.2.3")stage_id: ステージごとに上書きしたい場合はここに"Stage1"などを入力。
空なら現在シーン名が自動で使われます。- 開発中は
dry_run = trueにしておくと、安全にログだけ確認できます。
手順②: Player から AnalyticsSender を参照する
プレイヤーのスクリプトから、AnalyticsSender を呼び出せるようにします。
シンプルに「シーンルートから探す」か、「エクスポート変数でドラッグ&ドロップ参照」する方法が楽です。
例: Player.gd(CharacterBody2D)
extends CharacterBody2D
@export var analytics_sender: AnalyticsSender
func _ready() -> void:
# シーンツリーから自動で探すフォールバック
if analytics_sender == null:
analytics_sender = get_tree().current_scene.get_node_or_null("AnalyticsSender")
シーン構成図はこんなイメージです。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── (スクリプト: Player.gd) Main (Node) ├── Player (CharacterBody2D) ├── ... └── AnalyticsSender (Node) ← コンポーネント
手順③: 死亡時に send_player_death() を呼ぶ
プレイヤーが死んだタイミングで、AnalyticsSender にイベントを投げましょう。
プレイヤーのHP管理やダメージ処理の中に、1行だけ追加するイメージです。
func die(cause: String) -> void:
# 死亡アニメーションやリトライ処理など…
queue_free()
# 位置情報なども一緒に送りたい場合
if analytics_sender:
analytics_sender.send_player_death(cause, {
"x": global_position.x,
"y": global_position.y,
})
例えば、トゲに当たったときは cause = "spike"、
穴に落ちたときは cause = "fall" のように分けておくと、後で分析しやすくなります。
手順④: 他のイベントにも使い回す(敵・動く床・チェックポイントなど)
AnalyticsSender は「プレイヤー死亡」専用ではなく、どんなイベントにも使える汎用コンポーネントです。
例えば、
- 敵死亡イベント:
send_event("enemy_killed", {"enemy_type": "slime"}) - 動く床から落ちた回数:
send_event("fall_from_moving_platform", {...}) - チェックポイント到達:
send_event("checkpoint_reached", {"index": 2})
敵シーンの例:
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── (スクリプト: Enemy.gd)
extends CharacterBody2D
@export var analytics_sender: AnalyticsSender
@export var enemy_type: String = "slime"
func _ready() -> void:
if analytics_sender == null:
analytics_sender = get_tree().current_scene.get_node_or_null("AnalyticsSender")
func die() -> void:
queue_free()
if analytics_sender:
analytics_sender.send_event("enemy_killed", {
"enemy_type": enemy_type,
})
このように、PlayerやEnemy自体は「分析の具体的な送信処理」を一切知らず、
「AnalyticsSenderにイベント名と辞書を投げるだけ」という形にしておくと、コンポーネント指向らしい綺麗な分離になりますね。
メリットと応用
AnalyticsSender コンポーネントを使うことで、次のようなメリットがあります。
- 巨大なPlayerクラスを避けられる
HTTP送信やJSON整形といった「インフラ寄りの処理」を、プレイヤーや敵のロジックから切り離せます。 - シーン構造がシンプルになる
各ステージにバラバラなHTTPRequestノードを置く必要がなく、AnalyticsSender1つに集約できます。 - 使い回しがしやすい
別プロジェクトにも、AnalyticsSender.gdをコピペして、エンドポイントURLを変えるだけで流用可能です。 - 後から仕様変更しても安全
送信フォーマットやヘッダ、エンドポイントを変えたくなっても、修正箇所はコンポーネント1か所だけです。 - テストが楽
dry_runやverbose_logを使えば、実際にサーバーを立てなくてもログの中身を確認できます。
応用としては、
- セッションIDやユーザーIDを付与して、プレイヤー単位での行動分析
- 「1プレイ中のイベント履歴」をローカルに貯めて、まとめて送信するバッチモード
- Godotの
ConfigFileやProjectSettingsからエンドポイントを読み込む
など、いろいろ発展させられます。
最後に、セッションIDを自動生成して全イベントに付ける改造案のコード例を載せておきます。
var session_id: String = ""
func _ready() -> void:
_http_request = HTTPRequest.new()
add_child(_http_request)
_http_request.request_completed.connect(_on_request_completed)
_http_request.timeout = int(timeout_seconds)
# 起動ごとにランダムなセッションIDを生成
session_id = "%s-%d" % [OS.get_unique_id(), Time.get_unix_time_from_system()]
func send_event(event_name: String, payload: Dictionary = {}) -> void:
if disable_in_editor and Engine.is_editor_hint():
return
if endpoint_url.is_empty():
push_warning("[AnalyticsSender] endpoint_url is empty. Event '%s' not sent." % event_name)
return
var body: Dictionary = {
"game_id": game_id,
"game_version": game_version,
"stage_id": stage_id,
"event_name": event_name,
"timestamp": Time.get_unix_time_from_system(),
"session_id": session_id, # ★ ここでセッションIDを付与
}
for key in payload.keys():
body[key] = payload[key]
# 以下、送信処理は先ほどの実装と同様…
こんな感じで、「AnalyticsSender」をベースに自分のゲームに合った分析コンポーネントへ育てていくと、
継承地獄にハマらず、スッキリしたコンポーネント指向の設計ができて気持ちいいですね。
