Godotでゲームを配布しはじめると、だんだん気になってくるのが「ユーザーの手元のバージョン、ちゃんと最新かな?」という問題ですよね。
素直にやろうとすると:
- 起動時にHTTPリクエストを投げるノードをシーンに置く
- レスポンスをパースして、今のバージョンと比較する
- UI側に「アップデートありますよ」ダイアログを出す
…といった処理を、毎回ゲームごとに書くことになります。さらに、Autoload に書くか、Main シーンに書くか、UIシーンに書くかで迷ったり、HTTPRequest ノードのシグナル接続が増えてシーンツリーがごちゃっとしてきたり。
そこで、起動時のバージョン確認を「1つのコンポーネント」に閉じ込めて、どのシーンにもポン付けできるようにしてしまいましょう。
今回紹介する 「VersionChecker」コンポーネント は、Godot 4 の HTTPClient / HTTPRequest をラップして、
- 起動時に自動でHTTPリクエスト
- 最新バージョンの JSON を取得して比較
- シグナルで「最新です / 古いです / エラー」を通知
といった処理を、どのノードにも「合成」できるようにしたものです。
深い継承ツリーや、メインシーンにベタ書きするのではなく、必要なノードに VersionChecker をアタッチするだけで済むようにしていきます。
【Godot 4】起動時にサクッと最新版チェック!「VersionChecker」コンポーネント
フルコード(GDScript / Godot 4)
## VersionChecker.gd
## 起動時にHTTPで最新版バージョンを確認するコンポーネント
## - 任意のノードにアタッチして使えるように、Node を継承
## - シグナルで結果を通知するので、UIやゲームロジックと疎結合で連携できる
extends Node
class_name VersionChecker
## ===========================
## エディタから設定できるパラメータ
## ===========================
@export_group("Version Settings")
## 現在のゲームバージョン
## 例: "1.0.0" など。ビルドスクリプトから自動で書き換える運用もおすすめです。
@export var current_version: String = "1.0.0"
## バージョン情報を取得するURL
## 例: "https://example.com/game/version.json"
## レスポンスは {"version": "1.2.3", "url": "https://.../download"} のようなJSONを想定。
@export var version_check_url: String = ""
## HTTPメソッド(通常はGETでOK)
@export var http_method: HTTPClient.Method = HTTPClient.METHOD_GET
## 起動時に自動でチェックするかどうか
@export var check_on_ready: bool = true
## タイムアウト秒数(0以下でタイムアウトなし)
@export_range(0.0, 120.0, 0.5)
@export var timeout_seconds: float = 10.0
@export_group("Version Compare")
## バージョン文字列の区切り文字
## "1.2.3" のような形式なら "." を使います。
@export var version_delimiter: String = "."
## バージョン比較時に、長さの違いをどう扱うか
## true: 足りない部分は 0 とみなす(1.2 == 1.2.0)
## false: 文字列としてそのまま比較(1.2 < 1.2.0 など)
@export var pad_short_versions: bool = true
@export_group("Debug")
## デバッグログを出すかどうか
@export var verbose_log: bool = true
## ===========================
## シグナル
## ===========================
## チェック開始時
signal check_started()
## チェック成功時(常に発火)
## - latest_version: サーバーから取得した最新版
## - is_latest: 現在バージョンが最新版以上なら true
signal check_completed(latest_version: String, is_latest: bool)
## チェック失敗時
## - error_message: エラー内容
signal check_failed(error_message: String)
## 新しいバージョンが存在するとき
## - latest_version: サーバーの最新版
## - update_url: JSONに "url" が含まれていればその値、なければ空文字
signal update_available(latest_version: String, update_url: String)
## すでに最新バージョンのとき
signal already_latest(current_version: String)
## ===========================
## 内部用
## ===========================
var _http_request: HTTPRequest
var _is_checking: bool = false
func _ready() -> void:
## HTTPRequest ノードを動的に生成して、自分の子として追加
_http_request = HTTPRequest.new()
add_child(_http_request)
_http_request.request_completed.connect(_on_request_completed)
if check_on_ready:
check_version()
## ===========================
## パブリックAPI
## ===========================
## バージョンチェックを開始する
func check_version() -> void:
if _is_checking:
if verbose_log:
push_warning("VersionChecker: check already in progress, skipping.")
return
if version_check_url.is_empty():
var msg := "VersionChecker: version_check_url is empty."
push_error(msg)
check_failed.emit(msg)
return
_is_checking = true
check_started.emit()
if verbose_log:
print("[VersionChecker] Start checking version from: ", version_check_url)
var err := _http_request.request(
version_check_url,
[], # headers
HTTPClient.METHOD_GET, # method(今回は常にGETで十分)
"" # body
)
if err != OK:
_is_checking = false
var msg := "VersionChecker: HTTP request failed to start. Error code: %d" % err
push_error(msg)
check_failed.emit(msg)
## ===========================
## 内部ロジック
## ===========================
func _on_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
_is_checking = false
if verbose_log:
print("[VersionChecker] HTTP completed. result=", result, ", code=", response_code)
if result != HTTPRequest.RESULT_SUCCESS:
var msg := "VersionChecker: HTTP request failed. result=%d, code=%d" % [result, response_code]
push_error(msg)
check_failed.emit(msg)
return
if response_code < 200 or response_code >= 300:
var msg := "VersionChecker: HTTP response code not OK: %d" % response_code
push_error(msg)
check_failed.emit(msg)
return
var text := body.get_string_from_utf8()
if verbose_log:
print("[VersionChecker] Response body: ", text)
var json := JSON.new()
var parse_err := json.parse(text)
if parse_err != OK:
var msg := "VersionChecker: Failed to parse JSON. error=%d" % parse_err
push_error(msg)
check_failed.emit(msg)
return
var data = json.get_data()
if typeof(data) != TYPE_DICTIONARY:
var msg := "VersionChecker: JSON root is not a Dictionary."
push_error(msg)
check_failed.emit(msg)
return
if not data.has("version"):
var msg := "VersionChecker: JSON does not contain 'version' field."
push_error(msg)
check_failed.emit(msg)
return
var latest_version := str(data["version"])
var update_url := ""
if data.has("url"):
update_url = str(data["url"])
var is_latest := _compare_versions(current_version, latest_version) >= 0
check_completed.emit(latest_version, is_latest)
if is_latest:
if verbose_log:
print("[VersionChecker] Already latest. current=", current_version, ", latest=", latest_version)
already_latest.emit(current_version)
else:
if verbose_log:
print("[VersionChecker] Update available! current=", current_version, ", latest=", latest_version)
update_available.emit(latest_version, update_url)
## バージョン文字列を比較する関数
## 戻り値:
## > 0 : v1 が v2 より新しい
## = 0 : 同じ
## < 0 : v1 が v2 より古い
func _compare_versions(v1: String, v2: String) -> int:
var parts1 := v1.split(version_delimiter, false)
var parts2 := v2.split(version_delimiter, false)
if pad_short_versions:
var max_len := max(parts1.size(), parts2.size())
while parts1.size() < max_len:
parts1.append("0")
while parts2.size() < max_len:
parts2.append("0")
var count := min(parts1.size(), parts2.size())
for i in count:
var a := parts1[i]
var b := parts2[i]
var a_num := a.to_int()
var b_num := b.to_int()
# 両方数値として解釈できる場合は数値比較
if str(a_num) == a and str(b_num) == b:
if a_num > b_num:
return 1
elif a_num < b_num:
return -1
else:
# 文字列として比較
if a > b:
return 1
elif a < b:
return -1
# ここまで差がなければ、長さで比較(pad_short_versions が false の場合のみ意味がある)
if not pad_short_versions:
if parts1.size() > parts2.size():
return 1
elif parts1.size() < parts2.size():
return -1
return 0
使い方の手順
ここでは、タイトル画面で起動時にバージョンチェックし、アップデートがあればダイアログを出す、という例で使い方を説明します。
シーン構成例
TitleScreen (Control) ├── Label ├── ButtonStart ├── ButtonQuit ├── VersionChecker (Node) <-- このコンポーネントをアタッチ └── UpdateDialog (AcceptDialog)
手順①: スクリプトを用意する
VersionChecker.gdをプロジェクトの任意の場所(例:res://addons/components/version_checker/)に保存します。- Godotエディタを再読み込みすると、
VersionCheckerがクラスとして認識され、ノード追加ダイアログから選べるようになります。
手順②: ノードにコンポーネントとして追加する
- タイトルシーン(
TitleScreen.tscnなど)を開きます。 - ルートの
Controlノード(または適当な管理ノード)を選択し、「子ノードを追加」。 - 検索窓に
VersionCheckerと打ち、出てきたノードを追加します。 - インスペクタで以下を設定します:
current_version: 例「1.0.0」version_check_url: 例「https://example.com/my_game/version.json」check_on_ready:On(起動時に自動チェックしたい場合)
サーバー側の version.json は、例えばこんな内容を想定しています:
{
"version": "1.2.0",
"url": "https://example.com/my_game/download"
}
手順③: シグナルをUIに接続する
今度は、TitleScreen のスクリプトから VersionChecker のシグナルを受け取って、ダイアログを表示してみましょう。
## TitleScreen.gd
extends Control
@onready var version_checker: VersionChecker = $VersionChecker
@onready var update_dialog: AcceptDialog = $UpdateDialog
@onready var label_status: Label = $Label
func _ready() -> void:
## シグナル接続(エディタからつないでもOK)
version_checker.update_available.connect(_on_update_available)
version_checker.already_latest.connect(_on_already_latest)
version_checker.check_failed.connect(_on_check_failed)
## 起動時に自動チェックしない設定の場合は、手動で呼ぶ
# version_checker.check_version()
func _on_update_available(latest_version: String, update_url: String) -> void:
label_status.text = "新しいバージョンがあります: %s" % latest_version
var text := "最新版 %s が利用可能です。" % latest_version
if update_url != "":
text += "\nダウンロードページ: %s" % update_url
update_dialog.title = "アップデートのお知らせ"
update_dialog.dialog_text = text
update_dialog.popup_centered()
func _on_already_latest(current_version: String) -> void:
label_status.text = "最新版を利用中: %s" % current_version
func _on_check_failed(error_message: String) -> void:
label_status.text = "バージョン確認に失敗しました"
push_warning("Version check failed: %s" % error_message)
これで、タイトルシーンを開いたタイミングで自動的にバージョン確認が走り、結果に応じて UI が更新されるようになります。VersionChecker がやっているのは「HTTPでJSONを取ってきて比較する」だけなので、タイトル画面だろうが、ランチャーシーンだろうが、どこにでも再利用できます。
手順④: 他のシーンにもポン付けして再利用
例えば、ゲーム起動用のランチャーを作って、そこにも同じコンポーネントを使うことができます:
Launcher (Control) ├── GameList (VBoxContainer) ├── StatusLabel (Label) └── VersionChecker (Node)
ランチャー用のスクリプトで、VersionChecker のシグナルを受け取ってステータスを出すだけ。
「バージョン確認のロジック」はすべてコンポーネント側に閉じ込めてあるので、どのシーンにも同じパターンで合成できるのがポイントですね。
メリットと応用
この VersionChecker コンポーネントを使うメリットは大きく分けて3つあります。
- シーン構造がスッキリする
起動時のHTTP処理をメインシーンやUIシーンにベタ書きすると、HTTPRequestノードやロジックがそこにべったり張り付いてしまいます。
代わりに「VersionCheckerノードを1個置くだけ」にしておけば、シーンツリーはUIとゲームロジック中心に保ちつつ、周辺機能はコンポーネントとして外出しできます。 - 再利用性が高い(継承不要)
「すべてのシーンのベースクラスにバージョンチェックを入れる」みたいな継承ベースの設計は、あとから変更しづらくなります。
コンポーネントとして独立させておけば、必要なシーンにだけアタッチすればよく、別プロジェクトに持っていくのも簡単です。 - テストしやすい・差し替えやすい
VersionCheckerは単体で動作するので、「テスト用のシーン」にポンと置いて動作確認することもできますし、
将来 API 仕様が変わったときも、このコンポーネントだけを差し替えれば全シーンに影響が行き渡るのも嬉しいポイントです。
「継承より合成」の良さがそのまま出ている例ですね。
改造案:一定間隔で自動再チェックする
オンラインゲームや常駐ランチャーなどでは、「起動時だけでなく、一定間隔で再チェックしたい」こともあります。
そんなときは、VersionChecker に簡単なタイマー機能を足してみましょう。
## VersionChecker.gd 内に追記する例
@export_group("Auto Recheck")
@export var auto_recheck: bool = false
@export_range(10.0, 3600.0, 10.0)
@export var recheck_interval_seconds: float = 300.0
var _recheck_timer: Timer
func _ready() -> void:
_http_request = HTTPRequest.new()
add_child(_http_request)
_http_request.request_completed.connect(_on_request_completed)
if auto_recheck:
_recheck_timer = Timer.new()
_recheck_timer.wait_time = recheck_interval_seconds
_recheck_timer.autostart = true
_recheck_timer.one_shot = false
add_child(_recheck_timer)
_recheck_timer.timeout.connect(_on_recheck_timeout)
if check_on_ready:
check_version()
func _on_recheck_timeout() -> void:
check_version()
こうしておけば、「ランチャーを開きっぱなしでも、一定時間ごとに最新版チェックを走らせる」といった用途にも対応できます。
もちろん、これもコンポーネントとして独立しているので、必要なプロジェクトにだけこの改造版を持っていけばOKです。
起動時のバージョン確認は、ゲームそのもののロジックとは独立した「周辺機能」です。
だからこそ、今回のようにコンポーネントとして切り出して、どのシーンにも合成できる形にしておくと、プロジェクトが大きくなっても管理しやすくなりますね。
