Godotでゲームやツールを作っていると、「今動いているビルドが最新かどうか」を知りたくなることが多いですよね。
でも、これを毎回シーンごとにスクリプトへ書いたり、メインシーンのスクリプトを継承で増やしていくと…だんだんカオスになってきます。

  • メインシーンのスクリプトが「入力処理」「UI制御」「バージョン確認」などで肥大化する
  • 別プロジェクトに持っていきたいのに、依存関係が多すぎてコピペしづらい
  • HTTPリクエストの処理やエラーハンドリングを毎回コピペしてバグの温床になる

こういうときこそ「継承より合成」です。
「バージョン確認だけを担当するコンポーネント」として切り出しておけば、好きなシーンにポン付けできますし、別プロジェクトにも簡単に再利用できます。

この記事では、起動時にサーバーへHTTPリクエストを送り、最新版があるか通知するためのコンポーネント
「VersionChecker」を実装していきます。

【Godot 4】起動時にサクッと最新版チェック!「VersionChecker」コンポーネント

今回作る VersionChecker は、ざっくり言うとこんな役割です。

  • 起動時(または任意のタイミング)にHTTPでサーバーへアクセス
  • サーバーが返したJSONのバージョン情報と、現在のバージョンを比較
  • 最新版がある場合はシグナルで通知(UI側で「アップデートあります!」を表示できる)
  • エラー時もシグナルで通知(オフラインでもゲーム自体は普通に動かす)

HTTP処理や比較ロジックは全部このコンポーネント側に閉じ込めておくので、
メインのゲームロジックはきれいなまま保てます。


フルコード:VersionChecker.gd


extends Node
class_name VersionChecker
## 起動時にサーバーへHTTPリクエストを送り、最新版があるか確認するコンポーネント。
##
## - 任意のノードにアタッチして使います(Root, UI, Manager など)。
## - サーバーからは JSON を受け取る想定です。
##   例:
##   {
##     "latest_version": "1.2.0",
##     "download_url": "https://example.com/download",
##     "message": "不具合修正とパフォーマンス改善を含むアップデートです。"
##   }

signal check_started
## バージョンチェック開始時に発火

signal check_succeeded(is_latest: bool, latest_version: String, payload: Dictionary)
## チェック成功時に発火
## - is_latest: 現在バージョンが最新なら true
## - latest_version: サーバーが返した最新版のバージョン文字列
## - payload: サーバーが返した JSON 全体(追加情報をUIで使いたい場合など)

signal check_failed(error_message: String)
## 何らかの理由でバージョンチェックに失敗した場合に発火

@export_category("VersionChecker - 基本設定")

@export var enabled: bool = true
## コンポーネントを有効にするかどうか。
## false にすると _ready で自動チェックしません(手動で check_version() を呼ぶ想定)。

@export var auto_check_on_ready: bool = true
## ノードの _ready() タイミングで自動的にバージョンチェックを行うかどうか。

@export var server_url: String = "https://example.com/version.json"
## バージョン情報を返すサーバーの URL。
## 例: "https://example.com/api/version" など。
## JSON でレスポンスが返ってくる想定です。

@export var current_version: String = "1.0.0"
## 現在のビルドのバージョン。
## Godot の Project Settings やバージョン定数から自動で埋めるのもアリですが、
## まずは手動入力でOKです。

@export var timeout_sec: float = 10.0
## HTTP リクエストのタイムアウト秒数。
## 回線が悪い環境でいつまでも待たされないようにするための設定です。

@export_enum("Debug", "Info", "Warning", "Error", "Silent")
var log_level: String = "Info"
## ログ出力のレベル。
## - Debug: 詳細なログを出す
## - Info: 通常の情報レベル
## - Warning: 警告のみ
## - Error: エラーのみ
## - Silent: 何も出さない

@export_category("VersionChecker - 比較設定")

@export var allow_prerelease_as_latest: bool = false
## プリリリース版(例: "1.2.0-beta")を「最新」とみなすかどうか。
## シンプルにメジャー/マイナー/パッチだけで比較したい場合は false のままでOKです。

@export var treat_unknown_format_as_latest: bool = false
## サーバーから返ってきたバージョン文字列の形式が不明な場合、
## 「とりあえず最新扱い」にするかどうか。

# 内部で使用する HTTPRequest ノード
var _http_request: HTTPRequest

