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)

手順①: スクリプトを用意する

  1. VersionChecker.gd をプロジェクトの任意の場所(例: res://addons/components/version_checker/)に保存します。
  2. Godotエディタを再読み込みすると、VersionChecker がクラスとして認識され、ノード追加ダイアログから選べるようになります。

手順②: ノードにコンポーネントとして追加する

  1. タイトルシーン(TitleScreen.tscn など)を開きます。
  2. ルートの Control ノード(または適当な管理ノード)を選択し、「子ノードを追加」。
  3. 検索窓に VersionChecker と打ち、出てきたノードを追加します。
  4. インスペクタで以下を設定します:
    • 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つあります。

  1. シーン構造がスッキリする
    起動時のHTTP処理をメインシーンやUIシーンにベタ書きすると、HTTPRequest ノードやロジックがそこにべったり張り付いてしまいます。
    代わりに「VersionCheckerノードを1個置くだけ」にしておけば、シーンツリーはUIとゲームロジック中心に保ちつつ、周辺機能はコンポーネントとして外出しできます。
  2. 再利用性が高い(継承不要)
    「すべてのシーンのベースクラスにバージョンチェックを入れる」みたいな継承ベースの設計は、あとから変更しづらくなります。
    コンポーネントとして独立させておけば、必要なシーンにだけアタッチすればよく、別プロジェクトに持っていくのも簡単です。
  3. テストしやすい・差し替えやすい
    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です。

起動時のバージョン確認は、ゲームそのもののロジックとは独立した「周辺機能」です。
だからこそ、今回のようにコンポーネントとして切り出して、どのシーンにも合成できる形にしておくと、プロジェクトが大きくなっても管理しやすくなりますね。