1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[UE5] World Bookmarks の使い方と ActorEditorContext を使ったレベルエディタ拡張

Last updated at Posted at 2025-12-23

概要

UE5.6にて、World Bookmarks という新機能が追加されました。
中々に便利そうなので、検証ついでに使い方や拡張方法などを残しておこうと思います。
2025-12-22_02h54_57.png

その際、Actor Editor Context というエディタ拡張に使えそうな機能を発見したので、そちらを使ったレベル編集機能の拡張についても、サンプルとともに軽く紹介しようと思います。

検証環境

  • Windows11
  • UE5.7.1(Launcher版)
  • Visual Studio v17.14.23

World Bookmarks とは

端的にいうと「レベルエディタ上の作業状態(ワールド・カメラ・ロード範囲など)を保存し、作業状況の誘導や再現を支援する機能」です。
アセットとして管理できるため、ローカルの整理に限らず他ユーザーとの共有も可能です。

例えば、以下のような情報がそのまま保存され、コンテンツブラウザ上でアセット管理できます。

  • 開いているレベルアセット名
  • レベルエディタのカメラ位置、FOV
  • (WorldPartition対応レベルでは)ロードしている領域
  • Actor Editor Context の状態
    2025-12-22_03h00_23.png

※ 従来の「ビューポートブックマーク(Ctrl+1 など)」とは以下の点が異なります。

  • 従来のブックマーク:カメラ位置の即時移動に限定した機能
  • World Bookmarks:レベル全体の編集状態を保存・復元するための機能

使い方

ブックマークの作り方

  1. 記録したい状態で、コンテンツブラウザ上で右クリックします。
    "ワールド" カテゴリから "ブックマーク" アセットを選択すると、すぐにアセットが作られます。
    2025-12-22_02h58_36.png
  2. あとは、アセットの名前を入れるだけでOKです。
    2025-12-22_03h03_14.png
    たったこれだけで、アセットを作ったときに開いていたレベルアセット名や、カメラ位置、FOVなどがすべて記録されています。
    make-bookmarks.gif

ブックマークのロード

ブックマークで記録した状態に復帰するには、ブックマークアセットを右クリックして、
"ブックマークをロード" を押せば即座に復帰してくれます。
今開いているレベルの切替が発生する場合は、アセット保存確認なども含め通常通りにレベル遷移が発生します。
load-bookmarks.gif

作成したブックマークのデータ更新

一度ブックマークを作った後、保存されている状態を変更したい場合は、ブックマークアセットを右クリックして、"ブックマークを更新" を押せば、その時点でのレベルエディタの状態で再度ブックマークのデータが更新されます。
2025-12-22_03h05_31.png

ワールドブックマークウィンドウ

このワールドブックマーク専用のアウトライナーウィンドウも用意されており、PJ全体のブックマークを一覧化して確認できます。
エディタ上部の "ウィンドウ" > "World Partition" > "ワールドブックマーク" から開くことができます。
2025-12-22_03h12_54.png
右上の歯車ボタンから、細かい表示設定の切替もできます。
2025-12-22_03h12_09.png
右クリックで表示されるコンテキストメニューでは、コンテンツブラウザ上ではなかった「ここからプレイ」といった項目も追加されます。
2025-12-22_03h16_27.png

カテゴリ分け

ブックマークアセットはカテゴリ指定ができます。カテゴリ指定すると、カテゴリごとに色がついて一覧化したときにわかりやすくなるメリットがあります。
(上の画像は、3項目にそれぞれ別のカテゴリを指定しています)
真っ先に想定される使い方は(ブックマークされた)レベルごとの整理・分類かと思いますが、他にも

  • アセット制作者別の仕分け
  • 用途ごとの仕分け(作業環境の共有・負荷測定の環境共有…etc.)

など色々使えそうです。

ホームブックマーク

プロジェクトのエディタを立ち上げたときに、自動で復帰処理してくれるワールドブックマークを指定できる機能です。
"エディタの環境設定" > "World Bookmark" の項目から設定できます。毎回指定のレベル・特定の範囲をロードして起動したい場合に便利そうです。
2025-12-23_03h26_05.png

デフォルトブックマーク

レベルを開いたとき、指定したワールドブックマークへの復帰処理を行ってくれます。各レベルのワールドセッティングスで指定できるようになっています。
2025-12-23_03h29_50.png