func _ready() -> void:
    if not enabled:
        _log_debug("VersionChecker is disabled. Skipping auto check.")
        return

    # HTTPRequest ノードを動的に追加
    _http_request = HTTPRequest.new()
    add_child(_http_request)
    _http_request.timeout = int(timeout_sec)
    _http_request.request_completed.connect(_on_request_completed)

    if auto_check_on_ready:
        check_version()


## 外部から呼び出してバージョンチェックを開始する
func check_version() -> void:
    if not enabled:
        _log_warning("VersionChecker is disabled. check_version() was called but will not run.")
        return

    if server_url.is_empty():
        _log_error("server_url is empty. Cannot perform version check.")
        emit_signal("check_failed", "Server URL is not set.")
        return

    emit_signal("check_started")
    _log_info("Starting version check... url=%s current=%s" % [server_url, current_version])

    var err := _http_request.request(server_url)
    if err != OK:
        _log_error("Failed to start HTTP request. Error code: %s" % err)
        emit_signal("check_failed", "Failed to start HTTP request. Error code: %s" % err)


## HTTPRequest のコールバック
func _on_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
    if result != HTTPRequest.RESULT_SUCCESS:
        _log_error("HTTP request failed. result=%s" % result)
        emit_signal("check_failed", "HTTP request failed. result=%s" % result)
        return

    if response_code < 200 or response_code >= 300:
        _log_error("Unexpected HTTP status code: %s" % response_code)
        emit_signal("check_failed", "Unexpected HTTP status code: %s" % response_code)
        return

    var body_text := body.get_string_from_utf8()
    _log_debug("Raw response body: %s" % body_text)

    var json := JSON.new()
    var parse_err := json.parse(body_text)
    if parse_err != OK:
        _log_error("Failed to parse JSON. Error: %s" % json.get_error_message())
        emit_signal("check_failed", "Failed to parse JSON: %s" % json.get_error_message())
        return

    var data := json.data
    if typeof(data) != TYPE_DICTIONARY:
        _log_error("JSON root is not a dictionary.")
        emit_signal("check_failed", "JSON root is not a dictionary.")
        return

    if not data.has("latest_version"):
        _log_error('JSON does not contain key "latest_version".')
        emit_signal("check_failed", 'JSON does not contain key "latest_version".')
        return

    var latest_version: String = str(data["latest_version"])
    _log_info("Latest version from server: %s" % latest_version)

    var is_latest := _compare_versions(current_version, latest_version)

    emit_signal("check_succeeded", is_latest, latest_version, data)

    if is_latest:
        _log_info("You are running the latest version (%s)." % current_version)
    else:
        _log_warning("A newer version is available! current=%s latest=%s" % [current_version, latest_version])


## バージョン文字列を比較する関数
## - current < latest なら false(更新あり)
## - current >= latest なら true(最新 or それ以上)
func _compare_versions(current: String, latest: String) -> bool:
    # まずプリリリース情報を取り除く or 残す
    var current_core := current
    var latest_core := latest

    var current_suffix := ""
    var latest_suffix := ""

    if "-" in current:
        var parts_cur := current.split("-", false, 1)
        current_core = parts_cur[0]
        current_suffix = parts_cur[1]
    if "-" in latest:
        var parts_lat := latest.split("-", false, 1)
        latest_core = parts_lat[0]
        latest_suffix = parts_lat[1]

    # "1.2.3" のような形式を分割
    var current_nums := current_core.split(".")
    var latest_nums := latest_core.split(".")

    if current_nums.size() != 3 or latest_nums.size() != 3:
        _log_warning("Version format not recognized. current=%s latest=%s" % [current, latest])
        # フォーマットが不明な場合の扱い
        return treat_unknown_format_as_latest

    var cur_major := int(current_nums[0])
    var cur_minor := int(current_nums[1])
    var cur_patch := int(current_nums[2])

    var lat_major := int(latest_nums[0])
    var lat_minor := int(latest_nums[1])
    var lat_patch := int(latest_nums[2])

    # メジャー比較
    if cur_major < lat_major:
        return false
    elif cur_major > lat_major:
        return true

    # マイナー比較
    if cur_minor < lat_minor:
        return false
    elif cur_minor > lat_minor:
        return true

    # パッチ比較
    if cur_patch < lat_patch:
        return false
    elif cur_patch > lat_patch:
        return true

    # ここまで同じなら、プリリリース扱いをどうするか
    if not allow_prerelease_as_latest:
        # プリリリースは「最新版ではない」とみなす
        # 例: current=1.2.0-beta, latest=1.2.0 → current は古いと判定
        if current_suffix != "" and latest_suffix == "":
            return false
        # 例: current=1.2.0, latest=1.2.0-beta → current は最新とみなす
        if current_suffix == "" and latest_suffix != "":
            return true

    # ここまで差がなければ同等とみなす
    return true


