Godotで放置ゲームを作る⑦:自動化モジュールとJSONセーブ/ロード

初心者向け実践開発

※本記事にはアフィリエイト広告(Udemy)を含みます。

前回のおさらい

前回⑥では、中央制御システムによる乗算バフを実装し、すべての生産量を底上げできるようになりました。今回は、クリック操作そのものを不要にする「自動化モジュール」と、ゲームの進行状況を保存するセーブ/ロード機能を実装します。

今回のゴール

STATION OPSの設備は、これまですべて「基数を増やせるだけ増やせる」設計でした。自動化モジュールだけは例外で、1回購入したらそれで終わりの設備にします。購入すると、以後は手動クリックの代わりに固定量の電力が自動で加算されるようになります。

あわせて、ここまで育てた設備をブラウザを閉じても失わないように、セーブ/ロード機能を実装します。保存形式にはJSONを採用します。

自動化モジュールを追加する

1回限りの購入にする設計

発電パネルなどはcount(所持数)を増やしていく設計でしたが、自動化モジュールは「買った/買っていない」の2状態だけを持てば十分です。そのため、真偽値(bool)の変数で管理します。

var automation_module_purchased: bool = false
var automation_module_cost: float = 50.0

const AUTOMATION_MODULE_FIXED_BONUS: float = 5.0


func can_buy_automation_module() -> bool:
    return central_control_count >= 1 and not automation_module_purchased


func buy_automation_module() -> bool:
    if not can_buy_automation_module():
        return false
    if parts < automation_module_cost:
        return false
    parts -= automation_module_cost
    automation_module_purchased = true
    resources_changed.emit()
    return true

can_buy_automation_module()の中でnot automation_module_purchasedを条件に加えることで、一度購入した後は二度と購入できないようにしています。

Tick処理に固定ボーナスを追加する

func _on_production_tick() -> void:
    var multiplier: float = get_production_multiplier()

    # (中略:発電パネル・酸素生成装置・部品工場の処理は⑤⑥と同じ)

    if automation_module_purchased:
        power += AUTOMATION_MODULE_FIXED_BONUS * multiplier

    resources_changed.emit()
    save_game()

固定ボーナスにも中央制御システムのmultiplierを掛けているのは、自動化モジュールも「生産」の一種として扱い、他の設備と同じ恩恵を受けられるようにするためです。

UIからクリックボタンを消す

購入後はクリックボタンを非表示にする理由

自動化モジュールを購入したということは、もう手動でクリックする必要がなくなったということです。使えないボタンを画面に残しておくと混乱を招くため、非表示にします。

func _update_display() -> void:
    click_button.visible = not GameManager.automation_module_purchased
    # (以下、これまでの表示更新処理)

JSON形式でセーブ/ロードを実装する

なぜJSON形式を選んだか

Godotには設定ファイル向けのConfigFileという仕組みもありますが、今回はJSON形式を選びました。JSONはGodot以外のツールとの連携や、将来的にセーブデータの内容を確認・編集する場面でも扱いやすい、汎用的なデータ形式だからです。

save_game()関数を書く

const SAVE_PATH: String = "user://savegame.json"


func save_game() -> void:
    var data: Dictionary = {
        "power": power,
        "oxygen": oxygen,
        "parts": parts,
        "power_panel_count": power_panel_count,
        "power_panel_cost": power_panel_cost,
        "oxygen_generator_count": oxygen_generator_count,
        "oxygen_generator_cost": oxygen_generator_cost,
        "parts_factory_count": parts_factory_count,
        "parts_factory_cost": parts_factory_cost,
        "central_control_count": central_control_count,
        "central_control_cost": central_control_cost,
        "automation_module_purchased": automation_module_purchased,
        "automation_module_cost": automation_module_cost,
    }
    var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
    if file == null:
        push_warning("セーブファイルを開けませんでした")
        return
    file.store_string(JSON.stringify(data))
    file.close()

user://は、Godotがユーザーごとのデータ保存用に用意している特別なパスです。OSごとに実際の保存場所は異なりますが、コード側はuser://savegame.jsonと書くだけで、環境を意識せずに扱えます。

JSON.stringify(data)で、辞書(Dictionary)のデータをJSON形式の文字列に変換してから、ファイルに書き込んでいます。

load_game()関数を書く

func load_game() -> void:
    if not FileAccess.file_exists(SAVE_PATH):
        return
    var file := FileAccess.open(SAVE_PATH, FileAccess.READ)
    if file == null:
        push_warning("セーブファイルを読み込めませんでした")
        return
    var text: String = file.get_as_text()
    file.close()

    var parsed = JSON.parse_string(text)
    if parsed == null or typeof(parsed) != TYPE_DICTIONARY:
        push_warning("セーブデータの形式が不正です")
        return

    power = parsed.get("power", power)
    oxygen = parsed.get("oxygen", oxygen)
    parts = parsed.get("parts", parts)
    power_panel_count = parsed.get("power_panel_count", power_panel_count)
    power_panel_cost = parsed.get("power_panel_cost", power_panel_cost)
    # (以下、save_game()と対になる形で残りの項目も同様に読み込む)

FileAccess.file_exists(SAVE_PATH)で、そもそもセーブファイルが存在するかどうかを先に確認しています。初回起動時にはまだファイルが存在しないため、この時点で処理を終え、変数は初期値のまま続行します。

parsed.get("power", power)という書き方は、「セーブデータにpowerのキーがあればその値を、なければ現在の値(初期値)のまま使う」という意味です。この書き方にしておくことで、後からセーブデータの項目を追加したときも、古いセーブデータを読み込んでエラーにならずに済みます。

どのタイミングで保存するか

今回は、設備を購入したタイミングと、毎秒のTick処理のタイミングの両方でsave_game()を呼んでいます。頻繁に保存する分ディスクへの書き込みは増えますが、STATION OPSのようにセーブデータが小さく、プレイ時間も15$301C30分程度のゲームであれば、実用上の問題はありません。

起動時にロードを反映する

func _ready() -> void:
    load_game()
    _production_timer = Timer.new()
    # (以下、④で書いたTimerの設定)

load_game()は、Timerを作る前、_ready()の一番最初に呼び出します。もしTimerを先に動かし始めてからロードしてしまうと、ロード前の初期値に対して一瞬でも生産処理が走ってしまう可能性があるためです。

動作確認

中央制御システムを1基持っている状態で自動化モジュールを購入し、クリックボタンが消えることを確認してください。そのままページを再読み込みし、電力・酸素・部品や各設備の所持数がリセットされずに引き継がれていれば、セーブ/ロードは成功です。

次回予告

次回⑧では、ECHOのセリフに演出を加え、記事シリーズの仕上げと公開作業を行います。

Godot学習におすすめのオンライン講座

セーブ/ロードの実装は地味に感じるかもしれませんが、多くのゲームで必須の機能です。体系的な教材でファイル操作の基礎から押さえておくと安心です。

Godotのコースを見る

参考リンク


コメント

タイトルとURLをコピーしました