そのほか、細かい使い方については、Epic公式が提供しているドキュメント を参照してください。
※ コンソールコマンド経由でテキストとして出力・復帰する仕組みも用意されています。

Actor Editor Context について

さて、ブックマークで保存される項目の中で1つだけ、 Actor Editor Context という抽象的な記載があったかと思います。
これは、

現在のレベルエディタ上の編集状況

とでもいうものです。例えば、

  • 編集中のサブレベル(WP不使用の場合のみ)
  • 編集中のレベルインスタンス名
  • データレイヤー
  • アウトライナー上のアクターフォルダ

などをレベルエディタ上に指定しておくことで、レベルエディタ上で新しく配置したアクター にそれらの設定を自動で反映することができます。
※既に配置済のアクターには影響しません。

現在の Actor Editor Context は、レベルエディタのビューポート右下に表示されるUIから確認できます。
2025-12-22_03h25_35.png
例えば、アクターフォルダについてはアウトライナーのフォルダの右クリックから設定ができます。
データレイヤーについては、データレイヤーアウトライナー上で指定したいデータレイヤーをダブルクリックすれば設定されます。
actor-editor-context.gif

これらのレベルエディタでの編集状況を設定・管理する機能自体を、Actor Editor Contextと総称するようです。
Epic公式が提供しているドキュメント
アクターフォルダ設定やデータレイヤー設定については、レベル編集している人なら既に使っている人も多いのではないでしょうか。

World Bookmarks との関連は?

実は、World Bookmarks のアセットの内部を見ると、"アクタエディタコンテキスト" の項目があります。
ここに、ブックマーク作成時に設定していた Current Context の情報が格納されます。

例えば、ブックマーク作成時にアクタフォルダやデータレイヤーの設定を Actor Editor Context に追加していれば…
2025-12-23_03h37_10.png

このように、その時に設定していたアクタフォルダやデータレイヤーが保存されています。
2025-12-23_03h38_19.png

ブックマークをロードすると、これらの状況も含めて再現してくれます。

ここからはちょっとUnreal C++の話をしていきます。

Actor Editor Context を使ってエディタ拡張してみる

Actor Editor Context はレベルエディタの実装に汎用的に使われている仕組みのようです。汎用性があるということは拡張の余地もあるはず…

ということで、ちょっとC++でエディタ拡張をしてみましょう。

Actor Editor Context関連のクラス構成

実際にコードを見てみると、Actor Editor Context の仕組みは抽象化されていることがわかります。
各機能側が IActorEditorContextClient を実装し、
UActorEditorContextSubsystem に登録することで成立する拡張ポイントになっています。
公式のデータレイヤー編集のサブシステムクラス (UDataLayerEditorSubsystem) でもIActorEditorContextClientを継承して、必要な処理を実装しているだけです。
Clientは全て純粋仮想関数なので、自由に実装できますね。
そして、継承側クラスの初期化・終了処理時に Actor Editor Context自体の管理クラス(UActorEditorContextSubsystem) への登録・登録解除を行えば、後はいい感じに処理してくれるという設計です。

IActorEditorContextClient.h抜粋
struct IActorEditorContextClient
{
    // コメントはこちらで追記しています。
    // Contextが変化したときの処理の実装
	virtual void OnExecuteActorEditorContextAction(UWorld* InWorld, const EActorEditorContextAction& InType, AActor* InActor = nullptr) = 0;
    // 以下がWorldBookmarksでの復帰関連の処理
    // 保存時の処理
	virtual void CaptureActorEditorContextState(UWorld* InWorld, UActorEditorContextStateCollection* InStateCollection) const = 0;
    // 復帰時の処理
	virtual void RestoreActorEditorContextState(UWorld* InWorld, const UActorEditorContextStateCollection* InStateCollection) = 0;
    // 以下はCurrent ContextのUI表示用の処理
    // 情報表示用のSlate生成用の処理(下画像の緑枠の部分を指定)
	virtual bool GetActorEditorContextDisplayInfo(UWorld* InWorld, FActorEditorContextClientDisplayInfo& OutDiplayInfo) const = 0;
    // falseを返すと、画像青枠の×ボタンが出なくなる
	virtual bool CanResetContext(UWorld* InWorld) const = 0;
    // 状態表示用のSlateを生成して返す(下画像の緑枠の部分をWidgetの形で渡す)
	virtual TSharedRef<SWidget> GetActorEditorContextWidget(UWorld* InWorld) const = 0;
    // 自クラスのデリゲートを持たせて返す関数
	virtual FOnActorEditorContextClientChanged& GetOnActorEditorContextClientChanged() = 0;
};

