Godotのリストに戻る

【Godot】Autoloadによるシーンをまたぐデータ管理

作成: 2025-06-20更新: 2025-06-20ノード・シーンブログ記事を読む

概要

Autoloadは、ゲーム起動時に自動的に読み込まれ、シーン遷移しても破棄されない特別なノードです。UnityのDontDestroyOnLoadとシングルトンパターンを組み合わせたような機能を提供します。

最初は「シーンをまたいでデータを持ち越すにはどうすればいいんだろう?」と悩んでいた時、Autoloadの存在を知って感動しました。プレイヤーのステータス、宝箱の開封状態、インベントリなど、ゲームの「継続性」に関わるあらゆるデータを、この仕組みで管理できるようになりました。

Autoload設定画面

Autoloadの特徴

  • 永続性: シーン遷移で破棄されない
  • グローバルアクセス: どこからでもアクセス可能
  • 自動初期化: ゲーム起動時に自動でインスタンス化
  • 単一インスタンス: シングルトンとして動作

設定方法

1. スクリプトの作成

# GameManager.gd
extends Node

# ゲーム全体で共有するデータ
var player_health: int = 100
var player_max_health: int = 100
var current_level: int = 1
var score: int = 0

# 収集済みアイテム
var collected_items: Array[String] = []
var opened_chests: Array[String] = []

# セーブポイント情報
var last_checkpoint: String = ""
var spawn_position: Vector2 = Vector2.ZERO

2. Autoloadへの登録

  1. プロジェクト設定 → AutoLoad
  2. パスにスクリプトを選択
  3. ノード名を設定(例:GameManager)
  4. 「追加」をクリック

基本的な使い方

グローバル変数のアクセス

# どのスクリプトからでもアクセス可能
func _ready():
    # 読み取り
    var health = GameManager.player_health
    
    # 書き込み
    GameManager.score += 100
    
    # 関数の呼び出し
    GameManager.save_game()

実践的な実装例

宝箱の開封状態管理

# GameManager.gd (Autoload)
extends Node

var opened_chests: Dictionary = {}

func mark_chest_opened(chest_id: String):
    opened_chests[chest_id] = true
    
func is_chest_opened(chest_id: String) -> bool:
    return opened_chests.has(chest_id) and opened_chests[chest_id]

func reset_all_chests():
    opened_chests.clear()
# TreasureChest.gd
extends Area2D

@export var chest_id: String = "forest_chest_01"
@export var item: Resource

func _ready():
    if GameManager.is_chest_opened(chest_id):
        # 既に開封済み
        $Sprite2D.frame = 1  # 開いた状態の画像
        $CollisionShape2D.disabled = true

func _on_body_entered(body):
    if body.is_in_group("player") and not GameManager.is_chest_opened(chest_id):
        open_chest()

func open_chest():
    # アイテム付与
    give_item_to_player()
    
    # 開封状態を記録
    GameManager.mark_chest_opened(chest_id)
    
    # アニメーション
    $AnimationPlayer.play("open")
    $CollisionShape2D.disabled = true

プレイヤーデータの永続化

# PlayerData.gd (Autoload)
extends Node

signal health_changed(new_health)
signal level_up(new_level)

var stats = {
    "level": 1,
    "experience": 0,
    "health": 100,
    "max_health": 100,
    "attack": 10,
    "defense": 5
}

var inventory: Array[Dictionary] = []
var equipped_items: Dictionary = {}

func add_experience(amount: int):
    stats.experience += amount
    check_level_up()

func check_level_up():
    var required_exp = stats.level * 100
    if stats.experience >= required_exp:
        stats.level += 1
        stats.max_health += 10
        stats.attack += 2
        stats.defense += 1
        emit_signal("level_up", stats.level)

func take_damage(amount: int):
    stats.health = max(0, stats.health - amount)
    emit_signal("health_changed", stats.health)
    
    if stats.health == 0:
        handle_player_death()

func heal(amount: int):
    stats.health = min(stats.max_health, stats.health + amount)
    emit_signal("health_changed", stats.health)

セーブ・ロードシステム

# SaveManager.gd (Autoload)
extends Node

const SAVE_PATH = "user://savegame.save"

func save_game():
    var save_dict = {
        "player_stats": PlayerData.stats,
        "player_inventory": PlayerData.inventory,
        "opened_chests": GameManager.opened_chests,
        "current_scene": get_tree().current_scene.scene_file_path,
        "timestamp": Time.get_unix_time_from_system()
    }
    
    var save_file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
    save_file.store_var(save_dict)
    save_file.close()

func load_game():
    if not FileAccess.file_exists(SAVE_PATH):
        return false
        
    var save_file = FileAccess.open(SAVE_PATH, FileAccess.READ)
    var save_dict = save_file.get_var()
    save_file.close()
    
    # データの復元
    PlayerData.stats = save_dict.get("player_stats", PlayerData.stats)
    PlayerData.inventory = save_dict.get("player_inventory", [])
    GameManager.opened_chests = save_dict.get("opened_chests", {})
    
    # シーンの読み込み
    var scene_path = save_dict.get("current_scene", "")
    if scene_path != "":
        get_tree().change_scene_to_file(scene_path)
    
    return true

複数のAutoloadの連携

# GameEvents.gd (Autoload) - イベントバス
extends Node

signal enemy_defeated(enemy_type: String)
signal item_collected(item_name: String)
signal quest_completed(quest_id: String)

# QuestManager.gd (Autoload)
extends Node

var active_quests: Dictionary = {}

func _ready():
    GameEvents.enemy_defeated.connect(_on_enemy_defeated)
    GameEvents.item_collected.connect(_on_item_collected)

func _on_enemy_defeated(enemy_type: String):
    for quest_id in active_quests:
        var quest = active_quests[quest_id]
        if quest.type == "defeat_enemies" and quest.target == enemy_type:
            quest.current += 1
            check_quest_completion(quest_id)

パフォーマンスとメモリ管理

注意点

# 悪い例:大量のデータを常に保持
var all_game_textures: Dictionary = {}  # メモリを圧迫

# 良い例:必要な時にロード
func get_texture(texture_name: String) -> Texture2D:
    return load("res://textures/" + texture_name + ".png")

クリーンアップ

func reset_game_state():
    # ゲーム開始時やタイトルに戻る際にリセット
    opened_chests.clear()
    collected_items.clear()
    current_level = 1
    score = 0

ベストプラクティス

  1. 責任の分離: 機能ごとに別々のAutoloadを作成
  2. 初期化順序: 依存関係がある場合は順序に注意
  3. シグナルの活用: 直接的な依存を避ける
  4. デバッグ機能: 開発時のみ有効なデバッグ用Autoload

実装して学んだこと

Autoloadを使うようになってから、ゲームのデータ管理が劇的に改善しました。特に宝箱の開封状態管理は、以前は各シーンで個別に管理していたため、「セーブデータが早くなると一度開けた宝箱が復活してしまう」というバグがありました。

Autoloadで一元管理するようになってからは、こういった問題が一気に解決しました。また、シグナルを使ったイベントシステムを構築することで、異なるAutoload間での連携がスムーズにできるようになり、コードの結合度を低く保つことができました。

最初は「全部一つのAutoloadにまとめてしまえば楽じゃないか?」と思っていましたが、機能ごとに分けることのメリットを実感しています。コードの可読性、保守性、デバッグのしやすさ、すべてが向上しました。