👓

【UE5 EUW Python】packageに含めるPersistent LevelをDefaultGame.iniに追加する

に公開

Pacageの際にPersistent Levelが含まれなくてレベル遷移ができない

Game Default Mapは最初に開くMapなので、Packageに含まれます。
他のPersisitent Levelが必要な場合は、
「List of maps to include in a packaged build」を追加します。

たとえば、「Lvl_Game_002」が「List of maps to include in a packaged build」に追加されずにPakage化し、「Lvl_Game_002」に遷移しようとすると以下のように見つからないというログが出力されます。

LogLongPackageNames: Warning: Can't Find URL: Lvl_Game_002: . Invalidating and reverting to Default URL.

AIで質問すると「Additional Asset Directories to Cook」にPersistent LevelのLevelアセットが含まれるディレクトリを含めればいいという回答が得れれますが、現状「List of maps to include in a packaged build」にPersistent Levelに含める必要があります。

レベルが増えていくようなゲームだと問題が発生します。

  • 追加のし忘れ
  • 追加が面倒くさい

EUWのボタンを押せば追加されるような機能を作りました。

packageに含めるmapをDefaultGame.iniに追加する

「Additional Asset Directories to Cook」にLevelのPathをt追加すると、「GameDefault.ini」に設定が追加されます。

GameDefault.ini
[/Script/EngineSettings.GeneralProjectSettings]
+MapsToCook=(FilePath="/Game/BlockBreaker/Maps/Game/Round/Lvl_Game_001")

EUWからPythonで「GameDefault.ini」にLevelを追加する処理を作成します。

【UI】

【Blueprint】

「Execute Python Script」ノードのソースコードは以下になります。

import unreal
import os, io, re

# ==== 指定フォルダ配下の .umap を /Game/... 形式で列挙 ====
def list_worlds_under(path):
    reg = unreal.AssetRegistryHelpers.get_asset_registry()
    flt = unreal.ARFilter(class_names=["World"], package_paths=[path], recursive_paths=True)
    assets = reg.get_assets(flt)
    out = []
    for a in assets:
        # object_path が無い環境でも package_name は常に取得可
        out.append(str(a.package_name))
    return out


# ==== DefaultGame.ini の MapsToCook だけを置換(他は一切いじらない) ====
def add_maps_to_packaging_via_plain_ini(map_paths, clear_existing=False, merge_existing=False):
    """
    [/Script/UnrealEd.ProjectPackagingSettings] セクションの MapsToCook 行のみを入れ替え。
    - 他キー/空行/順番はそのまま維持
    - 既存 MapsToCook ブロックの「最初の行の位置」に新ブロックを挿入
    - 既存が無ければセクション末尾に追加
    - merge_existing=True で既存 + 新規を順序維持マージ、False で完全置換
    """
    cfg_dir = unreal.Paths.project_config_dir()
    ini_path = os.path.join(cfg_dir, "DefaultGame.ini")

    sect_header = "[/Script/UnrealEd.ProjectPackagingSettings]"
    key = "MapsToCook"

    # --- 読み込み ---
    if os.path.exists(ini_path):
        with io.open(ini_path, "r", encoding="utf-8") as f:
            lines = f.read().splitlines()
    else:
        lines = []

    # --- セクション範囲検出 ---
    start = end = None
    for i, line in enumerate(lines):
        if line.strip() == sect_header:
            start = i
            for j in range(i + 1, len(lines)):
                if lines[j].startswith("[") and lines[j].endswith("]"):
                    end = j
                    break
            if end is None:
                end = len(lines)
            break

    # セクション無ければ新規作成
    if start is None:
        if lines and lines[-1].strip() != "":
            lines.append("")
        lines.append(sect_header)
        start = len(lines) - 1
        end = start + 1

    # --- セクション内容取得 ---
    section = lines[start + 1:end]

    def is_maps_line(s):
        st = s.lstrip()
        return st.startswith(key + "=") or st.startswith("+" + key + "=")

    # 既存 MapsToCook の最初の行位置と既存値
    first_maps_idx = None
    existing_maps = []
    for idx, s in enumerate(section):
        if is_maps_line(s):
            if first_maps_idx is None:
                first_maps_idx = idx
            # FilePath 抽出
            try:
                rhs = s.split("=", 1)[1].strip()
                if rhs.startswith("(") and rhs.endswith(")"):
                    inner = rhs[1:-1]
                    parts = [p.strip() for p in inner.split(",")]
                    for p in parts:
                        if p.startswith("FilePath="):
                            val = p.split("=", 1)[1].strip().strip('"')
                            if val:
                                existing_maps.append(val)
            except Exception:
                pass

    # MapsToCook 行を除いた版
    section_no_maps = [s for s in section if not is_maps_line(s)]

    # 挿入位置:既存の最初の Maps 行があればそこ以前の非Maps行の本数位置、無ければ末尾
    if first_maps_idx is not None:
        non_maps_before = sum(1 for i, s in enumerate(section) if i < first_maps_idx and not is_maps_line(s))
        insert_at = non_maps_before
    else:
        insert_at = len(section_no_maps)

    # 並びの安定化(Lvl_Game_001, 002... の数値順)
    def natural_key(s):
        return [int(t) if t.isdigit() else t for t in re.split(r"(\d+)", s)]

    if merge_existing and existing_maps:
        existing_order = list(dict.fromkeys(existing_maps))  # 重複除去で順序維持
        add_tail = [p for p in sorted(set(map_paths), key=natural_key) if p not in set(existing_order)]
        final_paths = existing_order + add_tail
    else:
        final_paths = sorted(set(map_paths), key=natural_key)

    new_block = [f'+{key}=(FilePath="{p}")' for p in final_paths]

    # セクション再構成(Maps 以外は順序そのまま)
    rebuilt_section = section_no_maps[:insert_at] + new_block + section_no_maps[insert_at:]

    # 全体再構成・保存
    new_lines = lines[:start + 1] + rebuilt_section + lines[end:]
    with io.open(ini_path, "w", encoding="utf-8", newline="\n") as f:
        f.write("\n".join(new_lines) + "\n")

    unreal.log(f"[MapsToCook] replaced -> {len(final_paths)} entries at {ini_path}")
    return len(final_paths)


# ==== 実行 ====

# map一覧を取得する
maps = list_worlds_under(map_root)

if not maps:
    # mapが見つからない
    unreal.log_warning(f"No maps found under: {map_root}")
else:
    # mapが見つかった
    unreal.log(f"Found {len(maps)} maps under {map_root}")
    # 既存 MapsToCook を完全置換(UI の並びを一定にしたい場合)
    add_maps_to_packaging_via_plain_ini(maps, clear_existing=False, merge_existing=False)

ボタンをクリックすると「GameDefault.ini」にレベルの一覧が追加されます。

「GameDefault.ini」は更新されますが、Project SettingsのUIにはプロジェクトを再起動すると反映されます。

関連URL

https://zenn.dev/posita33/books/ue5_starter_cpp_and_bp_002/viewer/chap_01-06_cpp-package_project_settings

Discussion