2025-12-23_03h39_53.png

試しに実装してみる

こういうのは、実際に使ってみるとわかりやすくなります。なにか作って試してみましょう。
今回はサンプル機能として「アクターを配置すると、特定のActorTagを勝手に付与してくれる」仕組みを作ってみます。
公式のデータレイヤー的な使い方と同じですね。(この機能が欲しくなる機会は無さそうですが…)

クラス構成もシンプルに、一部データ型を除いて EditorSubsystem 継承の管理クラス一本でやってみましょう。
実装内容については、公式の UDataLayerEditorSubsystem, FActorFolders あたりの作りを参考にしています。

サンプルコード
SampleActorTagEditorSubsystem.h
#pragma once

#include "EditorSubsystem.h"
#include "IActorEditorContextClient.h"
#include "ActorEditorContextState.h"

#include "SampleActorTagEditorSubsystem.generated.h"

UCLASS()
class UActorEditorContextActorTagsState : public UActorEditorContextClientState
{
	GENERATED_BODY()

public:
	UPROPERTY(VisibleAnywhere, Category = "Actor Tag Editor")
	FName ActorTagEditorStateName;
};

USTRUCT()
struct FActorTagEditorModeData
{
	GENERATED_BODY()

	UPROPERTY()
	FText DisplayText;

	UPROPERTY()
	TArray<FName> ActorTags;
};

UCLASS()
class USampleActorTagEditorSubsystem : public UEditorSubsystem, public IActorEditorContextClient
{
	GENERATED_BODY()

public:
	// EditorSubsystem interfaces
	virtual void Initialize(FSubsystemCollectionBase& Collection);
	virtual void Deinitialize();

	// IActorEditorContextClient interface
	virtual void OnExecuteActorEditorContextAction(UWorld* InWorld, const EActorEditorContextAction& InType, AActor* InActor = nullptr);
	virtual void CaptureActorEditorContextState(UWorld* InWorld, UActorEditorContextStateCollection* InStateCollection) const;
	virtual void RestoreActorEditorContextState(UWorld* InWorld, const UActorEditorContextStateCollection* InStateCollection);
	virtual bool GetActorEditorContextDisplayInfo(UWorld* InWorld, FActorEditorContextClientDisplayInfo& OutDiplayInfo) const;
	virtual bool CanResetContext(UWorld* InWorld) const;
	virtual TSharedRef<SWidget> GetActorEditorContextWidget(UWorld* InWorld) const ;
	virtual FOnActorEditorContextClientChanged& GetOnActorEditorContextClientChanged()
	{
		return OnActorEditorContextClientChanged;
	};

	//Global Getter
	static USampleActorTagEditorSubsystem* Get();

	// 実際にActorTagを付与する関数
	void OnNewActorsPlaced(UObject* ObjToUse, const TArray<AActor*>& PlacedActors);

	// リセット
	void Reset();

private:
	// ComboBoxの表示Text取得関数
	FText GetModeDisplayText(FName ModeName) const;

public:
	// このシステムが有効か?
	bool bEnabled = true;

private:
	// 選択中のモード
	FName CurrentMode = NAME_None;

    // Client更新をかけたいときに呼び出すデリゲート
	FOnActorEditorContextClientChanged OnActorEditorContextClientChanged;

	// モード一覧コンテナ, FNameは内部のモード識別子
	TMap<FName, FActorTagEditorModeData> ActorTagStateList;
	// ComboBox Widget用
	mutable TArray<TSharedPtr<FName>> CachedModeNameList;
	mutable TSharedPtr<SWidget> ModeComboBoxWidget;
};

SampleActorTagEditorSubsystem.cpp
#include "SampleActorTagEditorSubsystem.h"
#include "ActorEditorContextActorTagsState.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Input/SComboBox.h"
#include "Subsystems/ActorEditorContextSubsystem.h"

USampleActorTagEditorSubsystem* USampleActorTagEditorSubsystem::Get()
{
	checkf(GEditor, TEXT("GEditor is nullptr!!"));
	return GEditor->GetEditorSubsystem<USampleActorTagEditorSubsystem>();
}

void USampleActorTagEditorSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    // memo: テスト用なのでここでは直接データ設定してしまいます。
	static const FName NAME_Test{ TEXT("TestTag") };
	static const FName NAME_Mode1{ TEXT("Mode1") };
	static const FName NAME_Mode2{ TEXT("Mode2") };
	ActorTagStateList.Emplace(
		NAME_None,
		{
			FText::FromString(TEXT("未選択")),
			{}
		}
	);
	ActorTagStateList.Emplace(
		NAME_Mode1,
		{
			FText::FromString(TEXT("モード1")),
			{NAME_Mode1, NAME_Test}
		}
	);
	ActorTagStateList.Emplace(
		NAME_Mode2,
		{
			FText::FromString(TEXT("モード2")),
			{NAME_Mode2, NAME_Test}
		}
	);

	CurrentMode = NAME_None;
    // ActorEditorContextSubsystemの初期化保証
	Collection.InitializeDependency(UActorEditorContextSubsystem::StaticClass());

    // ActorEditorContextClientを登録
	UActorEditorContextSubsystem::Get()->RegisterClient(this);

    // アクターが新しく配置されたときのデリゲートに処理を登録
	FEditorDelegates::OnNewActorsPlaced.AddUObject(this, &USampleActorTagEditorSubsystem::OnNewActorsPlaced);
}

void USampleActorTagEditorSubsystem::Deinitialize()
{
    // デリゲートのお片付け
	FEditorDelegates::OnNewActorsPlaced.RemoveAll(this);
	UActorEditorContextSubsystem::Get()->UnregisterClient(this);
}

void USampleActorTagEditorSubsystem::OnExecuteActorEditorContextAction(UWorld* InWorld, const EActorEditorContextAction& InType, AActor* InActor)
{
}

void USampleActorTagEditorSubsystem::CaptureActorEditorContextState(UWorld* InWorld, UActorEditorContextStateCollection* InStateCollection) const
{
	if (!bEnabled)
	{
		return;
	}

	if (!IsValid(InWorld))
	{
		return;
	}
    // Bookmarksへの保存用のオブジェクトを生成
	auto* State = NewObject<UActorEditorContextActorTagsState>(InStateCollection);
	State->ActorTagEditorStateName = CurrentMode;
	InStateCollection->AddState(State);
}

void USampleActorTagEditorSubsystem::RestoreActorEditorContextState(UWorld* InWorld, const UActorEditorContextStateCollection* InStateCollection)
{
	if (!bEnabled)
	{
		return;
	}

	if (!IsValid(InWorld))
	{
		return;
	}

    // Bookmarks内の保存用オブジェクトから状態を復帰
	if (const UActorEditorContextActorTagsState* State = InStateCollection->GetState<UActorEditorContextActorTagsState>())
	{
		if (ActorTagStateList.Contains(State->ActorTagEditorStateName))
		{
			CurrentMode = State->ActorTagEditorStateName;
			OnActorEditorContextClientChanged.Broadcast(this);
		}
	}
}

bool USampleActorTagEditorSubsystem::GetActorEditorContextDisplayInfo(UWorld* InWorld, FActorEditorContextClientDisplayInfo& OutDiplayInfo) const
{
	if (!bEnabled)
	{
		return false;
	}

	OutDiplayInfo.Title = TEXT("Actor Tagモード");
	OutDiplayInfo.Brush = FAppStyle::GetBrush(TEXT("Icons.PlaceActors"));
	return true;
}

bool USampleActorTagEditorSubsystem::CanResetContext(UWorld* InWorld) const
{
	return true;
}