# ===== ログ出力系のユーティリティ =====

func _log_debug(msg: String) -> void:
    if log_level == "Debug":
        print("[VersionChecker][DEBUG]: %s" % msg)

func _log_info(msg: String) -> void:
    if log_level in ["Debug", "Info"]:
        print("[VersionChecker][INFO]: %s" % msg)

func _log_warning(msg: String) -> void:
    if log_level in ["Debug", "Info", "Warning"]:
        push_warning("[VersionChecker][WARN]: %s" % msg)

func _log_error(msg: String) -> void:
    if log_level in ["Debug", "Info", "Warning", "Error"]:
        push_error("[VersionChecker][ERROR]: %s" % msg)

使い方の手順

ここからは、実際にシーンへ組み込んで使う手順を見ていきましょう。
例として「タイトル画面で起動時に最新版チェックして、UIに表示する」ケースを扱います。

手順①: スクリプトをプロジェクトに追加

  1. res://components/VersionChecker.gd のような場所に、上記コードを保存します。
  2. Godotエディタで再読み込みすると、スクリプトクラスとして VersionChecker が使えるようになります。

手順②: タイトルシーンにコンポーネントをアタッチ

例えば、次のようなタイトルシーンを想定します。

TitleScreen (Control)
 ├── Panel
 │    ├── Label_Title
 │    └── Label_VersionInfo
 └── VersionChecker (Node)  ← このノードにスクリプトをアタッチ
  1. TitleScreen シーンを開きます。
  2. 子ノードとして Node を追加し、名前を VersionChecker に変更します。
  3. そのノードに、先ほど作成した VersionChecker.gd をアタッチします。
  4. インスペクタで以下のパラメータを設定します:
    • enabled: チェックを使いたいなら true
    • auto_check_on_ready: タイトル表示と同時にチェックしたいなら true
    • server_url: あなたのサーバーのURL(例: https://example.com/version.json
    • current_version: 現在のゲームバージョン(例: 1.0.0
    • log_level: 開発中は Debug、リリース時は Warning など

手順③: UI側でシグナルを受け取って表示する

今度は、タイトル画面のスクリプトで VersionChecker のシグナルを受け取ってみましょう。

TitleScreen のルートノード(Control)に、次のようなスクリプトを付ける例です。


extends Control

@onready var version_checker: VersionChecker = $VersionChecker
@onready var label_version_info: Label = $Panel/Label_VersionInfo

func _ready() -> void:
    # 現在バージョンをとりあえず表示
    label_version_info.text = "Version: %s (checking...)" % version_checker.current_version

    # シグナル接続
    version_checker.check_started.connect(_on_check_started)
    version_checker.check_succeeded.connect(_on_check_succeeded)
    version_checker.check_failed.connect(_on_check_failed)


func _on_check_started() -> void:
    label_version_info.text = "Version: %s (checking...)" % version_checker.current_version


func _on_check_succeeded(is_latest: bool, latest_version: String, payload: Dictionary) -> void:
    if is_latest:
        label_version_info.text = "Version: %s (最新)" % version_checker.current_version
    else:
        # サーバーから追加メッセージが来ていれば表示
        var msg := ""
        if payload.has("message"):
            msg = str(payload["message"])
        label_version_info.text = "Version: %s → 新しいバージョン %s があります!\n%s" % [
            version_checker.current_version,
            latest_version,
            msg
        ]


func _on_check_failed(error_message: String) -> void:
    # ネットワークエラーなど。ゲーム自体は続行可能なので、軽めの表示にしておく。
    label_version_info.text = "Version: %s (更新チェック失敗)" % version_checker.current_version
    push_warning("Version check failed: %s" % error_message)

これで、タイトル画面を開いたタイミングで自動的にバージョンチェックが走り、
最新版があればラベルにメッセージを出せるようになります。

手順④: 他のシーン(プレイヤー、敵、ツールなど)にも再利用

このコンポーネントは どのシーンにもポン付け可能 です。例えば:

  • エディタツールのランチャー画面
  • デバッグ用の「開発者メニュー」シーン
  • ゲーム内の「設定」画面から手動でバージョンチェックを呼ぶ

たとえば、デバッグメニューから手動でチェックする構成はこんな感じです。

DebugMenu (Control)
 ├── Button_CheckUpdate
 ├── Label_Result
 └── VersionChecker (Node)

extends Control

@onready var version_checker: VersionChecker = $VersionChecker
@onready var label_result: Label = $Label_Result
@onready var button_check: Button = $Button_CheckUpdate

func _ready() -> void:
    version_checker.enabled = true
    version_checker.auto_check_on_ready = false  # 手動で呼ぶ
    button_check.pressed.connect(_on_button_check_pressed)

    version_checker.check_started.connect(func():
        label_result.text = "チェック中..."
    )

    version_checker.check_succeeded.connect(func(is_latest, latest_version, payload):
        if is_latest:
            label_result.text = "最新バージョンです (%s)" % latest_version
        else:
            label_result.text = "新しいバージョンがあります: %s" % latest_version
    )

    version_checker.check_failed.connect(func(msg):
        label_result.text = "チェック失敗: %s" % msg
    )

func _on_button_check_pressed() -> void:
    version_checker.check_version()

このように、「バージョンチェック」という関心事を1つのコンポーネントに閉じ込めておくことで、
どのシーンからでも同じインターフェースで扱えるようになります。


メリットと応用

VersionChecker コンポーネントを導入することで得られるメリットを整理してみましょう。

  • シーン構造がスッキリ
    メインシーンやタイトルシーンのスクリプトにHTTP処理をベタ書きしなくて済みます。
    「UIはUI」「バージョンチェックはVersionChecker」と責務が分離され、見通しが良くなります。
  • 再利用性が高い
    別プロジェクトでも、VersionChecker.gd をコピペしてノードにアタッチするだけで同じ仕組みを使えます。
    継承ベースでメインシーンにべったり書いてしまうと、こうはいきません。
  • テストや差し替えがしやすい
    開発中は server_url をローカルのモックサーバーに向ける、
    リリース版は本番のURLにする、といった切り替えもエディタから簡単に行えます。
  • エラー処理が一箇所にまとまる
    ネットワークエラーやJSONパースエラーなどの処理がコンポーネント内部に閉じているので、
    UI側は「成功した」「失敗した」だけを意識すればOKです。

さらに、応用としては:

  • サーバーから返ってきた download_url を使って、ブラウザを開くボタンを表示
  • 「強制アップデート」フラグを見て、古いバージョンはタイトルから先に進ませない
  • バージョンごとの変更点(changelog)をポップアップで表示

など、いろいろな拡張が考えられます。

改造案:強制アップデート判定を追加する

たとえば、サーバー側のJSONに "force_update": true というフラグを追加しておき、
それを見て「このバージョンではゲームを続行させない」仕組みを簡単に追加できます。

TitleScreen 側に、こんな関数を足すだけです。


func _on_check_succeeded(is_latest: bool, latest_version: String, payload: Dictionary) -> void:
    if payload.get("force_update", false):
        # 強制アップデート
        label_version_info.text = "このバージョンはサポート対象外です。\n最新版 (%s) をダウンロードしてください。" % latest_version
        # ここでメインゲームへの遷移ボタンを無効化したり、ダウンロードページを開くボタンを表示したりする
        $Button_StartGame.disabled = true
        return

    if is_latest:
        label_version_info.text = "Version: %s (最新)" % version_checker.current_version
    else:
        label_version_info.text = "Version: %s → 新しいバージョン %s があります!" % [
            version_checker.current_version,
            latest_version
        ]

こんなふうに、「バージョンチェック」はコンポーネントに任せて、
「どう振る舞うか」はシーン側で自由に決める
というスタイルにしておくと、
Godotプロジェクトが大きくなっても管理しやすくなりますね。

継承でメインシーンをどんどん肥大化させる前に、
ぜひこういったコンポーネント指向の作り方を取り入れてみましょう。