TSharedRef<SWidget> USampleActorTagEditorSubsystem::GetActorEditorContextWidget(UWorld* InWorld) const
{
	if (!bEnabled)
	{
		return SNullWidget::NullWidget;
	}

	if (ModeComboBoxWidget.IsValid())
	{
		return ModeComboBoxWidget.ToSharedRef();
	}

	CachedModeNameList.Reset();
	for (const auto& Pair : ActorTagStateList)
	{
		CachedModeNameList.Emplace(MakeShared<FName>(Pair.Key));
	}

	auto GetCurrentItem = [this]() -> TSharedPtr<FName>
		{
			for (const TSharedPtr<FName>& Item : CachedModeNameList)
			{
				if (*Item == CurrentMode)
				{
					return Item;
				}
			}
			return nullptr;
		};

	ModeComboBoxWidget = 
		SNew(SVerticalBox)
		// ラベル
		+ SVerticalBox::Slot()
		.AutoHeight()
		.Padding(FMargin(4.f, 2.f))
		[
			SNew(STextBlock)
			.Text(FText::FromString(TEXT("モード選択")))
		]

		// コンボボックス
		+ SVerticalBox::Slot()
		.AutoHeight()
		.Padding(FMargin(4.f, 2.f))
		[
			SNew(SComboBox<TSharedPtr<FName>>)
			.OptionsSource(&CachedModeNameList)
			.OnGenerateWidget_Lambda([this](TSharedPtr<FName> Item)
			{
				return SNew(STextBlock)
        			.Text(GetModeDisplayText(*Item));
			})
			.OnSelectionChanged_Lambda([this](TSharedPtr<FName> NewSelection, ESelectInfo::Type)
			{
				if (NewSelection.IsValid())
				{
					auto* MutableThis = const_cast<USampleActorTagEditorSubsystem*>(this);
					MutableThis->CurrentMode = *NewSelection;
				}
			})
			[
				SNew(STextBlock)
				.Text_Lambda([this]()
				{
					return GetModeDisplayText(CurrentMode);
				})
			]
		];
	return ModeComboBoxWidget.ToSharedRef();
}

void USampleActorTagEditorSubsystem::OnNewActorsPlaced(UObject* ObjToUse, const TArray<AActor*>& InPlacedActors)
{
	if (!bEnabled)
	{
		return;
	}
    // 実際にアクタータグを付与する処理
	for (AActor* PlacedActor : InPlacedActors)
	{
		if (!IsValid(PlacedActor))
		{
			continue;
		}
		const FActorTagEditorModeData* ModeData = ActorTagStateList.Find(CurrentMode);
		if (!ModeData)
		{
			return;
		}

		PlacedActor->Modify();
		for (const FName& Tag : ModeData->ActorTags)
		{
			PlacedActor->Tags.AddUnique(Tag);
		}
	}
}

FText USampleActorTagEditorSubsystem::GetModeDisplayText(FName ModeName) const
{
	if (const FActorTagEditorModeData* Data = ActorTagStateList.Find(ModeName))
	{
		return Data->DisplayText;
	}
	return FText::FromName(ModeName);
}

void USampleActorTagEditorSubsystem::Reset()
{
	// Mode を未選択に
	CurrentMode = NAME_None;

	// UI 破棄
	ModeComboBoxWidget.Reset();

	// Context 更新通知
	OnActorEditorContextClientChanged.Broadcast(this);
}

※コピペする場合は、Editorモジュール内に定義して使ってください。適宜、依存先モジュールの追加も必要です。また、複製やコピペ、アクターの置換など全てのケースについて網羅している状態ではありません。
細かい解説は省きますが、EditorSubsystemを使った一般的な構成かと思います。
Slate部分についても、単にデータ一覧をもとにComboBoxを生成しているだけです。

実演

こんな感じになります。
新しく配置したアクターのTagsの欄に、Mode1, Mode2といったタグが付与されていることがわかると思います。add-tag-mode.gif

Bookmarksとの連携も試してみる

ちゃんと、今回追加したActor Tagモードの状態も、ブックマークに保存・復帰できていますね。
restore-bookmark.gif

ブックマークからも設定の中身が見れるので安心ですね。
2025-12-23_03h57_52.png

まとめ

UE5.6の新機能である、World Bookmarks の検証をしつつ、Actor Editor Context を使ったエディタ拡張のやり方と軽いサンプル作成・検証を行ってみました。

World Bookmarksは、複数名でのチーム作業の際にレベル上の位置や画角などを口頭で伝える手間を省いてくれそうです。特に人数が多いチームほど、作業環境の共有という点でも使っていけそうです。

Actor Editor Context 自体も、レベルエディタ上に追加のUIを表示・操作できるという点で、かなり使えそうな機能ですね。
単に情報を表示するに限らず、今回のようにユーザー側で何か操作・選択させたデータを付与する、といった機能も作れそうです。

謝辞

公式の補足記事みたいな内容ですが…ここまで読んでくださった方、ありがとうございました。:bow:

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?